Skip to content
Open
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
2 changes: 2 additions & 0 deletions examples/pdf-server/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
dist/
*.jsonl
.vercel
.env*.local
5 changes: 5 additions & 0 deletions examples/pdf-server/.vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
src
*.jsonl
*.test.ts
.vercel
70 changes: 70 additions & 0 deletions examples/pdf-server/api/mcp.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Polyfill DOMMatrix/ImageData/Path2D before pdfjs-dist loads.
// Uses dynamic import() so polyfills execute before pdfjs-dist initializes.
if (typeof globalThis.DOMMatrix === "undefined") {
globalThis.DOMMatrix = class DOMMatrix {
constructor(init) {
this.a = 1;
this.b = 0;
this.c = 0;
this.d = 1;
this.e = 0;
this.f = 0;
if (Array.isArray(init))
[this.a, this.b, this.c, this.d, this.e, this.f] = init;
}
get isIdentity() {
return (
this.a === 1 &&
this.b === 0 &&
this.c === 0 &&
this.d === 1 &&
this.e === 0 &&
this.f === 0
);
}
translate() {
return new DOMMatrix();
}
scale() {
return new DOMMatrix();
}
inverse() {
return new DOMMatrix();
}
multiply() {
return new DOMMatrix();
}
transformPoint(p) {
return p ?? { x: 0, y: 0 };
}
static fromMatrix() {
return new DOMMatrix();
}
};
}
if (typeof globalThis.ImageData === "undefined") {
globalThis.ImageData = class ImageData {
constructor(w, h) {
this.width = w;
this.height = h;
this.data = new Uint8ClampedArray(w * h * 4);
}
};
}
if (typeof globalThis.Path2D === "undefined") {
globalThis.Path2D = class Path2D {
moveTo() {}
lineTo() {}
bezierCurveTo() {}
quadraticCurveTo() {}
arc() {}
arcTo() {}
ellipse() {}
rect() {}
closePath() {}
};
}

// Dynamic import so polyfills above execute first.
const { default: handler } = await import("../dist/http.js");
export default handler;
94 changes: 94 additions & 0 deletions examples/pdf-server/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Vercel serverless handler for the PDF MCP server.
*
* Stateless: each request creates a fresh MCP server instance.
* The CommandQueue persists state across requests via Redis (Upstash).
*
* Deploy: vercel deploy --prod
* Env vars: UPSTASH_REDIS_REST_URL + TOKEN, or KV_REST_API_URL + TOKEN
*/

// Must be first import — pdfjs-dist checks for DOMMatrix at module init.
import "./serverless-polyfills.js";

import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import type { IncomingMessage, ServerResponse } from "node:http";
import { createServer } from "./server.js";

type Req = IncomingMessage & { body?: unknown };
type Res = ServerResponse;

function setCors(res: Res): void {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Accept, Mcp-Session-Id",
);
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
}

export default async function handler(req: Req, res: Res): Promise<void> {
setCors(res);

if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}

if (req.method === "DELETE") {
res.writeHead(200);
res.end();
return;
}

const url = new URL(
req.url ?? "/",
`http://${req.headers.host ?? "localhost"}`,
);

if (url.pathname !== "/mcp" && url.pathname !== "/api/mcp") {
const redisUrl =
process.env.UPSTASH_REDIS_REST_URL ?? process.env.KV_REST_API_URL;
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(
`PDF MCP Server\n\nMCP endpoint: ${url.origin}/mcp\n` +
`Redis: ${redisUrl ? "configured" : "not configured (in-memory)"}`,
);
return;
}

// Stateless: fresh server + transport per request.
// The interact tool + command queue require Redis for cross-request state.
// Without Redis, only read-only tools (list_pdfs, display_pdf) are exposed.
const hasRedis = !!(
process.env.UPSTASH_REDIS_REST_URL ?? process.env.KV_REST_API_URL
);
const server = createServer({ enableInteract: hasRedis });
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless — no sessions needed
});

res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});

try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
}),
);
}
}
}
2 changes: 1 addition & 1 deletion examples/pdf-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"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 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\" && bun build http.ts --outfile dist/http.js --target node --external pdfjs-dist",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun --watch main.ts --enable-interact",
"serve:stdio": "bun main.ts --stdio",
Expand Down
Loading
Loading