From 13c8c799cb7f106b23b75aebdb2dc8cb553c6f21 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Apr 2026 22:52:43 -0400 Subject: [PATCH 1/3] fix(pdf-server): stub DOMMatrix/ImageData/Path2D for npx without @napi-rs/canvas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pdfjs-dist/legacy/build/pdf.mjs does `new DOMMatrix()` at module scope. Its own polyfill loads @napi-rs/canvas (an optionalDependency), but npx frequently misses the platform-native binary (npm/cli#4828), so the server crashed on `npx @modelcontextprotocol/server-pdf --stdio`: ReferenceError: DOMMatrix is not defined at pdfjs-dist/legacy/build/pdf.mjs:17078 The server only uses pdfjs for text/metadata/form-field extraction — never canvas rendering — so no-op stubs suffice and avoid shipping ~130MB of native binaries. The polyfill is a separate ESM module kept --external in the bun bundle so it executes before pdfjs's hoisted static import; inlined body code would run too late. Broken since 1.3.0 (#506). Thanks to Bryan Thompson for the analysis and fix shape from the npm-to-mcpb pipeline. --- examples/pdf-server/package.json | 2 +- examples/pdf-server/pdfjs-polyfill.ts | 26 ++++++++++++++++++++++++++ examples/pdf-server/server.ts | 5 ++++- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 examples/pdf-server/pdfjs-polyfill.ts diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 520e2342..b1030554 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -14,7 +14,7 @@ "dist" ], "scripts": { - "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node --external pdfjs-dist && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --external pdfjs-dist --banner \"#!/usr/bin/env node\"", + "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build pdfjs-polyfill.ts --outdir dist --target node && bun build server.ts --outdir dist --target node --external pdfjs-dist --external \"./pdfjs-polyfill.js\" && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --external pdfjs-dist --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts --enable-interact", "serve:stdio": "bun main.ts --stdio", diff --git a/examples/pdf-server/pdfjs-polyfill.ts b/examples/pdf-server/pdfjs-polyfill.ts new file mode 100644 index 00000000..80e17056 --- /dev/null +++ b/examples/pdf-server/pdfjs-polyfill.ts @@ -0,0 +1,26 @@ +/** + * Stub polyfills for pdfjs-dist's legacy build under Node.js. + * + * pdfjs-dist/legacy/build/pdf.mjs eagerly does `new DOMMatrix()` at module + * scope. Its own polyfill loads `@napi-rs/canvas` (an optionalDependency), + * but `npx` / fresh installs frequently miss the platform-native binary + * (npm optional-deps bug, see https://github.com/npm/cli/issues/4828), + * so the import crashes before our server code runs. + * + * The server only uses pdfjs for text/metadata/form-field extraction — + * never canvas rendering — so no-op stubs are sufficient and avoid + * shipping ~130MB of native binaries we don't need. + * + * MUST be a separate ESM module imported before pdfjs-dist (and kept + * `--external` in the bun bundle) so it executes before pdfjs's + * module-level initializer. Inlined body code would run too late + * because static imports are hoisted. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +const g = globalThis as any; + +// Only stub if missing — let a real @napi-rs/canvas (or jsdom) win. +g.DOMMatrix ??= class DOMMatrix {}; +g.ImageData ??= class ImageData {}; +g.Path2D ??= class Path2D {}; diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index 3c6168bc..ba16431c 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -25,7 +25,10 @@ import { type CallToolResult, type ReadResourceResult, } from "@modelcontextprotocol/sdk/types.js"; -// Use the legacy build to avoid DOMMatrix dependency in Node.js +// Stub DOMMatrix/ImageData/Path2D before pdfjs-dist loads — its legacy +// build instantiates DOMMatrix at module scope and the @napi-rs/canvas +// polyfill is unreliable under npx. See ./pdfjs-polyfill.ts for details. +import "./pdfjs-polyfill.js"; import { getDocument, VerbosityLevel, From 677131938b2773c748014be79eb0da52d461a5c8 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Apr 2026 23:01:05 -0400 Subject: [PATCH 2/3] fix(pdf-server): MCPB bundle missing runtime deps + display_name with spaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mcpb pack` zips whatever is on disk; in this monorepo all runtime deps are hoisted to root node_modules, so the published .mcpb shipped without pdfjs-dist/ajv/express/etc. and crashed on import in Claude Desktop. build-mcpb.mjs stages dist/ + manifest into a clean temp dir, runs a non-workspace `npm install --omit=dev --omit=optional` (the new DOMMatrix polyfill makes @napi-rs/canvas's ~130MB of native binaries unnecessary), syncs manifest.version to package.json, then packs. Result: 12.3MB / 38.8MB unpacked, full MCP handshake + tools/list pass. Also rename display_name "PDF (By Anthropic)" → "pdf-viewer" — spaces in display_name break MCP App UI rendering for MCPBs in Claude Desktop. CI now smoke-tests the extracted bundle starts (`dist/index.js --stdio` prints "Ready") so a deps-less bundle can't ship again. --- .github/workflows/ci.yml | 10 +++++- .github/workflows/npm-publish.yml | 8 ++--- .gitignore | 4 ++- examples/pdf-server/build-mcpb.mjs | 56 ++++++++++++++++++++++++++++++ examples/pdf-server/manifest.json | 2 +- examples/pdf-server/package.json | 3 +- 6 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 examples/pdf-server/build-mcpb.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c227080f..fb419382 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,15 @@ jobs: - name: Build MCPB bundle (pdf-server) if: runner.os == 'Linux' && matrix.name == 'Linux x64' - run: npx -y @anthropic-ai/mcpb pack + run: node build-mcpb.mjs + working-directory: examples/pdf-server + + - name: Smoke-test MCPB bundle starts + if: runner.os == 'Linux' && matrix.name == 'Linux x64' + run: | + unzip -q pdf-server.mcpb -d .mcpb-smoke + node .mcpb-smoke/dist/index.js --stdio < /dev/null 2>&1 | tee /tmp/mcpb-smoke.log + grep -q "Ready" /tmp/mcpb-smoke.log working-directory: examples/pdf-server e2e: diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index d9695853..ece35d39 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -159,12 +159,8 @@ jobs: cache: npm - run: npm ci - - name: Build pdf-server - run: npm run build --workspace examples/pdf-server - - - name: Pack MCPB bundle - run: npx -y @anthropic-ai/mcpb pack - working-directory: examples/pdf-server + - name: Build pdf-server + MCPB bundle + run: npm run build:mcpb --workspace examples/pdf-server - name: Upload MCPB to release run: gh release upload "${{ github.event.release.tag_name }}" examples/pdf-server/*.mcpb --clobber diff --git a/.gitignore b/.gitignore index abb76600..5b5486bc 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ __pycache__/ *.pyc # MCPB bundles (built artifacts) -*.mcpb \ No newline at end of file +*.mcpb +.mcpb-stage/ +.mcpb-smoke/ \ No newline at end of file diff --git a/examples/pdf-server/build-mcpb.mjs b/examples/pdf-server/build-mcpb.mjs new file mode 100644 index 00000000..c1c34690 --- /dev/null +++ b/examples/pdf-server/build-mcpb.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node +/** + * Build a self-contained .mcpb bundle for pdf-server. + * + * `mcpb pack` zips whatever is on disk; in this monorepo all runtime deps + * are hoisted to the root node_modules, so packing in-place produces a + * bundle with no pdfjs-dist/ajv/etc. that crashes in Claude Desktop. + * + * This script stages dist/ + manifest into a clean temp dir, runs a fresh + * non-workspace `npm install --omit=dev --omit=optional` (the polyfill in + * dist/pdfjs-polyfill.js makes @napi-rs/canvas's ~130MB of native binaries + * unnecessary), syncs the manifest version to package.json, then packs. + */ + +import { + cpSync, + rmSync, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const stage = path.join(here, ".mcpb-stage"); +const out = path.join(here, "pdf-server.mcpb"); + +const pkg = JSON.parse(readFileSync(path.join(here, "package.json"), "utf8")); +const manifest = JSON.parse( + readFileSync(path.join(here, "manifest.json"), "utf8"), +); +manifest.version = pkg.version; + +rmSync(stage, { recursive: true, force: true }); +mkdirSync(stage); + +for (const f of ["dist", "icon.png", "README.md", ".mcpbignore"]) { + cpSync(path.join(here, f), path.join(stage, f), { recursive: true }); +} +writeFileSync( + path.join(stage, "manifest.json"), + JSON.stringify(manifest, null, 2), +); +writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2)); + +const run = (cmd) => execSync(cmd, { cwd: stage, stdio: "inherit" }); +run( + "npm install --omit=dev --omit=optional --no-audit --no-fund --no-package-lock " + + "--registry=https://registry.npmjs.org/", +); +run(`npx -y @anthropic-ai/mcpb pack . ${JSON.stringify(out)}`); + +rmSync(stage, { recursive: true, force: true }); +console.log(`\n✅ ${path.relative(process.cwd(), out)}`); diff --git a/examples/pdf-server/manifest.json b/examples/pdf-server/manifest.json index 47c6bbc7..204656a5 100644 --- a/examples/pdf-server/manifest.json +++ b/examples/pdf-server/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": "0.3", "name": "@modelcontextprotocol/server-pdf", - "display_name": "PDF (By Anthropic)", + "display_name": "pdf-viewer", "version": "1.0.1", "description": "Read and interact with PDF files using an interactive viewer with search, navigation, and text extraction", "author": { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index b1030554..1660ecf2 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -21,7 +21,8 @@ "start": "cross-env NODE_ENV=development npm run build && npm run serve", "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "build:mcpb": "npm run build && node build-mcpb.mjs" }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", From 3a89518013d3b9d54654b0e4226bbb48addc1a82 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 1 Apr 2026 23:04:36 -0400 Subject: [PATCH 3/3] fix(pdf-server): list interact in MCPB manifest tools (was stale since #491) --- examples/pdf-server/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pdf-server/manifest.json b/examples/pdf-server/manifest.json index 204656a5..f4b897ed 100644 --- a/examples/pdf-server/manifest.json +++ b/examples/pdf-server/manifest.json @@ -34,8 +34,8 @@ "description": "Display an interactive PDF viewer with search and navigation" }, { - "name": "read_pdf_bytes", - "description": "Read a range of bytes from a PDF file (max 512KB per request)" + "name": "interact", + "description": "Interact with a PDF viewer: annotate, navigate, search, extract text/screenshots, fill forms" } ], "keywords": ["pdf", "documents", "viewer", "reading"],