ext-apps/examples/pdf-server
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Olivier Chafik 6b12fff1d0
chore: bump ext-apps to 1.2.0 (#527)
* chore: bump ext-apps to 1.1.3

Changes since 1.1.2:
- fix: autoResize reporting viewport height when content is taller (#525)
- fix: remove duplicate --color-text-ghost from McpUiStyleVariableKey (#523)
- fix: examples servers' npm run start:stdio (#507)
- pdf-server: ignore client roots by default (#510)
- docs: add Security documentation section and SEO improvements (#524)
- docs: add MCP client configuration examples to CONTRIBUTING.md (#508, #526)
- docs: use apps.extensions.modelcontextprotocol.io domain for docs URLs (#499)
- docs: add SECURITY.md with GitHub Security Advisories guidance (#472)

* chore: switch to minor bump 1.2.0 (new APIs in #470)
2026-03-06 15:43:55 +00:00
..
plugin Add MCPB packaging and Claude Code plugin for pdf-server (#491) 2026-02-24 00:51:43 +00:00
src fix(pdf-server): remove page-loading overlay (#493) 2026-02-23 21:25:25 +00:00
.gitignore pdf-server: robust path validation + folder/file root support (#497) 2026-02-24 18:23:57 +00:00
.mcpbignore pdf-server: robust path validation + folder/file root support (#497) 2026-02-24 18:23:57 +00:00
grid-cell.png examples: add PDF viewer w/ chunked data loading, full-screen, model context updates, private tool (#267) 2026-01-14 14:25:24 +00:00
icon.png Add MCPB packaging and Claude Code plugin for pdf-server (#491) 2026-02-24 00:51:43 +00:00
main.ts ignore client roots by default in pdf-server (#510) 2026-02-28 00:42:11 +00:00
manifest.json Add MCPB packaging and Claude Code plugin for pdf-server (#491) 2026-02-24 00:51:43 +00:00
mcp-app.html feat(pdf-server): lazy range-based loading + eager background preloading (#480) 2026-02-21 16:33:48 -08:00
package.json chore: bump ext-apps to 1.2.0 (#527) 2026-03-06 15:43:55 +00:00
pdf-icon.svg Adding a simple (bad) pdf icon for directory (#330) 2026-01-23 16:04:46 +00:00
README.md ignore client roots by default in pdf-server (#510) 2026-02-28 00:42:11 +00:00
screenshot.png examples: add PDF viewer w/ chunked data loading, full-screen, model context updates, private tool (#267) 2026-01-14 14:25:24 +00:00
server.test.ts ignore client roots by default in pdf-server (#510) 2026-02-28 00:42:11 +00:00
server.ts ignore client roots by default in pdf-server (#510) 2026-02-28 00:42:11 +00:00
tsconfig.json feat(pdf-server): lazy range-based loading + eager background preloading (#480) 2026-02-21 16:33:48 -08:00
tsconfig.server.json feat: compile example servers to JavaScript for npm consumption (#259) 2026-01-14 23:46:17 +00:00
vite.config.ts examples: add PDF viewer w/ chunked data loading, full-screen, model context updates, private tool (#267) 2026-01-14 14:25:24 +00:00

PDF Server

Screenshot

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"
      ]
    }
  }
}

Local Development

To test local modifications, use this configuration (replace ~/code/ext-apps with your clone path):

{
  "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):

// 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({
    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:

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();
};

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/

Security: Client Roots

MCP clients may advertise roots — file:// URIs pointing to directories on the client's file system. The server uses these to allow access to local files under those directories.

  • Stdio mode (--stdio): Client roots are always enabled — the client is typically on the same machine (e.g. Claude Desktop), so the roots are safe.
  • HTTP mode (default): Client roots are ignored by default — the client may be remote, and its roots would be resolved against the server's filesystem. To opt in, pass --use-client-roots:
# Trust that the HTTP client is local and its roots are safe
bun examples/pdf-server/main.ts --use-client-roots

When roots are ignored the server logs:

[pdf-server] Client roots are ignored (default for remote transports). Pass --use-client-roots to allow the client to expose local directories.

Allowed Sources

  • Local files: Must be passed as CLI arguments (or via client roots when enabled)
  • 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