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
2 changed files with 2415 additions and 1867 deletions

View file

@ -23,10 +23,12 @@
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.7.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/node": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"jose": "^6.0.0"
"jose": "^6.0.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",

View file

@ -21,13 +21,13 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js";
import type {
CallToolResult,
ReadResourceResult,
} from "@modelcontextprotocol/sdk/types.js";
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import {
McpServer,
UrlElicitationRequiredError,
type CallToolResult,
type ReadResourceResult,
} from "@modelcontextprotocol/server";
import cors from "cors";
import express, { type Express, type Request, type Response } from "express";
import { SignJWT, jwtVerify } from "jose";
@ -35,6 +35,7 @@ import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js). Derived
// from import.meta.url rather than import.meta.filename/dirname, which are
@ -621,7 +622,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Show Auth Button",
description:
"Public tool: shows a button that triggers the protected get_secret tool.",
inputSchema: {},
inputSchema: z.object({}),
_meta: { ui: { resourceUri: buttonUri } },
},
async (): Promise<CallToolResult> => ({
@ -654,7 +655,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Get Secret",
description:
"Protected tool: returns secret data. Requires authentication.",
inputSchema: {},
inputSchema: z.object({}),
_meta: { ui: { resourceUri: secretUri } },
},
async (): Promise<CallToolResult> => {
@ -698,7 +699,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Revoke Auth Token",
description:
"Protected tool: revokes the caller's entire auth session (access + refresh token).",
inputSchema: {},
inputSchema: z.object({}),
},
async (): Promise<CallToolResult> => {
if (!authInfo) {
@ -736,7 +737,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Elicit URL (session-callback)",
description:
"URL elicitation via elicitInput. Blocks until the client accepts; sends ElicitCompleteNotification before returning.",
inputSchema: {},
inputSchema: z.object({}),
},
async (_args, extra): Promise<CallToolResult> => {
const eid = crypto.randomUUID();
@ -751,7 +752,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
message: "Please open this URL to continue.",
elicitationId: eid,
},
{ relatedRequestId: extra.requestId, timeout: 300_000 },
{ relatedRequestId: extra.mcpReq.id, timeout: 300_000 },
);
// Send completion notification before returning — lands on the same SSE
// stream as this tool response. For Streamable HTTP this is the ONLY
@ -783,7 +784,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Elicit by -32042 Error",
description:
"Throws UrlElicitationRequiredError (-32042). Client must open the URL, then retry. On retry, if the last-issued elicitationId was completed, sends ElicitCompleteNotification and succeeds.",
inputSchema: {},
inputSchema: z.object({}),
},
async (): Promise<CallToolResult> => {
// Retry path: check if the user completed the elicitation we last issued.
@ -1047,7 +1048,7 @@ export function createApp(): Express {
}
const server = createServer(authInfo, req);
const transport = new StreamableHTTPServerTransport({
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {