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:
parent
68c58b60b7
commit
04fed524a9
8 changed files with 106 additions and 14 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
8
.github/workflows/npm-publish.yml
vendored
8
.github/workflows/npm-publish.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -16,3 +16,5 @@ __pycache__/
|
|||
|
||||
# MCPB bundles (built artifacts)
|
||||
*.mcpb
|
||||
.mcpb-stage/
|
||||
.mcpb-smoke/
|
||||
56
examples/pdf-server/build-mcpb.mjs
Normal file
56
examples/pdf-server/build-mcpb.mjs
Normal 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)}`);
|
||||
|
|
@ -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": {
|
||||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -14,14 +14,15 @@
|
|||
"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",
|
||||
"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",
|
||||
|
|
|
|||
26
examples/pdf-server/pdfjs-polyfill.ts
Normal file
26
examples/pdf-server/pdfjs-polyfill.ts
Normal 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 {};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue