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)
|
- 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:
|
||||||
|
|
|
||||||
8
.github/workflows/npm-publish.yml
vendored
8
.github/workflows/npm-publish.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -16,3 +16,5 @@ __pycache__/
|
||||||
|
|
||||||
# MCPB bundles (built artifacts)
|
# MCPB bundles (built artifacts)
|
||||||
*.mcpb
|
*.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",
|
"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"],
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
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 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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue