diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 7adbb16..8f432b4 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -7,10 +7,7 @@ import { ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, - PromptListChangedNotificationSchema, ReadResourceResultSchema, - ResourceListChangedNotificationSchema, - ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod/v4"; @@ -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", + (notification) => { + receivedNotifications.push(notification.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", + (notification) => { + receivedNotifications.push(notification.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", + (notification) => { + receivedNotifications.push(notification.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2771,20 +2777,6 @@ describe("isToolVisibilityAppOnly", () => { ); }).toThrow(/already registered/); }); - - it("direct setNotificationHandler throws for event-mapped methods", () => { - const app2 = new App(testAppInfo, {}, { autoResize: false }); - app2.addEventListener("toolinput", () => {}); - expect(() => { - app2.setNotificationHandler( - // @ts-expect-error — exercising throw path with raw schema - { - shape: { method: { value: "ui/notifications/tool-input" } }, - }, - () => {}, - ); - }).toThrow(/already registered/); - }); }); }); diff --git a/src/app.test.ts b/src/app.test.ts new file mode 100644 index 0000000..dcf2d52 --- /dev/null +++ b/src/app.test.ts @@ -0,0 +1,300 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { + InMemoryTransport, + Server, + type JSONRPCMessage, +} from "@modelcontextprotocol/server"; +import { z } from "zod/v4"; + +import { App } from "./app"; +import { + LATEST_PROTOCOL_VERSION, + McpUiInitializeRequestSchema, + McpUiInitializeResultSchema, + McpUiInitializedNotificationSchema, +} from "./types"; + +const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; + +type ConnectedPair = { + app: App; + server: Server; + sentByApp: JSONRPCMessage[]; + appsInitialized: Promise; +}; + +async function connectPair( + app = new App( + { name: "view-app", version: "1.0.0" }, + { tools: { listChanged: true } }, + { autoResize: false }, + ), +): Promise { + const server = new Server( + { name: "inner-mcp-server", version: "0.0.0" }, + { + capabilities: { resources: {}, tools: {} }, + supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], + }, + ); + const [appTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const sentByApp: JSONRPCMessage[] = []; + const send = appTransport.send.bind(appTransport); + appTransport.send = async (message, options) => { + sentByApp.push(message); + await send(message, options); + }; + + let resolveAppsInitialized!: () => void; + const appsInitialized = new Promise((resolve) => { + resolveAppsInitialized = resolve; + }); + + server.setRequestHandler( + "ui/initialize", + { + params: McpUiInitializeRequestSchema.shape.params, + result: McpUiInitializeResultSchema, + }, + (params) => { + expect(params.protocolVersion).toBe(LATEST_PROTOCOL_VERSION); + return { + protocolVersion: LATEST_PROTOCOL_VERSION, + hostCapabilities: { openLinks: {} }, + hostInfo: { name: "apps-host", version: "2.0.0" }, + hostContext: { theme: "dark" as const, locale: "en-US" }, + }; + }, + ); + server.setNotificationHandler( + "ui/notifications/initialized", + { params: McpUiInitializedNotificationSchema.shape.params }, + () => resolveAppsInitialized(), + ); + server.setRequestHandler("tools/call", (request) => ({ + content: [{ type: "text", text: `called ${request.params.name}` }], + })); + server.setRequestHandler("resources/read", (request) => ({ + contents: [{ uri: request.params.uri, text: "resource body" }], + })); + server.setRequestHandler("resources/list", () => ({ + resources: [{ uri: "test://resource", name: "Test resource" }], + })); + server.setRequestHandler("sampling/createMessage", () => ({ + model: "test-model", + role: "assistant", + content: { type: "text", text: "sampled" }, + })); + + await server.connect(serverTransport); + await app.connect(appTransport); + await appsInitialized; + + return { app, server, sentByApp, appsInitialized }; +} + +const connected: Array> = []; + +afterEach(async () => { + await Promise.all( + connected + .splice(0) + .flatMap(({ app, server }) => [app.close(), server.close()]), + ); +}); + +describe("App base MCP SDK v2 Client migration", () => { + it("completes the legacy MCP handshake before the authoritative Apps handshake", async () => { + const pair = await connectPair(); + connected.push(pair); + + const lifecycleMethods = pair.sentByApp + .filter( + (message): message is JSONRPCMessage & { method: string } => + "method" in message, + ) + .map((message) => message.method) + .filter((method) => + [ + "initialize", + "notifications/initialized", + "ui/initialize", + "ui/notifications/initialized", + ].includes(method), + ); + + 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", + }); + expect(pair.app.getHostCapabilities()).toEqual({ openLinks: {} }); + expect(pair.app.getHostContext()).toEqual({ + theme: "dark", + locale: "en-US", + }); + }); + + it("preserves registered App tools with the base MCP SDK v2 Client handler context", async () => { + const pair = await connectPair(); + connected.push(pair); + let receivedSignal: AbortSignal | undefined; + + pair.app.registerTool( + "greet", + { description: "Greets a person" }, + async (extra) => { + receivedSignal = extra.mcpReq.signal; + return { content: [{ type: "text", text: "hello" }] }; + }, + ); + + const listed = await pair.server.request({ + method: "tools/list", + params: {}, + }); + const result = await pair.server.request({ + method: "tools/call", + params: { name: "greet", arguments: {} }, + }); + + expect(listed.tools.map((tool) => tool.name)).toEqual(["greet"]); + expect(result.content).toEqual([{ type: "text", text: "hello" }]); + expect(receivedSignal).toBeInstanceOf(AbortSignal); + }); + + it("uses base MCP SDK v2 method-keyed sends for standard MCP requests", async () => { + const pair = await connectPair(); + connected.push(pair); + + const toolResult = await pair.app.callServerTool({ + name: "refresh", + arguments: {}, + }); + const resource = await pair.app.readServerResource({ + uri: "test://resource", + }); + const resources = await pair.app.listServerResources(); + const sample = await pair.app.createSamplingMessage({ + messages: [{ role: "user", content: { type: "text", text: "hello" } }], + maxTokens: 20, + }); + + expect(toolResult.content).toEqual([ + { type: "text", text: "called refresh" }, + ]); + expect(resource.contents[0]).toEqual({ + uri: "test://resource", + text: "resource body", + }); + expect(resources.resources.map(({ uri }) => uri)).toEqual([ + "test://resource", + ]); + expect(sample.content).toEqual({ type: "text", text: "sampled" }); + }); + + it("preserves reconnect behavior after close", async () => { + const first = await connectPair(); + await first.app.close(); + await first.server.close(); + + 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", + }); + }); + + it("preserves auto-resize setup after Apps initialization", async () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: true }, + ); + let setupCalls = 0; + app.setupSizeChangedNotifications = () => { + setupCalls += 1; + return () => {}; + }; + + const pair = await connectPair(app); + connected.push(pair); + + expect(setupCalls).toBe(1); + }); + + it("preserves strict late-handler timing errors", async () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: false, strict: true }, + ); + const pair = await connectPair(app); + connected.push(pair); + + expect(() => { + app.ontoolinput = () => {}; + }).toThrow(/handler registered after connect/); + }); + + it("uses base MCP SDK replacement semantics for request handlers", async () => { + const pair = await connectPair(); + connected.push(pair); + let handledBy = 0; + + pair.app.setRequestHandler("ping", () => { + handledBy = 1; + return {}; + }); + pair.app.setRequestHandler("ping", () => { + handledBy = 2; + return {}; + }); + + await pair.server.request({ method: "ping", params: {} }); + expect(handledBy).toBe(2); + }); + + it("uses base MCP SDK replacement semantics for non-event notifications", async () => { + const pair = await connectPair(); + connected.push(pair); + const calls: number[] = []; + const params = z.object({}); + + pair.app.setNotificationHandler("test/notification", { params }, () => { + calls.push(1); + }); + pair.app.setNotificationHandler("test/notification", { params }, () => { + calls.push(2); + }); + await pair.server.notification({ + method: "test/notification", + params: {}, + }); + + expect(calls).toEqual([2]); + }); +}); diff --git a/src/app.ts b/src/app.ts index c586c31..3c8f5b7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,38 +1,27 @@ import { + Client, + type ClientContext, + type ClientOptions, type RequestOptions, - mergeCapabilities, - Protocol, - ProtocolOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; - -import { - 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"; + type CallToolRequest, + type CallToolResult, + type CreateMessageRequest, + type CreateMessageResult, + type CreateMessageResultWithTools, + type Implementation, + type ListResourcesRequest, + type ListResourcesResult, + type ListToolsRequest, + type ListToolsResult, + type LoggingMessageNotification, + type ReadResourceRequest, + type ReadResourceResult, + type Tool, + type ToolAnnotations, + type ToolListChangedNotification, + type Transport, +} from "@modelcontextprotocol/client"; +import { EmptyResultSchema } from "@modelcontextprotocol/core"; import { EventDispatcher } from "./events"; export { EventDispatcher } from "./events"; import { PostMessageTransport } from "./message-transport"; @@ -56,6 +45,7 @@ import { McpUiResourceTeardownRequest, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResult, + McpUiResourceTeardownResultSchema, McpUiRequestTeardownNotification, McpUiSizeChangedNotification, McpUiToolCancelledNotification, @@ -69,15 +59,36 @@ import { McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeResultSchema, } from "./types"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { StandardSchemaV1, standardSchemaToJsonSchema, validateStandardSchema, } from "./standard-schema"; -import { z, type ZodLiteral, type ZodObject } from "zod/v4"; +import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4"; -type MethodSchema = ZodObject<{ method: ZodLiteral }>; +type MethodSchema = ZodObject<{ + method: ZodLiteral; + params: ZodType; +}>; + +const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; + +function mergeAppCapabilities( + base: McpUiAppCapabilities, + additional: McpUiAppCapabilities, +): McpUiAppCapabilities { + const result = { ...base, ...additional }; + if (additional.experimental !== undefined) { + result.experimental = { + ...base.experimental, + ...additional.experimental, + }; + } + if (additional.tools !== undefined) { + result.tools = { ...base.tools, ...additional.tools }; + } + return result; +} export type { StandardSchemaV1, @@ -167,7 +178,10 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; * * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options */ -export type AppOptions = ProtocolOptions & { +export type AppOptions = Omit< + ClientOptions, + "capabilities" | "supportedProtocolVersions" | "versionNegotiation" +> & { /** * Automatically report size changes to the host using `ResizeObserver`. * @@ -207,9 +221,7 @@ export type AppOptions = ProtocolOptions & { allowUnsafeEval?: boolean; }; -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; +type RequestHandlerExtra = ClientContext; /** * Result of an app-registered tool callback. When `Out` is provided, @@ -343,38 +355,15 @@ export type AppEventMap = { * await app.connect(); * ``` */ -export class App extends Protocol { +export class App extends Client { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; private _registeredTools: { [name: string]: RegisteredAppTool } = {}; private _initializedSent = false; - private readonly _registeredMethods = new Set(); private readonly _registeredEvents = new Set(); private readonly _events = new EventDispatcher(); - // ProtocolWithEvents previously owned these registration guards. Keep them - // on App to preserve the existing double-registration behavior while event - // fan-out moves to protocol-independent composition. Arrow-function fields - // ensure Protocol's constructor uses its own methods before the guards init. - override setRequestHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setRequestHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setRequestHandler"); - super.setRequestHandler(schema, handler); - }; - - override setNotificationHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setNotificationHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setNotificationHandler"); - super.setNotificationHandler(schema, handler); - }; - /** * Warn if a host-bound method is called before {@link connect `connect`} has * completed the `ui/initialize` → `ui/notifications/initialized` handshake. @@ -479,13 +468,17 @@ export class App extends Protocol { if (this._registeredEvents.has(event)) return; const schema = this.eventSchemas[event]; if (!schema) throw new Error(`Unknown event: ${String(event)}`); - this.setNotificationHandler(schema, (notification) => { - const params = (notification as { params: AppEventMap[K] }).params; - if (event === "hostcontextchanged") { - this._hostContext = { ...this._hostContext, ...params }; - } - this._events.dispatch(event, params); - }); + this.setNotificationHandler( + schema.shape.method.value, + { params: schema.shape.params }, + (params) => { + const eventParams = params as AppEventMap[K]; + if (event === "hostcontextchanged") { + this._hostContext = { ...this._hostContext, ...eventParams }; + } + this._events.dispatch(event, eventParams); + }, + ); this._registeredEvents.add(event); } @@ -508,33 +501,11 @@ export class App extends Protocol { } } - protected replaceRequestHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setRequestHandler"] = (schema, handler) => { - const method = (schema as MethodSchema).shape.method.value; - this._registeredMethods.add(method); - super.setRequestHandler(schema, handler); - }; - - 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); - } - /** * Create a new MCP App instance. * * @param _appInfo - App identification (name and version) - * @param _capabilities - Features and capabilities this app provides + * @param _appCapabilities - Features and capabilities this app provides * @param options - Configuration options including `autoResize` behavior * * @example @@ -548,16 +519,29 @@ export class App extends Protocol { */ constructor( private _appInfo: Implementation, - private _capabilities: McpUiAppCapabilities = {}, + private _appCapabilities: McpUiAppCapabilities = {}, private options: AppOptions = { autoResize: true }, ) { - super(options); + // 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], + }); if (!options.allowUnsafeEval) { z.config({ jitless: true }); } - this.setRequestHandler(PingRequestSchema, (request) => { + this.setRequestHandler("ping", (request) => { console.log("Received ping:", request.params); return {}; }); @@ -567,13 +551,16 @@ export class App extends Protocol { this.setEventHandler("hostcontextchanged", undefined); } - private registerCapabilities(capabilities: McpUiAppCapabilities): void { + 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 = mergeAppCapabilities( + this._appCapabilities, + capabilities, + ); } registerTool< @@ -596,7 +583,7 @@ export class App extends Protocol { } const app = this; const notify = () => { - if (app._initializedSent && app._capabilities.tools?.listChanged) { + if (app._initializedSent && app._appCapabilities.tools?.listChanged) { void app.sendToolListChanged(); } }; @@ -668,8 +655,8 @@ export class App extends Protocol { // 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(); @@ -1081,11 +1068,15 @@ export class App extends Protocol { ) { this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); this._onteardown = callback; - this.replaceRequestHandler( - McpUiResourceTeardownRequestSchema, - (request, extra) => { + this.setRequestHandler( + "ui/resource-teardown", + { + params: McpUiResourceTeardownRequestSchema.shape.params, + result: McpUiResourceTeardownResultSchema, + }, + (params, extra) => { if (!this._onteardown) throw new Error("No onteardown handler set"); - return this._onteardown(request.params, extra); + return this._onteardown(params, extra); }, ); } @@ -1135,7 +1126,7 @@ export class App extends Protocol { ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => { + this.setRequestHandler("tools/call", (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); }); @@ -1206,7 +1197,7 @@ export class App extends Protocol { callback, ); this._onlisttools = callback; - this.replaceRequestHandler(ListToolsRequestSchema, (request, extra) => { + this.setRequestHandler("tools/list", (request, extra) => { if (!this._onlisttools) throw new Error("No onlisttools handler set"); return this._onlisttools(request.params, extra); }); @@ -1216,7 +1207,7 @@ export class App extends Protocol { * Verify that the host supports the capability required for the given request method. * @internal */ - assertCapabilityForMethod(method: AppRequest["method"]): void { + protected override assertCapabilityForMethod(method: string): void { switch (method) { case "sampling/createMessage": if (!this._hostCapabilities?.sampling) { @@ -1232,11 +1223,11 @@ export class App extends Protocol { * Verify that the app declared the capability required for the given request method. * @internal */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { + protected override assertRequestHandlerCapability(method: string): void { switch (method) { case "tools/call": case "tools/list": - if (!this._capabilities.tools) { + if (!this._appCapabilities.tools) { throw new Error( `Client does not support tool capability (required for ${method})`, ); @@ -1254,26 +1245,10 @@ export class App extends Protocol { * Verify that the app supports the capability required for the given notification method. * @internal */ - assertNotificationCapability(_method: AppNotification["method"]): void { + protected override assertNotificationCapability(_method: string): 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"); - } - /** * Call a tool on the originating MCP server (proxied through the host). * @@ -1322,7 +1297,6 @@ export class App extends Protocol { } 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 @@ -1379,11 +1353,7 @@ export class App extends Protocol { options?: RequestOptions, ): Promise { this._assertInitialized("readServerResource"); - return await this.request( - { method: "resources/read", params }, - ReadResourceResultSchema, - options, - ); + return await this.request({ method: "resources/read", params }, options); } /** @@ -1427,11 +1397,7 @@ export class App extends Protocol { options?: RequestOptions, ): Promise { this._assertInitialized("listServerResources"); - return await this.request( - { method: "resources/list", params }, - ListResourcesResultSchema, - options, - ); + return await this.request({ method: "resources/list", params }, options); } /** @@ -1508,12 +1474,8 @@ export class App extends Protocol { options?: RequestOptions, ): Promise { this._assertInitialized("createSamplingMessage"); - const resultSchema = params.tools - ? CreateMessageResultWithToolsSchema - : CreateMessageResultSchema; return await this.request( { method: "sampling/createMessage", params }, - resultSchema, options, ); } @@ -2030,14 +1992,23 @@ export class App extends Protocol { ); } this._initializedSent = false; - await super.connect(transport); + // 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 { + await super.connect(transport); + const result = await this.request( { method: "ui/initialize", params: { - appCapabilities: this._capabilities, + appCapabilities: this._appCapabilities, appInfo: this._appInfo, protocolVersion: LATEST_PROTOCOL_VERSION, },