Migrate to MCP SDK 2.0 with official Protocol and isolated role peers (#720)

App and AppBridge extend the public Protocol from @modelcontextprotocol/client; the iframe wire protocol (ui/initialize only) is unchanged. Peers move to the split SDK 2.0 packages with client and server as optional role peers. Fixups (lockfile, peer ranges, restored handler guards, docs, wire-compat tests) follow in a separate PR.

Co-authored-by: Andrew Khadder <andrew@manufact.com>
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
Closes #702
This commit is contained in:
Olivier Chafik 2026-09-08 12:44:49 +01:00 • committed by GitHub
commit 1a713173cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 2415 additions and 1867 deletions

View file

@ -6,10 +6,10 @@
import fs from "node:fs";
import path from "node:path";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import type { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import {
@ -39,7 +39,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});

View file

@ -26,8 +26,11 @@
},
"dependencies": {
"@cantoo/pdf-lib": "^2.6.5",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/ext-apps": "^1.7.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/express": "2.0.0",
"@modelcontextprotocol/node": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"pdfjs-dist": "^5.0.0",

View file

@ -2,8 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
import { PDFDocument } from "@cantoo/pdf-lib";
import { makeRandomJpeg } from "../../tests/helpers/range-counting-server";

View file

@ -13,18 +13,19 @@
import { randomUUID } from "crypto";
import fs from "node:fs";
import path from "node:path";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
registerAppResource,
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import {
RootsListChangedNotificationSchema,
McpServer,
type CallToolResult,
type ElicitResult,
type PrimitiveSchemaDefinition,
type ReadResourceResult,
} from "@modelcontextprotocol/sdk/types.js";
type Server,
} from "@modelcontextprotocol/server";
// 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.
@ -62,10 +63,6 @@ class FetchStandardFontDataFactory {
return new Uint8Array(await res.arrayBuffer());
}
}
import type {
PrimitiveSchemaDefinition,
ElicitResult,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// =============================================================================
@ -1282,7 +1279,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
refreshRoots(server.server);
};
server.server.setNotificationHandler(
RootsListChangedNotificationSchema,
"notifications/roots/list_changed",
async () => {
await refreshRoots(server.server);
},
@ -1292,10 +1289,12 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
const { readPdfRange } = sharedPdfCache;
// Tool: list_pdfs - List available PDFs
server.tool(
server.registerTool(
"list_pdfs",
"List available PDFs that can be displayed",
{},
{
description: "List available PDFs that can be displayed",
inputSchema: z.object({}),
},
async (): Promise<CallToolResult> => {
const seen = new Set<string>();
const localFiles: string[] = [];
@ -1377,7 +1376,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
title: "Read PDF Bytes",
description:
"Read a range of bytes from a PDF (max 512KB per request). The model should NOT call this tool directly.",
inputSchema: {
inputSchema: z.object({
url: z.string().describe("PDF URL or local file path"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
@ -1386,7 +1385,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
.max(MAX_CHUNK_BYTES)
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
},
}),
outputSchema: z.object({
url: z.string(),
bytes: z.string().describe("Base64 encoded bytes"),
@ -1473,7 +1472,7 @@ Returns a viewUUID in structuredContent. Pass it to \`interact\`:
Accepts local files (use list_pdfs), client MCP root directories, or any HTTPS URL.
Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before display.`,
inputSchema: {
inputSchema: z.object({
url: z
.string()
.default(DEFAULT_PDF)
@ -1489,7 +1488,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before
"If true and the PDF has form fields, prompt the user to fill them before displaying",
),
}),
},
}),
outputSchema: z.object({
viewUUID: z
.string()
@ -2426,7 +2425,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
**FORMS** — fill_form: fill fields with \`fields\` array of {name, value}.
**SAVE** — save_as: write the annotated PDF (annotations + form values) to a file. Pass \`path\` (absolute path or file://) for a new location, or omit \`path\` to overwrite the original. Set \`overwrite: true\` to replace an existing file (always required when omitting \`path\`).`,
inputSchema: {
inputSchema: z.object({
viewUUID: z
.string()
.describe(
@ -2523,7 +2522,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
.describe(
"Array of commands to execute sequentially. More efficient than separate calls. Tip: end with get_pages+getScreenshots to verify changes.",
),
},
}),
},
async (
{
@ -2600,7 +2599,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
const result = await processInteractCommand(
uuid,
commandList[i],
extra.signal,
extra.mcpReq.signal,
);
if (result.isError) {
const errText = result.content
@ -2657,7 +2656,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Submit Page Data",
description:
"Submit rendered page data for a get_pages request (used by viewer). The model should NOT call this tool directly.",
inputSchema: {
inputSchema: z.object({
requestId: z
.string()
.describe("The request ID from the get_pages command"),
@ -2670,7 +2669,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
}),
)
.describe("Page data entries"),
},
}),
_meta: { ui: { visibility: ["app"] } },
},
async ({ requestId, pages }): Promise<CallToolResult> => {
@ -2700,7 +2699,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Submit Save Data",
description:
"Submit annotated PDF bytes for a save_as request (used by viewer). The model should NOT call this tool directly.",
inputSchema: {
inputSchema: z.object({
requestId: z
.string()
.describe("The request ID from the save_as command"),
@ -2709,7 +2708,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
.string()
.optional()
.describe("Error message if the viewer failed to build bytes"),
},
}),
_meta: { ui: { visibility: ["app"] } },
},
async ({ requestId, data, error }): Promise<CallToolResult> => {
@ -2739,7 +2738,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Submit Viewer State",
description:
"Submit a viewer-state snapshot for a get_viewer_state request (used by viewer). The model should NOT call this tool directly.",
inputSchema: {
inputSchema: z.object({
requestId: z
.string()
.describe("The request ID from the get_viewer_state command"),
@ -2751,7 +2750,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
.string()
.optional()
.describe("Error message if the viewer failed to read state"),
},
}),
_meta: { ui: { visibility: ["app"] } },
},
async ({ requestId, state, error }): Promise<CallToolResult> => {
@ -2781,9 +2780,9 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Poll PDF Commands",
description:
"Poll for pending commands for a PDF viewer. The model should NOT call this tool directly.",
inputSchema: {
inputSchema: z.object({
viewUUID: z.string().describe("The viewUUID of the PDF viewer"),
},
}),
_meta: { ui: { visibility: ["app"] } },
},
async ({ viewUUID: uuid }): Promise<CallToolResult> => {
@ -2828,10 +2827,10 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Save PDF",
description:
"Save annotated PDF bytes back to a local file. The model should NOT call this tool directly — use interact with action: save_as instead.",
inputSchema: {
inputSchema: z.object({
url: z.string().describe("Original PDF URL or local file path"),
data: z.string().describe("Base64-encoded PDF bytes"),
},
}),
outputSchema: z.object({
filePath: z.string(),
mtimeMs: z.number(),

View file

@ -12,8 +12,10 @@ import {
applyDocumentTheme,
applyHostStyleVariables,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js";
import type {
CallToolResult,
ContentBlock,
} from "@modelcontextprotocol/client";
import { z } from "zod";
import * as pdfjsLib from "pdfjs-dist";
import { AnnotationLayer, AnnotationMode, TextLayer } from "pdfjs-dist";