ext-apps/examples/pdf-server
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Olivier Chafik 89a684546b
fix(examples): resolve DIST_DIR path for npm package execution (#277)
* fix(examples): resolve DIST_DIR path for npm package execution

When servers are compiled to dist/server.js and published to npm,
`import.meta.dirname` already points to dist/, so joining with "dist"
results in dist/dist/ which doesn't exist.

This fix detects whether we're running from TypeScript source or
compiled JavaScript and adjusts the path accordingly:
- From source (server.ts): use dist/ subdirectory
- From compiled (dist/server.js): use current directory

Fixes ENOENT errors like:
"ENOENT: no such file or directory, open '.../dist/dist/mcp-app.html'"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(pdf-server): mark pdfjs-dist as external in bundle

The pdf.worker.mjs file wasn't being found when running from npm
because bun bundled pdfjs-dist but didn't include the worker file.

By marking pdfjs-dist as external, it uses the installed package
from node_modules at runtime where the worker is available.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 12:23:13 +00:00
..
src pdf-server: fix scrolling gap in inline mode (#280) 2026-01-15 12:22:58 +00:00
.gitignore feat: compile example servers to JavaScript for npm consumption (#259) 2026-01-14 23:46:17 +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
main.ts feat: compile example servers to JavaScript for npm consumption (#259) 2026-01-14 23:46:17 +00:00
mcp-app.html examples: add PDF viewer w/ chunked data loading, full-screen, model context updates, private tool (#267) 2026-01-14 14:25:24 +00:00
package.json fix(examples): resolve DIST_DIR path for npm package execution (#277) 2026-01-15 12:23:13 +00:00
README.md examples: add PDF viewer w/ chunked data loading, full-screen, model context updates, private tool (#267) 2026-01-14 14:25:24 +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.ts fix(examples): resolve DIST_DIR path for npm package execution (#277) 2026-01-15 12:23:13 +00:00
tsconfig.json examples: add PDF viewer w/ chunked data loading, full-screen, model context updates, private tool (#267) 2026-01-14 14:25:24 +00: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

A simple interactive PDF viewer that uses PDF.js. Launch it w/ a few PDF files and/or URLs as CLI args (+ support loading any additional pdf from arxiv.org).

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 shows a possible workaround:

Server side (pdf-loader.ts):

// Returns chunks with pagination metadata
async function loadPdfBytesChunk(entry, offset, byteCount) {
  return {
    bytes: base64Chunk,
    offset,
    byteCount,
    totalBytes,
    hasMore: offset + byteCount < totalBytes,
  };
}

Client side (mcp-app.ts):

// Load in chunks with progress
while (hasMore) {
  const chunk = await app.callServerTool("read_pdf_bytes", { pdfId, 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({
  structuredContent: {
    title: pdfTitle,
    currentPage,
    totalPages,
    pageText: pageText.slice(0, 5000),
    selection: selectedText ? { text, start, end } : undefined,
  },
});

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

Usage

# Default: loads a sample arxiv paper
bun examples/pdf-server/server.ts

# Load local files (converted to file:// URLs)
bun examples/pdf-server/server.ts ./docs/paper.pdf /path/to/thesis.pdf

# Load from URLs
bun examples/pdf-server/server.ts https://arxiv.org/pdf/2401.00001.pdf

# Mix local and remote
bun examples/pdf-server/server.ts ./local.pdf https://arxiv.org/pdf/2401.00001.pdf

# stdio mode for MCP clients
bun examples/pdf-server/server.ts --stdio ./papers/

Security: Dynamic URLs (via view_pdf tool) are restricted to arxiv.org. Local files must be in the initial list.

Tools

Tool Visibility Purpose
list_pdfs Model List indexed PDFs
display_pdf Model + UI Display interactive viewer in chat
read_pdf_bytes App only Chunked binary loading

Architecture

server.ts           # MCP server (233 lines)
├── src/
│   ├── types.ts        # Zod schemas (75 lines)
│   ├── pdf-indexer.ts  # URL-based indexing (44 lines)
│   ├── pdf-loader.ts   # Chunked loading (171 lines)
│   └── mcp-app.ts      # Interactive viewer UI

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()
Size negotiation app.sendSizeChanged()

Dependencies

  • pdfjs-dist: PDF rendering
  • @modelcontextprotocol/ext-apps: MCP Apps SDK