diff --git a/AGENTS.md b/AGENTS.md index f850aa3..4b0faa0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,8 +60,8 @@ rm -fR package-lock.json node_modules && \ ### Key Source Files -- `src/app.ts` - `App` subclasses the base MCP SDK `Client`, handles View initialization, tool calls, and messaging -- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Server` for the inner iframe channel and proxies through a separate outer `Client` +- `src/app.ts` - `App` subclasses the base MCP SDK `Protocol`, handles View initialization, tool calls, and messaging +- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Protocol` for the iframe channel and proxies through a separate outer `Client` - `src/server/index.ts` - Helpers for MCP servers to register tools/resources with UI metadata - `src/types.ts` - Protocol types re-exported from `spec.types.ts` and Zod schemas from `generated/schema.ts` (auto-generated during build) - `src/message-transport.ts` - `PostMessageTransport` for iframe communication @@ -70,12 +70,12 @@ rm -fR package-lock.json node_modules && \ ### Protocol Flow ``` -View (App/Client) <--PostMessageTransport--> Host (AppBridge/Server) <--separate MCP Client--> MCP Server +View (App/Protocol) <--PostMessageTransport--> Host (AppBridge/Protocol) <--separate MCP Client--> MCP Server ``` 1. Host creates iframe with view HTML 2. View creates `App` instance and calls `connect()` with `PostMessageTransport` -3. View and Host complete the temporary inner base MCP handshake, then the View sends `ui/initialize` and receives authoritative Apps capabilities and context +3. View sends `ui/initialize` (the only handshake on the iframe channel) and receives authoritative Apps capabilities and context 4. Host sends `sendToolInput()` with tool arguments after initialization 5. View can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()` 6. Host sends `sendToolResult()` when tool execution completes diff --git a/docs/overview.md b/docs/overview.md index 317ab43..63156ec 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -97,7 +97,7 @@ sequenceDiagram ``` 1. **Discovery** — The Host learns about tools and their UI resources when connecting to the server. -2. **Initialization** — When a UI tool is called, the Host renders the iframe. The SDK first completes its temporary inner base MCP handshake, then the View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). `ui/initialize` remains authoritative for Apps capabilities and identity, and `ui/notifications/initialized` remains the public View-ready signal. +2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` (the only handshake on the iframe channel) and receives host context (theme, capabilities, container dimensions). `ui/initialize` is authoritative for Apps capabilities and identity, and `ui/notifications/initialized` is the public View-ready signal. 3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering). This separation lets servers provide rich data to the UI without bloating the model's context. 4. **Interactive phase** — The user interacts with the View. The View can call tools, send messages, or update context. 5. **Teardown** — Before unmounting, the Host notifies the View so it can save state or release resources. diff --git a/package-lock.json b/package-lock.json index 9e0a71b..04b0d2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,9 @@ }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", + "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", + "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -2639,8 +2639,8 @@ }, "node_modules/@modelcontextprotocol/client": { "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz", - "integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==", + "resolved": "file:../tarballs/mcp-client.tgz", + "integrity": "sha512-aCTBtOjOSOj+SOLsoUfDjhD3Ngs4pMXzAczBmKRTesiz0N/3wUEXEmKgxhfbhF6Wtgk4tatWBUG9LDaj919K2Q==", "license": "MIT", "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.4", @@ -2657,7 +2657,7 @@ }, "node_modules/@modelcontextprotocol/core": { "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.4.tgz", + "resolved": "file:../tarballs/mcp-core.tgz", "integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==", "license": "MIT", "dependencies": { @@ -2784,8 +2784,8 @@ }, "node_modules/@modelcontextprotocol/server": { "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.4.tgz", - "integrity": "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg==", + "resolved": "file:../tarballs/mcp-server.tgz", + "integrity": "sha512-/UJubByzGV+mlU8fN8yyuMI9zgoq4Ic5b1V/6yhzxrKjmXpTbNkY5rD2jRsWBk+At0AVcXLkcDzmLF91oOJtKw==", "license": "MIT", "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.4", diff --git a/package.json b/package.json index ecbf386..59e1802 100644 --- a/package.json +++ b/package.json @@ -77,9 +77,9 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/client": "2.0.0-beta.4", - "@modelcontextprotocol/core": "2.0.0-beta.4", - "@modelcontextprotocol/server": "2.0.0-beta.4", + "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", + "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", + "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -129,7 +129,10 @@ "seroval": "1.4.1", "seroval-plugins": "1.4.2", "solid-js": "1.9.10", - "@types/node": "20.19.27" + "@types/node": "20.19.27", + "@modelcontextprotocol/core": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-core.tgz", + "@modelcontextprotocol/client": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-client.tgz", + "@modelcontextprotocol/server": "file:/root/src/mcp/projects/v2-2026-ext-apps/tarballs/mcp-server.tgz" }, "dependencies": { "@standard-schema/spec": "^1.1.0" diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 81e072f..a80a961 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -82,7 +82,7 @@ describe("App <-> AppBridge integration", () => { expect(initializedFired).toBe(true); }); - it("keeps standard initialization separate from the Apps-ready gate", async () => { + it("sends only the Apps handshake on the wire and gates the Apps-ready callback", async () => { const methods: string[] = []; let releaseAppsInitialized!: () => void; let reachedAppsInitialized!: () => void; @@ -115,14 +115,9 @@ describe("App <-> AppBridge integration", () => { await reachedGate; expect(methods).toEqual([ - "initialize", - "notifications/initialized", "ui/initialize", "ui/notifications/initialized", ]); - expect(app.getNegotiatedProtocolVersion()).toBe("2025-11-25"); - expect(bridge.getNegotiatedProtocolVersion()).toBe("2025-11-25"); - expect(bridge.getClientVersion()).toEqual(testAppInfo); expect(bridge.getAppVersion()).toEqual(testAppInfo); expect(singularCalls).toBe(0); expect(listenerCalls).toBe(0); @@ -143,8 +138,7 @@ describe("App <-> AppBridge integration", () => { const hostCaps = app.getHostCapabilities(); expect(hostCaps).toEqual(testHostCapabilities); - expect(bridge.getHostCapabilities()).toEqual(testHostCapabilities); - expect(bridge.getCapabilities()).toEqual({}); + expect(bridge.getCapabilities()).toEqual(testHostCapabilities); }); it("Bridge receives app info and capabilities after initialization", async () => { diff --git a/src/app-bridge.ts b/src/app-bridge.ts index d353cf7..467c33d 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1,6 +1,8 @@ -import { type Client } from "@modelcontextprotocol/client"; import { - Server, + Protocol, + type BaseContext, + type Client, + type ProtocolOptions, type CallToolRequest, type CallToolResult, type CreateMessageRequest, @@ -24,13 +26,10 @@ import { type RequestOptions, type ResourceListChangedNotification, type Result, - type ServerCapabilities, - type ServerContext, - type ServerOptions, type Tool, type ToolListChangedNotification, type Transport, -} from "@modelcontextprotocol/server"; +} from "@modelcontextprotocol/client"; import { EmptyResultSchema, LoggingMessageNotificationSchema, @@ -43,8 +42,6 @@ type MethodSchema = ZodObject<{ params: ZodType; }>; -const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; - import { type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, @@ -96,20 +93,6 @@ export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; import { RESOURCE_URI_META_KEY } from "./app"; export { PostMessageTransport } from "./message-transport"; -function proxiedInnerCapabilities( - capabilities: ServerCapabilities, -): ServerCapabilities { - return { - ...(capabilities.tools && { tools: capabilities.tools }), - ...(capabilities.resources && { - resources: { - listChanged: capabilities.resources.listChanged, - }, - }), - ...(capabilities.prompts && { prompts: capabilities.prompts }), - }; -} - /** * Extract UI resource URI from tool metadata. * @@ -215,13 +198,10 @@ export function buildAllowAttribute( * * @property hostContext - Optional initial host context to provide to the view * - * @see `ServerOptions` from @modelcontextprotocol/server for available options + * @see `ProtocolOptions` from @modelcontextprotocol/client for available options * @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure */ -export type HostOptions = Omit< - ServerOptions, - "capabilities" | "supportedProtocolVersions" -> & { +export type HostOptions = Omit & { hostContext?: McpUiHostContext; }; @@ -241,7 +221,7 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION]; * * @internal */ -type RequestHandlerExtra = ServerContext; +type RequestHandlerExtra = BaseContext; /** * Maps DOM-style event names to their notification `params` types. @@ -315,7 +295,7 @@ export type AppBridgeEventMap = { * await bridge.connect(transport); * ``` */ -export class AppBridge extends Server { +export class AppBridge extends Protocol { private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; @@ -328,26 +308,17 @@ export class AppBridge extends Server { * handler registration. AppBridge uses it to preserve the existing warning * when a View calls host methods before `ui/notifications/initialized`, * without adding overload-forwarding adapters around `setRequestHandler`. - * Chaining through `super._wrapHandler()` retains the base `Server`'s own - * role-specific behavior, including tool-result validation. The two - * initialization methods and `ping` are exempt because they are valid before - * the Apps-ready gate. This wraps registered callbacks, not JSON-RPC dispatch. + * `ui/initialize` and `ping` are exempt because they are valid before the + * Apps-ready gate. This wraps registered callbacks, not JSON-RPC dispatch. * * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} */ protected override _wrapHandler( method: string, - handler: ( - request: JSONRPCRequest, - context: ServerContext, - ) => Promise, - ): (request: JSONRPCRequest, context: ServerContext) => Promise { + handler: (request: JSONRPCRequest, context: BaseContext) => Promise, + ): (request: JSONRPCRequest, context: BaseContext) => Promise { const wrapped = super._wrapHandler(method, handler); - if ( - method === "initialize" || - method === "ui/initialize" || - method === "ping" - ) { + if (method === "ui/initialize" || method === "ping") { return wrapped; } return async (request, context) => { @@ -439,14 +410,6 @@ export class AppBridge extends Server { } } - private ensureInnerCapabilities(capabilities: ServerCapabilities): void { - const current = super.getCapabilities(); - const missing = Object.keys(capabilities).some( - (key) => current[key as keyof ServerCapabilities] === undefined, - ); - if (missing) this.registerCapabilities(capabilities); - } - /** * Create a new AppBridge instance. * @@ -486,30 +449,7 @@ export class AppBridge extends Server { private _hostCapabilities: McpUiHostCapabilities, options?: HostOptions, ) { - // TEMPORARY INNER-CHANNEL NEGOTIATION: - // App and AppBridge use the public v2 Client/Server roles, so the iframe - // channel first completes the SDK's standard legacy MCP initialization. - // ui/initialize remains authoritative for Apps capabilities, identity, and - // host context, and ui/notifications/initialized remains the View-ready gate. - // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only - // after the MCP Apps negotiation specification is resolved: - // https://github.com/modelcontextprotocol/ext-apps/issues/708 - super(_hostInfo, { - ...options, - capabilities: {}, - supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], - }); - - // The base Server's standard initialized callback is MCP-ready state. The - // public AppBridge callback remains exclusively the Apps View-ready gate. - super.setNotificationHandler("notifications/initialized", () => {}); - Object.defineProperty(this, "oninitialized", { - configurable: true, - enumerable: true, - get: () => this.getEventHandler("initialized"), - set: (callback: AppBridge["oninitialized"]) => - this.setEventHandler("initialized", callback), - }); + super(options); this._ensureEventSlot("initialized"); @@ -544,6 +484,35 @@ export class AppBridge extends Server { ); } + /** @internal */ + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } + + /** + * Verify that the view supports the capability required for the given request method. + * @internal + */ + protected assertCapabilityForMethod(_method: string): void { + // Host-to-view requests are not capability-gated. + } + + /** + * Verify that a request handler is registered and supported for the given method. + * @internal + */ + protected assertRequestHandlerCapability(_method: string): void { + // Host handler registration is not capability-gated. + } + + /** + * Verify that the host supports the capability required for the given notification method. + * @internal + */ + protected assertNotificationCapability(_method: string): void { + // Host notifications are not capability-gated. + } + /** * Get the view's capabilities discovered during initialization. * @@ -713,9 +682,18 @@ export class AppBridge extends Server { * @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`}. */ - override oninitialized?: ( - params?: McpUiInitializedNotification["params"], - ) => void; + get oninitialized(): + | ((params?: McpUiInitializedNotification["params"]) => void) + | undefined { + return this.getEventHandler("initialized"); + } + set oninitialized( + callback: + | ((params?: McpUiInitializedNotification["params"]) => void) + | undefined, + ) { + this.setEventHandler("initialized", callback); + } /** * Register a handler for message requests from the view. @@ -1178,7 +1156,6 @@ export class AppBridge extends Server { ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.ensureInnerCapabilities({ tools: {} }); this.setRequestHandler("tools/call", async (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); @@ -1299,7 +1276,6 @@ export class AppBridge extends Server { callback, ); this._onlistresources = callback; - this.ensureInnerCapabilities({ resources: {} }); this.setRequestHandler("resources/list", async (request, extra) => { if (!this._onlistresources) throw new Error("No onlistresources handler set"); @@ -1353,7 +1329,6 @@ export class AppBridge extends Server { callback, ); this._onlistresourcetemplates = callback; - this.ensureInnerCapabilities({ resources: {} }); this.setRequestHandler( "resources/templates/list", async (request, extra) => { @@ -1410,7 +1385,6 @@ export class AppBridge extends Server { callback, ); this._onreadresource = callback; - this.ensureInnerCapabilities({ resources: {} }); this.setRequestHandler("resources/read", async (request, extra) => { if (!this._onreadresource) throw new Error("No onreadresource handler set"); @@ -1492,7 +1466,6 @@ export class AppBridge extends Server { callback, ); this._onlistprompts = callback; - this.ensureInnerCapabilities({ prompts: {} }); this.setRequestHandler("prompts/list", async (request, extra) => { if (!this._onlistprompts) throw new Error("No onlistprompts handler set"); return this._onlistprompts(request.params, extra); @@ -1532,7 +1505,7 @@ export class AppBridge extends Server { * * @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure */ - getHostCapabilities(): McpUiHostCapabilities { + getCapabilities(): McpUiHostCapabilities { return this._hostCapabilities; } @@ -1565,7 +1538,7 @@ export class AppBridge extends Server { return { protocolVersion, - hostCapabilities: this.getHostCapabilities(), + hostCapabilities: this.getCapabilities(), hostInfo: this._hostInfo, hostContext: this._hostContext, }; @@ -1931,7 +1904,6 @@ export class AppBridge extends Server { if (!serverCapabilities) { throw new Error("Client server capabilities not available"); } - this.registerCapabilities(proxiedInnerCapabilities(serverCapabilities)); if (serverCapabilities.tools) { this.oncalltool = async (params, extra) => { @@ -1989,9 +1961,9 @@ export class AppBridge extends Server { } } - // The standard MCP handshake only establishes the temporary inner Server - // role. ui/notifications/initialized remains the public View-ready signal. - + // Protocol.connect only wires the transport — no MCP handshake happens on + // the iframe channel. The View drives ui/initialize; + // ui/notifications/initialized remains the public View-ready signal. return super.connect(transport); } } diff --git a/src/app.test.ts b/src/app.test.ts index dcf2d52..9057710 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -14,8 +14,6 @@ import { McpUiInitializedNotificationSchema, } from "./types"; -const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; - type ConnectedPair = { app: App; server: Server; @@ -34,7 +32,6 @@ async function connectPair( { name: "inner-mcp-server", version: "0.0.0" }, { capabilities: { resources: {}, tools: {} }, - supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], }, ); const [appTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -103,8 +100,8 @@ afterEach(async () => { ); }); -describe("App base MCP SDK v2 Client migration", () => { - it("completes the legacy MCP handshake before the authoritative Apps handshake", async () => { +describe("App base MCP SDK v2 Protocol migration", () => { + it("performs only the Apps handshake — no MCP initialize on the wire", async () => { const pair = await connectPair(); connected.push(pair); @@ -123,25 +120,19 @@ describe("App base MCP SDK v2 Client migration", () => { ].includes(method), ); + // The filter above includes the core MCP lifecycle methods, so this + // equality also proves no `initialize`/`notifications/initialized` was sent + // — the wire contract deployed v1 hosts depend on. expect(lifecycleMethods).toEqual([ - "initialize", - "notifications/initialized", "ui/initialize", "ui/notifications/initialized", ]); - expect(pair.app.getNegotiatedProtocolVersion()).toBe( - INNER_MCP_PROTOCOL_VERSION, - ); }); it("uses ui/initialize as the authority for Apps host state", async () => { const pair = await connectPair(); connected.push(pair); - expect(pair.app.getServerVersion()).toEqual({ - name: "inner-mcp-server", - version: "0.0.0", - }); expect(pair.app.getHostVersion()).toEqual({ name: "apps-host", version: "2.0.0", @@ -219,9 +210,6 @@ describe("App base MCP SDK v2 Client migration", () => { const second = await connectPair(first.app); connected.push(second); - expect(second.app.getNegotiatedProtocolVersion()).toBe( - INNER_MCP_PROTOCOL_VERSION, - ); expect(second.app.getHostVersion()).toEqual({ name: "apps-host", version: "2.0.0", diff --git a/src/app.ts b/src/app.ts index 7e752ce..c639784 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,7 @@ import { - Client, - type ClientContext, - type ClientOptions, + Protocol, + type BaseContext, + type ProtocolOptions, type RequestOptions, type CallToolRequest, type CallToolResult, @@ -71,8 +71,6 @@ type MethodSchema = ZodObject<{ params: ZodType; }>; -const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; - function mergeAppCapabilities( base: McpUiAppCapabilities, additional: McpUiAppCapabilities, @@ -174,14 +172,11 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; /** * Options for configuring {@link App `App`} behavior. * - * Extends `ClientOptions` from the base MCP SDK with `App`-specific configuration. + * Extends `ProtocolOptions` from the base MCP SDK with `App`-specific configuration. * - * @see `ClientOptions` from @modelcontextprotocol/client for inherited options + * @see `ProtocolOptions` from @modelcontextprotocol/client for inherited options */ -export type AppOptions = Omit< - ClientOptions, - "capabilities" | "supportedProtocolVersions" | "versionNegotiation" -> & { +export type AppOptions = Omit & { /** * Automatically report size changes to the host using `ResizeObserver`. * @@ -221,7 +216,7 @@ export type AppOptions = Omit< allowUnsafeEval?: boolean; }; -type RequestHandlerExtra = ClientContext; +type RequestHandlerExtra = BaseContext; /** * Result of an app-registered tool callback. When `Out` is provided, @@ -354,7 +349,7 @@ export type AppEventMap = { * await app.connect(); * ``` */ -export class App extends Client { +export class App extends Protocol { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; @@ -521,20 +516,7 @@ export class App extends Client { private _appCapabilities: McpUiAppCapabilities = {}, private options: AppOptions = { autoResize: true }, ) { - // TEMPORARY INNER-CHANNEL NEGOTIATION: - // App and AppBridge use the public v2 Client/Server roles, so the iframe - // channel first completes the SDK's standard legacy MCP initialization. - // ui/initialize remains authoritative for Apps capabilities, identity, and - // host context, and ui/notifications/initialized remains the View-ready gate. - // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only - // after the MCP Apps negotiation specification is resolved: - // https://github.com/modelcontextprotocol/ext-apps/issues/708 - super(_appInfo, { - ...options, - capabilities: {}, - versionNegotiation: { mode: "legacy" }, - supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], - }); + super(options); if (!options.allowUnsafeEval) { z.config({ jitless: true }); @@ -550,6 +532,11 @@ export class App extends Client { this.setEventHandler("hostcontextchanged", undefined); } + /** @internal */ + protected buildContext(ctx: BaseContext): BaseContext { + return ctx; + } + private registerAppCapabilities(capabilities: McpUiAppCapabilities): void { if (this.transport) { throw new Error( @@ -1992,15 +1979,10 @@ export class App extends Client { } this._initializedSent = false; - // TEMPORARY INNER-CHANNEL NEGOTIATION: - // App and AppBridge use the public v2 Client/Server roles, so the iframe - // channel first completes the SDK's standard legacy MCP initialization. - // ui/initialize remains authoritative for Apps capabilities, identity, and - // host context, and ui/notifications/initialized remains the View-ready gate. - // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only - // after the MCP Apps negotiation specification is resolved: - // https://github.com/modelcontextprotocol/ext-apps/issues/708 try { + // Protocol.connect only wires the transport — no MCP handshake. The + // ui/initialize request below is the sole negotiation on this channel, + // matching the v1 wire behavior deployed hosts expect. await super.connect(transport); const result = await this.request(