-
Notifications
You must be signed in to change notification settings - Fork 61
examples: add PDF viewer w/ chunked data loading, full-screen, model context updates, private tool #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
abca7ab to
4064f67
Compare
PDF viewer with PDF.js featuring: - Chunked binary loading with progress bar - Text extraction for AI context - arXiv paper support (fetch by ID) - Page navigation with keyboard shortcuts - Zoom controls (including Ctrl+0 reset) - Fullscreen mode support - Horizontal swipe for page changes (disabled when zoomed) - Page persistence in localStorage - Text selection via PDF.js TextLayer - Clickable title link to source URL - Rounded corners and subtle border styling
- Accept any HTTP(s) URLs instead of ArXiv-only - Use HTTP Range requests for chunked binary loading - Remove ArXiv-specific code (arxiv.ts, metadata fetching) - Remove CLAUDE.md index generation - Flatten hierarchical folder structure to simple entries list - Remove dead code: getPdfSummary, httpFileSizes - Simplify base64 encoding using Buffer - Simplify chunk extraction using slice() - Consolidate DEFAULT_PDF_URL constant The server now works with any PDF URL, not just arXiv papers. HTTP Range requests stream chunks on-demand when supported.
- Add pdfTitle to updateModelContext structuredContent - Include selection position (text, start, end) when text is selected - Add debounced selectionchange listener to update context on selection
The UI needs the default value in the schema to show it properly.
- Remove hard-coded test paths from main()
- Remove unused resources: pdfs://metadata/{pdfId}, pdfs://content/{pdfId}
- Remove unused metadata fields: subject, creator, producer, creationDate, modDate
- Remove unused entry fields: relativePath, estimatedTextSize
- Remove filterEntriesByFolder and folder filter from list_pdfs
- Remove redundant output schema validation (trust typed returns)
- Simplify scanDirectory and createLocalEntry signatures
Total: 1836 → 1666 lines (-170 lines, -9%)
Simplified the example to focus on key MCP Apps SDK patterns: - Chunked data through size-limited tool calls - Model context updates (page text + selection) - Display modes (fullscreen vs inline) - External links (openLink) Changes: - Remove local file support (HTTP URLs only) - Restrict dynamic URLs to arxiv.org for security - Simplify types: url instead of sourcePath/sourceType - Simplify indexer: 168 → 44 lines - Simplify loader: 318 → 171 lines - Simplify server: 337 → 233 lines - Fix selection text normalization - Rewrite README with didactic focus Total: 1836 → 1236 lines (-33%)
- Local paths are converted to file:// URLs on startup - file:// URLs must be in the initial list (strict validation) - Dynamic URLs still restricted to arxiv.org only - Updated README with local file examples
- Add logging to selectionchange handler to verify it fires - Add fallback matching without spaces (TextLayer spans may lack spaces) - Log selection detection success/failure for debugging The issue: PDF.js TextLayer renders text as positioned spans without space characters between them. When selecting across spans: - pageText has spaces (items joined with ' ') - sel.toString() may not have spaces - indexOf fails to match The fix tries exact match first, then falls back to spaceless matching.
Model context now looks like: ```markdown --- url: https://arxiv.org/pdf/... page: 5/144 --- Page text with <pdf-selection>selected text</pdf-selection> inline. ``` This is cleaner for the model to parse and includes the source URL.
Added two well-designed helpers:
formatPageContent(text, maxLength, selection?)
- Centers truncation window around selection if present
- Adds <truncated-content/> markers at elision points
- Wraps selection in <pdf-selection> tags
- Allocates 60% context before, 40% after for readability
findSelectionInText(pageText, selectedText)
- Tries exact match first
- Falls back to spaceless match for TextLayer quirks
- Returns { start, end } or undefined
Example output with selection:
```
<truncated-content/>
...context before... <pdf-selection>selected text</pdf-selection> ...context after...
<truncated-content/>
```
When selection is too large for the budget: <truncated-content/><pdf-selection><truncated-content/>start...end<truncated-content/></pdf-selection><truncated-content/> This keeps the selection structure intact while showing beginning and end.
…r as default - Remove read_pdf_text tool (viewer extracts text client-side with pdfjs) - Remove PdfTextChunk and ReadPdfTextInput types - Remove loadPdfTextChunk from pdf-loader - Change default PDF to 'Attention Is All You Need' (1706.03762) - Update README with modest language
…isplay_pdf Major simplifications: - Use URL directly as identifier (no hashing) - Remove displayName - show elided URL with full URL as tooltip - Rename view_pdf to display_pdf with better description - Update all references from pdfId to url - Simplify storage key and model context The tool description now explains it displays an interactive viewer in the chat.
arxiv.org/abs/... -> arxiv.org/pdf/... Applied both at startup and when loading dynamic URLs.
Account for devicePixelRatio when rendering canvas: - Scale canvas dimensions by dpr - Scale context by dpr - Keep CSS size at logical pixels
9c5eb10 to
6008f60
Compare
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-map
@modelcontextprotocol/server-pdf
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-shadertoy
@modelcontextprotocol/server-sheet-music
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-transcript
@modelcontextprotocol/server-video-resource
@modelcontextprotocol/server-wiki-explorer
commit: |
jonathanhefner
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like all the E2E screenshots are being regenerated without any masking. Is that issue particular to this branch, or coming from another base branch, or an existing issue in main?
@jonathanhefner The screenshots w/o masking (and their cropped / resized cell derivatives) are new (#253), used for the top-level gallery and the readmes of most servers. The e2e test goldens are still w/ masks |
examples/pdf-server/server.ts
Outdated
| content: [ | ||
| { | ||
| type: "text", | ||
| text: `Viewing ${entry.url} (${entry.metadata.pageCount} pages)`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe makes sense to expand this a bit to better explain situation to the agent (otherwise it may go :
"Displaying a widget with interactive PDF viewer to the user. Viewing ${entry.url} (${entry.metadata.pageCount} pages)"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the tool description already sets the stage for what's happening (Claude seems to understand) but I'll rework this too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
text: `Displaying interactive PDF viewer${entry.metadata.title ? ` for "${entry.metadata.title}"` : ""} (${entry.url}, ${entry.metadata.pageCount} pages)`,
Ah, I see. But why are they being replaced in this PR? Is that a side effect of adding a new screenshot (for a new example) to the gallery? |
@jonathanhefner No good reason (reverted all but pdf-server), the ones w/ masked date seem to vary in length (maybe day of week changes it?), I'll look at regularizing that in a follow up. |
Fixes 'PDF not found' error when server restarts between display_pdf (which adds the entry) and read_pdf_bytes (which previously only looked up existing entries). Now read_pdf_bytes mirrors display_pdf's logic and dynamically adds arxiv URLs to the index.
Summary
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).
MCP Apps Patterns Demonstrated
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:
2. Model Context Updates
The viewer keeps the model informed about what the user sees:
3. Display Modes
sendSizeChanged()to fit contentrequestDisplayMode(), with internal scrolling when zoomed4. External Links
Opens source URLs via
app.openLink()5. App-Only Tools
read_pdf_bytesis hidden from the model (visibility: ["app"]), used only by the viewer UI.Architecture
Usage
Security: Dynamic URLs via
display_pdfare restricted to arxiv.org. Local files must be in the initial list.Tools
list_pdfsdisplay_pdfread_pdf_bytes