fix: misc fixes for pdf-server, map-server, and say-server (#329)
* 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
This commit is contained in:
parent
4fc71656a3
commit
023f1fd233
1 changed files with 452 additions and 495 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||

|
||||
|
||||
A simple interactive PDF viewer that uses [PDF.js](https://mozilla.github.io/pdf.js/). Launch it w/ a few PDF files and/or URLs as CLI args (+ support loading any additional pdf from arxiv.org).
|
||||
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
|
||||
|
||||
|
|
@ -29,20 +29,14 @@ Add to your MCP client configuration (stdio transport):
|
|||
|
||||
### 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:
|
||||
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** (`pdf-loader.ts`):
|
||||
**Server side** (`server.ts`):
|
||||
|
||||
```typescript
|
||||
// Returns chunks with pagination metadata
|
||||
async function loadPdfBytesChunk(entry, offset, byteCount) {
|
||||
return {
|
||||
bytes: base64Chunk,
|
||||
offset,
|
||||
byteCount,
|
||||
totalBytes,
|
||||
hasMore: offset + byteCount < totalBytes,
|
||||
};
|
||||
{
|
||||
(bytes, offset, byteCount, totalBytes, hasMore);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -51,7 +45,7 @@ async function loadPdfBytesChunk(entry, offset, byteCount) {
|
|||
```typescript
|
||||
// Load in chunks with progress
|
||||
while (hasMore) {
|
||||
const chunk = await app.callServerTool("read_pdf_bytes", { pdfId, offset });
|
||||
const chunk = await app.callServerTool("read_pdf_bytes", { url, offset });
|
||||
chunks.push(base64ToBytes(chunk.bytes));
|
||||
offset += chunk.byteCount;
|
||||
hasMore = chunk.hasMore;
|
||||
|
|
@ -65,13 +59,12 @@ The viewer keeps the model informed about what the user is seeing:
|
|||
|
||||
```typescript
|
||||
app.updateModelContext({
|
||||
structuredContent: {
|
||||
title: pdfTitle,
|
||||
currentPage,
|
||||
totalPages,
|
||||
pageText: pageText.slice(0, 5000),
|
||||
selection: selectedText ? { text, start, end } : undefined,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `PDF viewer | "${title}" | Current Page: ${page}/${total}\n\nPage content:\n${pageText}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -101,58 +94,75 @@ 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:
|
||||
|
||||
```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/server.ts
|
||||
bun examples/pdf-server/main.ts
|
||||
|
||||
# Load local files (converted to file:// URLs)
|
||||
bun examples/pdf-server/server.ts ./docs/paper.pdf /path/to/thesis.pdf
|
||||
bun examples/pdf-server/main.ts ./docs/paper.pdf /path/to/thesis.pdf
|
||||
|
||||
# Load from URLs
|
||||
bun examples/pdf-server/server.ts https://arxiv.org/pdf/2401.00001.pdf
|
||||
bun examples/pdf-server/main.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
|
||||
bun examples/pdf-server/main.ts ./local.pdf https://arxiv.org/pdf/2401.00001.pdf
|
||||
|
||||
# stdio mode for MCP clients
|
||||
bun examples/pdf-server/server.ts --stdio ./papers/
|
||||
bun examples/pdf-server/main.ts --stdio ./papers/
|
||||
```
|
||||
|
||||
**Security**: Dynamic URLs (via `view_pdf` tool) are restricted to arxiv.org. Local files must be in the initial list.
|
||||
## 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 indexed PDFs |
|
||||
| `display_pdf` | Model + UI | Display interactive viewer in chat |
|
||||
| `read_pdf_bytes` | App only | Chunked binary loading |
|
||||
| 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 (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
|
||||
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()` |
|
||||
| Size negotiation | `app.sendSizeChanged()` |
|
||||
| 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
|
||||
- `pdfjs-dist`: PDF rendering (frontend only)
|
||||
- `@modelcontextprotocol/ext-apps`: MCP Apps SDK
|
||||
|
|
|
|||
Loading…
Reference in a new issue