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:
Olivier Chafik 2026-01-23 12:35:07 +00:00 • committed by GitHub
commit 023f1fd233
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -6,15 +6,20 @@
* - Text selection via PDF.js TextLayer
* - Page navigation, zoom
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import {
App,
type McpUiHostContext,
applyDocumentTheme,
applyHostStyleVariables,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import * as pdfjsLib from "pdfjs-dist";
import { TextLayer } from "pdfjs-dist";
import "./global.css";
import "./mcp-app.css";
// const MAX_MODEL_CONTEXT_LENGTH = 5000;
const MAX_MODEL_CONTEXT_LENGTH = 1500;
const MAX_MODEL_CONTEXT_LENGTH = 15000;
const CHUNK_SIZE = 500 * 1024; // 500KB chunks
// Configure PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@ -247,7 +252,7 @@ function findSelectionInText(
return undefined;
}
// Extract text from current page and update model context as markdown
// Extract text from current page and update model context
async function updatePageContext() {
if (!pdfDocument) return;
@ -276,22 +281,24 @@ async function updatePageContext() {
);
}
// Format content with selection and truncation
// Format content with selection markers and truncation
const content = formatPageContent(
pageText,
MAX_MODEL_CONTEXT_LENGTH,
selection,
);
const markdown = `---
title: ${pdfTitle || ""}
url: ${pdfUrl}
current-page: ${currentPage}/${totalPages}
---
// Build context with tool ID for multi-tool disambiguation
const toolId = app.getHostContext()?.toolInfo?.id;
const header = [
`PDF viewer${toolId ? ` (${toolId})` : ""}`,
pdfTitle ? `"${pdfTitle}"` : pdfUrl,
`Current Page: ${currentPage}/${totalPages}`,
].join(" | ");
${content}`;
const contextText = `${header}\n\nPage content:\n${content}`;
app.updateModelContext({ content: [{ type: "text", text: markdown }] });
app.updateModelContext({ content: [{ type: "text", text: contextText }] });
} catch (err) {
log.error("Error updating context:", err);
}
@ -635,7 +642,6 @@ function updateProgress(loaded: number, total: number) {
// Load PDF in chunks with progress
async function loadPdfInChunks(urlToLoad: string): Promise<Uint8Array> {
const CHUNK_SIZE = 500 * 1024; // 500KB chunks
const chunks: Uint8Array[] = [];
let offset = 0;
let totalBytes = 0;
@ -750,6 +756,16 @@ app.onerror = (err) => {
function handleHostContextChanged(ctx: McpUiHostContext) {
log.info("Host context changed:", ctx);
// Apply theme from host
if (ctx.theme) {
applyDocumentTheme(ctx.theme);
}
// Apply host CSS variables
if (ctx.styles?.variables) {
applyHostStyleVariables(ctx.styles.variables);
}
// Apply safe area insets
if (ctx.safeAreaInsets) {
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;