Users frequently call callServerTool("tool_name", args) instead of the
correct callServerTool({ name: "tool_name", arguments: args }), resulting
in a silent/confusing failure. Detect this and throw immediately with
a message that shows the correct call shape.
Also fixes the incorrect example in examples/pdf-server/README.md that
demonstrated this same wrong usage pattern.
Fixes #386
https://claude.ai/code/session_01GqAyN4Ux7svWoU2HqsSLZF
Co-authored-by: Claude <noreply@anthropic.com>
189 lines
4.9 KiB
Markdown
189 lines
4.9 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"
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Local Development
|
|
|
|
To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"pdf": {
|
|
"command": "bash",
|
|
"args": [
|
|
"-c",
|
|
"cd ~/code/ext-apps/examples/pdf-server && npm run build >&2 && node dist/index.js --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({
|
|
name: "read_pdf_bytes",
|
|
arguments: { 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
|