Skip to content

Commit 04fed52

Browse files
authored
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)
1 parent 68c58b6 commit 04fed52

8 files changed

Lines changed: 106 additions & 14 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,15 @@ jobs:
103103

104104
- name: Build MCPB bundle (pdf-server)
105105
if: runner.os == 'Linux' && matrix.name == 'Linux x64'
106-
run: npx -y @anthropic-ai/mcpb pack
106+
run: node build-mcpb.mjs
107+
working-directory: examples/pdf-server
108+
109+
- name: Smoke-test MCPB bundle starts
110+
if: runner.os == 'Linux' && matrix.name == 'Linux x64'
111+
run: |
112+
unzip -q pdf-server.mcpb -d .mcpb-smoke
113+
node .mcpb-smoke/dist/index.js --stdio < /dev/null 2>&1 | tee /tmp/mcpb-smoke.log
114+
grep -q "Ready" /tmp/mcpb-smoke.log
107115
working-directory: examples/pdf-server
108116

109117
e2e:

.github/workflows/npm-publish.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,8 @@ jobs:
159159
cache: npm
160160
- run: npm ci
161161

162-
- name: Build pdf-server
163-
run: npm run build --workspace examples/pdf-server
164-
165-
- name: Pack MCPB bundle
166-
run: npx -y @anthropic-ai/mcpb pack
167-
working-directory: examples/pdf-server
162+
- name: Build pdf-server + MCPB bundle
163+
run: npm run build:mcpb --workspace examples/pdf-server
168164

169165
- name: Upload MCPB to release
170166
run: gh release upload "${{ github.event.release.tag_name }}" examples/pdf-server/*.mcpb --clobber

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ __pycache__/
1515
*.pyc
1616

1717
# MCPB bundles (built artifacts)
18-
*.mcpb
18+
*.mcpb
19+
.mcpb-stage/
20+
.mcpb-smoke/

examples/pdf-server/build-mcpb.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Build a self-contained .mcpb bundle for pdf-server.
4+
*
5+
* `mcpb pack` zips whatever is on disk; in this monorepo all runtime deps
6+
* are hoisted to the root node_modules, so packing in-place produces a
7+
* bundle with no pdfjs-dist/ajv/etc. that crashes in Claude Desktop.
8+
*
9+
* This script stages dist/ + manifest into a clean temp dir, runs a fresh
10+
* non-workspace `npm install --omit=dev --omit=optional` (the polyfill in
11+
* dist/pdfjs-polyfill.js makes @napi-rs/canvas's ~130MB of native binaries
12+
* unnecessary), syncs the manifest version to package.json, then packs.
13+
*/
14+
15+
import {
16+
cpSync,
17+
rmSync,
18+
mkdirSync,
19+
readFileSync,
20+
writeFileSync,
21+
} from "node:fs";
22+
import { execSync } from "node:child_process";
23+
import { fileURLToPath } from "node:url";
24+
import path from "node:path";
25+
26+
const here = path.dirname(fileURLToPath(import.meta.url));
27+
const stage = path.join(here, ".mcpb-stage");
28+
const out = path.join(here, "pdf-server.mcpb");
29+
30+
const pkg = JSON.parse(readFileSync(path.join(here, "package.json"), "utf8"));
31+
const manifest = JSON.parse(
32+
readFileSync(path.join(here, "manifest.json"), "utf8"),
33+
);
34+
manifest.version = pkg.version;
35+
36+
rmSync(stage, { recursive: true, force: true });
37+
mkdirSync(stage);
38+
39+
for (const f of ["dist", "icon.png", "README.md", ".mcpbignore"]) {
40+
cpSync(path.join(here, f), path.join(stage, f), { recursive: true });
41+
}
42+
writeFileSync(
43+
path.join(stage, "manifest.json"),
44+
JSON.stringify(manifest, null, 2),
45+
);
46+
writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2));
47+
48+
const run = (cmd) => execSync(cmd, { cwd: stage, stdio: "inherit" });
49+
run(
50+
"npm install --omit=dev --omit=optional --no-audit --no-fund --no-package-lock " +
51+
"--registry=https://registry.npmjs.org/",
52+
);
53+
run(`npx -y @anthropic-ai/mcpb pack . ${JSON.stringify(out)}`);
54+
55+
rmSync(stage, { recursive: true, force: true });
56+
console.log(`\n✅ ${path.relative(process.cwd(), out)}`);

examples/pdf-server/manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": "0.3",
33
"name": "@modelcontextprotocol/server-pdf",
4-
"display_name": "PDF (By Anthropic)",
4+
"display_name": "pdf-viewer",
55
"version": "1.0.1",
66
"description": "Read and interact with PDF files using an interactive viewer with search, navigation, and text extraction",
77
"author": {
@@ -34,8 +34,8 @@
3434
"description": "Display an interactive PDF viewer with search and navigation"
3535
},
3636
{
37-
"name": "read_pdf_bytes",
38-
"description": "Read a range of bytes from a PDF file (max 512KB per request)"
37+
"name": "interact",
38+
"description": "Interact with a PDF viewer: annotate, navigate, search, extract text/screenshots, fill forms"
3939
}
4040
],
4141
"keywords": ["pdf", "documents", "viewer", "reading"],

examples/pdf-server/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
"dist"
1515
],
1616
"scripts": {
17-
"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\"",
17+
"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\"",
1818
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
1919
"serve": "bun --watch main.ts --enable-interact",
2020
"serve:stdio": "bun main.ts --stdio",
2121
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
2222
"start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio",
2323
"dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"",
24-
"prepublishOnly": "npm run build"
24+
"prepublishOnly": "npm run build",
25+
"build:mcpb": "npm run build && node build-mcpb.mjs"
2526
},
2627
"dependencies": {
2728
"@modelcontextprotocol/ext-apps": "^1.0.0",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Stub polyfills for pdfjs-dist's legacy build under Node.js.
3+
*
4+
* pdfjs-dist/legacy/build/pdf.mjs eagerly does `new DOMMatrix()` at module
5+
* scope. Its own polyfill loads `@napi-rs/canvas` (an optionalDependency),
6+
* but `npx` / fresh installs frequently miss the platform-native binary
7+
* (npm optional-deps bug, see https://github.com/npm/cli/issues/4828),
8+
* so the import crashes before our server code runs.
9+
*
10+
* The server only uses pdfjs for text/metadata/form-field extraction —
11+
* never canvas rendering — so no-op stubs are sufficient and avoid
12+
* shipping ~130MB of native binaries we don't need.
13+
*
14+
* MUST be a separate ESM module imported before pdfjs-dist (and kept
15+
* `--external` in the bun bundle) so it executes before pdfjs's
16+
* module-level initializer. Inlined body code would run too late
17+
* because static imports are hoisted.
18+
*/
19+
20+
/* eslint-disable @typescript-eslint/no-explicit-any */
21+
const g = globalThis as any;
22+
23+
// Only stub if missing — let a real @napi-rs/canvas (or jsdom) win.
24+
g.DOMMatrix ??= class DOMMatrix {};
25+
g.ImageData ??= class ImageData {};
26+
g.Path2D ??= class Path2D {};

examples/pdf-server/server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ import {
2525
type CallToolResult,
2626
type ReadResourceResult,
2727
} from "@modelcontextprotocol/sdk/types.js";
28-
// Use the legacy build to avoid DOMMatrix dependency in Node.js
28+
// Stub DOMMatrix/ImageData/Path2D before pdfjs-dist loads — its legacy
29+
// build instantiates DOMMatrix at module scope and the @napi-rs/canvas
30+
// polyfill is unreliable under npx. See ./pdfjs-polyfill.ts for details.
31+
import "./pdfjs-polyfill.js";
2932
import {
3033
getDocument,
3134
VerbosityLevel,

0 commit comments

Comments
 (0)