* 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
4.5 KiB
PDF Server
An interactive PDF viewer using 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):
{
"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):
// Returns chunks with pagination metadata
{
(bytes, offset, byteCount, totalBytes, hasMore);
}
Client side (mcp-app.ts):
// 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:
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
// 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):
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:
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyDocumentTheme(ctx.theme);
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
};
Usage
# 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
