Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 2 additions & 6 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ __pycache__/
*.pyc

# MCPB bundles (built artifacts)
*.mcpb
*.mcpb
.mcpb-stage/
.mcpb-smoke/
56 changes: 56 additions & 0 deletions examples/pdf-server/build-mcpb.mjs
Original file line number Diff line number Diff line change
@@ -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)}`);
6 changes: 3 additions & 3 deletions examples/pdf-server/manifest.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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"],
Expand Down
5 changes: 3 additions & 2 deletions examples/pdf-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions examples/pdf-server/pdfjs-polyfill.ts
Original file line number Diff line number Diff line change
@@ -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 {};
5 changes: 4 additions & 1 deletion examples/pdf-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading