From 00a3db443f770de8f05ab1c05db334ed9e78f803 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Fri, 10 Jul 2026 13:04:41 -0700 Subject: [PATCH 1/7] chore: v2 SDK migration groundwork (phase 0) Add @modelcontextprotocol/{client,server,core}@2.0.0-beta.3 alongside the v1 SDK. Make PostMessageTransport structurally satisfy both v1 and v2 Transport interfaces, source generated Zod schemas from @modelcontextprotocol/core (cast to v1 types until the type migration phase), and add a gating spike proving a v2 Client/Server pair can run the ui/initialize handshake and host->view requests without negotiating an MCP protocol version (via the public transport.sessionId skip). --- package-lock.json | 448 +++--------------------- package.json | 3 + scripts/generate-schemas.ts | 74 ++-- src/generated/schema.json | 182 ++++++---- src/generated/schema.ts | 41 ++- src/message-transport-v2-compat.test.ts | 27 ++ src/message-transport.ts | 36 +- src/v2-migration-spike.test.ts | 443 +++++++++++++++++++++++ 8 files changed, 757 insertions(+), 497 deletions(-) create mode 100644 src/message-transport-v2-compat.test.ts create mode 100644 src/v2-migration-spike.test.ts diff --git a/package-lock.json b/package-lock.json index 29e905875..87e27c67a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,10 @@ }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", + "@modelcontextprotocol/client": "2.0.0-beta.3", + "@modelcontextprotocol/core": "2.0.0-beta.3", "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.3", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -89,23 +92,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-host/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-host/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", "version": "1.7.4", @@ -133,23 +119,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-preact/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-preact/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", "version": "1.7.4", @@ -180,23 +149,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-react/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-react/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", "version": "1.7.4", @@ -224,23 +176,6 @@ "vite-plugin-solid": "^2.11.12" } }, - "examples/basic-server-solid/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-solid/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", "version": "1.7.4", @@ -268,23 +203,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-svelte/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-svelte/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", "version": "1.7.4", @@ -310,23 +228,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-vanillajs/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-vanillajs/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", "version": "1.7.4", @@ -354,23 +255,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/basic-server-vue/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/basic-server-vue/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", "version": "1.7.4", @@ -397,23 +281,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/budget-allocator-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/budget-allocator-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", "version": "1.7.4", @@ -444,23 +311,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/cohort-heatmap-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/cohort-heatmap-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", "version": "1.7.4", @@ -487,23 +337,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/customer-segmentation-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/customer-segmentation-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", "version": "1.7.4", @@ -529,23 +362,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/debug-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/debug-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/integration-server": { "version": "1.7.4", "dependencies": { @@ -573,23 +389,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/integration-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/integration-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", "version": "1.7.4", @@ -615,23 +414,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/lazy-auth-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/lazy-auth-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", "version": "1.7.4", @@ -657,23 +439,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/map-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/map-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", "version": "1.7.4", @@ -701,23 +466,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/pdf-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/pdf-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", "version": "1.7.4", @@ -748,16 +496,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/quickstart/node_modules/@types/node": { - "version": "22.19.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.5.tgz", - "integrity": "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", "version": "1.7.4", @@ -798,23 +536,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/scenario-modeler-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/scenario-modeler-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", "version": "1.7.4", @@ -840,23 +561,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/shadertoy-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/shadertoy-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", "version": "1.7.4", @@ -883,23 +587,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/sheet-music-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/sheet-music-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", "version": "1.7.4", @@ -927,23 +614,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/system-monitor-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/system-monitor-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", "version": "1.7.4", @@ -976,23 +646,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/threejs-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/threejs-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", "version": "1.7.4", @@ -1019,23 +672,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/transcript-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/transcript-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", "version": "1.7.4", @@ -1061,23 +697,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/video-resource-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/video-resource-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", "version": "1.7.4", @@ -1105,23 +724,6 @@ "vite-plugin-singlefile": "^2.3.0" } }, - "examples/wiki-explorer-server/node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "examples/wiki-explorer-server/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -2576,6 +2178,37 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.3.tgz", + "integrity": "sha512-o9z9YCGNxWNdklPdjcD0tr8pocC9OgjdArqgx3OjPMlTe0M/SSPC2olOJs5ueq77HSpz36mND0f0KteqEhKF3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.3.tgz", + "integrity": "sha512-NqcrMybYMnTxVTjEhCAPiDmpkkRivjHcjMdjGB97s6JuM+jVkYDzhQaF1z0n6eS0gnKjvLI1l60t1617g44kow==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@modelcontextprotocol/ext-apps": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.1.tgz", @@ -2653,6 +2286,19 @@ } } }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.3.tgz", + "integrity": "sha512-EQxVIWf2XMAgjR38ZRZnX1iiOWH42tT/YJG8N4dVjAnrEGSEfPirR6FYfDC72yuNSvVYbPDTD1H3KHLLHPd18A==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@modelcontextprotocol/server-basic-preact": { "resolved": "examples/basic-server-preact", "link": true diff --git a/package.json b/package.json index 8c6ead57b..e013bb476 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,10 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", + "@modelcontextprotocol/client": "2.0.0-beta.3", + "@modelcontextprotocol/core": "2.0.0-beta.3", "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "2.0.0-beta.3", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index 109f35247..ad626f764 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -12,7 +12,7 @@ * * ts-to-zod generates `import { z } from "zod"`. We rewrite this to * `import { z } from "zod/v4"` because the generated schemas compose with - * schemas imported from `@modelcontextprotocol/sdk/types.js`, which the SDK + * schemas imported from `@modelcontextprotocol/core`, which the v2 SDK * constructs via `zod/v4`. Mixing schemas from the v3 and v4 APIs at runtime * fails with errors like `keyValidator._parse is not a function` (v3 internals * calling into v4 objects, or vice versa). The `zod/v4` subpath is exported by @@ -25,7 +25,15 @@ * `RequestId`, and `Tool` from `@modelcontextprotocol/sdk`, it generates `z.any()` * as a placeholder. * - * **Solution**: Import the schemas from MCP SDK and remove the z.any() placeholders. + * **Solution**: Import the schemas from `@modelcontextprotocol/core` (v2 public + * Zod-schema package) and remove the z.any() placeholders. Cast each imported + * schema to `z.ZodType` using the still-v1 TypeScript types from + * `@modelcontextprotocol/sdk/types.js`, so composed `z.infer` types stay + * assignable to `spec.types.ts` until the type migration phase. Without the + * cast, v2 inferred shapes (e.g. `Tool.inputSchema.properties` as + * `Record`) diverge from v1 types and break declaration + * emit (`JSONObject` cannot be named) plus schema integration tests. + * * * ### 3. Index Signatures (`z.record().and()` → `z.object().passthrough()`) * @@ -69,18 +77,26 @@ const SCHEMA_TEST_OUTPUT_FILE = join(GENERATED_DIR, "schema.test.ts"); const JSON_SCHEMA_OUTPUT_FILE = join(GENERATED_DIR, "schema.json"); /** - * External types from MCP SDK that ts-to-zod can't resolve. - * With PascalCase naming (via getSchemaName), generated placeholders match MCP SDK exports. + * External types from MCP that ts-to-zod can't resolve. + * With PascalCase naming (via getSchemaName), generated placeholders match + * `@modelcontextprotocol/core` exports. Values come from core; TypeScript + * types still come from the v1 SDK until a later migration phase. */ const EXTERNAL_TYPE_SCHEMAS = [ - "ContentBlockSchema", - "CallToolResultSchema", - "EmbeddedResourceSchema", - "ImplementationSchema", - "RequestIdSchema", - "ResourceLinkSchema", - "ToolSchema", -]; + { schema: "ContentBlockSchema", type: "ContentBlock" }, + { schema: "CallToolResultSchema", type: "CallToolResult" }, + { schema: "EmbeddedResourceSchema", type: "EmbeddedResource" }, + { schema: "ImplementationSchema", type: "Implementation" }, + { schema: "RequestIdSchema", type: "RequestId" }, + { schema: "ResourceLinkSchema", type: "ResourceLink" }, + { schema: "ToolSchema", type: "Tool" }, +] as const; + +/** Public Zod-schema package for MCP protocol schemas (v2). */ +const MCP_SCHEMA_PACKAGE = "@modelcontextprotocol/core"; + +/** v1 types package — still the source of TypeScript types in spec.types.ts. */ +const MCP_V1_TYPES_PACKAGE = "@modelcontextprotocol/sdk/types.js"; async function main() { console.log("🔧 Generating Zod schemas from spec.types.ts...\n"); @@ -161,7 +177,7 @@ async function generateJsonSchema() { ) { const typeName = name.replace(/Schema$/, ""); try { - // Use unrepresentable: "any" to handle external types (MCP SDK schemas) + // Use unrepresentable: "any" to handle external types (MCP core schemas) // that can't be directly represented in JSON Schema jsonSchema.$defs[typeName] = toJSONSchema(schema as $ZodType, { unrepresentable: "any", @@ -184,20 +200,38 @@ async function generateJsonSchema() { * Post-process generated schemas for project compatibility. */ function postProcess(content: string): string { - // 1. Rewrite to zod/v4 and add MCP SDK schema imports. + // 1. Rewrite to zod/v4 and add MCP core schema imports. // zod/v4 aligns with the SDK's own zod import — composing v3 and v4 // schema instances throws at parse time. See header comment for details. - const mcpImports = EXTERNAL_TYPE_SCHEMAS.join(",\n "); + // + // Import schemas from @modelcontextprotocol/core (runtime validation), then + // re-bind as z.ZodType so z.infer stays compatible with v1 TypeScript + // types still used in spec.types.ts. Full type migration is a later phase. + const typeImports = EXTERNAL_TYPE_SCHEMAS.map((e) => e.type).join(",\n "); + const schemaImports = EXTERNAL_TYPE_SCHEMAS.map( + (e) => `${e.schema} as ${e.schema}FromCore`, + ).join(",\n "); + const schemaBindings = EXTERNAL_TYPE_SCHEMAS.map( + (e) => + `const ${e.schema}: z.ZodType<${e.type}> = ${e.schema}FromCore as z.ZodType<${e.type}>;`, + ).join("\n"); + content = content.replace( 'import { z } from "zod";', `import { z } from "zod/v4"; +import type { + ${typeImports}, +} from "${MCP_V1_TYPES_PACKAGE}"; import { - ${mcpImports}, -} from "@modelcontextprotocol/sdk/types.js";`, + ${schemaImports}, +} from "${MCP_SCHEMA_PACKAGE}"; + +${schemaBindings} +`, ); - // 2. Remove z.any() placeholders for external types (now imported from MCP SDK) - for (const schema of EXTERNAL_TYPE_SCHEMAS) { + // 2. Remove z.any() placeholders for external types (now imported from MCP core) + for (const { schema } of EXTERNAL_TYPE_SCHEMAS) { content = content.replace( new RegExp(`(?:export )?const ${schema} = z\\.any\\(\\);\\n?`, "g"), "", @@ -213,7 +247,7 @@ import { content = content.replace( "// Generated by ts-to-zod", `// Generated by ts-to-zod -// Post-processed for Zod v3/v4 compatibility and MCP SDK integration +// Post-processed for Zod v3/v4 compatibility and MCP core schema integration // Run: npm run generate:schemas`, ); diff --git a/src/generated/schema.json b/src/generated/schema.json index 80b4ac60d..5f2124a95 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -596,7 +596,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -611,25 +613,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -1286,7 +1273,40 @@ } }, "required": ["method", "params"], - "additionalProperties": false + "additionalProperties": false, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiHostContext": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -1359,7 +1379,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -1374,25 +1396,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -2044,7 +2051,40 @@ "additionalProperties": false } }, - "additionalProperties": {} + "additionalProperties": {}, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiHostCss": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -2942,7 +2982,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -2957,25 +2999,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -3637,7 +3664,40 @@ "hostCapabilities", "hostContext" ], - "additionalProperties": {} + "additionalProperties": {}, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiInitializedNotification": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -5669,13 +5729,7 @@ ] } }, - "structuredContent": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, + "structuredContent": {}, "isError": { "type": "boolean" } diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 43687374e..7d62ef5bc 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -1,16 +1,39 @@ // Generated by ts-to-zod -// Post-processed for Zod v3/v4 compatibility and MCP SDK integration +// Post-processed for Zod v3/v4 compatibility and MCP core schema integration // Run: npm run generate:schemas import { z } from "zod/v4"; -import { - ContentBlockSchema, - CallToolResultSchema, - EmbeddedResourceSchema, - ImplementationSchema, - RequestIdSchema, - ResourceLinkSchema, - ToolSchema, +import type { + ContentBlock, + CallToolResult, + EmbeddedResource, + Implementation, + RequestId, + ResourceLink, + Tool, } from "@modelcontextprotocol/sdk/types.js"; +import { + ContentBlockSchema as ContentBlockSchemaFromCore, + CallToolResultSchema as CallToolResultSchemaFromCore, + EmbeddedResourceSchema as EmbeddedResourceSchemaFromCore, + ImplementationSchema as ImplementationSchemaFromCore, + RequestIdSchema as RequestIdSchemaFromCore, + ResourceLinkSchema as ResourceLinkSchemaFromCore, + ToolSchema as ToolSchemaFromCore, +} from "@modelcontextprotocol/core"; + +const ContentBlockSchema: z.ZodType = + ContentBlockSchemaFromCore as z.ZodType; +const CallToolResultSchema: z.ZodType = + CallToolResultSchemaFromCore as z.ZodType; +const EmbeddedResourceSchema: z.ZodType = + EmbeddedResourceSchemaFromCore as z.ZodType; +const ImplementationSchema: z.ZodType = + ImplementationSchemaFromCore as z.ZodType; +const RequestIdSchema: z.ZodType = + RequestIdSchemaFromCore as z.ZodType; +const ResourceLinkSchema: z.ZodType = + ResourceLinkSchemaFromCore as z.ZodType; +const ToolSchema: z.ZodType = ToolSchemaFromCore as z.ZodType; /** * @description Color theme preference for the host environment. diff --git a/src/message-transport-v2-compat.test.ts b/src/message-transport-v2-compat.test.ts new file mode 100644 index 000000000..b06e5135d --- /dev/null +++ b/src/message-transport-v2-compat.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "bun:test"; +import type { Transport as TransportV2 } from "@modelcontextprotocol/client"; + +import { PostMessageTransport } from "./message-transport"; + +/** + * Compile-time assertion: PostMessageTransport must remain assignable to the + * v2 SDK `Transport` interface exported from `@modelcontextprotocol/client`. + * If this line errors under `tsc`, a v1/v2 shape regression was introduced. + */ +const _postMessageTransportIsV2Transport: TransportV2 = + null as unknown as PostMessageTransport; +void _postMessageTransportIsV2Transport; + +describe("PostMessageTransport v2 Transport compat", () => { + it("exposes the v2-only optional Transport members as unset", () => { + // Minimal runtime smoke test so this file is not type-only empty. + // postMessage is a shared channel — hasPerRequestStream stays undefined. + const transport = new PostMessageTransport( + { postMessage() {} } as unknown as Window, + {} as MessageEventSource, + ); + + expect(transport.hasPerRequestStream).toBeUndefined(); + expect(transport.setSupportedProtocolVersions).toBeUndefined(); + }); +}); diff --git a/src/message-transport.ts b/src/message-transport.ts index 9d195435a..b2443c505 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -14,7 +14,12 @@ import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types"; * * This transport enables bidirectional communication between MCP Apps running in * iframes and their host applications using the browser's `postMessage` API. It - * implements the MCP SDK's `Transport` interface. + * implements the MCP TypeScript SDK `Transport` interface for both v1 + * (`@modelcontextprotocol/sdk`) and v2 (`@modelcontextprotocol/client` / + * `@modelcontextprotocol/server`). v2-only optional members + * ({@link hasPerRequestStream}, {@link setSupportedProtocolVersions}) and + * extended {@link TransportSendOptions} fields are accepted but unused — + * postMessage is a shared single channel, not a per-request HTTP stream. * * ## Security * @@ -121,9 +126,20 @@ export class PostMessageTransport implements Transport { * to all frames. The receiver should validate the message source for security. * * @param message - JSON-RPC message to send - * @param options - Optional send options (currently unused) + * @param options - Optional send options (accepted for v1/v2 SDK compatibility; + * unused — postMessage has no per-request stream or HTTP headers) */ - async send(message: JSONRPCMessage, options?: TransportSendOptions) { + async send( + message: JSONRPCMessage, + // Widen beyond v1 TransportSendOptions so callers using the v2 SDK can pass + // requestSignal / onRequestStreamEnd / headers without type errors. All are + // ignored: this transport shares one postMessage channel. + options?: TransportSendOptions & { + requestSignal?: AbortSignal; + onRequestStreamEnd?: () => void; + headers?: Readonly>; + }, + ) { // Skip debug log for high-frequency streaming notifications — these // can fire dozens of times per second and flood the console. if ((message as { method?: string }).method !== TOOL_INPUT_PARTIAL_METHOD) { @@ -187,4 +203,18 @@ export class PostMessageTransport implements Transport { * @param version - The negotiated protocol version string */ setProtocolVersion?: (version: string) => void; + + /** + * v2 SDK: `true` when the transport opens one underlying request per outbound + * JSON-RPC message (Streamable HTTP POST-per-request). Left unset — postMessage + * shares a single channel, so per-request abort/stream options are ignored. + */ + readonly hasPerRequestStream?: boolean; + + /** + * v2 SDK: sets supported protocol versions for transport-level header + * validation. Optional no-op hook for hosts that call it during connect; + * postMessage has no HTTP version headers to validate. + */ + setSupportedProtocolVersions?: (versions: string[]) => void; } diff --git a/src/v2-migration-spike.test.ts b/src/v2-migration-spike.test.ts new file mode 100644 index 000000000..7139dcb24 --- /dev/null +++ b/src/v2-migration-spike.test.ts @@ -0,0 +1,443 @@ +/** + * Phase 0 gating spike: migrate ext-apps View/Host JSON-RPC off v1 SDK onto + * v2 `@modelcontextprotocol/client` + `@modelcontextprotocol/server` WITHOUT + * ever negotiating an MCP protocol version on the view↔host link. + * + * Background (v2 Client, packages/client/src/client/client.ts): + * - `Client.connect()` always runs MCP `initialize` (legacy) or `server/discover` + * (auto/pin) unless a reconnect path is taken. + * - `Client._shouldDropInbound` silently drops ALL inbound requests when + * `_negotiatedProtocolVersion` is a modern (2026-07-28) version. If the + * negotiated version stays `undefined`, the drop never arms. + * + * Strategy under test (public API only — no protected/private overrides): + * set `transport.sessionId` before `Client.connect()`. Documented in + * typescript-sdk `docs/migration/upgrade-to-v2.md` ("connect() skips the + * initialize handshake when the transport already exposes a sessionId"). + * + * Transport: published `InMemoryTransport` from `@modelcontextprotocol/client` + * (also re-exported by `@modelcontextprotocol/server`). Additionally exercises + * ext-apps `PostMessageTransport` with the fake-window harness from + * `message-transport.test.ts`. + */ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { + Client, + InMemoryTransport, + type Transport, +} from "@modelcontextprotocol/client"; +import { Server } from "@modelcontextprotocol/server"; +import { z } from "zod"; + +import { PostMessageTransport } from "./message-transport"; + +// --------------------------------------------------------------------------- +// Custom method schemas (vendor-prefixed; 3-arg setRequestHandler / request) +// --------------------------------------------------------------------------- + +const UiInitializeParams = z.object({ + appInfo: z.object({ name: z.string(), version: z.string() }), +}); +const UiInitializeResult = z.object({ + hostCapabilities: z.object({ openLinks: z.boolean().optional() }).optional(), +}); + +const UiResourceTeardownParams = z.object({ + reason: z.string(), +}); +const UiResourceTeardownResult = z.object({ + closed: z.literal(true), +}); + +// --------------------------------------------------------------------------- +// Subclasses — public API only (no underscore / protected overrides) +// --------------------------------------------------------------------------- + +class SpikeAppClient extends Client { + /** Counts how many times the ui/resource-teardown handler ran. */ + teardownHits = 0; + pingHits = 0; + toolsCallHits = 0; + + wireHostFacingHandlers(): void { + this.setRequestHandler( + "ui/resource-teardown", + { params: UiResourceTeardownParams, result: UiResourceTeardownResult }, + async (params) => { + this.teardownHits += 1; + expect(params.reason).toBeTruthy(); + return { closed: true as const }; + }, + ); + + // Spec method — 2-arg form (no schema bundle). + this.setRequestHandler("ping", async () => { + this.pingHits += 1; + return {}; + }); + + // Host also sends tools/call toward the view in production; confirm the + // Client will accept a handler and answer without MCP initialize. + this.setRequestHandler("tools/call", async (request) => { + this.toolsCallHits += 1; + return { + content: [ + { + type: "text" as const, + text: `called:${String(request.params.name)}`, + }, + ], + }; + }); + } +} + +class SpikeAppServer extends Server { + wireViewFacingHandlers(): void { + this.setRequestHandler( + "ui/initialize", + { params: UiInitializeParams, result: UiInitializeResult }, + async (params) => { + expect(params.appInfo.name).toBeTruthy(); + return { hostCapabilities: { openLinks: true } }; + }, + ); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +type ProtocolVersionCapture = { + versions: string[]; + setProtocolVersion: (version: string) => void; +}; + +function captureProtocolVersion(transport: Transport): ProtocolVersionCapture { + const versions: string[] = []; + const previous = transport.setProtocolVersion?.bind(transport); + transport.setProtocolVersion = (version: string) => { + versions.push(version); + previous?.(version); + }; + return { + versions, + setProtocolVersion: transport.setProtocolVersion, + }; +} + +/** + * Public escape hatch: pretends this is a session resume so Client.connect + * attaches the transport via Protocol.connect and returns WITHOUT running + * `_legacyHandshake` / `_connectNegotiated`. + * + * Source: Client.connect sessionId branch + * (typescript-sdk packages/client/src/client/client.ts ~941–952). + */ +function skipMcpInitialize(transport: Transport): void { + transport.sessionId = transport.sessionId ?? "ext-apps-spike-no-mcp-init"; +} + +async function connectWithoutMcpInitialize( + client: SpikeAppClient, + server: SpikeAppServer, + clientTransport: Transport, + serverTransport: Transport, +): Promise<{ clientVersions: string[]; serverVersions: string[] }> { + const clientCapture = captureProtocolVersion(clientTransport); + const serverCapture = captureProtocolVersion(serverTransport); + + skipMcpInitialize(clientTransport); + + client.wireHostFacingHandlers(); + server.wireViewFacingHandlers(); + + // Server.connect is Protocol.connect (Server does not override) — attach only. + await server.connect(serverTransport); + await client.connect(clientTransport); + + return { + clientVersions: clientCapture.versions, + serverVersions: serverCapture.versions, + }; +} + +async function assertNoNegotiationRoundTrip( + client: SpikeAppClient, + server: SpikeAppServer, + protocolVersionsSet: string[], +): Promise { + // Q2: negotiated version / capabilities stay undefined; setProtocolVersion never fired. + expect(client.getServerCapabilities()).toBeUndefined(); + expect(client.getNegotiatedProtocolVersion()).toBeUndefined(); + expect(server.getClientCapabilities()).toBeUndefined(); + expect(server.getNegotiatedProtocolVersion()).toBeUndefined(); + expect(protocolVersionsSet).toEqual([]); + + // Custom ui/initialize: client → server (replaces MCP initialize for Apps). + const initResult = await client.request( + { + method: "ui/initialize", + params: { appInfo: { name: "spike-app", version: "0.0.0" } }, + }, + UiInitializeResult, + ); + expect(initResult).toEqual({ hostCapabilities: { openLinks: true } }); + + // Still no MCP negotiation after custom handshake. + expect(client.getNegotiatedProtocolVersion()).toBeUndefined(); + expect(server.getNegotiatedProtocolVersion()).toBeUndefined(); + expect(protocolVersionsSet).toEqual([]); + + // Server → client custom request must REACH the Client handler. + const teardown = await server.request( + { + method: "ui/resource-teardown", + params: { reason: "unmount" }, + }, + UiResourceTeardownResult, + ); + expect(teardown).toEqual({ closed: true }); + expect(client.teardownHits).toBe(1); + + // Server → client ping (spec method). + const pingResult = await server.ping(); + expect(pingResult).toEqual({}); + expect(client.pingHits).toBe(1); + + // Server → client tools/call (spec method the host proxies in production). + const toolResult = await server.request({ + method: "tools/call", + params: { name: "demo", arguments: {} }, + }); + expect(toolResult).toMatchObject({ + content: [{ type: "text", text: "called:demo" }], + }); + expect(client.toolsCallHits).toBe(1); +} + +// --------------------------------------------------------------------------- +// Q1 — Does Client.connect auto-send MCP initialize? +// --------------------------------------------------------------------------- + +describe("v2 migration spike — Q1 Client.connect initialize behavior", () => { + it("auto-sends MCP initialize on a fresh connect (no public skip option on ConnectOptions)", async () => { + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + const outbound: Array<{ method?: string }> = []; + const originalSend = clientTransport.send.bind(clientTransport); + clientTransport.send = async (message, options) => { + outbound.push(message as { method?: string }); + return originalSend(message, options); + }; + + const client = new Client({ name: "q1-client", version: "1.0.0" }); + const server = new Server( + { name: "q1-server", version: "1.0.0" }, + { capabilities: {} }, + ); + + await server.connect(serverTransport); + await client.connect(clientTransport); + + const methods = outbound.map((m) => m.method).filter(Boolean); + expect(methods).toContain("initialize"); + expect(methods).toContain("notifications/initialized"); + expect(client.getNegotiatedProtocolVersion()).toBeDefined(); + expect(client.getServerCapabilities()).toBeDefined(); + + // Documented public ConnectOptions / ClientOptions surface (no skipInitialize): + // - ConnectOptions.prior → adopts DiscoverResult (SETS modern negotiated version) + // - ClientOptions.versionNegotiation → 'legacy' | 'auto' | { pin } (all handshake) + // - transport.sessionId already set → reconnect path skips handshake (see next test) + await client.close(); + await server.close(); + }); + + it("skips MCP initialize when transport.sessionId is already set (documented reconnect guard)", async () => { + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + const outbound: Array<{ method?: string }> = []; + const originalSend = clientTransport.send.bind(clientTransport); + clientTransport.send = async (message, options) => { + outbound.push(message as { method?: string }); + return originalSend(message, options); + }; + + clientTransport.sessionId = "preexisting-session"; + + const client = new Client({ name: "q1-skip-client", version: "1.0.0" }); + const server = new Server( + { name: "q1-skip-server", version: "1.0.0" }, + { capabilities: {} }, + ); + + await server.connect(serverTransport); + await client.connect(clientTransport); + + const methods = outbound.map((m) => m.method).filter(Boolean); + expect(methods).not.toContain("initialize"); + expect(methods).not.toContain("server/discover"); + expect(client.getNegotiatedProtocolVersion()).toBeUndefined(); + expect(client.getServerCapabilities()).toBeUndefined(); + + await client.close(); + await server.close(); + }); +}); + +// --------------------------------------------------------------------------- +// Q2 + Q3 — no-negotiation custom handshake + server→client requests +// --------------------------------------------------------------------------- + +describe("v2 migration spike — Q2/Q3 no-negotiation strategy (InMemoryTransport)", () => { + it("exchanges ui/initialize then delivers server→client ui/resource-teardown + ping without MCP initialize", async () => { + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + const client = new SpikeAppClient({ + name: "spike-view", + version: "0.0.0", + }); + const server = new SpikeAppServer( + { name: "spike-host", version: "0.0.0" }, + { capabilities: {} }, + ); + + const { clientVersions, serverVersions } = + await connectWithoutMcpInitialize( + client, + server, + clientTransport, + serverTransport, + ); + + await assertNoNegotiationRoundTrip(client, server, [ + ...clientVersions, + ...serverVersions, + ]); + + // Q3: Server accepted inbound ui/initialize and sent outbound requests with + // getClientCapabilities() still undefined — no v1-style "must initialize" + // gate on Server.request / inbound custom handlers. + expect(server.getClientCapabilities()).toBeUndefined(); + + await client.close(); + await server.close(); + }); +}); + +// --------------------------------------------------------------------------- +// Same checks over PostMessageTransport (fake-window harness) +// --------------------------------------------------------------------------- + +type Listener = (event: MessageEvent) => void; + +function createFakeWindow() { + const listeners = new Map>(); + return { + addEventListener(type: string, listener: Listener) { + if (!listeners.has(type)) listeners.set(type, new Set()); + listeners.get(type)!.add(listener); + }, + removeEventListener(type: string, listener: Listener) { + listeners.get(type)?.delete(listener); + }, + dispatch(type: string, event: unknown) { + listeners.get(type)?.forEach((l) => l(event as MessageEvent)); + }, + }; +} + +/** + * Two PostMessageTransports sharing one fake `window`, cross-wired so each + * side's postMessage arrives as a MessageEvent with the peer as `source`. + */ +function createLinkedPostMessagePair(): { + viewTransport: PostMessageTransport; + hostTransport: PostMessageTransport; +} { + const viewWindow = { id: "view" }; + const hostWindow = { id: "host" }; + + const hostAsTarget = { + postMessage(data: unknown) { + // View → host: event.source must be the view window. + ( + globalThis as { window: ReturnType } + ).window.dispatch("message", { source: viewWindow, data }); + }, + }; + const viewAsTarget = { + postMessage(data: unknown) { + // Host → view: event.source must be the host window. + ( + globalThis as { window: ReturnType } + ).window.dispatch("message", { source: hostWindow, data }); + }, + }; + + const viewTransport = new PostMessageTransport( + hostAsTarget as unknown as Window, + hostWindow as unknown as MessageEventSource, + ); + const hostTransport = new PostMessageTransport( + viewAsTarget as unknown as Window, + viewWindow as unknown as MessageEventSource, + ); + + return { viewTransport, hostTransport }; +} + +describe("v2 migration spike — PostMessageTransport (fake window)", () => { + let restoreConsole: () => void; + + beforeEach(() => { + (globalThis as { window?: unknown }).window = createFakeWindow(); + const origDebug = console.debug; + const origError = console.error; + console.debug = () => {}; + console.error = () => {}; + restoreConsole = () => { + console.debug = origDebug; + console.error = origError; + }; + }); + + afterEach(() => { + restoreConsole(); + delete (globalThis as { window?: unknown }).window; + }); + + it("same no-negotiation round-trip over PostMessageTransport", async () => { + const { viewTransport, hostTransport } = createLinkedPostMessagePair(); + + const client = new SpikeAppClient({ + name: "spike-view-pm", + version: "0.0.0", + }); + const server = new SpikeAppServer( + { name: "spike-host-pm", version: "0.0.0" }, + { capabilities: {} }, + ); + + const { clientVersions, serverVersions } = + await connectWithoutMcpInitialize( + client, + server, + viewTransport as unknown as Transport, + hostTransport as unknown as Transport, + ); + + await assertNoNegotiationRoundTrip(client, server, [ + ...clientVersions, + ...serverVersions, + ]); + + await client.close(); + await server.close(); + }); +}); From 0c9049f9a31b3bcfb2781b728516ee5277281f8f Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Fri, 10 Jul 2026 13:40:56 -0700 Subject: [PATCH 2/7] refactor: re-parent App/AppBridge onto v2 Client/Server (phase 1) App now extends the v2 Client and AppBridge the v2 Server, replacing the private-in-v2 Protocol base. ProtocolWithEvents is deleted in favor of a composed NotificationEventEmitter + MethodClaimRegistry preserving the public on*/addEventListener semantics and double-set protection. App skips the MCP initialize handshake via the documented transport.sessionId reconnect guard so the iframe link never negotiates a protocol version (keeping ui/initialize as the only handshake on the wire). v1 capability assert overrides moved into their public call sites. The composed host->MCP-server client in AppBridge intentionally remains on v1 for a later phase. --- examples/basic-server-react/src/mcp-app.tsx | 20 +- src/app-bridge.examples.ts | 6 +- src/app-bridge.test.ts | 50 +- src/app-bridge.ts | 622 +++++++++++++------- src/app.ts | 365 +++++++----- src/events.ts | 363 ++++++------ src/types.ts | 28 + src/v2-migration-spike.test.ts | 4 +- 8 files changed, 855 insertions(+), 603 deletions(-) diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx index 6bb554025..57995008d 100644 --- a/examples/basic-server-react/src/mcp-app.tsx +++ b/examples/basic-server-react/src/mcp-app.tsx @@ -3,20 +3,26 @@ */ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { StrictMode, useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import styles from "./mcp-app.module.css"; - -function extractTime(callToolResult: CallToolResult): string { - const { text } = callToolResult.content?.find((c) => c.type === "text")!; - return text; +type ToolResult = { + content?: Array<{ type: string; text?: string }>; + structuredContent?: unknown; + isError?: boolean; +}; + +function extractTime(callToolResult: ToolResult): string { + const sc = callToolResult.structuredContent as { time?: string } | undefined; + if (sc?.time) return sc.time; + const { text } = callToolResult.content?.find((c) => c.type === "text") ?? {}; + return text ?? ""; } function GetTimeApp() { - const [toolResult, setToolResult] = useState(null); + const [toolResult, setToolResult] = useState(null); const [hostContext, setHostContext] = useState(); // `useApp` (1) creates an `App` instance, (2) calls `onAppCreated` to @@ -65,7 +71,7 @@ function GetTimeApp() { interface GetTimeAppInnerProps { app: App; - toolResult: CallToolResult | null; + toolResult: ToolResult | null; hostContext?: McpUiHostContext; } function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) { diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index d5dc5bc74..f9bdf18cc 100644 --- a/src/app-bridge.examples.ts +++ b/src/app-bridge.examples.ts @@ -245,7 +245,11 @@ function AppBridge_oncreatesamplingmessage_forwardToLlm( //#region AppBridge_oncreatesamplingmessage_forwardToLlm bridge.oncreatesamplingmessage = async (params, extra) => { // Apply rate limiting, user approval, cost controls here - return await myLlmProvider.complete(params, { signal: extra.signal }); + // Cast: host LLM helpers may still be typed against v1 SDK params during Phase 1. + return await myLlmProvider.complete( + params as CreateMessageRequest["params"], + { signal: extra.signal }, + ); }; //#endregion AppBridge_oncreatesamplingmessage_forwardToLlm } diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index a4fe1de1e..4a1dcf147 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { InMemoryTransport } from "@modelcontextprotocol/client"; import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js"; import { @@ -7,11 +7,8 @@ import { ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, - PromptListChangedNotificationSchema, ReadResourceResultSchema, - ResourceListChangedNotificationSchema, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/core"; import { z } from "zod/v4"; import { App } from "./app"; @@ -2313,9 +2310,12 @@ describe("App <-> AppBridge integration", () => { it("sendToolListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(ToolListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + app.setNotificationHandler( + "notifications/tools/list_changed", + (n: { params?: unknown }) => { + receivedNotifications.push(n.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2328,9 +2328,12 @@ describe("App <-> AppBridge integration", () => { it("sendResourceListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(ResourceListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + app.setNotificationHandler( + "notifications/resources/list_changed", + (n: { params?: unknown }) => { + receivedNotifications.push(n.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2343,9 +2346,12 @@ describe("App <-> AppBridge integration", () => { it("sendPromptListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(PromptListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + app.setNotificationHandler( + "notifications/prompts/list_changed", + (n: { params?: unknown }) => { + receivedNotifications.push(n.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2758,15 +2764,17 @@ describe("isToolVisibilityAppOnly", () => { testHostInfo, testHostCapabilities, ); + const TestParams = z.object({}); + const TestResult = z.object({}); bridge2.setRequestHandler( - // @ts-expect-error — exercising throw path with raw schema - { shape: { method: { value: "test/method" } } }, + "test/method", + { params: TestParams, result: TestResult }, () => ({}), ); expect(() => { bridge2.setRequestHandler( - // @ts-expect-error — exercising throw path with raw schema - { shape: { method: { value: "test/method" } } }, + "test/method", + { params: TestParams, result: TestResult }, () => ({}), ); }).toThrow(/already registered/); @@ -2777,9 +2785,11 @@ describe("isToolVisibilityAppOnly", () => { app2.addEventListener("toolinput", () => {}); expect(() => { app2.setNotificationHandler( - // @ts-expect-error — exercising throw path with raw schema + "ui/notifications/tool-input", { - shape: { method: { value: "ui/notifications/tool-input" } }, + params: z.object({ + arguments: z.record(z.string(), z.unknown()).optional(), + }), }, () => {}, ); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 23383c40f..9477f34fc 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1,58 +1,65 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { Client as V1Client } from "@modelcontextprotocol/sdk/client/index.js"; import { + CallToolResultSchema as V1CallToolResultSchema, + ListPromptsResultSchema as V1ListPromptsResultSchema, + ListResourcesResultSchema as V1ListResourcesResultSchema, + ListResourceTemplatesResultSchema as V1ListResourceTemplatesResultSchema, + PromptListChangedNotificationSchema, + ReadResourceResultSchema as V1ReadResourceResultSchema, + ResourceListChangedNotificationSchema, + ToolListChangedNotificationSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { + Server, + type ServerOptions, + type Implementation, + type RequestOptions, + type Transport, +} from "@modelcontextprotocol/server"; +import type { CallToolRequest, - CallToolRequestSchema, CallToolResult, - CallToolResultSchema, CreateMessageRequest, - CreateMessageRequestSchema, CreateMessageResult, CreateMessageResultWithTools, EmptyResult, - Implementation, ListPromptsRequest, - ListPromptsRequestSchema, ListPromptsResult, - ListPromptsResultSchema, ListResourcesRequest, - ListResourcesRequestSchema, ListResourcesResult, - ListResourcesResultSchema, ListResourceTemplatesRequest, - ListResourceTemplatesRequestSchema, ListResourceTemplatesResult, - ListResourceTemplatesResultSchema, ListToolsRequest, - ListToolsRequestSchema, - ListToolsResultSchema, + ListToolsResult, LoggingMessageNotification, - LoggingMessageNotificationSchema, PingRequest, - PingRequestSchema, PromptListChangedNotification, - PromptListChangedNotificationSchema, ReadResourceRequest, - ReadResourceRequestSchema, ReadResourceResult, - ReadResourceResultSchema, ResourceListChangedNotification, - ResourceListChangedNotificationSchema, - Tool, ToolListChangedNotification, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js"; +} from "./types"; +import { + CallToolResultSchema, + EmptyResultSchema, + ListPromptsResultSchema, + ListResourcesResultSchema, + ListResourceTemplatesResultSchema, + ListToolsResultSchema, + LoggingMessageNotificationSchema, + ReadResourceResultSchema, +} from "@modelcontextprotocol/core"; import { - Protocol, - ProtocolOptions, - RequestOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { ProtocolWithEvents } from "./events"; + MethodClaimRegistry, + NotificationEventEmitter, + methodOf, + paramsSchemaOf, + toRequestHandlerExtra, + warnIfRequestHandlerReplaced, + type RequestHandlerExtra, +} from "./events"; import { - type AppNotification, - type AppRequest, - type AppResult, type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, type McpUiToolCancelledNotification, @@ -71,15 +78,19 @@ import { McpUiInitializeRequest, McpUiInitializeRequestSchema, McpUiInitializeResult, + McpUiInitializeResultSchema, McpUiMessageRequest, McpUiMessageRequestSchema, McpUiMessageResult, + McpUiMessageResultSchema, McpUiOpenLinkRequest, McpUiOpenLinkRequestSchema, McpUiOpenLinkResult, + McpUiOpenLinkResultSchema, McpUiDownloadFileRequest, McpUiDownloadFileRequestSchema, McpUiDownloadFileResult, + McpUiDownloadFileResultSchema, McpUiResourceTeardownRequest, McpUiResourceTeardownResultSchema, McpUiRequestTeardownNotification, @@ -90,6 +101,7 @@ import { McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResult, + McpUiRequestDisplayModeResultSchema, McpUiResourcePermissions, McpUiToolMeta, } from "./types"; @@ -122,14 +134,16 @@ export { PostMessageTransport } from "./message-transport"; * }); * ``` */ -export function getToolUiResourceUri(tool: Partial): string | undefined { +export function getToolUiResourceUri(tool: object): string | undefined { // Try new nested format first: _meta.ui.resourceUri - const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined; + const uiMeta = (tool as { _meta?: { ui?: McpUiToolMeta } })._meta?.ui; let uri: unknown = uiMeta?.resourceUri; // Fall back to deprecated flat format: _meta["ui/resourceUri"] if (uri === undefined) { - uri = tool._meta?.[RESOURCE_URI_META_KEY]; + uri = (tool as { _meta?: Record })._meta?.[ + RESOURCE_URI_META_KEY + ]; } if (typeof uri === "string" && uri.startsWith("ui://")) { @@ -146,8 +160,8 @@ export function getToolUiResourceUri(tool: Partial): string | undefined { * @param tool - Tool object with visibility metadata * @returns True if the tool is visible to the model only, false otherwise */ -export function isToolVisibilityModelOnly(tool: Partial): boolean { - const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined; +export function isToolVisibilityModelOnly(tool: object): boolean { + const uiMeta = (tool as { _meta?: { ui?: McpUiToolMeta } })._meta?.ui; const visibility = uiMeta?.visibility; if (!visibility) return false; if (visibility.length === 1 && visibility[0] === "model") return true; @@ -160,8 +174,8 @@ export function isToolVisibilityModelOnly(tool: Partial): boolean { * @param tool - Tool object with visibility metadata * @returns True if the tool is visible to the app only, false otherwise */ -export function isToolVisibilityAppOnly(tool: Partial): boolean { - const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined; +export function isToolVisibilityAppOnly(tool: object): boolean { + const uiMeta = (tool as { _meta?: { ui?: McpUiToolMeta } })._meta?.ui; const visibility = uiMeta?.visibility; if (!visibility) return false; if (visibility.length === 1 && visibility[0] === "app") return true; @@ -203,10 +217,10 @@ export function buildAllowAttribute( * * @property hostContext - Optional initial host context to provide to the view * - * @see `ProtocolOptions` from @modelcontextprotocol/sdk for available options + * @see `ServerOptions` from @modelcontextprotocol/server for available options * @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure */ -export type HostOptions = ProtocolOptions & { +export type HostOptions = ServerOptions & { hostContext?: McpUiHostContext; }; @@ -227,10 +241,6 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION]; * * @internal */ -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; - /** * Maps DOM-style event names to their notification `params` types. * @@ -303,53 +313,13 @@ export type AppBridgeEventMap = { * await bridge.connect(transport); * ``` */ -export class AppBridge extends ProtocolWithEvents< - AppRequest, - AppNotification, - AppResult, - AppBridgeEventMap -> { +export class AppBridge extends Server { private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; private _initializedReceived = false; - - /** - * Wrap every handler registered via `replaceRequestHandler` with a check - * that the View has sent `ui/notifications/initialized`. Warns (never - * throws) so lenient hosts keep working while still surfacing the - * misordering that leaves strict hosts with a permanently hidden iframe. - * `ui/initialize` and `ping` use `setRequestHandler` directly and are - * intentionally exempt. - * - * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} - */ - private _baseReplaceRequestHandler = this.replaceRequestHandler; - protected override replaceRequestHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setRequestHandler"] = (schema, handler) => { - this._baseReplaceRequestHandler(schema, (request, extra) => { - if (!this._initializedReceived) { - console.warn( - `[ext-apps] AppBridge received '${request.method}' before ` + - `ui/notifications/initialized. The View is calling host ` + - `methods before completing the handshake; it should await ` + - `app.connect() first.`, - ); - } - return handler(request, extra); - }); - }; - - protected readonly eventSchemas = { - sizechange: McpUiSizeChangedNotificationSchema, - sandboxready: McpUiSandboxProxyReadyNotificationSchema, - initialized: McpUiInitializedNotificationSchema, - requestteardown: McpUiRequestTeardownNotificationSchema, - loggingmessage: LoggingMessageNotificationSchema, - }; + private readonly _claims = new MethodClaimRegistry(); + private readonly _events!: NotificationEventEmitter; /** * Create a new AppBridge instance. @@ -384,40 +354,222 @@ export class AppBridge extends ProtocolWithEvents< * }; * ``` */ + /** Host UI capabilities (openLinks, serverTools, …) — distinct from Server._capabilities. */ + private _hostCapabilities: McpUiHostCapabilities; + constructor( - private _client: Client | null, + private _client: V1Client | null, private _hostInfo: Implementation, - private _capabilities: McpUiHostCapabilities, + hostCapabilities: McpUiHostCapabilities, options?: HostOptions, ) { - super(options); + const { hostContext, ...serverOptions } = options ?? {}; + // AppBridge proxies tools/resources/prompts/logging between view and MCP + // server. Declare the corresponding Server capabilities so v2's + // assertRequestHandlerCapability / assertNotificationCapability allow + // handler registration and list_changed notifications. (v1 AppBridge + // stubbed these checks as no-ops.) + // NOTE: must not use a constructor param named `_capabilities` — that + // would overwrite Server's MCP capability store. + super(_hostInfo, { + ...serverOptions, + capabilities: { + tools: { listChanged: true }, + resources: { listChanged: true, subscribe: true }, + prompts: { listChanged: true }, + logging: {}, + ...serverOptions.capabilities, + }, + }); + this._hostCapabilities = hostCapabilities; + + // Server declares `oninitialized` as a class field (own data property). + // Delete it so AppBridge's prototype getter/setter for + // `ui/notifications/initialized` is not shadowed. + delete (this as { oninitialized?: unknown }).oninitialized; + Object.defineProperty(this, "oninitialized", { + get: () => this.getEventHandler("initialized"), + set: ( + callback: + | ((params: McpUiInitializedNotification["params"]) => void) + | undefined, + ) => { + this.setEventHandler("initialized", callback); + }, + enumerable: true, + configurable: true, + }); + + this._events = new NotificationEventEmitter( + { + sizechange: { + method: methodOf(McpUiSizeChangedNotificationSchema), + params: paramsSchemaOf(McpUiSizeChangedNotificationSchema), + }, + sandboxready: { + method: methodOf(McpUiSandboxProxyReadyNotificationSchema), + params: paramsSchemaOf(McpUiSandboxProxyReadyNotificationSchema), + }, + initialized: { + method: methodOf(McpUiInitializedNotificationSchema), + params: paramsSchemaOf(McpUiInitializedNotificationSchema), + }, + requestteardown: { + method: methodOf(McpUiRequestTeardownNotificationSchema), + params: paramsSchemaOf(McpUiRequestTeardownNotificationSchema), + }, + loggingmessage: { + method: methodOf(LoggingMessageNotificationSchema), + params: paramsSchemaOf(LoggingMessageNotificationSchema), + }, + }, + (method, paramsSchema, dispatch) => { + this._claims.claim(method); + Server.prototype.setNotificationHandler.call( + this, + method, + { params: paramsSchema }, + (params: unknown) => dispatch(params), + ); + }, + ); this.addEventListener("initialized", () => { this._initializedReceived = true; }); - this._hostContext = options?.hostContext || {}; + this._hostContext = hostContext || {}; - this.setRequestHandler(McpUiInitializeRequestSchema, (request) => - this._oninitialize(request), + this.setRequestHandler( + "ui/initialize", + { + params: paramsSchemaOf(McpUiInitializeRequestSchema), + result: McpUiInitializeResultSchema, + }, + (params: McpUiInitializeRequest["params"]) => + this._onUiInitialize(params), ); - this.setRequestHandler(PingRequestSchema, (request, extra) => { - this.onping?.(request.params, extra); - return {}; - }); + this.setRequestHandler( + "ping", + ( + request: PingRequest, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { + this.onping?.(request.params, toRequestHandlerExtra(ctx)); + return {}; + }, + ); // Default handler for requestDisplayMode - returns current mode from host context. // Hosts can override this by setting bridge.onrequestdisplaymode = ... - this.replaceRequestHandler( - McpUiRequestDisplayModeRequestSchema, - (request) => { + this._replaceRequestHandler( + "ui/request-display-mode", + { + params: paramsSchemaOf(McpUiRequestDisplayModeRequestSchema), + result: McpUiRequestDisplayModeResultSchema, + }, + () => { const currentMode = this._hostContext.displayMode ?? "inline"; return { mode: currentMode }; }, ); } + /** + * Shadowed with double-set protection. Arrow field so Protocol/Server + * constructors (ping, initialize, …) still hit the base implementation. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override setRequestHandler = (...args: any[]): void => { + const method = args[0] as string; + this._claims.assertAndClaim(method, "setRequestHandler"); + (Server.prototype.setRequestHandler as Function).apply(this, args); + }; + + /** + * Shadowed with double-set protection. Arrow field — see setRequestHandler. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override setNotificationHandler = (...args: any[]): void => { + const method = args[0] as string; + this._claims.assertAndClaim(method, "setNotificationHandler"); + (Server.prototype.setNotificationHandler as Function).apply(this, args); + }; + + /** + * Replace a request handler (on* replace semantics), wrapping with the + * initialized-received warning. `ui/initialize` and `ping` use + * setRequestHandler directly and are intentionally exempt. + * + * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _replaceRequestHandler = (...args: any[]): void => { + const method = args[0] as string; + this._claims.claim(method); + const warn = () => { + if (!this._initializedReceived) { + console.warn( + `[ext-apps] AppBridge received '${method}' before ` + + `ui/notifications/initialized. The View is calling host ` + + `methods before completing the handshake; it should await ` + + `app.connect() first.`, + ); + } + }; + if (typeof args[1] === "function") { + const handler = args[1]; + (Server.prototype.setRequestHandler as Function).call( + this, + method, + (request: unknown, ctx: unknown) => { + warn(); + return handler(request, ctx); + }, + ); + } else { + const schemas = args[1]; + const handler = args[2]; + (Server.prototype.setRequestHandler as Function).call( + this, + method, + schemas, + (params: unknown, ctx: unknown) => { + warn(); + return handler(params, ctx); + }, + ); + } + }; + + private setEventHandler( + event: K, + handler: ((params: AppBridgeEventMap[K]) => void) | undefined, + ): void { + this._events.setEventHandler(event, handler); + } + + private getEventHandler( + event: K, + ): ((params: AppBridgeEventMap[K]) => void) | undefined { + return this._events.getEventHandler(event); + } + + addEventListener( + event: K, + handler: (params: AppBridgeEventMap[K]) => void, + ): void { + this._events.addEventListener(event, handler); + } + + removeEventListener( + event: K, + handler: (params: AppBridgeEventMap[K]) => void, + ): void { + this._events.removeEventListener(event, handler); + } + /** * Get the view's capabilities discovered during initialization. * @@ -588,18 +740,17 @@ export class AppBridge extends ProtocolWithEvents< * @see {@link sendToolInput `sendToolInput`} for sending tool arguments to the View * @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. */ - get oninitialized(): - | ((params: McpUiInitializedNotification["params"]) => void) - | undefined { - return this.getEventHandler("initialized"); - } - set oninitialized( - callback: - | ((params: McpUiInitializedNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("initialized", callback); - } + /** + * Called when the view completes initialization. + * + * Implemented via `Object.defineProperty` in the constructor so it does not + * conflict with Server's `oninitialized` class field (MCP + * `notifications/initialized`). Public Apps API unchanged. + * + * @see {@link McpUiInitializedNotification `McpUiInitializedNotification`} for the notification type + * @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead. + */ + declare oninitialized: (() => void) | undefined; /** * Register a handler for message requests from the view. @@ -650,13 +801,20 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); + warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); this._onmessage = callback; - this.replaceRequestHandler( - McpUiMessageRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "ui/message", + { + params: paramsSchemaOf(McpUiMessageRequestSchema), + result: McpUiMessageResultSchema, + }, + ( + params: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onmessage) throw new Error("No onmessage handler set"); - return this._onmessage(request.params, extra); + return this._onmessage(params, toRequestHandlerExtra(ctx)); }, ); } @@ -719,13 +877,20 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); + warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); this._onopenlink = callback; - this.replaceRequestHandler( - McpUiOpenLinkRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "ui/open-link", + { + params: paramsSchemaOf(McpUiOpenLinkRequestSchema), + result: McpUiOpenLinkResultSchema, + }, + ( + params: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onopenlink) throw new Error("No onopenlink handler set"); - return this._onopenlink(request.params, extra); + return this._onopenlink(params, toRequestHandlerExtra(ctx)); }, ); } @@ -787,18 +952,25 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced( + warnIfRequestHandlerReplaced( "ondownloadfile", this._ondownloadfile, callback, ); this._ondownloadfile = callback; - this.replaceRequestHandler( - McpUiDownloadFileRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "ui/download-file", + { + params: paramsSchemaOf(McpUiDownloadFileRequestSchema), + result: McpUiDownloadFileResultSchema, + }, + ( + params: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._ondownloadfile) throw new Error("No ondownloadfile handler set"); - return this._ondownloadfile(request.params, extra); + return this._ondownloadfile(params, toRequestHandlerExtra(ctx)); }, ); } @@ -887,18 +1059,25 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced( + warnIfRequestHandlerReplaced( "onrequestdisplaymode", this._onrequestdisplaymode, callback, ); this._onrequestdisplaymode = callback; - this.replaceRequestHandler( - McpUiRequestDisplayModeRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "ui/request-display-mode", + { + params: paramsSchemaOf(McpUiRequestDisplayModeRequestSchema), + result: McpUiRequestDisplayModeResultSchema, + }, + ( + params: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onrequestdisplaymode) throw new Error("No onrequestdisplaymode handler set"); - return this._onrequestdisplaymode(request.params, extra); + return this._onrequestdisplaymode(params, toRequestHandlerExtra(ctx)); }, ); } @@ -984,18 +1163,25 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced( + warnIfRequestHandlerReplaced( "onupdatemodelcontext", this._onupdatemodelcontext, callback, ); this._onupdatemodelcontext = callback; - this.replaceRequestHandler( - McpUiUpdateModelContextRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "ui/update-model-context", + { + params: paramsSchemaOf(McpUiUpdateModelContextRequestSchema), + result: EmptyResultSchema, + }, + ( + params: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onupdatemodelcontext) throw new Error("No onupdatemodelcontext handler set"); - return this._onupdatemodelcontext(request.params, extra); + return this._onupdatemodelcontext(params, toRequestHandlerExtra(ctx)); }, ); } @@ -1041,13 +1227,16 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); + warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.replaceRequestHandler( - CallToolRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "tools/call", + ( + request: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); - return this._oncalltool(request.params, extra); + return this._oncalltool(request.params, toRequestHandlerExtra(ctx)); }, ); } @@ -1074,7 +1263,11 @@ export class AppBridge extends ProtocolWithEvents< * ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm" * bridge.oncreatesamplingmessage = async (params, extra) => { * // Apply rate limiting, user approval, cost controls here - * return await myLlmProvider.complete(params, { signal: extra.signal }); + * // Cast: host LLM helpers may still be typed against v1 SDK params during Phase 1. + * return await myLlmProvider.complete( + * params as CreateMessageRequest["params"], + * { signal: extra.signal }, + * ); * }; * ``` * @@ -1088,9 +1281,12 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise, ) { this.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - return callback(request.params, extra); + "sampling/createMessage", + async ( + request: CreateMessageRequest, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { + return callback(request.params, toRequestHandlerExtra(ctx)); }, ); } @@ -1162,18 +1358,24 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced( + warnIfRequestHandlerReplaced( "onlistresources", this._onlistresources, callback, ); this._onlistresources = callback; - this.replaceRequestHandler( - ListResourcesRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "resources/list", + ( + request: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onlistresources) throw new Error("No onlistresources handler set"); - return this._onlistresources(request.params, extra); + return this._onlistresources( + request.params, + toRequestHandlerExtra(ctx), + ); }, ); } @@ -1219,18 +1421,24 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced( + warnIfRequestHandlerReplaced( "onlistresourcetemplates", this._onlistresourcetemplates, callback, ); this._onlistresourcetemplates = callback; - this.replaceRequestHandler( - ListResourceTemplatesRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "resources/templates/list", + ( + request: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onlistresourcetemplates) throw new Error("No onlistresourcetemplates handler set"); - return this._onlistresourcetemplates(request.params, extra); + return this._onlistresourcetemplates( + request.params, + toRequestHandlerExtra(ctx), + ); }, ); } @@ -1276,18 +1484,21 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced( + warnIfRequestHandlerReplaced( "onreadresource", this._onreadresource, callback, ); this._onreadresource = callback; - this.replaceRequestHandler( - ReadResourceRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "resources/read", + ( + request: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onreadresource) throw new Error("No onreadresource handler set"); - return this._onreadresource(request.params, extra); + return this._onreadresource(request.params, toRequestHandlerExtra(ctx)); }, ); } @@ -1361,18 +1572,21 @@ export class AppBridge extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced( + warnIfRequestHandlerReplaced( "onlistprompts", this._onlistprompts, callback, ); this._onlistprompts = callback; - this.replaceRequestHandler( - ListPromptsRequestSchema, - async (request, extra) => { + this._replaceRequestHandler( + "prompts/list", + ( + request: any, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onlistprompts) throw new Error("No onlistprompts handler set"); - return this._onlistprompts(request.params, extra); + return this._onlistprompts(request.params, toRequestHandlerExtra(ctx)); }, ); } @@ -1403,46 +1617,6 @@ export class AppBridge extends ProtocolWithEvents< }); } - /** - * Verify that the guest supports the capability required for the given request method. - * @internal - */ - assertCapabilityForMethod(method: AppRequest["method"]): void { - // TODO - } - - /** - * Verify that a request handler is registered and supported for the given method. - * @internal - */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { - // TODO - } - - /** - * Verify that the host supports the capability required for the given notification method. - * @internal - */ - assertNotificationCapability(method: AppNotification["method"]): void { - // TODO - } - - /** - * Verify that task creation is supported for the given request method. - * @internal - */ - protected assertTaskCapability(_method: string): void { - throw new Error("Tasks are not supported in MCP Apps"); - } - - /** - * Verify that task handler is supported for the given method. - * @internal - */ - protected assertTaskHandlerCapability(_method: string): void { - throw new Error("Task handlers are not supported in MCP Apps"); - } - /** * Get the host capabilities passed to the constructor. * @@ -1451,17 +1625,17 @@ export class AppBridge extends ProtocolWithEvents< * @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure */ getCapabilities(): McpUiHostCapabilities { - return this._capabilities; + return this._hostCapabilities; } /** * Handle the ui/initialize request from the guest. * @internal */ - private async _oninitialize( - request: McpUiInitializeRequest, + private async _onUiInitialize( + params: McpUiInitializeRequest["params"], ): Promise { - const requestedVersion = request.params.protocolVersion; + const requestedVersion = params.protocolVersion; if (this._appInfo !== undefined) { console.warn( @@ -1472,8 +1646,8 @@ export class AppBridge extends ProtocolWithEvents< ); } - this._appCapabilities = request.params.appCapabilities; - this._appInfo = request.params.appInfo; + this._appCapabilities = params.appCapabilities; + this._appInfo = params.appInfo; const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes( requestedVersion, @@ -1764,12 +1938,15 @@ export class AppBridge extends ProtocolWithEvents< * @param options - Request options (timeout, abort signal, etc.) * @returns Promise resolving to the tool call result */ - callTool(params: CallToolRequest["params"], options?: RequestOptions) { + callTool( + params: CallToolRequest["params"], + options?: RequestOptions, + ): Promise { return this.request( { method: "tools/call", params }, CallToolResultSchema, options, - ); + ) as Promise; } /** @@ -1781,12 +1958,15 @@ export class AppBridge extends ProtocolWithEvents< * @param options - Request options (timeout, abort signal, etc.) * @returns Promise resolving to the list of tools */ - listTools(params: ListToolsRequest["params"], options?: RequestOptions) { + listTools( + params: ListToolsRequest["params"], + options?: RequestOptions, + ): Promise { return this.request( { method: "tools/list", params }, ListToolsResultSchema, options, - ); + ) as Promise; } /** @@ -1862,7 +2042,7 @@ export class AppBridge extends ProtocolWithEvents< this.oncalltool = async (params, extra) => { return this._client!.request( { method: "tools/call", params }, - CallToolResultSchema, + V1CallToolResultSchema, { signal: extra.signal }, ); }; @@ -1877,21 +2057,21 @@ export class AppBridge extends ProtocolWithEvents< this.onlistresources = async (params, extra) => { return this._client!.request( { method: "resources/list", params }, - ListResourcesResultSchema, + V1ListResourcesResultSchema, { signal: extra.signal }, ); }; this.onlistresourcetemplates = async (params, extra) => { return this._client!.request( { method: "resources/templates/list", params }, - ListResourceTemplatesResultSchema, + V1ListResourceTemplatesResultSchema, { signal: extra.signal }, ); }; this.onreadresource = async (params, extra) => { return this._client!.request( { method: "resources/read", params }, - ReadResourceResultSchema, + V1ReadResourceResultSchema, { signal: extra.signal }, ); }; @@ -1906,7 +2086,7 @@ export class AppBridge extends ProtocolWithEvents< this.onlistprompts = async (params, extra) => { return this._client!.request( { method: "prompts/list", params }, - ListPromptsResultSchema, + V1ListPromptsResultSchema, { signal: extra.signal }, ); }; diff --git a/src/app.ts b/src/app.ts index adfad5c77..e166fa95e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,39 +1,42 @@ import { + Client, + type ClientOptions, + type Implementation, type RequestOptions, - mergeCapabilities, - ProtocolOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; - + type Transport, +} from "@modelcontextprotocol/client"; import { + CreateMessageResultSchema, + CreateMessageResultWithToolsSchema, + EmptyResultSchema, +} from "@modelcontextprotocol/core"; +import type { CallToolRequest, - CallToolRequestSchema, CallToolResult, - CallToolResultSchema, CreateMessageRequest, CreateMessageResult, - CreateMessageResultSchema, CreateMessageResultWithTools, - CreateMessageResultWithToolsSchema, - EmptyResultSchema, - Implementation, ListResourcesRequest, ListResourcesResult, - ListResourcesResultSchema, ListToolsRequest, - ListToolsRequestSchema, ListToolsResult, LoggingMessageNotification, - PingRequestSchema, ReadResourceRequest, ReadResourceResult, - ReadResourceResultSchema, Tool, ToolAnnotations, ToolListChangedNotification, -} from "@modelcontextprotocol/sdk/types.js"; -import { AppNotification, AppRequest, AppResult } from "./types"; -import { ProtocolWithEvents } from "./events"; -export { ProtocolWithEvents }; +} from "./types"; +import { + MethodClaimRegistry, + NotificationEventEmitter, + mergeCapabilities, + methodOf, + paramsSchemaOf, + toRequestHandlerExtra, + warnIfRequestHandlerReplaced, + type RequestHandlerExtra, +} from "./events"; import { PostMessageTransport } from "./message-transport"; import { LATEST_PROTOCOL_VERSION, @@ -55,6 +58,7 @@ import { McpUiResourceTeardownRequest, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResult, + McpUiResourceTeardownResultSchema, McpUiRequestTeardownNotification, McpUiSizeChangedNotification, McpUiToolCancelledNotification, @@ -68,7 +72,6 @@ import { McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeResultSchema, } from "./types"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { StandardSchemaV1, standardSchemaToJsonSchema, @@ -81,6 +84,7 @@ export type { StandardSchemaWithJSON, } from "./standard-schema"; +export type { RequestHandlerExtra } from "./events"; export { PostMessageTransport } from "./message-transport"; export * from "./types"; export { @@ -160,11 +164,11 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; /** * Options for configuring {@link App `App`} behavior. * - * Extends `ProtocolOptions` from the MCP SDK with `App`-specific configuration. + * Extends `ClientOptions` from the MCP TypeScript SDK v2 with `App`-specific configuration. * - * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options + * @see `ClientOptions` from @modelcontextprotocol/client for inherited options */ -export type AppOptions = ProtocolOptions & { +export type AppOptions = ClientOptions & { /** * Automatically report size changes to the host using `ResizeObserver`. * @@ -204,10 +208,6 @@ export type AppOptions = ProtocolOptions & { allowUnsafeEval?: boolean; }; -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; - /** * Result of an app-registered tool callback. When `Out` is provided, * `structuredContent` is required and typed (unless `isError: true`). @@ -271,7 +271,7 @@ export type RegisteredAppTool = { /** * Maps DOM-style event names to their notification `params` types. * - * Used by {@link App `App`} (which extends {@link ProtocolWithEvents `ProtocolWithEvents`}) + * Used by {@link App `App`} (which composes {@link NotificationEventEmitter}) * to provide type-safe `addEventListener` / `removeEventListener` and * singular `on*` handler support. */ @@ -306,13 +306,13 @@ export type AppEventMap = { * * ## Inherited Methods * - * As a subclass of {@link ProtocolWithEvents `ProtocolWithEvents`}, `App` inherits: - * - `setRequestHandler()` - Register handlers for requests from host - * - `setNotificationHandler()` - Register handlers for notifications from host + * As a subclass of v2 `Client`, `App` provides: + * - `setRequestHandler()` - Register handlers for requests from host (shadowed with double-set protection) + * - `setNotificationHandler()` - Register handlers for notifications from host (shadowed with double-set protection) * - `addEventListener()` - Append a listener for a notification event (multi-listener) * - `removeEventListener()` - Remove a previously added listener * - * @see {@link ProtocolWithEvents `ProtocolWithEvents`} for the DOM-model event system + * @see {@link NotificationEventEmitter} for the DOM-model event system * * ## Notification Setters (DOM-model `on*` handlers) * @@ -342,17 +342,14 @@ export type AppEventMap = { * await app.connect(); * ``` */ -export class App extends ProtocolWithEvents< - AppRequest, - AppNotification, - AppResult, - AppEventMap -> { +export class App extends Client { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; private _registeredTools: { [name: string]: RegisteredAppTool } = {}; private _initializedSent = false; + private readonly _claims = new MethodClaimRegistry(); + private readonly _events!: NotificationEventEmitter; /** * Warn if a host-bound method is called before {@link connect `connect`} has @@ -378,14 +375,6 @@ export class App extends ProtocolWithEvents< console.warn(`${msg}. This will throw in a future release.`); } - protected readonly eventSchemas = { - toolinput: McpUiToolInputNotificationSchema, - toolinputpartial: McpUiToolInputPartialNotificationSchema, - toolresult: McpUiToolResultNotificationSchema, - toolcancelled: McpUiToolCancelledNotificationSchema, - hostcontextchanged: McpUiHostContextChangedNotificationSchema, - }; - /** * Events the host typically sends once, shortly after the handshake. * Registering a handler for one of these *after* {@link connect `connect`} @@ -427,29 +416,33 @@ export class App extends ProtocolWithEvents< console.warn(msg); } - protected override setEventHandler( + private setEventHandler( event: K, handler: ((params: AppEventMap[K]) => void) | undefined, ): void { if (handler) this._assertHandlerTiming(event); - super.setEventHandler(event, handler); + this._events.setEventHandler(event, handler); + } + + private getEventHandler( + event: K, + ): ((params: AppEventMap[K]) => void) | undefined { + return this._events.getEventHandler(event); } - override addEventListener( + addEventListener( event: K, handler: (params: AppEventMap[K]) => void, ): void { this._assertHandlerTiming(event); - super.addEventListener(event, handler); + this._events.addEventListener(event, handler); } - protected override onEventDispatch( + removeEventListener( event: K, - params: AppEventMap[K], + handler: (params: AppEventMap[K]) => void, ): void { - if (event === "hostcontextchanged") { - this._hostContext = { ...this._hostContext, ...params }; - } + this._events.removeEventListener(event, handler); } /** @@ -468,35 +461,129 @@ export class App extends ProtocolWithEvents< * ); * ``` */ + /** App UI capabilities — distinct from Client._capabilities. */ + private _appCapabilities: McpUiAppCapabilities; + constructor( private _appInfo: Implementation, - private _capabilities: McpUiAppCapabilities = {}, + appCapabilities: McpUiAppCapabilities = {}, private options: AppOptions = { autoResize: true }, ) { - super(options); + const { + autoResize: _autoResize, + strict: _strict, + allowUnsafeEval: _allowUnsafeEval, + ...clientOptions + } = options; + // NOTE: must not use a constructor param named `_capabilities` — that + // would overwrite Client's MCP capability store. + super(_appInfo, clientOptions); + this._appCapabilities = appCapabilities; + + this._events = new NotificationEventEmitter( + { + toolinput: { + method: methodOf(McpUiToolInputNotificationSchema), + params: paramsSchemaOf(McpUiToolInputNotificationSchema), + }, + toolinputpartial: { + method: methodOf(McpUiToolInputPartialNotificationSchema), + params: paramsSchemaOf(McpUiToolInputPartialNotificationSchema), + }, + toolresult: { + method: methodOf(McpUiToolResultNotificationSchema), + params: paramsSchemaOf(McpUiToolResultNotificationSchema), + }, + toolcancelled: { + method: methodOf(McpUiToolCancelledNotificationSchema), + params: paramsSchemaOf(McpUiToolCancelledNotificationSchema), + }, + hostcontextchanged: { + method: methodOf(McpUiHostContextChangedNotificationSchema), + params: paramsSchemaOf(McpUiHostContextChangedNotificationSchema), + }, + }, + (method, paramsSchema, dispatch) => { + this._claims.claim(method); + Client.prototype.setNotificationHandler.call( + this, + method, + { params: paramsSchema }, + (params: unknown) => dispatch(params), + ); + }, + (event, params) => { + if (event === "hostcontextchanged") { + this._hostContext = { ...this._hostContext, ...params }; + } + }, + ); if (!options.allowUnsafeEval) { z.config({ jitless: true }); } - this.setRequestHandler(PingRequestSchema, (request) => { + this.setRequestHandler("ping", (request: { params?: unknown }) => { console.log("Received ping:", request.params); return {}; }); // Eagerly register the hostcontextchanged event slot so that - // onEventDispatch (which merges into _hostContext) fires even if the + // onDispatch (which merges into _hostContext) fires even if the // user never assigns onhostcontextchanged or calls addEventListener. this.setEventHandler("hostcontextchanged", undefined); } - private registerCapabilities(capabilities: McpUiAppCapabilities): void { + /** + * Shadowed with double-set protection. Arrow field so Protocol's constructor + * (which registers ping/cancelled/progress) still hits the base implementation. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override setRequestHandler = (...args: any[]): void => { + const method = args[0] as string; + this._claims.assertAndClaim(method, "setRequestHandler"); + (Client.prototype.setRequestHandler as Function).apply(this, args); + }; + + /** + * Shadowed with double-set protection. Arrow field — see setRequestHandler. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override setNotificationHandler = (...args: any[]): void => { + const method = args[0] as string; + this._claims.assertAndClaim(method, "setNotificationHandler"); + (Client.prototype.setNotificationHandler as Function).apply(this, args); + }; + + /** + * Replace a request handler, bypassing double-set protection (on* replace semantics). + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _replaceRequestHandler = (...args: any[]): void => { + const method = args[0] as string; + this._claims.claim(method); + (Client.prototype.setRequestHandler as Function).apply(this, args); + }; + + /** Tools capability check formerly in assertRequestHandlerCapability. */ + private _assertToolsCapability(method: string): void { + if (!this._appCapabilities.tools) { + throw new Error( + `Client does not support tool capability (required for ${method})`, + ); + } + } + + private _registerAppCapabilities(capabilities: McpUiAppCapabilities): void { if (this.transport) { throw new Error( "Cannot register capabilities after transport is established", ); } - this._capabilities = mergeCapabilities(this._capabilities, capabilities); + this._appCapabilities = mergeCapabilities( + this._appCapabilities, + capabilities, + ); } registerTool< @@ -519,7 +606,7 @@ export class App extends ProtocolWithEvents< } const app = this; const notify = () => { - if (app._initializedSent && app._capabilities.tools?.listChanged) { + if (app._initializedSent && app._appCapabilities.tools?.listChanged) { void app.sendToolListChanged(); } }; @@ -591,8 +678,8 @@ export class App extends ProtocolWithEvents< // to declare { tools: {} } in the constructor just to use registerTool. // Only do this pre-connect; post-connect the capability was already // advertised (or wasn't) and can't change. - if (!this._capabilities.tools && !this.transport) { - this.registerCapabilities({ tools: { listChanged: true } }); + if (!this._appCapabilities.tools && !this.transport) { + this._registerAppCapabilities({ tools: { listChanged: true } }); } this.ensureToolHandlersInitialized(); @@ -924,7 +1011,7 @@ export class App extends ProtocolWithEvents< * without replacing. * * Notification params are automatically merged into the internal host context - * via {@link onEventDispatch `onEventDispatch`} before any handler or listener + * via the event emitter's onDispatch hook before any handler or listener * fires. This means {@link getHostContext `getHostContext`} will return the * updated values even before your callback runs. * @@ -1002,13 +1089,20 @@ export class App extends ProtocolWithEvents< ) => McpUiResourceTeardownResult | Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); + warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); this._onteardown = callback; - this.replaceRequestHandler( - McpUiResourceTeardownRequestSchema, - (request, extra) => { + this._replaceRequestHandler( + "ui/resource-teardown", + { + params: paramsSchemaOf(McpUiResourceTeardownRequestSchema), + result: McpUiResourceTeardownResultSchema, + }, + ( + params: McpUiResourceTeardownRequest["params"], + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { if (!this._onteardown) throw new Error("No onteardown handler set"); - return this._onteardown(request.params, extra); + return this._onteardown(params, toRequestHandlerExtra(ctx)); }, ); } @@ -1056,12 +1150,19 @@ export class App extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); + warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => { - if (!this._oncalltool) throw new Error("No oncalltool handler set"); - return this._oncalltool(request.params, extra); - }); + this._assertToolsCapability("tools/call"); + this._replaceRequestHandler( + "tools/call", + ( + request: CallToolRequest, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { + if (!this._oncalltool) throw new Error("No oncalltool handler set"); + return this._oncalltool(request.params, toRequestHandlerExtra(ctx)); + }, + ); } /** @@ -1123,78 +1224,19 @@ export class App extends ProtocolWithEvents< ) => Promise) | undefined, ) { - this.warnIfRequestHandlerReplaced( - "onlisttools", - this._onlisttools, - callback, - ); + warnIfRequestHandlerReplaced("onlisttools", this._onlisttools, callback); this._onlisttools = callback; - this.replaceRequestHandler(ListToolsRequestSchema, (request, extra) => { - if (!this._onlisttools) throw new Error("No onlisttools handler set"); - return this._onlisttools(request.params, extra); - }); - } - - /** - * Verify that the host supports the capability required for the given request method. - * @internal - */ - assertCapabilityForMethod(method: AppRequest["method"]): void { - switch (method) { - case "sampling/createMessage": - if (!this._hostCapabilities?.sampling) { - throw new Error( - `Host does not support sampling (required for ${method})`, - ); - } - break; - } - } - - /** - * Verify that the app declared the capability required for the given request method. - * @internal - */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { - switch (method) { - case "tools/call": - case "tools/list": - if (!this._capabilities.tools) { - throw new Error( - `Client does not support tool capability (required for ${method})`, - ); - } - return; - case "ping": - case "ui/resource-teardown": - return; - default: - throw new Error(`No handler for method ${method} registered`); - } - } - - /** - * Verify that the app supports the capability required for the given notification method. - * @internal - */ - assertNotificationCapability(_method: AppNotification["method"]): void { - // TODO - } - - /** - * Verify that task creation is supported for the given request method. - * @internal - */ - protected assertTaskCapability(_method: string): void { - throw new Error("Tasks are not supported in MCP Apps"); - } - - /** - * Verify that task handler is supported for the given method. - * @internal - */ - protected assertTaskHandlerCapability(_method: string): void { - throw new Error("Task handlers are not supported in MCP Apps"); + this._assertToolsCapability("tools/list"); + this._replaceRequestHandler( + "tools/list", + ( + request: ListToolsRequest, + ctx: { sessionId?: string; mcpReq: { signal: AbortSignal } }, + ) => { + if (!this._onlisttools) throw new Error("No onlisttools handler set"); + return this._onlisttools(request.params, toRequestHandlerExtra(ctx)); + }, + ); } /** @@ -1243,18 +1285,18 @@ export class App extends ProtocolWithEvents< `Did you mean: callServerTool({ name: "${params}", arguments: { ... } })?`, ); } - return await this.request( + return (await this.request( { method: "tools/call", params }, - CallToolResultSchema, { // Hosts may interpose long-running or user-interactive steps before the // tool result arrives. Opting in here lets a host heartbeat keep the // request alive past the default timeout; callers can still override. + // v2 RequestOptions: onprogress + resetTimeoutOnProgress (unchanged names). onprogress: () => {}, resetTimeoutOnProgress: true, ...options, }, - ); + )) as CallToolResult; } /** @@ -1302,11 +1344,10 @@ export class App extends ProtocolWithEvents< options?: RequestOptions, ): Promise { this._assertInitialized("readServerResource"); - return await this.request( + return (await this.request( { method: "resources/read", params }, - ReadResourceResultSchema, options, - ); + )) as ReadResourceResult; } /** @@ -1350,11 +1391,10 @@ export class App extends ProtocolWithEvents< options?: RequestOptions, ): Promise { this._assertInitialized("listServerResources"); - return await this.request( + return (await this.request( { method: "resources/list", params }, - ListResourcesResultSchema, options, - ); + )) as ListResourcesResult; } /** @@ -1431,14 +1471,20 @@ export class App extends ProtocolWithEvents< options?: RequestOptions, ): Promise { this._assertInitialized("createSamplingMessage"); + // Moved from v1 assertCapabilityForMethod — check before sending. + if (!this._hostCapabilities?.sampling) { + throw new Error( + "Host does not support sampling (required for sampling/createMessage)", + ); + } const resultSchema = params.tools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema; - return await this.request( + return (await this.request( { method: "sampling/createMessage", params }, resultSchema, options, - ); + )) as CreateMessageResult | CreateMessageResultWithTools; } /** @@ -1953,14 +1999,17 @@ export class App extends ProtocolWithEvents< ); } this._initializedSent = false; + // Public reconnect guard: skip MCP initialize so negotiated protocol + // version stays undefined and Client._shouldDropInbound never arms. + transport.sessionId = transport.sessionId ?? "ext-apps-view-no-mcp-init"; await super.connect(transport); try { const result = await this.request( - { + { method: "ui/initialize", params: { - appCapabilities: this._capabilities, + appCapabilities: this._appCapabilities, appInfo: this._appInfo, protocolVersion: LATEST_PROTOCOL_VERSION, }, diff --git a/src/events.ts b/src/events.ts index 4d17ca4f2..49a33be9a 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,39 +1,12 @@ -import { Protocol } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { - Request, - Notification, - Result, -} from "@modelcontextprotocol/sdk/types.js"; -import { ZodLiteral, ZodObject } from "zod/v4"; - -type MethodSchema = ZodObject<{ method: ZodLiteral }>; - -/** - * Per-event state: a singular `on*` handler (replace semantics) plus a - * listener array (`addEventListener` semantics), mirroring the DOM model - * where `el.onclick` and `el.addEventListener("click", …)` coexist. - */ -interface EventSlot { - onHandler?: ((params: T) => void) | undefined; - listeners: ((params: T) => void)[]; -} - /** - * Intermediate base class that adds DOM-style event support on top of the - * MCP SDK's `Protocol`. - * - * The base `Protocol` class stores one handler per method: - * `setRequestHandler()` and `setNotificationHandler()` replace any existing - * handler for the same method silently. This class introduces a two-channel - * event model inspired by the DOM: + * DOM-style notification event fan-out and double-set protection for + * {@link App} / {@link AppBridge}, composed onto v2 Client/Server rather than + * subclassing Protocol. * * ### Singular `on*` handler (like `el.onclick`) * - * Subclasses expose `get`/`set` pairs that delegate to - * {@link setEventHandler `setEventHandler`} / - * {@link getEventHandler `getEventHandler`}. Assigning replaces the previous - * handler; assigning `undefined` clears it. `addEventListener` listeners are - * unaffected. + * Assigning replaces the previous handler; assigning `undefined` clears it. + * `addEventListener` listeners are unaffected. * * ### Multi-listener (`addEventListener` / `removeEventListener`) * @@ -42,55 +15,138 @@ interface EventSlot { * * ### Dispatch order * - * When a notification arrives for a mapped event: - * 1. {@link onEventDispatch `onEventDispatch`} (subclass side-effects) + * 1. Optional `onDispatch` side-effects (e.g. merge host context) * 2. The singular `on*` handler (if set) * 3. All `addEventListener` listeners in insertion order * * ### Double-set protection * - * Direct calls to {@link setRequestHandler `setRequestHandler`} / - * {@link setNotificationHandler `setNotificationHandler`} throw if a handler - * for the same method has already been registered (through any path), so - * accidental overwrites surface as errors instead of silent bugs. + * {@link MethodClaimRegistry} tracks methods claimed by event registration or + * internal handlers. App/AppBridge shadow `setRequestHandler` / + * `setNotificationHandler` to throw when the same method is claimed twice. + */ + +import type { StandardSchemaV1 } from "@modelcontextprotocol/client"; + +/** + * Request-handler context passed to App/AppBridge `on*` request callbacks. * - * @typeParam EventMap - Maps event names to the listener's `params` type. + * Preserves the v1-era `extra.signal` surface used by hosts and examples. + * Mapped from v2 `ctx` via {@link toRequestHandlerExtra}. */ -export abstract class ProtocolWithEvents< - SendRequestT extends Request, - SendNotificationT extends Notification, - SendResultT extends Result, - EventMap extends Record, -> extends Protocol { - private _registeredMethods = new Set(); - private _eventSlots = new Map(); +export type RequestHandlerExtra = { + signal: AbortSignal; + sessionId?: string; +}; - /** - * Event name → notification schema. Subclasses populate this so that - * the event system can lazily register a dispatcher with the correct - * schema on first use. - */ - protected abstract readonly eventSchemas: { - [K in keyof EventMap]: MethodSchema; +/** + * Adapt v2 handler context to the public `extra` shape. + */ +export function toRequestHandlerExtra(ctx: { + sessionId?: string; + mcpReq: { signal: AbortSignal }; +}): RequestHandlerExtra { + return { + signal: ctx.mcpReq.signal, + sessionId: ctx.sessionId, }; +} + +/** + * Deep-merge capability objects (replaces v1 `mergeCapabilities`). + */ +export function mergeCapabilities(base: T, additional: T): T { + const result = { ...(base as Record) }; + for (const [key, value] of Object.entries(additional)) { + const existing = result[key]; + if ( + existing && + typeof existing === "object" && + !Array.isArray(existing) && + value && + typeof value === "object" && + !Array.isArray(value) + ) { + result[key] = mergeCapabilities( + existing as Record, + value as Record, + ); + } else { + result[key] = value; + } + } + return result as T; +} + +/** + * Tracks JSON-RPC methods claimed by event registration or internal handlers + * so accidental double-registration via `setRequestHandler` / + * `setNotificationHandler` throws instead of silently replacing. + */ +export class MethodClaimRegistry { + private readonly _methods = new Set(); + + /** Record a claim without throwing (replace / first-register paths). */ + claim(method: string): void { + this._methods.add(method); + } + + /** Throw if already claimed, then claim. */ + assertAndClaim(method: string, via: string): void { + if (this._methods.has(method)) { + throw new Error( + `Handler for "${method}" already registered (via ${via}). ` + + `Use addEventListener() to attach multiple listeners, ` + + `or the on* setter for replace semantics.`, + ); + } + this._methods.add(method); + } - /** - * Called once per incoming notification, before any handlers or listeners - * fire. Subclasses may override to perform side effects such as merging - * notification params into cached state. - */ - protected onEventDispatch( - _event: K, - _params: EventMap[K], - ): void {} + has(method: string): boolean { + return this._methods.has(method); + } +} + +interface EventSlot { + onHandler?: ((params: T) => void) | undefined; + listeners: ((params: T) => void)[]; +} - // ── Event system (DOM model) ──────────────────────────────────────── +export type EventMethodSchema = { + method: string; + params: StandardSchemaV1; +}; + +/** + * Registers one v2 notification handler per event method and fans out to + * `on*` + `addEventListener` listeners. + */ +export class NotificationEventEmitter< + EventMap extends Record, +> { + private readonly _eventSlots = new Map(); + + constructor( + private readonly _eventSchemas: { + [K in keyof EventMap]: EventMethodSchema; + }, + /** + * Called once per event on first use. Must register a v2 + * `setNotificationHandler(method, { params }, handler)` and claim the + * method in the caller's {@link MethodClaimRegistry}. + */ + private readonly _registerDispatcher: ( + method: string, + paramsSchema: StandardSchemaV1, + dispatch: (params: unknown) => void, + ) => void, + private readonly _onDispatch?: ( + event: K, + params: EventMap[K], + ) => void, + ) {} - /** - * Lazily create the event slot and register a single dispatcher with the - * base `Protocol`. The dispatcher fans out to the `on*` handler and all - * `addEventListener` listeners. - */ private _ensureEventSlot( event: K, ): EventSlot { @@ -98,39 +154,25 @@ export abstract class ProtocolWithEvents< | EventSlot | undefined; if (!slot) { - const schema = this.eventSchemas[event]; + const schema = this._eventSchemas[event]; if (!schema) { throw new Error(`Unknown event: ${String(event)}`); } slot = { listeners: [] }; this._eventSlots.set(event, slot as EventSlot); - // Claim this method so direct setNotificationHandler calls throw. - const method = schema.shape.method.value; - this._registeredMethods.add(method); - - const s = slot; // stable reference for the closure - super.setNotificationHandler(schema, (n) => { - const params = (n as { params: EventMap[K] }).params; - this.onEventDispatch(event, params); - // 1. Singular on* handler - s.onHandler?.(params); - // 2. addEventListener listeners — snapshot to tolerate removal during - // dispatch (e.g., a listener that calls removeEventListener on itself) - for (const l of [...s.listeners]) l(params); + const s = slot; + this._registerDispatcher(schema.method, schema.params, (params) => { + const p = params as EventMap[K]; + this._onDispatch?.(event, p); + s.onHandler?.(p); + for (const l of [...s.listeners]) l(p); }); } return slot; } - /** - * Set or clear the singular `on*` handler for an event. - * - * Replace semantics — like the DOM's `el.onclick = fn`. Assigning - * `undefined` clears the handler without affecting `addEventListener` - * listeners. - */ - protected setEventHandler( + setEventHandler( event: K, handler: ((params: EventMap[K]) => void) | undefined, ): void { @@ -144,31 +186,13 @@ export abstract class ProtocolWithEvents< slot.onHandler = handler; } - /** - * Get the singular `on*` handler for an event, or `undefined` if none is - * set. `addEventListener` listeners are not reflected here. - */ - protected getEventHandler( + getEventHandler( event: K, ): ((params: EventMap[K]) => void) | undefined { return (this._eventSlots.get(event) as EventSlot | undefined) ?.onHandler; } - /** - * Add a listener for a notification event. - * - * Unlike the singular `on*` handler, calling this multiple times appends - * listeners rather than replacing them. All registered listeners fire in - * insertion order after the `on*` handler when the notification arrives. - * - * Registration is lazy: the first call (for a given event, from either - * this method or the `on*` setter) registers a dispatcher with the base - * `Protocol`. - * - * @param event - Event name (a key of the `EventMap` type parameter). - * @param handler - Listener invoked with the notification `params`. - */ addEventListener( event: K, handler: (params: EventMap[K]) => void, @@ -176,11 +200,6 @@ export abstract class ProtocolWithEvents< this._ensureEventSlot(event).listeners.push(handler); } - /** - * Remove a previously registered event listener. The dispatcher stays - * registered even if the listener array becomes empty; future - * notifications simply have no listeners to call. - */ removeEventListener( event: K, handler: (params: EventMap[K]) => void, @@ -192,88 +211,44 @@ export abstract class ProtocolWithEvents< const idx = slot.listeners.indexOf(handler); if (idx !== -1) slot.listeners.splice(idx, 1); } +} - // ── Handler registration with double-set protection ───────────────── - - // The two overrides below are arrow-function class fields rather than - // prototype methods so that Protocol's constructor — which registers its - // own ping/cancelled/progress handlers via `this.setRequestHandler` - // before our fields initialize — hits the base implementation and skips - // tracking. Converting these to proper methods would crash with - // `_registeredMethods` undefined during super(). - - /** - * Registers a request handler. Throws if a handler for the same method - * has already been registered — use the `on*` setter (replace semantics) - * or `addEventListener` (multi-listener) for notification events. - * - * @throws {Error} if a handler for this method is already registered. - */ - override setRequestHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setRequestHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setRequestHandler"); - super.setRequestHandler(schema, handler); - }; - - /** - * Registers a notification handler. Throws if a handler for the same - * method has already been registered — use the `on*` setter (replace - * semantics) or `addEventListener` (multi-listener) for mapped events. - * - * @throws {Error} if a handler for this method is already registered. - */ - override setNotificationHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setNotificationHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setNotificationHandler"); - super.setNotificationHandler(schema, handler); - }; - - /** - * Warn if a request handler `on*` setter is replacing a previously-set - * handler. Call from each request setter before updating the backing field. - */ - protected warnIfRequestHandlerReplaced( - name: string, - previous: unknown, - next: unknown, - ): void { - if (previous && next) { - console.warn( - `[MCP Apps] ${name} handler replaced. ` + - `Previous handler will no longer be called.`, - ); - } +/** + * Warn if a request-handler `on*` setter is replacing a previously-set + * handler. Call from each request setter before updating the backing field. + */ +export function warnIfRequestHandlerReplaced( + name: string, + previous: unknown, + next: unknown, +): void { + if (previous && next) { + console.warn( + `[MCP Apps] ${name} handler replaced. ` + + `Previous handler will no longer be called.`, + ); } +} - /** - * Replace a request handler, bypassing double-set protection. Used by - * `on*` request-handler setters that need replace semantics. - */ - protected replaceRequestHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setRequestHandler"] = (schema, handler) => { - const method = (schema as MethodSchema).shape.method.value; - this._registeredMethods.add(method); - super.setRequestHandler(schema, handler); - }; +/** + * Extract the `params` Zod schema from a generated `{ method, params }` schema. + */ +export function paramsSchemaOf< + S extends { shape: { params: StandardSchemaV1 } }, +>(schema: S): S["shape"]["params"] { + return schema.shape.params; +} - private _assertMethodNotRegistered(schema: unknown, via: string): void { - const method = (schema as MethodSchema).shape.method.value; - if (this._registeredMethods.has(method)) { - throw new Error( - `Handler for "${method}" already registered (via ${via}). ` + - `Use addEventListener() to attach multiple listeners, ` + - `or the on* setter for replace semantics.`, - ); - } - this._registeredMethods.add(method); +/** + * Extract the literal method string from a generated `{ method, params }` schema. + */ +export function methodOf< + S extends { shape: { method: { value?: string; values?: Set } } }, +>(schema: S): string { + const method = schema.shape.method; + if (typeof method.value === "string") return method.value; + if (method.values && method.values.size > 0) { + return method.values.values().next().value as string; } + throw new Error("Could not extract method literal from schema"); } diff --git a/src/types.ts b/src/types.ts index 7fc6b7188..ea6472cc2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -158,9 +158,37 @@ import { ReadResourceRequest, ReadResourceResult, ResourceListChangedNotification, + Tool, + ToolAnnotations, ToolListChangedNotification, } from "@modelcontextprotocol/sdk/types.js"; +export type { + CallToolRequest, + CallToolResult, + CreateMessageRequest, + CreateMessageResult, + CreateMessageResultWithTools, + EmptyResult, + ListPromptsRequest, + ListPromptsResult, + ListResourcesRequest, + ListResourcesResult, + ListResourceTemplatesRequest, + ListResourceTemplatesResult, + ListToolsRequest, + ListToolsResult, + LoggingMessageNotification, + PingRequest, + PromptListChangedNotification, + ReadResourceRequest, + ReadResourceResult, + ResourceListChangedNotification, + Tool, + ToolAnnotations, + ToolListChangedNotification, +}; + /** * All request types in the MCP Apps protocol. * diff --git a/src/v2-migration-spike.test.ts b/src/v2-migration-spike.test.ts index 7139dcb24..107a6ccb5 100644 --- a/src/v2-migration-spike.test.ts +++ b/src/v2-migration-spike.test.ts @@ -367,7 +367,7 @@ function createLinkedPostMessagePair(): { postMessage(data: unknown) { // View → host: event.source must be the view window. ( - globalThis as { window: ReturnType } + globalThis as unknown as { window: ReturnType } ).window.dispatch("message", { source: viewWindow, data }); }, }; @@ -375,7 +375,7 @@ function createLinkedPostMessagePair(): { postMessage(data: unknown) { // Host → view: event.source must be the host window. ( - globalThis as { window: ReturnType } + globalThis as unknown as { window: ReturnType } ).window.dispatch("message", { source: hostWindow, data }); }, }; From 6067c8d721bc21ef813efb098631229fe13b9139 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Fri, 10 Jul 2026 14:00:43 -0700 Subject: [PATCH 3/7] refactor: move types and composed MCP client to v2 SDK (phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppBridge's composed host->MCP-server client is now the v2 @modelcontextprotocol/client Client (breaking: constructor type), with proxying via schema-less request() to preserve single-page list forwarding. All protocol TypeScript types now come from v2 exports (names unchanged), removing every v1 import from src/ outside src/server/ (phase 4). PostMessageTransport runtime validation moved to @modelcontextprotocol/core. The generator keeps a narrow, type-level z.ZodType re-binding: core does not export the JSON type names its schema types reference (TS4023 on declaration emit) — tracked as a TODO(sdk-v2) with an upstream issue drafted. --- examples/basic-host/package.json | 1 + examples/basic-host/src/implementation.ts | 14 ++-- examples/basic-host/src/index.tsx | 2 +- examples/basic-server-react/src/mcp-app.tsx | 14 +--- package-lock.json | 2 +- scripts/generate-schemas.ts | 45 ++++++----- src/app-bridge.examples.ts | 33 +++----- src/app-bridge.test.ts | 12 ++- src/app-bridge.ts | 88 ++++++++------------- src/app.examples.ts | 9 +-- src/app.ts | 4 +- src/generated/schema.ts | 2 +- src/message-transport.test.ts | 2 +- src/message-transport.ts | 14 ++-- src/react/useApp.tsx | 3 +- src/server/index.ts | 2 +- src/spec.types.ts | 2 +- src/types.ts | 6 +- 18 files changed, 115 insertions(+), 140 deletions(-) diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 1b45dcf18..d7c284c02 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -11,6 +11,7 @@ "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 31e36983e..5ef11d88b 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -1,8 +1,12 @@ import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js"; +import { + Client, + SSEClientTransport, + StreamableHTTPClientTransport, + type CallToolResult, + type Resource, + type Tool, +} from "@modelcontextprotocol/client"; import { getTheme, onThemeChange } from "./theme"; import { HOST_STYLE_VARIABLES } from "./host-styles"; @@ -94,7 +98,7 @@ export function callTool( input: Record, ): ToolCallInfo { log.info("Calling tool", name, "with input", input); - const resultPromise = serverInfo.client.callTool({ name, arguments: input }) as Promise; + const resultPromise = serverInfo.client.callTool({ name, arguments: input }); const tool = serverInfo.tools.get(name); if (!tool) { diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 3d488a792..00c0d52b2 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -1,5 +1,5 @@ import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge"; -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { Tool } from "@modelcontextprotocol/client"; import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation"; diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx index 57995008d..6ddecc114 100644 --- a/examples/basic-server-react/src/mcp-app.tsx +++ b/examples/basic-server-react/src/mcp-app.tsx @@ -1,19 +1,13 @@ /** * @file App that demonstrates a few features using MCP Apps SDK + React. */ -import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; +import type { App, CallToolResult, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; import { StrictMode, useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import styles from "./mcp-app.module.css"; -type ToolResult = { - content?: Array<{ type: string; text?: string }>; - structuredContent?: unknown; - isError?: boolean; -}; - -function extractTime(callToolResult: ToolResult): string { +function extractTime(callToolResult: CallToolResult): string { const sc = callToolResult.structuredContent as { time?: string } | undefined; if (sc?.time) return sc.time; const { text } = callToolResult.content?.find((c) => c.type === "text") ?? {}; @@ -22,7 +16,7 @@ function extractTime(callToolResult: ToolResult): string { function GetTimeApp() { - const [toolResult, setToolResult] = useState(null); + const [toolResult, setToolResult] = useState(null); const [hostContext, setHostContext] = useState(); // `useApp` (1) creates an `App` instance, (2) calls `onAppCreated` to @@ -71,7 +65,7 @@ function GetTimeApp() { interface GetTimeAppInnerProps { app: App; - toolResult: ToolResult | null; + toolResult: CallToolResult | null; hostContext?: McpUiHostContext; } function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) { diff --git a/package-lock.json b/package-lock.json index 87e27c67a..e7284288e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "name": "@modelcontextprotocol/ext-apps-basic-host", "version": "1.7.4", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", @@ -2182,7 +2183,6 @@ "version": "2.0.0-beta.3", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.3.tgz", "integrity": "sha512-o9z9YCGNxWNdklPdjcD0tr8pocC9OgjdArqgx3OjPMlTe0M/SSPC2olOJs5ueq77HSpz36mND0f0KteqEhKF3A==", - "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.5", diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index ad626f764..21c4ba819 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -22,17 +22,24 @@ * * **Problem**: ts-to-zod cannot resolve types imported from external packages. * When it encounters types like `ContentBlock`, `CallToolResult`, `Implementation`, - * `RequestId`, and `Tool` from `@modelcontextprotocol/sdk`, it generates `z.any()` - * as a placeholder. + * `RequestId`, and `Tool` from `@modelcontextprotocol/client`, it generates + * `z.any()` as a placeholder. * - * **Solution**: Import the schemas from `@modelcontextprotocol/core` (v2 public - * Zod-schema package) and remove the z.any() placeholders. Cast each imported - * schema to `z.ZodType` using the still-v1 TypeScript types from - * `@modelcontextprotocol/sdk/types.js`, so composed `z.infer` types stay - * assignable to `spec.types.ts` until the type migration phase. Without the - * cast, v2 inferred shapes (e.g. `Tool.inputSchema.properties` as - * `Record`) diverge from v1 types and break declaration - * emit (`JSONObject` cannot be named) plus schema integration tests. + * **Solution**: Import the matching schemas from `@modelcontextprotocol/core` + * (v2 public Zod-schema package) and remove the z.any() placeholders. Cast each + * imported schema to `z.ZodType` using the matching TypeScript types from + * `@modelcontextprotocol/client`. Without the cast, core's inferred shapes + * reference `JSONObject` (and similar) that TypeScript cannot name when emitting + * `.d.ts` for composed schemas (`TS4023`). + * + * TODO(sdk-v2): remove the `z.ZodType` re-binding once + * `@modelcontextprotocol/core` exports its JSON type names + * (`JSONObject` / `JSONValue` / `JSONArray`) — they are declared in core's + * `.d.mts` but not exported, which is what makes them unnameable downstream. + * `@modelcontextprotocol/client` already exports the same names, so this is a + * small upstream fix; an issue draft is tracked in the ext-apps v2 migration + * notes. If upstream declines, revisit this layer's architecture (e.g. emit + * param-level schemas locally instead of composing core's). * * * ### 3. Index Signatures (`z.record().and()` → `z.object().passthrough()`) @@ -79,8 +86,10 @@ const JSON_SCHEMA_OUTPUT_FILE = join(GENERATED_DIR, "schema.json"); /** * External types from MCP that ts-to-zod can't resolve. * With PascalCase naming (via getSchemaName), generated placeholders match - * `@modelcontextprotocol/core` exports. Values come from core; TypeScript - * types still come from the v1 SDK until a later migration phase. + * `@modelcontextprotocol/core` exports. Schema values come from core; TypeScript + * types come from `@modelcontextprotocol/client` (same v2 surface as + * `spec.types.ts`). Schemas are cast to `z.ZodType` so declaration emit + * does not try to name core-internal aliases like `JSONObject` (TS4023). */ const EXTERNAL_TYPE_SCHEMAS = [ { schema: "ContentBlockSchema", type: "ContentBlock" }, @@ -95,8 +104,8 @@ const EXTERNAL_TYPE_SCHEMAS = [ /** Public Zod-schema package for MCP protocol schemas (v2). */ const MCP_SCHEMA_PACKAGE = "@modelcontextprotocol/core"; -/** v1 types package — still the source of TypeScript types in spec.types.ts. */ -const MCP_V1_TYPES_PACKAGE = "@modelcontextprotocol/sdk/types.js"; +/** v2 TypeScript types package — matches imports in spec.types.ts. */ +const MCP_TYPES_PACKAGE = "@modelcontextprotocol/client"; async function main() { console.log("🔧 Generating Zod schemas from spec.types.ts...\n"); @@ -204,9 +213,9 @@ function postProcess(content: string): string { // zod/v4 aligns with the SDK's own zod import — composing v3 and v4 // schema instances throws at parse time. See header comment for details. // - // Import schemas from @modelcontextprotocol/core (runtime validation), then - // re-bind as z.ZodType so z.infer stays compatible with v1 TypeScript - // types still used in spec.types.ts. Full type migration is a later phase. + // Import schemas from @modelcontextprotocol/core, then re-bind as + // z.ZodType with T from @modelcontextprotocol/client so .d.ts emit does + // not try to name core-internal aliases (JSONObject → TS4023). const typeImports = EXTERNAL_TYPE_SCHEMAS.map((e) => e.type).join(",\n "); const schemaImports = EXTERNAL_TYPE_SCHEMAS.map( (e) => `${e.schema} as ${e.schema}FromCore`, @@ -221,7 +230,7 @@ function postProcess(content: string): string { `import { z } from "zod/v4"; import type { ${typeImports}, -} from "${MCP_V1_TYPES_PACKAGE}"; +} from "${MCP_TYPES_PACKAGE}"; import { ${schemaImports}, } from "${MCP_SCHEMA_PACKAGE}"; diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index f9bdf18cc..ca37d56c9 100644 --- a/src/app-bridge.examples.ts +++ b/src/app-bridge.examples.ts @@ -7,17 +7,12 @@ * @module */ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { - CallToolResult, - CallToolResultSchema, - CreateMessageRequest, - CreateMessageResult, - ListResourcesResultSchema, - ReadResourceResultSchema, - ListPromptsResultSchema, -} from "@modelcontextprotocol/sdk/types.js"; + Client, + type Transport, + type CreateMessageRequest, + type CreateMessageResult, +} from "@modelcontextprotocol/client"; import { AppBridge, PostMessageTransport } from "./app-bridge.js"; import type { McpUiDisplayMode } from "./types.js"; @@ -223,7 +218,6 @@ function AppBridge_oncalltool_forwardToServer( bridge.oncalltool = async (params, extra) => { return mcpClient.request( { method: "tools/call", params }, - CallToolResultSchema, { signal: extra.signal }, ); }; @@ -245,11 +239,7 @@ function AppBridge_oncreatesamplingmessage_forwardToLlm( //#region AppBridge_oncreatesamplingmessage_forwardToLlm bridge.oncreatesamplingmessage = async (params, extra) => { // Apply rate limiting, user approval, cost controls here - // Cast: host LLM helpers may still be typed against v1 SDK params during Phase 1. - return await myLlmProvider.complete( - params as CreateMessageRequest["params"], - { signal: extra.signal }, - ); + return await myLlmProvider.complete(params, { signal: extra.signal }); }; //#endregion AppBridge_oncreatesamplingmessage_forwardToLlm } @@ -265,7 +255,6 @@ function AppBridge_onlistresources_returnResources( bridge.onlistresources = async (params, extra) => { return mcpClient.request( { method: "resources/list", params }, - ListResourcesResultSchema, { signal: extra.signal }, ); }; @@ -283,7 +272,6 @@ function AppBridge_onreadresource_returnResource( bridge.onreadresource = async (params, extra) => { return mcpClient.request( { method: "resources/read", params }, - ReadResourceResultSchema, { signal: extra.signal }, ); }; @@ -301,7 +289,6 @@ function AppBridge_onlistprompts_returnPrompts( bridge.onlistprompts = async (params, extra) => { return mcpClient.request( { method: "prompts/list", params }, - ListPromptsResultSchema, { signal: extra.signal }, ); }; @@ -448,10 +435,10 @@ async function AppBridge_sendToolResult_afterExecution( args: Record, ) { //#region AppBridge_sendToolResult_afterExecution - const result = await mcpClient.request( - { method: "tools/call", params: { name: "get_weather", arguments: args } }, - CallToolResultSchema, - ); + const result = await mcpClient.request({ + method: "tools/call", + params: { name: "get_weather", arguments: args }, + }); bridge.sendToolResult(result); //#endregion AppBridge_sendToolResult_afterExecution } diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 4a1dcf147..0a26b5455 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; import { InMemoryTransport } from "@modelcontextprotocol/client"; -import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js"; +import type { Client, ServerCapabilities } from "@modelcontextprotocol/client"; import { EmptyResultSchema, ListPromptsResultSchema, @@ -31,11 +30,18 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); */ function createMockClient( serverCapabilities: ServerCapabilities = {}, -): Pick { +): Pick< + Client, + | "getServerCapabilities" + | "request" + | "notification" + | "setNotificationHandler" +> { return { getServerCapabilities: () => serverCapabilities, request: async () => ({}) as never, notification: async () => {}, + setNotificationHandler: () => {}, }; } diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 9477f34fc..a8050f3a7 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1,21 +1,10 @@ -import { Client as V1Client } from "@modelcontextprotocol/sdk/client/index.js"; import { - CallToolResultSchema as V1CallToolResultSchema, - ListPromptsResultSchema as V1ListPromptsResultSchema, - ListResourcesResultSchema as V1ListResourcesResultSchema, - ListResourceTemplatesResultSchema as V1ListResourceTemplatesResultSchema, - PromptListChangedNotificationSchema, - ReadResourceResultSchema as V1ReadResourceResultSchema, - ResourceListChangedNotificationSchema, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { - Server, - type ServerOptions, + Client, type Implementation, type RequestOptions, type Transport, -} from "@modelcontextprotocol/server"; +} from "@modelcontextprotocol/client"; +import { Server, type ServerOptions } from "@modelcontextprotocol/server"; import type { CallToolRequest, CallToolResult, @@ -358,7 +347,7 @@ export class AppBridge extends Server { private _hostCapabilities: McpUiHostCapabilities; constructor( - private _client: V1Client | null, + private _client: Client | null, private _hostInfo: Implementation, hostCapabilities: McpUiHostCapabilities, options?: HostOptions, @@ -1203,14 +1192,13 @@ export class AppBridge extends Server { * bridge.oncalltool = async (params, extra) => { * return mcpClient.request( * { method: "tools/call", params }, - * CallToolResultSchema, * { signal: extra.signal }, * ); * }; * ``` * - * @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type - * @see `CallToolResult` from @modelcontextprotocol/sdk for the result type + * @see `CallToolRequest` from @modelcontextprotocol/client for the request type + * @see `CallToolResult` from @modelcontextprotocol/client for the result type */ private _oncalltool?: ( params: CallToolRequest["params"], @@ -1263,16 +1251,12 @@ export class AppBridge extends Server { * ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm" * bridge.oncreatesamplingmessage = async (params, extra) => { * // Apply rate limiting, user approval, cost controls here - * // Cast: host LLM helpers may still be typed against v1 SDK params during Phase 1. - * return await myLlmProvider.complete( - * params as CreateMessageRequest["params"], - * { signal: extra.signal }, - * ); + * return await myLlmProvider.complete(params, { signal: extra.signal }); * }; * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types */ set oncreatesamplingmessage( callback: ( @@ -1303,12 +1287,12 @@ export class AppBridge extends Server { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/tools/list_changed", () => { * bridge.sendToolListChanged(); * }); * ``` * - * @see `ToolListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `ToolListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { return this.notification({ @@ -1334,14 +1318,13 @@ export class AppBridge extends Server { * bridge.onlistresources = async (params, extra) => { * return mcpClient.request( * { method: "resources/list", params }, - * ListResourcesResultSchema, * { signal: extra.signal }, * ); * }; * ``` * - * @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type + * @see `ListResourcesRequest` from @modelcontextprotocol/client for the request type + * @see `ListResourcesResult` from @modelcontextprotocol/client for the result type */ private _onlistresources?: ( params: ListResourcesRequest["params"], @@ -1403,8 +1386,8 @@ export class AppBridge extends Server { * }; * ``` * - * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListResourceTemplatesResult` from @modelcontextprotocol/sdk for the result type + * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client for the request type + * @see `ListResourceTemplatesResult` from @modelcontextprotocol/client for the result type */ private _onlistresourcetemplates?: ( params: ListResourceTemplatesRequest["params"], @@ -1460,14 +1443,13 @@ export class AppBridge extends Server { * bridge.onreadresource = async (params, extra) => { * return mcpClient.request( * { method: "resources/read", params }, - * ReadResourceResultSchema, * { signal: extra.signal }, * ); * }; * ``` * - * @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type - * @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type + * @see `ReadResourceRequest` from @modelcontextprotocol/client for the request type + * @see `ReadResourceResult` from @modelcontextprotocol/client for the result type */ private _onreadresource?: ( params: ReadResourceRequest["params"], @@ -1515,12 +1497,12 @@ export class AppBridge extends Server { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/resources/list_changed", () => { * bridge.sendResourceListChanged(); * }); * ``` * - * @see `ResourceListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `ResourceListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendResourceListChanged( params: ResourceListChangedNotification["params"] = {}, @@ -1548,14 +1530,13 @@ export class AppBridge extends Server { * bridge.onlistprompts = async (params, extra) => { * return mcpClient.request( * { method: "prompts/list", params }, - * ListPromptsResultSchema, * { signal: extra.signal }, * ); * }; * ``` * - * @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type + * @see `ListPromptsRequest` from @modelcontextprotocol/client for the request type + * @see `ListPromptsResult` from @modelcontextprotocol/client for the result type */ private _onlistprompts?: ( params: ListPromptsRequest["params"], @@ -1603,12 +1584,12 @@ export class AppBridge extends Server { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/prompts/list_changed", () => { * bridge.sendPromptListChanged(); * }); * ``` * - * @see `PromptListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `PromptListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { return this.notification({ @@ -1810,10 +1791,10 @@ export class AppBridge extends Server { * * @example * ```ts source="./app-bridge.examples.ts#AppBridge_sendToolResult_afterExecution" - * const result = await mcpClient.request( - * { method: "tools/call", params: { name: "get_weather", arguments: args } }, - * CallToolResultSchema, - * ); + * const result = await mcpClient.request({ + * method: "tools/call", + * params: { name: "get_weather", arguments: args }, + * }); * bridge.sendToolResult(result); * ``` * @@ -2040,44 +2021,44 @@ export class AppBridge extends Server { if (serverCapabilities.tools) { this.oncalltool = async (params, extra) => { + // Use request() (not callTool) so options like signal forward 1:1. return this._client!.request( { method: "tools/call", params }, - V1CallToolResultSchema, { signal: extra.signal }, ); }; if (serverCapabilities.tools.listChanged) { this._client.setNotificationHandler( - ToolListChangedNotificationSchema, + "notifications/tools/list_changed", (n) => this.sendToolListChanged(n.params), ); } } if (serverCapabilities.resources) { + // Prefer request() over listResources/listResourceTemplates so a + // missing cursor is forwarded as a single page (v2 list* verbs + // auto-aggregate when cursor is absent). this.onlistresources = async (params, extra) => { return this._client!.request( { method: "resources/list", params }, - V1ListResourcesResultSchema, { signal: extra.signal }, ); }; this.onlistresourcetemplates = async (params, extra) => { return this._client!.request( { method: "resources/templates/list", params }, - V1ListResourceTemplatesResultSchema, { signal: extra.signal }, ); }; this.onreadresource = async (params, extra) => { return this._client!.request( { method: "resources/read", params }, - V1ReadResourceResultSchema, { signal: extra.signal }, ); }; if (serverCapabilities.resources.listChanged) { this._client.setNotificationHandler( - ResourceListChangedNotificationSchema, + "notifications/resources/list_changed", (n) => this.sendResourceListChanged(n.params), ); } @@ -2086,13 +2067,12 @@ export class AppBridge extends Server { this.onlistprompts = async (params, extra) => { return this._client!.request( { method: "prompts/list", params }, - V1ListPromptsResultSchema, { signal: extra.signal }, ); }; if (serverCapabilities.prompts.listChanged) { this._client.setNotificationHandler( - PromptListChangedNotificationSchema, + "notifications/prompts/list_changed", (n) => this.sendPromptListChanged(n.params), ); } diff --git a/src/app.examples.ts b/src/app.examples.ts index 81bdd4440..15dadd802 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -7,11 +7,8 @@ * @module */ -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; -import type { - McpServer, - ToolCallback, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Tool, SamplingMessage } from "@modelcontextprotocol/client"; +import type { McpServer, ToolCallback } from "./server/index.js"; import { App, PostMessageTransport, @@ -369,7 +366,7 @@ async function App_createSamplingMessage_simple(app: App) { */ async function App_createSamplingMessage_withTools( app: App, - messages: import("@modelcontextprotocol/sdk/types.js").SamplingMessage[], + messages: SamplingMessage[], ) { //#region App_createSamplingMessage_withTools if (!app.getHostCapabilities()?.sampling?.tools) return; diff --git a/src/app.ts b/src/app.ts index e166fa95e..822540ef0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1455,8 +1455,8 @@ export class App extends Client { * } * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types */ async createSamplingMessage( params: CreateMessageRequest["params"] & { tools?: undefined }, diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 7d62ef5bc..7dd746d4b 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -10,7 +10,7 @@ import type { RequestId, ResourceLink, Tool, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/client"; import { ContentBlockSchema as ContentBlockSchemaFromCore, CallToolResultSchema as CallToolResultSchemaFromCore, diff --git a/src/message-transport.test.ts b/src/message-transport.test.ts index d5f509204..2a115f3f9 100644 --- a/src/message-transport.test.ts +++ b/src/message-transport.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; -import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; +import type { JSONRPCMessage } from "@modelcontextprotocol/client"; import { PostMessageTransport } from "./message-transport"; diff --git a/src/message-transport.ts b/src/message-transport.ts index b2443c505..bbb9e02a8 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -1,12 +1,10 @@ import { - JSONRPCMessage, - JSONRPCMessageSchema, - MessageExtraInfo, -} from "@modelcontextprotocol/sdk/types.js"; -import { - Transport, - TransportSendOptions, -} from "@modelcontextprotocol/sdk/shared/transport.js"; + type JSONRPCMessage, + type MessageExtraInfo, + type Transport, + type TransportSendOptions, +} from "@modelcontextprotocol/client"; +import { JSONRPCMessageSchema } from "@modelcontextprotocol/core"; import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types"; /** diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index 61892464a..0e64a74d6 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; -import { Implementation } from "@modelcontextprotocol/sdk/types.js"; -import { Client } from "@modelcontextprotocol/sdk/client"; +import type { Implementation } from "@modelcontextprotocol/client"; import { App, AppOptions, diff --git a/src/server/index.ts b/src/server/index.ts index c90514acd..62c91e43b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -61,7 +61,7 @@ import type { // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; -export type { ResourceMetadata, ToolCallback }; +export type { McpServer, ResourceMetadata, ToolCallback }; /** * Base tool configuration matching the standard MCP server tool options. diff --git a/src/spec.types.ts b/src/spec.types.ts index 7a8b33761..5a6470184 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -18,7 +18,7 @@ import type { RequestId, ResourceLink, Tool, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/client"; /** * Current protocol version supported by this SDK. diff --git a/src/types.ts b/src/types.ts index ea6472cc2..578721004 100644 --- a/src/types.ts +++ b/src/types.ts @@ -136,8 +136,8 @@ export { McpUiToolMetaSchema, } from "./generated/schema.js"; -// Re-export SDK types used in protocol type unions -import { +// Re-export MCP protocol types used in AppRequest/AppNotification/AppResult unions +import type { CallToolRequest, CallToolResult, CreateMessageRequest, @@ -161,7 +161,7 @@ import { Tool, ToolAnnotations, ToolListChangedNotification, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/client"; export type { CallToolRequest, From 2295c012896a9a5881b4238d19ae9b59f36a6827 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Fri, 10 Jul 2026 14:26:00 -0700 Subject: [PATCH 4/7] test: add v2 migration invariant guards (phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New v2-invariants.test.ts runs the real App+AppBridge over PostMessageTransport and asserts the iframe link never negotiates an MCP protocol version (no initialize/server/discover on the wire, getNegotiatedProtocolVersion stays undefined) — the invariant that keeps Client._shouldDropInbound from silently dropping host->view requests. Includes a public-API canary demonstrating the modern-era drop hazard, gap tests (app->bridge ping, tools/list_changed fan-in), and shared transport test helpers extracted from the phase 0 spike. --- src/test-transport-harness.ts | 108 +++++++++++++++ src/v2-invariants.test.ts | 233 +++++++++++++++++++++++++++++++++ src/v2-migration-spike.test.ts | 82 +----------- 3 files changed, 346 insertions(+), 77 deletions(-) create mode 100644 src/test-transport-harness.ts create mode 100644 src/v2-invariants.test.ts diff --git a/src/test-transport-harness.ts b/src/test-transport-harness.ts new file mode 100644 index 000000000..934530cee --- /dev/null +++ b/src/test-transport-harness.ts @@ -0,0 +1,108 @@ +/** + * Shared test harness for view↔host transport pairs. + * + * Used by the v2 migration spike and Phase 3 invariant regression tests. + * Prefer {@link createLinkedPostMessagePair} when exercising production + * PostMessageTransport wiring; fall back to InMemoryTransport for speed. + */ +import type { JSONRPCMessage, Transport } from "@modelcontextprotocol/client"; + +import { PostMessageTransport } from "./message-transport"; + +export type Listener = (event: MessageEvent) => void; + +/** Minimal `window` stub for bun's DOM-less test environment. */ +export function createFakeWindow() { + const listeners = new Map>(); + return { + addEventListener(type: string, listener: Listener) { + if (!listeners.has(type)) listeners.set(type, new Set()); + listeners.get(type)!.add(listener); + }, + removeEventListener(type: string, listener: Listener) { + listeners.get(type)?.delete(listener); + }, + dispatch(type: string, event: unknown) { + listeners.get(type)?.forEach((l) => l(event as MessageEvent)); + }, + }; +} + +/** + * Two PostMessageTransports sharing one fake `window`, cross-wired so each + * side's postMessage arrives as a MessageEvent with the peer as `source`. + */ +export function createLinkedPostMessagePair(): { + viewTransport: PostMessageTransport; + hostTransport: PostMessageTransport; +} { + const viewWindow = { id: "view" }; + const hostWindow = { id: "host" }; + + const hostAsTarget = { + postMessage(data: unknown) { + ( + globalThis as unknown as { window: ReturnType } + ).window.dispatch("message", { source: viewWindow, data }); + }, + }; + const viewAsTarget = { + postMessage(data: unknown) { + ( + globalThis as unknown as { window: ReturnType } + ).window.dispatch("message", { source: hostWindow, data }); + }, + }; + + const viewTransport = new PostMessageTransport( + hostAsTarget as unknown as Window, + hostWindow as unknown as MessageEventSource, + ); + const hostTransport = new PostMessageTransport( + viewAsTarget as unknown as Window, + viewWindow as unknown as MessageEventSource, + ); + + return { viewTransport, hostTransport }; +} + +export type ProtocolVersionCapture = { + versions: string[]; +}; + +/** Spy `transport.setProtocolVersion` and record every call. */ +export function captureProtocolVersion( + transport: Transport, +): ProtocolVersionCapture { + const versions: string[] = []; + const previous = transport.setProtocolVersion?.bind(transport); + transport.setProtocolVersion = (version: string) => { + versions.push(version); + previous?.(version); + }; + return { versions }; +} + +export type WireCapture = { + messages: JSONRPCMessage[]; + methods: () => Array; +}; + +/** Spy `transport.send` and record every outbound JSON-RPC message. */ +export function captureWire(transport: Transport): WireCapture { + const messages: JSONRPCMessage[] = []; + const previous = transport.send.bind(transport); + transport.send = async (message, options) => { + messages.push(message); + return previous(message, options); + }; + return { + messages, + methods: () => + messages + .map((m) => + "method" in m ? (m.method as string | undefined) : undefined, + ) + .filter((m): m is string => typeof m === "string"), + }; +} diff --git a/src/v2-invariants.test.ts b/src/v2-invariants.test.ts new file mode 100644 index 000000000..1e0fb0a7f --- /dev/null +++ b/src/v2-invariants.test.ts @@ -0,0 +1,233 @@ +/** + * Phase 3 regression guards for the v2 SDK migration. + * + * Load-bearing invariant: the view↔host iframe link must NEVER negotiate an + * MCP protocol version. App.connect() sets a synthetic transport.sessionId + * before super.connect() so v2 Client.connect() takes the session-resumption + * path and skips MCP initialize. If a modern (2026-07-28) era were negotiated, + * Client._shouldDropInbound would silently drop ALL inbound requests on App. + * + * Coverage note (vs src/app-bridge.test.ts): + * - Teardown round-trips, bridge→app ping, registerTool/listTools/callTool, + * and createSamplingMessage are already covered there — not duplicated here. + * - This file focuses on the no-negotiation CI guard, the missing app→bridge + * ping direction, App→bridge tools/list_changed, and the modern-era drop + * canary (bare Client, not App). + */ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { + Client, + InMemoryTransport, + type DiscoverResult, + type Transport, +} from "@modelcontextprotocol/client"; + +import { App } from "./app"; +import { AppBridge, type McpUiHostCapabilities } from "./app-bridge"; +import { + captureProtocolVersion, + captureWire, + createFakeWindow, + createLinkedPostMessagePair, +} from "./test-transport-harness"; + +const flush = () => new Promise((resolve) => setTimeout(resolve, 20)); + +const testHostInfo = { name: "InvariantHost", version: "1.0.0" }; +const testAppInfo = { name: "InvariantApp", version: "1.0.0" }; +const testHostCapabilities: McpUiHostCapabilities = { + openLinks: {}, + serverTools: {}, + logging: {}, +}; + +/** Modern protocol revision that arms Client._shouldDropInbound. */ +const MODERN_PROTOCOL_VERSION = "2026-07-28"; + +function modernPrior(): DiscoverResult { + return { + supportedVersions: [MODERN_PROTOCOL_VERSION], + capabilities: {}, + serverInfo: { name: "canary-server", version: "1.0.0" }, + }; +} + +// --------------------------------------------------------------------------- +// 1. No-negotiation invariant (CI guard) — real App + AppBridge +// --------------------------------------------------------------------------- + +describe("v2 invariants — no MCP negotiation on App↔AppBridge", () => { + let restoreConsole: () => void; + let app: App; + let bridge: AppBridge; + let viewTransport: Transport; + let hostTransport: Transport; + let viewWire: ReturnType; + let hostWire: ReturnType; + let viewVersions: string[]; + let hostVersions: string[]; + + beforeEach(async () => { + (globalThis as { window?: unknown }).window = createFakeWindow(); + const origDebug = console.debug; + const origError = console.error; + console.debug = () => {}; + console.error = () => {}; + restoreConsole = () => { + console.debug = origDebug; + console.error = origError; + }; + + const pair = createLinkedPostMessagePair(); + viewTransport = pair.viewTransport as unknown as Transport; + hostTransport = pair.hostTransport as unknown as Transport; + + viewWire = captureWire(viewTransport); + hostWire = captureWire(hostTransport); + viewVersions = captureProtocolVersion(viewTransport).versions; + hostVersions = captureProtocolVersion(hostTransport).versions; + + app = new App( + testAppInfo, + { tools: { listChanged: true } }, + { autoResize: false }, + ); + bridge = new AppBridge(null, testHostInfo, testHostCapabilities); + + await bridge.connect(hostTransport); + await app.connect(viewTransport); + }); + + afterEach(async () => { + await app.close().catch(() => {}); + await bridge.close().catch(() => {}); + restoreConsole(); + delete (globalThis as { window?: unknown }).window; + }); + + it("keeps negotiated protocol version undefined after ui/initialize", () => { + expect(app.getNegotiatedProtocolVersion()).toBeUndefined(); + expect(app.getServerCapabilities()).toBeUndefined(); + expect(bridge.getNegotiatedProtocolVersion()).toBeUndefined(); + expect(bridge.getClientCapabilities()).toBeUndefined(); + }); + + it("never sends MCP initialize or notifications/initialized on the wire", () => { + const methods = [...viewWire.methods(), ...hostWire.methods()]; + + expect(methods).toContain("ui/initialize"); + expect(methods).toContain("ui/notifications/initialized"); + expect(methods).not.toContain("initialize"); + expect(methods).not.toContain("notifications/initialized"); + expect(methods).not.toContain("server/discover"); + }); + + it("never calls transport.setProtocolVersion", () => { + expect(viewVersions).toEqual([]); + expect(hostVersions).toEqual([]); + }); + + it("still delivers host→view tools/call (drop hazard not armed)", async () => { + app.registerTool("echo", {}, async () => ({ + content: [{ type: "text" as const, text: "pong-tool" }], + })); + + const list = await bridge.listTools({}); + expect(list.tools.map((t) => t.name)).toContain("echo"); + + const result = await bridge.callTool({ name: "echo", arguments: {} }); + expect(result.content).toEqual([{ type: "text", text: "pong-tool" }]); + }); +}); + +// --------------------------------------------------------------------------- +// Gaps vs app-bridge.test.ts: app→bridge ping + App→bridge list_changed +// --------------------------------------------------------------------------- + +describe("v2 invariants — coverage gaps (App↔AppBridge)", () => { + let app: App; + let bridge: AppBridge; + let appTransport: InMemoryTransport; + let bridgeTransport: InMemoryTransport; + + beforeEach(async () => { + [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair(); + app = new App( + testAppInfo, + { tools: { listChanged: true } }, + { autoResize: false }, + ); + bridge = new AppBridge(null, testHostInfo, testHostCapabilities); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + }); + + afterEach(async () => { + await appTransport.close(); + await bridgeTransport.close(); + }); + + it("app.ping() round-trips to the bridge (app→bridge direction)", async () => { + // bridge→app ping is covered in app-bridge.test.ts; this is the reverse. + const result = await app.ping(); + expect(result).toEqual({}); + }); + + it("notifications/tools/list_changed from App reaches the bridge", async () => { + let listChangedHits = 0; + bridge.setNotificationHandler("notifications/tools/list_changed", () => { + listChangedHits += 1; + }); + + const tool = app.registerTool("changing", {}, async () => ({ + content: [], + })); + await flush(); + // registerTool notifies when already initialized. + expect(listChangedHits).toBeGreaterThanOrEqual(1); + + const afterRegister = listChangedHits; + tool.disable(); + await flush(); + expect(listChangedHits).toBeGreaterThan(afterRegister); + }); +}); + +// --------------------------------------------------------------------------- +// 6. Drop-hazard canary — documents the danger App.connect() avoids +// --------------------------------------------------------------------------- + +describe("v2 invariants — modern-era inbound drop canary", () => { + it("connect({ prior }) modern era drops inbound requests (onerror)", async () => { + // Public API only: ConnectOptions.prior adopts a DiscoverResult and sets + // _negotiatedProtocolVersion to a modern revision without poking privates. + // Against a live v2 Server, versionNegotiation:'auto' also works, but + // prior is the zero-round-trip public path that deterministically arms + // the drop (see typescript-sdk modernEraInboundDrop.test.ts). + const [clientTx, serverTx] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: "drop-canary", version: "1.0.0" }); + const errors: Error[] = []; + client.onerror = (error) => { + errors.push(error); + }; + + await client.connect(clientTx, { prior: modernPrior() }); + expect(client.getNegotiatedProtocolVersion()).toBe(MODERN_PROTOCOL_VERSION); + + // Misbehaving "modern" peer sends a server→client request. The channel + // is deleted in the 2026 era — Client must drop it, not answer. + await serverTx.send({ + jsonrpc: "2.0", + id: "rogue-1", + method: "ping", + params: {}, + }); + await flush(); + + expect( + errors.some((e) => e.message.includes("Dropped inbound request")), + ).toBe(true); + + await client.close(); + }); +}); diff --git a/src/v2-migration-spike.test.ts b/src/v2-migration-spike.test.ts index 107a6ccb5..fc3786a9d 100644 --- a/src/v2-migration-spike.test.ts +++ b/src/v2-migration-spike.test.ts @@ -29,7 +29,11 @@ import { import { Server } from "@modelcontextprotocol/server"; import { z } from "zod"; -import { PostMessageTransport } from "./message-transport"; +import { + captureProtocolVersion, + createFakeWindow, + createLinkedPostMessagePair, +} from "./test-transport-harness"; // --------------------------------------------------------------------------- // Custom method schemas (vendor-prefixed; 3-arg setRequestHandler / request) @@ -109,24 +113,6 @@ class SpikeAppServer extends Server { // Helpers // --------------------------------------------------------------------------- -type ProtocolVersionCapture = { - versions: string[]; - setProtocolVersion: (version: string) => void; -}; - -function captureProtocolVersion(transport: Transport): ProtocolVersionCapture { - const versions: string[] = []; - const previous = transport.setProtocolVersion?.bind(transport); - transport.setProtocolVersion = (version: string) => { - versions.push(version); - previous?.(version); - }; - return { - versions, - setProtocolVersion: transport.setProtocolVersion, - }; -} - /** * Public escape hatch: pretends this is a session resume so Client.connect * attaches the transport via Protocol.connect and returns WITHOUT running @@ -334,64 +320,6 @@ describe("v2 migration spike — Q2/Q3 no-negotiation strategy (InMemoryTranspor // Same checks over PostMessageTransport (fake-window harness) // --------------------------------------------------------------------------- -type Listener = (event: MessageEvent) => void; - -function createFakeWindow() { - const listeners = new Map>(); - return { - addEventListener(type: string, listener: Listener) { - if (!listeners.has(type)) listeners.set(type, new Set()); - listeners.get(type)!.add(listener); - }, - removeEventListener(type: string, listener: Listener) { - listeners.get(type)?.delete(listener); - }, - dispatch(type: string, event: unknown) { - listeners.get(type)?.forEach((l) => l(event as MessageEvent)); - }, - }; -} - -/** - * Two PostMessageTransports sharing one fake `window`, cross-wired so each - * side's postMessage arrives as a MessageEvent with the peer as `source`. - */ -function createLinkedPostMessagePair(): { - viewTransport: PostMessageTransport; - hostTransport: PostMessageTransport; -} { - const viewWindow = { id: "view" }; - const hostWindow = { id: "host" }; - - const hostAsTarget = { - postMessage(data: unknown) { - // View → host: event.source must be the view window. - ( - globalThis as unknown as { window: ReturnType } - ).window.dispatch("message", { source: viewWindow, data }); - }, - }; - const viewAsTarget = { - postMessage(data: unknown) { - // Host → view: event.source must be the host window. - ( - globalThis as unknown as { window: ReturnType } - ).window.dispatch("message", { source: hostWindow, data }); - }, - }; - - const viewTransport = new PostMessageTransport( - hostAsTarget as unknown as Window, - hostWindow as unknown as MessageEventSource, - ); - const hostTransport = new PostMessageTransport( - viewAsTarget as unknown as Window, - viewWindow as unknown as MessageEventSource, - ); - - return { viewTransport, hostTransport }; -} - describe("v2 migration spike — PostMessageTransport (fake window)", () => { let restoreConsole: () => void; From 676d8c9da47105f8c3769468f6ec30321a49dcf7 Mon Sep 17 00:00:00 2001 From: Andrew Khadder Date: Fri, 10 Jul 2026 15:41:27 -0700 Subject: [PATCH 5/7] refactor: migrate examples, docs, and packaging to v2 SDK (phase 4) Swap @modelcontextprotocol/sdk for the v2 packages (client/core/server) across peerDependencies, bundler externals, all example servers, docs, and skills; add a typedoc plugin linking v2 SDK symbol references. --- README.md | 8 +- build.bun.ts | 12 +- docs/migrate_from_openai_apps.md | 4 +- docs/patterns.tsx | 9 +- docs/quickstart.md | 14 +- examples/basic-host/package.json | 1 - examples/basic-server-preact/main.ts | 10 +- examples/basic-server-preact/package.json | 5 +- examples/basic-server-preact/server.ts | 4 +- examples/basic-server-preact/src/mcp-app.tsx | 2 +- examples/basic-server-react/main.ts | 10 +- examples/basic-server-react/package.json | 4 +- examples/basic-server-react/server.ts | 4 +- examples/basic-server-solid/main.ts | 10 +- examples/basic-server-solid/package.json | 5 +- examples/basic-server-solid/server.ts | 4 +- examples/basic-server-solid/src/mcp-app.tsx | 2 +- examples/basic-server-svelte/main.ts | 10 +- examples/basic-server-svelte/package.json | 5 +- examples/basic-server-svelte/server.ts | 4 +- examples/basic-server-svelte/src/App.svelte | 2 +- examples/basic-server-vanillajs/main.ts | 10 +- examples/basic-server-vanillajs/package.json | 5 +- examples/basic-server-vanillajs/server.ts | 4 +- .../basic-server-vanillajs/src/mcp-app.ts | 2 +- examples/basic-server-vue/main.ts | 10 +- examples/basic-server-vue/package.json | 5 +- examples/basic-server-vue/server.ts | 4 +- examples/basic-server-vue/src/App.vue | 40 ++- examples/budget-allocator-server/main.ts | 10 +- examples/budget-allocator-server/package.json | 4 +- examples/budget-allocator-server/server.ts | 4 +- examples/cohort-heatmap-server/main.ts | 10 +- examples/cohort-heatmap-server/package.json | 4 +- examples/cohort-heatmap-server/server.ts | 6 +- examples/customer-segmentation-server/main.ts | 10 +- .../customer-segmentation-server/package.json | 4 +- .../customer-segmentation-server/server.ts | 6 +- examples/debug-server/main.ts | 10 +- examples/debug-server/package.json | 4 +- examples/debug-server/server.ts | 4 +- examples/integration-server/main.ts | 10 +- examples/integration-server/package.json | 5 +- examples/integration-server/server.ts | 6 +- examples/integration-server/src/mcp-app.tsx | 2 +- examples/lazy-auth-server/package.json | 3 +- examples/lazy-auth-server/server.ts | 20 +- examples/map-server/main.ts | 10 +- examples/map-server/package.json | 5 +- examples/map-server/server.ts | 12 +- examples/map-server/src/mcp-app.ts | 2 +- examples/pdf-server/main.ts | 10 +- examples/pdf-server/package.json | 5 +- examples/pdf-server/server.test.ts | 3 +- examples/pdf-server/server.ts | 60 ++-- examples/pdf-server/src/mcp-app.ts | 4 +- examples/qr-server/package.json | 3 +- examples/quickstart/main.ts | 10 +- examples/quickstart/package.json | 4 +- examples/quickstart/server.ts | 2 +- examples/say-server/package.json | 3 +- examples/scenario-modeler-server/main.ts | 10 +- examples/scenario-modeler-server/package.json | 4 +- examples/scenario-modeler-server/server.ts | 6 +- examples/shadertoy-server/main.ts | 10 +- examples/shadertoy-server/package.json | 4 +- examples/shadertoy-server/server.ts | 4 +- examples/sheet-music-server/main.ts | 10 +- examples/sheet-music-server/package.json | 4 +- examples/sheet-music-server/server.ts | 4 +- examples/system-monitor-server/main.ts | 10 +- examples/system-monitor-server/package.json | 4 +- examples/system-monitor-server/server.ts | 4 +- examples/threejs-server/main.ts | 10 +- examples/threejs-server/package.json | 5 +- examples/threejs-server/server.ts | 14 +- .../threejs-server/src/mcp-app-wrapper.tsx | 2 +- examples/transcript-server/main.ts | 10 +- examples/transcript-server/package.json | 4 +- examples/transcript-server/server.ts | 4 +- examples/video-resource-server/main.ts | 10 +- examples/video-resource-server/package.json | 5 +- examples/video-resource-server/server.ts | 11 +- examples/video-resource-server/src/mcp-app.ts | 2 +- examples/wiki-explorer-server/main.ts | 10 +- examples/wiki-explorer-server/package.json | 5 +- examples/wiki-explorer-server/server.ts | 4 +- examples/wiki-explorer-server/src/mcp-app.ts | 2 +- package-lock.json | 294 +++++++----------- package.json | 5 +- .../mcp-apps/skills/convert-web-app/SKILL.md | 6 +- .../mcp-apps/skills/create-mcp-app/SKILL.md | 2 +- scripts/generate-schemas.ts | 1 - scripts/typedoc-plugin-v2-sdk-links.mjs | 28 ++ src/app.ts | 12 +- src/events.ts | 2 +- src/message-transport.ts | 12 +- src/server/index.examples.ts | 7 +- src/server/index.test.ts | 2 +- src/server/index.ts | 75 +++-- typedoc.config.mjs | 37 ++- 101 files changed, 604 insertions(+), 515 deletions(-) create mode 100644 scripts/typedoc-plugin-v2-sdk-links.mjs diff --git a/README.md b/README.md index 2fd89ffee..45d87b410 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,15 @@ resources: ## Getting Started ```bash -npm install -S @modelcontextprotocol/ext-apps +npm install -S @modelcontextprotocol/ext-apps \ + @modelcontextprotocol/client @modelcontextprotocol/server @modelcontextprotocol/core ``` +Peer dependencies are the MCP TypeScript SDK v2 packages +(`@modelcontextprotocol/client`, `/server`, `/core` at `2.0.0-beta.3`). Install +the ones your entry point needs — Views use `client` + `core`, server helpers +use `server`, and `app-bridge` uses all three. + **New here?** Start with the [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html) to build your first MCP App. diff --git a/build.bun.ts b/build.bun.ts index a129d5ddd..77308a32e 100644 --- a/build.bun.ts +++ b/build.bun.ts @@ -31,9 +31,15 @@ function buildJs( }); } -// zod is a peerDependency — keep it external so consumers share a single -// zod instance (instanceof ZodError / schema.extend() break with duplicate copies). -const PEER_EXTERNALS = ["@modelcontextprotocol/sdk", "zod"]; +// Peer deps stay external so consumers share a single instance (zod +// instanceof / schema.extend() break with duplicates; MCP packages must +// match the host's installed v2 SDK). Bundled into *-with-deps variants. +const PEER_EXTERNALS = [ + "@modelcontextprotocol/client", + "@modelcontextprotocol/core", + "@modelcontextprotocol/server", + "zod", +]; await Promise.all([ buildJs("src/app.ts", { diff --git a/docs/migrate_from_openai_apps.md b/docs/migrate_from_openai_apps.md index d91281b41..78e822901 100644 --- a/docs/migrate_from_openai_apps.md +++ b/docs/migrate_from_openai_apps.md @@ -66,7 +66,7 @@ The server-side changes involve updating metadata structure and using helper fun #### Before (OpenAI) ```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { z } from "zod"; function createServer() { @@ -126,7 +126,7 @@ function createServer() { #### After (MCP Apps) ```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { registerAppTool, registerAppResource, diff --git a/docs/patterns.tsx b/docs/patterns.tsx index 6b876926b..1690bd6d9 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -17,16 +17,13 @@ import { randomUUID } from "node:crypto"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; -import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; +import { ReadResourceResultSchema } from "@modelcontextprotocol/core"; import type { McpUiHostContext } from "../src/types.js"; import { useEffect, useState } from "react"; import { useApp } from "../src/react/index.js"; import { registerAppTool } from "../src/server/index.js"; -import { - McpServer, - ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server"; import { z } from "zod"; /** diff --git a/docs/quickstart.md b/docs/quickstart.md index 3e00796c5..a041ac45a 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -37,7 +37,7 @@ Install the dependencies you'll need: ```bash npm init -y -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/server @modelcontextprotocol/node @modelcontextprotocol/express express cors npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env ``` @@ -178,7 +178,7 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; @@ -240,10 +240,10 @@ export function createServer(): McpServer { ```ts source="../examples/quickstart/main.ts" -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -263,7 +263,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, }); diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index d7c284c02..5b735fc67 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -13,7 +13,6 @@ "dependencies": { "@modelcontextprotocol/client": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" diff --git a/examples/basic-server-preact/main.ts b/examples/basic-server-preact/main.ts index 76426326d..0e78df9a7 100644 --- a/examples/basic-server-preact/main.ts +++ b/examples/basic-server-preact/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -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 { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,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, }); diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 512720205..fa0c65b25 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.3", + "@modelcontextprotocol/express": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.3", + "@modelcontextprotocol/server": "2.0.0-beta.3", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", diff --git a/examples/basic-server-preact/server.ts b/examples/basic-server-preact/server.ts index bf14ea599..133a0e196 100644 --- a/examples/basic-server-preact/server.ts +++ b/examples/basic-server-preact/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; diff --git a/examples/basic-server-preact/src/mcp-app.tsx b/examples/basic-server-preact/src/mcp-app.tsx index a8bdc68b8..351a8ec21 100644 --- a/examples/basic-server-preact/src/mcp-app.tsx +++ b/examples/basic-server-preact/src/mcp-app.tsx @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import { useCallback, useEffect, useState } from "preact/hooks"; import { render } from "preact"; import styles from "./mcp-app.module.css"; diff --git a/examples/basic-server-react/main.ts b/examples/basic-server-react/main.ts index ec187b68a..7b40b8118 100644 --- a/examples/basic-server-react/main.ts +++ b/examples/basic-server-react/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,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, }); diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index e4d12333f..06177e22a 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -34,8 +34,10 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.3", + "@modelcontextprotocol/server": "2.0.0-beta.3", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 23f6dda5a..bfff3170a 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -1,6 +1,6 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; diff --git a/examples/basic-server-solid/main.ts b/examples/basic-server-solid/main.ts index c8d9de225..095ef8dfd 100644 --- a/examples/basic-server-solid/main.ts +++ b/examples/basic-server-solid/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -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 { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,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, }); diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 8f75ac8c2..c61b95e6c 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.3", + "@modelcontextprotocol/express": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.3", + "@modelcontextprotocol/server": "2.0.0-beta.3", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", diff --git a/examples/basic-server-solid/server.ts b/examples/basic-server-solid/server.ts index 2ed2d9356..9c7259d7c 100644 --- a/examples/basic-server-solid/server.ts +++ b/examples/basic-server-solid/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; diff --git a/examples/basic-server-solid/src/mcp-app.tsx b/examples/basic-server-solid/src/mcp-app.tsx index 66d47f3f6..2d78b7b33 100644 --- a/examples/basic-server-solid/src/mcp-app.tsx +++ b/examples/basic-server-solid/src/mcp-app.tsx @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import { createEffect, createSignal, onMount, Show } from "solid-js"; import { render } from "solid-js/web"; import styles from "./mcp-app.module.css"; diff --git a/examples/basic-server-svelte/main.ts b/examples/basic-server-svelte/main.ts index 6c50a254f..a48a8711f 100644 --- a/examples/basic-server-svelte/main.ts +++ b/examples/basic-server-svelte/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -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 { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,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, }); diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index f877bf7c8..54b4bc0c0 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.3", + "@modelcontextprotocol/express": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.3", + "@modelcontextprotocol/server": "2.0.0-beta.3", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", diff --git a/examples/basic-server-svelte/server.ts b/examples/basic-server-svelte/server.ts index da603ca8d..6604df724 100644 --- a/examples/basic-server-svelte/server.ts +++ b/examples/basic-server-svelte/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; diff --git a/examples/basic-server-svelte/src/App.svelte b/examples/basic-server-svelte/src/App.svelte index fde128dde..cabcc317d 100644 --- a/examples/basic-server-svelte/src/App.svelte +++ b/examples/basic-server-svelte/src/App.svelte @@ -7,7 +7,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; function extractTime(result: CallToolResult): string { const { text } = result.content?.find((c) => c.type === "text")!; diff --git a/examples/basic-server-vanillajs/main.ts b/examples/basic-server-vanillajs/main.ts index 286fa34f0..4de733187 100644 --- a/examples/basic-server-vanillajs/main.ts +++ b/examples/basic-server-vanillajs/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -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 { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,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, }); diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 5d47f2cdc..d37d0109a 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.3", + "@modelcontextprotocol/express": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.3", + "@modelcontextprotocol/server": "2.0.0-beta.3", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 5b2daf704..56dd0ee4b 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -1,6 +1,6 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index c3168c214..6b5084a3a 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; import "./global.css"; import "./mcp-app.css"; diff --git a/examples/basic-server-vue/main.ts b/examples/basic-server-vue/main.ts index 669a07189..c465ebdd0 100644 --- a/examples/basic-server-vue/main.ts +++ b/examples/basic-server-vue/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -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 { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,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, }); diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index d27ddc91b..a704bf871 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.3", + "@modelcontextprotocol/express": "2.0.0-beta.3", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.3", + "@modelcontextprotocol/server": "2.0.0-beta.3", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", diff --git a/examples/basic-server-vue/server.ts b/examples/basic-server-vue/server.ts index 5aa72de03..5733f65a2 100644 --- a/examples/basic-server-vue/server.ts +++ b/examples/basic-server-vue/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; diff --git a/examples/basic-server-vue/src/App.vue b/examples/basic-server-vue/src/App.vue index 196ad64da..d7b79971a 100644 --- a/examples/basic-server-vue/src/App.vue +++ b/examples/basic-server-vue/src/App.vue @@ -7,14 +7,13 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; function extractTime(result: CallToolResult): string { const { text } = result.content?.find((c) => c.type === "text")!; return text; } - const app = ref(null); const hostContext = ref(); const serverTime = ref("Loading..."); @@ -67,7 +66,10 @@ async function handleGetTime() { if (!app.value) return; try { console.info("Calling get-time tool..."); - const result = await app.value.callServerTool({ name: "get-time", arguments: {} }); + const result = await app.value.callServerTool({ + name: "get-time", + arguments: {}, + }); console.info("get-time result:", result); serverTime.value = extractTime(result); } catch (e) { @@ -108,17 +110,22 @@ async function handleOpenLink() {