* fix(map-server): simplify location update message format * fix(say-server): pass host to streamable_http_app for non-localhost deployments * refactor(pdf-server): simplify to stateless range-query architecture - Remove caching/indexing in favor of direct HTTP Range requests - Use HEAD requests to get file size (no full download) - Use Range requests to proxy byte chunks (no caching) - Local files: fs.read with offset/length - Remote files: HTTP Range header - Zero memory footprint (no PDF caching) - Truly stateless (no session management needed) - Single server.ts (~350 lines) + main.ts CLI entry point - Instant startup (no preloading) - Security: whitelist local files, allowed origins Removed files: - src/pdf-indexer.ts - src/pdf-loader.ts - src/types.ts * feat(pdf-server): add dark mode support - Add CSS variables with light-dark() function for automatic theme switching - Sync theme with host via applyDocumentTheme() - Apply host CSS variables via applyHostStyleVariables() * feat(pdf-server): add list_pdfs tool back Lists local files and allowed remote origins. Also moves CHUNK_SIZE to module level constant. * feat(pdf-server): improve tool descriptions and increase context limit - display_pdf: list all allowed domains in description - Increase MAX_MODEL_CONTEXT_LENGTH to 15000 - Move CHUNK_SIZE to module level constant * chore(say-server): remove deployment docs from repo Keep server.py fix for non-localhost deployments. Deployment docs kept locally but not in repo. * refactor(pdf-server): simplify updateModelContext format - Add widget ID for multi-widget disambiguation - Use concise header: 'PDF viewer (id) | title | Current Page: X/Y' - Add 'Page content:' prefix before text - Keep selection markers (<pdf-selection>...</pdf-selection>) * refactor: rename widgetUUID to viewUUID for consistency Aligns with main's standardization on 'view' terminology (PR #325). Also dynamically generate allowed domains in display_pdf description. * viewUUID * fix(map-server): fix syntax error in location update message * fix(pdf-server): restore package.json, improve server docs - Restore --external pdfjs-dist in build - Rewrite server.ts header for newcomers (list tools, not implementation details) - Minimize main.ts diff * chore(pdf-server): remove unused addAllowedOrigin/addAllowedLocalFile * docs(pdf-server): update README for new architecture * docs(pdf-server): restore full README, add theming section, remove get_pdf_info * rm * fix(pdf-server): restore createMcpExpressApp and error handling - Use createMcpExpressApp for DNS rebinding protection - Restore error callback in app.listen() * fix(say-server): pass host to streamable_http_app for non-localhost deployments * chore(say-server): remove deployment docs from repo * widgetId->tool id * fix(map-server): add null check for center in location update chore(pdf-server): dedupe and sort allowed origins * fix(map-server): use transparent background for rounded corners * prettier
168 lines
4.5 KiB
Markdown
168 lines
4.5 KiB
Markdown
# PDF Server
|
|
|
|

|
|
|
|
An interactive PDF viewer using [PDF.js](https://mozilla.github.io/pdf.js/). Supports local files and remote URLs from academic sources (arxiv, biorxiv, zenodo, etc).
|
|
|
|
## MCP Client Configuration
|
|
|
|
Add to your MCP client configuration (stdio transport):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"pdf": {
|
|
"command": "npx",
|
|
"args": [
|
|
"-y",
|
|
"--silent",
|
|
"--registry=https://registry.npmjs.org/",
|
|
"@modelcontextprotocol/server-pdf",
|
|
"--stdio"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## What This Example Demonstrates
|
|
|
|
### 1. Chunked Data Through Size-Limited Tool Calls
|
|
|
|
On some host platforms, tool calls have size limits, so large PDFs cannot be sent in a single response. This example streams PDFs in chunks using HTTP Range requests:
|
|
|
|
**Server side** (`server.ts`):
|
|
|
|
```typescript
|
|
// Returns chunks with pagination metadata
|
|
{
|
|
(bytes, offset, byteCount, totalBytes, hasMore);
|
|
}
|
|
```
|
|
|
|
**Client side** (`mcp-app.ts`):
|
|
|
|
```typescript
|
|
// Load in chunks with progress
|
|
while (hasMore) {
|
|
const chunk = await app.callServerTool("read_pdf_bytes", { url, offset });
|
|
chunks.push(base64ToBytes(chunk.bytes));
|
|
offset += chunk.byteCount;
|
|
hasMore = chunk.hasMore;
|
|
updateProgress(offset, chunk.totalBytes);
|
|
}
|
|
```
|
|
|
|
### 2. Model Context Updates
|
|
|
|
The viewer keeps the model informed about what the user is seeing:
|
|
|
|
```typescript
|
|
app.updateModelContext({
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `PDF viewer | "${title}" | Current Page: ${page}/${total}\n\nPage content:\n${pageText}`,
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
This enables the model to answer questions about the current page or selected text.
|
|
|
|
### 3. Display Modes: Fullscreen vs Inline
|
|
|
|
- **Inline mode**: App requests height changes to fit content
|
|
- **Fullscreen mode**: App fills the screen with internal scrolling
|
|
|
|
```typescript
|
|
// Request fullscreen
|
|
app.requestDisplayMode({ mode: "fullscreen" });
|
|
|
|
// Listen for mode changes
|
|
app.ondisplaymodechange = (mode) => {
|
|
if (mode === "fullscreen") enableScrolling();
|
|
else disableScrolling();
|
|
};
|
|
```
|
|
|
|
### 4. External Links (openLink)
|
|
|
|
The viewer demonstrates opening external links (e.g., to the original arxiv page):
|
|
|
|
```typescript
|
|
titleEl.onclick = () => app.openLink(sourceUrl);
|
|
```
|
|
|
|
### 5. View Persistence
|
|
|
|
Page position is saved per-view using `viewUUID` and localStorage.
|
|
|
|
### 6. Dark Mode / Theming
|
|
|
|
The viewer syncs with the host's theme using CSS `light-dark()` and the SDK's theming APIs:
|
|
|
|
```typescript
|
|
app.onhostcontextchanged = (ctx) => {
|
|
if (ctx.theme) applyDocumentTheme(ctx.theme);
|
|
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
|
|
};
|
|
```
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
# Default: loads a sample arxiv paper
|
|
bun examples/pdf-server/main.ts
|
|
|
|
# Load local files (converted to file:// URLs)
|
|
bun examples/pdf-server/main.ts ./docs/paper.pdf /path/to/thesis.pdf
|
|
|
|
# Load from URLs
|
|
bun examples/pdf-server/main.ts https://arxiv.org/pdf/2401.00001.pdf
|
|
|
|
# Mix local and remote
|
|
bun examples/pdf-server/main.ts ./local.pdf https://arxiv.org/pdf/2401.00001.pdf
|
|
|
|
# stdio mode for MCP clients
|
|
bun examples/pdf-server/main.ts --stdio ./papers/
|
|
```
|
|
|
|
## Allowed Sources
|
|
|
|
- **Local files**: Must be passed as CLI arguments
|
|
- **Remote URLs**: arxiv.org, biorxiv.org, medrxiv.org, chemrxiv.org, zenodo.org, osf.io, hal.science, ssrn.com, and more
|
|
|
|
## Tools
|
|
|
|
| Tool | Visibility | Purpose |
|
|
| ---------------- | ---------- | -------------------------------------- |
|
|
| `list_pdfs` | Model | List available local files and origins |
|
|
| `display_pdf` | Model + UI | Display interactive viewer |
|
|
| `read_pdf_bytes` | App only | Stream PDF data in chunks |
|
|
|
|
## Architecture
|
|
|
|
```
|
|
server.ts # MCP server + tools
|
|
main.ts # CLI entry point
|
|
src/
|
|
└── mcp-app.ts # Interactive viewer UI (PDF.js)
|
|
```
|
|
|
|
## Key Patterns Shown
|
|
|
|
| Pattern | Implementation |
|
|
| ----------------- | ------------------------------------------- |
|
|
| App-only tools | `_meta: { ui: { visibility: ["app"] } }` |
|
|
| Chunked responses | `hasMore` + `offset` pagination |
|
|
| Model context | `app.updateModelContext()` |
|
|
| Display modes | `app.requestDisplayMode()` |
|
|
| External links | `app.openLink()` |
|
|
| View persistence | `viewUUID` + localStorage |
|
|
| Theming | `applyDocumentTheme()` + CSS `light-dark()` |
|
|
|
|
## Dependencies
|
|
|
|
- `pdfjs-dist`: PDF rendering (frontend only)
|
|
- `@modelcontextprotocol/ext-apps`: MCP Apps SDK
|