fix(pdf-server): npx DOMMatrix crash + broken MCPB bundle (#584)

* fix(pdf-server): stub DOMMatrix/ImageData/Path2D for npx without @napi-rs/canvas

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.

* fix(pdf-server): MCPB bundle missing runtime deps + display_name with spaces

`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.

* fix(pdf-server): list interact in MCPB manifest tools (was stale since #491)
This commit is contained in:
Olivier Chafik 2026-04-02 04:16:33 +01:00 • committed by GitHub
commit 04fed524a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 106 additions and 14 deletions

View file

@ -103,7 +103,15 @@ jobs:
- name: Build MCPB bundle (pdf-server) - name: Build MCPB bundle (pdf-server)
if: runner.os == 'Linux' && matrix.name == 'Linux x64' 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 working-directory: examples/pdf-server
e2e: e2e:

View file

@ -159,12 +159,8 @@ jobs:
cache: npm cache: npm
- run: npm ci - run: npm ci
- name: Build pdf-server - name: Build pdf-server + MCPB bundle
run: npm run build --workspace examples/pdf-server run: npm run build:mcpb --workspace examples/pdf-server
- name: Pack MCPB bundle
run: npx -y @anthropic-ai/mcpb pack
working-directory: examples/pdf-server
- name: Upload MCPB to release - name: Upload MCPB to release
run: gh release upload "${{ github.event.release.tag_name }}" examples/pdf-server/*.mcpb --clobber run: gh release upload "${{ github.event.release.tag_name }}" examples/pdf-server/*.mcpb --clobber

4
.gitignore vendored
View file

@ -15,4 +15,6 @@ __pycache__/
*.pyc *.pyc
# MCPB bundles (built artifacts) # MCPB bundles (built artifacts)
*.mcpb *.mcpb
.mcpb-stage/
.mcpb-smoke/

View file

@ -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)}`);

View file

@ -1,7 +1,7 @@
{ {
"manifest_version": "0.3", "manifest_version": "0.3",
"name": "@modelcontextprotocol/server-pdf", "name": "@modelcontextprotocol/server-pdf",
"display_name": "PDF (By Anthropic)", "display_name": "pdf-viewer",
"version": "1.0.1", "version": "1.0.1",
"description": "Read and interact with PDF files using an interactive viewer with search, navigation, and text extraction", "description": "Read and interact with PDF files using an interactive viewer with search, navigation, and text extraction",
"author": { "author": {
@ -34,8 +34,8 @@
"description": "Display an interactive PDF viewer with search and navigation" "description": "Display an interactive PDF viewer with search and navigation"
}, },
{ {
"name": "read_pdf_bytes", "name": "interact",
"description": "Read a range of bytes from a PDF file (max 512KB per request)" "description": "Interact with a PDF viewer: annotate, navigate, search, extract text/screenshots, fill forms"
} }
], ],
"keywords": ["pdf", "documents", "viewer", "reading"], "keywords": ["pdf", "documents", "viewer", "reading"],

View file

@ -14,14 +14,15 @@
"dist" "dist"
], ],
"scripts": { "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", "watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun --watch main.ts --enable-interact", "serve": "bun --watch main.ts --enable-interact",
"serve:stdio": "bun main.ts --stdio", "serve:stdio": "bun main.ts --stdio",
"start": "cross-env NODE_ENV=development npm run build && npm run serve", "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", "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\"", "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": { "dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/ext-apps": "^1.0.0",

View file

@ -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 {};

View file

@ -25,7 +25,10 @@ import {
type CallToolResult, type CallToolResult,
type ReadResourceResult, type ReadResourceResult,
} from "@modelcontextprotocol/sdk/types.js"; } 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 { import {
getDocument, getDocument,
VerbosityLevel, VerbosityLevel,