refactor: migrate AppBridge to SDK v2 server

This commit is contained in:
Andrew Khadder 2026-07-14 15:34:33 -07:00
commit d19a1fb83a
No known key found for this signature in database
2 changed files with 546 additions and 317 deletions

View file

@ -1,14 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Server, type ServerCapabilities } from "@modelcontextprotocol/server";
import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
import {
EmptyResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ReadResourceResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { App } from "./app"; import { App } from "./app";
@ -19,6 +11,7 @@ import {
getToolUiResourceUri, getToolUiResourceUri,
isToolVisibilityModelOnly, isToolVisibilityModelOnly,
isToolVisibilityAppOnly, isToolVisibilityAppOnly,
McpUiOpenLinkResultSchema,
type McpUiHostCapabilities, type McpUiHostCapabilities,
} from "./app-bridge"; } from "./app-bridge";
@ -31,11 +24,18 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
*/ */
function createMockClient( function createMockClient(
serverCapabilities: ServerCapabilities = {}, serverCapabilities: ServerCapabilities = {},
): Pick<Client, "getServerCapabilities" | "request" | "notification"> { ): Pick<
Client,
| "getServerCapabilities"
| "request"
| "notification"
| "setNotificationHandler"
> {
return { return {
getServerCapabilities: () => serverCapabilities, getServerCapabilities: () => serverCapabilities,
request: async () => ({}) as never, request: async () => ({}) as never,
notification: async () => {}, notification: async () => {},
setNotificationHandler: () => {},
}; };
} }
@ -82,6 +82,58 @@ describe("App <-> AppBridge integration", () => {
expect(initializedFired).toBe(true); expect(initializedFired).toBe(true);
}); });
it("keeps standard initialization separate from the Apps-ready gate", async () => {
const methods: string[] = [];
let releaseAppsInitialized!: () => void;
let reachedAppsInitialized!: () => void;
const appsInitializedGate = new Promise<void>((resolve) => {
releaseAppsInitialized = resolve;
});
const reachedGate = new Promise<void>((resolve) => {
reachedAppsInitialized = resolve;
});
const originalSend = appTransport.send.bind(appTransport);
appTransport.send = async (message, options) => {
if ("method" in message) methods.push(message.method);
if (
"method" in message &&
message.method === "ui/notifications/initialized"
) {
reachedAppsInitialized();
await appsInitializedGate;
}
return originalSend(message, options);
};
let singularCalls = 0;
let listenerCalls = 0;
bridge.oninitialized = () => singularCalls++;
bridge.addEventListener("initialized", () => listenerCalls++);
await bridge.connect(bridgeTransport);
const connecting = app.connect(appTransport);
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);
releaseAppsInitialized();
await connecting;
expect(singularCalls).toBe(1);
expect(listenerCalls).toBe(1);
});
it("App receives host info and capabilities after connect", async () => { it("App receives host info and capabilities after connect", async () => {
await bridge.connect(bridgeTransport); await bridge.connect(bridgeTransport);
await app.connect(appTransport); await app.connect(appTransport);
@ -91,6 +143,8 @@ describe("App <-> AppBridge integration", () => {
const hostCaps = app.getHostCapabilities(); const hostCaps = app.getHostCapabilities();
expect(hostCaps).toEqual(testHostCapabilities); expect(hostCaps).toEqual(testHostCapabilities);
expect(bridge.getHostCapabilities()).toEqual(testHostCapabilities);
expect(bridge.getCapabilities()).toEqual({});
}); });
it("Bridge receives app info and capabilities after initialization", async () => { it("Bridge receives app info and capabilities after initialization", async () => {
@ -611,6 +665,30 @@ describe("App <-> AppBridge integration", () => {
expect(result.isError).toBe(true); expect(result.isError).toBe(true);
}); });
it("rejects invalid params for a custom ui request", async () => {
bridge.onopenlink = async () => ({});
await app.connect(appTransport);
await expect(
app.request(
{
method: "ui/open-link",
params: { url: 42 },
},
McpUiOpenLinkResultSchema,
),
).rejects.toThrow();
});
it("rejects invalid results from a custom ui handler", async () => {
bridge.onopenlink = async () => ({ isError: "yes" }) as never;
await app.connect(appTransport);
await expect(
app.openLink({ url: "https://example.com" }),
).rejects.toThrow();
});
}); });
describe("deprecated method aliases", () => { describe("deprecated method aliases", () => {
@ -680,10 +758,7 @@ describe("App <-> AppBridge integration", () => {
await app.connect(appTransport); await app.connect(appTransport);
// Bridge can send ping via the protocol's request method // Bridge can send ping via the protocol's request method
const result = await bridge.request( const result = await bridge.request({ method: "ping", params: {} });
{ method: "ping", params: {} },
EmptyResultSchema,
);
expect(result).toEqual({}); expect(result).toEqual({});
}); });
@ -798,7 +873,7 @@ describe("App <-> AppBridge integration", () => {
}); });
await bridge.callTool({ name: "noargs", arguments: {} }); await bridge.callTool({ name: "noargs", arguments: {} });
expect(receivedExtra).toBeDefined(); expect(receivedExtra).toBeDefined();
expect(receivedExtra.signal).toBeInstanceOf(AbortSignal); expect(receivedExtra.mcpReq.signal).toBeInstanceOf(AbortSignal);
}); });
it("isError result skips output schema validation", async () => { it("isError result skips output schema validation", async () => {
@ -2180,10 +2255,10 @@ describe("App <-> AppBridge integration", () => {
await app.connect(appTransport); await app.connect(appTransport);
// App sends resources/list request via the protocol's request method // App sends resources/list request via the protocol's request method
const result = await app.request( const result = await app.request({
{ method: "resources/list", params: requestParams }, method: "resources/list",
ListResourcesResultSchema, params: requestParams,
); });
expect(receivedRequests).toHaveLength(1); expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams); expect(receivedRequests[0]).toMatchObject(requestParams);
@ -2224,10 +2299,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport); await bridge.connect(bridgeTransport);
await app.connect(appTransport); await app.connect(appTransport);
const result = await app.request( const result = await app.request({
{ method: "resources/read", params: requestParams }, method: "resources/read",
ReadResourceResultSchema, params: requestParams,
); });
expect(receivedRequests).toHaveLength(1); expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams); expect(receivedRequests[0]).toMatchObject(requestParams);
@ -2275,10 +2350,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport); await bridge.connect(bridgeTransport);
await app.connect(appTransport); await app.connect(appTransport);
const result = await app.request( const result = await app.request({
{ method: "resources/templates/list", params: requestParams }, method: "resources/templates/list",
ListResourceTemplatesResultSchema, params: requestParams,
); });
expect(receivedRequests).toHaveLength(1); expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams); expect(receivedRequests[0]).toMatchObject(requestParams);
@ -2298,10 +2373,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport); await bridge.connect(bridgeTransport);
await app.connect(appTransport); await app.connect(appTransport);
const result = await app.request( const result = await app.request({
{ method: "prompts/list", params: requestParams }, method: "prompts/list",
ListPromptsResultSchema, params: requestParams,
); });
expect(receivedRequests).toHaveLength(1); expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams); expect(receivedRequests[0]).toMatchObject(requestParams);
@ -2310,6 +2385,7 @@ describe("App <-> AppBridge integration", () => {
it("sendToolListChanged sends notification to app", async () => { it("sendToolListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = []; const receivedNotifications: unknown[] = [];
bridge.oncalltool = async () => ({ content: [] });
app.setNotificationHandler( app.setNotificationHandler(
"notifications/tools/list_changed", "notifications/tools/list_changed",
(notification) => { (notification) => {
@ -2328,6 +2404,7 @@ describe("App <-> AppBridge integration", () => {
it("sendResourceListChanged sends notification to app", async () => { it("sendResourceListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = []; const receivedNotifications: unknown[] = [];
bridge.onlistresources = async () => ({ resources: [] });
app.setNotificationHandler( app.setNotificationHandler(
"notifications/resources/list_changed", "notifications/resources/list_changed",
(notification) => { (notification) => {
@ -2346,6 +2423,7 @@ describe("App <-> AppBridge integration", () => {
it("sendPromptListChanged sends notification to app", async () => { it("sendPromptListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = []; const receivedNotifications: unknown[] = [];
bridge.onlistprompts = async () => ({ prompts: [] });
app.setNotificationHandler( app.setNotificationHandler(
"notifications/prompts/list_changed", "notifications/prompts/list_changed",
(notification) => { (notification) => {
@ -2364,6 +2442,184 @@ describe("App <-> AppBridge integration", () => {
}); });
}); });
describe("AppBridge outer v2 Client proxy", () => {
let outerClient: Client;
let outerServer: Server;
let outerClientTransport: InMemoryTransport;
let outerServerTransport: InMemoryTransport;
let outerInitializeCount: number;
beforeEach(async () => {
[outerClientTransport, outerServerTransport] =
InMemoryTransport.createLinkedPair();
outerInitializeCount = 0;
const originalSend = outerClientTransport.send.bind(outerClientTransport);
outerClientTransport.send = async (message, options) => {
if ("method" in message && message.method === "initialize") {
outerInitializeCount++;
}
return originalSend(message, options);
};
outerServer = new Server(
{ name: "ActualServer", version: "1.0.0" },
{
capabilities: {
tools: { listChanged: true },
resources: { listChanged: true },
prompts: { listChanged: true },
},
supportedProtocolVersions: ["2025-11-25"],
},
);
outerServer.setRequestHandler("tools/call", async (request) => ({
content: [
{
type: "text",
text: `outer:${request.params.name}`,
},
],
}));
outerClient = new Client(
{ name: "HostOuterClient", version: "1.0.0" },
{
capabilities: {},
versionNegotiation: { mode: "legacy" },
supportedProtocolVersions: ["2025-11-25"],
},
);
await outerServer.connect(outerServerTransport);
await outerClient.connect(outerClientTransport);
});
afterEach(async () => {
await outerClient.close().catch(() => {});
await outerServer.close().catch(() => {});
});
it("proxies through the outer Client without reconnecting it", async () => {
const bridge = new AppBridge(
outerClient,
testHostInfo,
testHostCapabilities,
);
const app = new App(testAppInfo, {}, { autoResize: false });
let [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair();
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
expect(
await app.callServerTool({ name: "first", arguments: {} }),
).toMatchObject({
content: [{ type: "text", text: "outer:first" }],
});
expect(outerInitializeCount).toBe(1);
await app.close();
await bridge.close();
[appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair();
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
expect(
await app.callServerTool({ name: "second", arguments: {} }),
).toMatchObject({
content: [{ type: "text", text: "outer:second" }],
});
expect(outerInitializeCount).toBe(1);
expect(outerClient.getServerVersion()).toMatchObject({
name: "ActualServer",
version: "1.0.0",
});
await app.close();
await bridge.close();
});
it("propagates View cancellation to the actual outer Server", async () => {
let started!: () => void;
let aborted!: () => void;
const startedPromise = new Promise<void>((resolve) => {
started = resolve;
});
const abortedPromise = new Promise<void>((resolve) => {
aborted = resolve;
});
outerServer.setRequestHandler("tools/call", async (_request, context) => {
started();
await new Promise<void>((resolve) => {
context.mcpReq.signal.addEventListener(
"abort",
() => {
aborted();
resolve();
},
{ once: true },
);
});
return { content: [] };
});
const bridge = new AppBridge(
outerClient,
testHostInfo,
testHostCapabilities,
);
const app = new App(testAppInfo, {}, { autoResize: false });
const [appTransport, bridgeTransport] =
InMemoryTransport.createLinkedPair();
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
const controller = new AbortController();
const result = app.callServerTool(
{ name: "cancel-me", arguments: {} },
{ signal: controller.signal },
);
await startedPromise;
controller.abort("test cancellation");
await abortedPromise;
await expect(result).rejects.toThrow();
await app.close();
await bridge.close();
});
it("forwards outer list-changed notifications to the App", async () => {
const bridge = new AppBridge(
outerClient,
testHostInfo,
testHostCapabilities,
);
const app = new App(testAppInfo, {}, { autoResize: false });
const [appTransport, bridgeTransport] =
InMemoryTransport.createLinkedPair();
const received: string[] = [];
app.setNotificationHandler("notifications/tools/list_changed", () => {
received.push("tools");
});
app.setNotificationHandler("notifications/resources/list_changed", () => {
received.push("resources");
});
app.setNotificationHandler("notifications/prompts/list_changed", () => {
received.push("prompts");
});
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
await outerServer.sendToolListChanged();
await outerServer.sendResourceListChanged();
await outerServer.sendPromptListChanged();
await flush();
expect(received).toEqual(["tools", "resources", "prompts"]);
await app.close();
await bridge.close();
});
});
describe("getToolUiResourceUri", () => { describe("getToolUiResourceUri", () => {
describe("new nested format (_meta.ui.resourceUri)", () => { describe("new nested format (_meta.ui.resourceUri)", () => {
it("extracts resourceUri from _meta.ui.resourceUri", () => { it("extracts resourceUri from _meta.ui.resourceUri", () => {
@ -2758,24 +3014,16 @@ describe("isToolVisibilityAppOnly", () => {
expect(app.onteardown).toBe(handler); expect(app.onteardown).toBe(handler);
}); });
it("direct setRequestHandler throws when called twice", () => { it("direct setRequestHandler uses base SDK replacement semantics", () => {
const bridge2 = new AppBridge( const bridge2 = new AppBridge(
createMockClient() as Client, createMockClient() as Client,
testHostInfo, testHostInfo,
testHostCapabilities, testHostCapabilities,
); );
bridge2.setRequestHandler( bridge2.setRequestHandler("ping", () => ({}));
// @ts-expect-error — exercising throw path with raw schema
{ shape: { method: { value: "test/method" } } },
() => ({}),
);
expect(() => { expect(() => {
bridge2.setRequestHandler( bridge2.setRequestHandler("ping", () => ({}));
// @ts-expect-error — exercising throw path with raw schema }).not.toThrow();
{ shape: { method: { value: "test/method" } } },
() => ({}),
);
}).toThrow(/already registered/);
}); });
}); });
}); });

View file

@ -1,61 +1,51 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { type Client } from "@modelcontextprotocol/client";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { import {
CallToolRequest, Server,
CallToolRequestSchema, type CallToolRequest,
CallToolResult, type CallToolResult,
CallToolResultSchema, type CreateMessageRequest,
CreateMessageRequest, type CreateMessageResult,
CreateMessageRequestSchema, type CreateMessageResultWithTools,
CreateMessageResult, type EmptyResult,
CreateMessageResultWithTools, type Implementation,
EmptyResult, type JSONRPCRequest,
Implementation, type ListPromptsRequest,
ListPromptsRequest, type ListPromptsResult,
ListPromptsRequestSchema, type ListResourcesRequest,
ListPromptsResult, type ListResourcesResult,
ListPromptsResultSchema, type ListResourceTemplatesRequest,
ListResourcesRequest, type ListResourceTemplatesResult,
ListResourcesRequestSchema, type ListToolsRequest,
ListResourcesResult, type LoggingMessageNotification,
ListResourcesResultSchema, type PingRequest,
ListResourceTemplatesRequest, type PromptListChangedNotification,
ListResourceTemplatesRequestSchema, type ReadResourceRequest,
ListResourceTemplatesResult, type ReadResourceResult,
ListResourceTemplatesResultSchema, type RequestOptions,
ListToolsRequest, type ResourceListChangedNotification,
ListToolsRequestSchema, type Result,
ListToolsResultSchema, type ServerCapabilities,
LoggingMessageNotification, type ServerContext,
type ServerOptions,
type Tool,
type ToolListChangedNotification,
type Transport,
} from "@modelcontextprotocol/server";
import {
EmptyResultSchema,
LoggingMessageNotificationSchema, LoggingMessageNotificationSchema,
PingRequest, } from "@modelcontextprotocol/core";
PingRequestSchema,
PromptListChangedNotification,
PromptListChangedNotificationSchema,
ReadResourceRequest,
ReadResourceRequestSchema,
ReadResourceResult,
ReadResourceResultSchema,
ResourceListChangedNotification,
ResourceListChangedNotificationSchema,
Tool,
ToolListChangedNotification,
ToolListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js";
import {
Protocol,
ProtocolOptions,
RequestOptions,
} from "@modelcontextprotocol/sdk/shared/protocol.js";
import { EventDispatcher } from "./events"; import { EventDispatcher } from "./events";
import type { ZodLiteral, ZodObject } from "zod/v4"; import type { ZodLiteral, ZodObject, ZodType } from "zod/v4";
type MethodSchema = ZodObject<{ method: ZodLiteral<string> }>; type MethodSchema = ZodObject<{
method: ZodLiteral<string>;
params: ZodType;
}>;
const INNER_MCP_PROTOCOL_VERSION = "2025-11-25";
import { import {
type AppNotification,
type AppRequest,
type AppResult,
type McpUiSandboxResourceReadyNotification, type McpUiSandboxResourceReadyNotification,
type McpUiSizeChangedNotification, type McpUiSizeChangedNotification,
type McpUiToolCancelledNotification, type McpUiToolCancelledNotification,
@ -74,15 +64,19 @@ import {
McpUiInitializeRequest, McpUiInitializeRequest,
McpUiInitializeRequestSchema, McpUiInitializeRequestSchema,
McpUiInitializeResult, McpUiInitializeResult,
McpUiInitializeResultSchema,
McpUiMessageRequest, McpUiMessageRequest,
McpUiMessageRequestSchema, McpUiMessageRequestSchema,
McpUiMessageResult, McpUiMessageResult,
McpUiMessageResultSchema,
McpUiOpenLinkRequest, McpUiOpenLinkRequest,
McpUiOpenLinkRequestSchema, McpUiOpenLinkRequestSchema,
McpUiOpenLinkResult, McpUiOpenLinkResult,
McpUiOpenLinkResultSchema,
McpUiDownloadFileRequest, McpUiDownloadFileRequest,
McpUiDownloadFileRequestSchema, McpUiDownloadFileRequestSchema,
McpUiDownloadFileResult, McpUiDownloadFileResult,
McpUiDownloadFileResultSchema,
McpUiResourceTeardownRequest, McpUiResourceTeardownRequest,
McpUiResourceTeardownResultSchema, McpUiResourceTeardownResultSchema,
McpUiRequestTeardownNotification, McpUiRequestTeardownNotification,
@ -93,6 +87,7 @@ import {
McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeRequest,
McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeRequestSchema,
McpUiRequestDisplayModeResult, McpUiRequestDisplayModeResult,
McpUiRequestDisplayModeResultSchema,
McpUiResourcePermissions, McpUiResourcePermissions,
McpUiToolMeta, McpUiToolMeta,
} from "./types"; } from "./types";
@ -101,6 +96,20 @@ export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app";
import { RESOURCE_URI_META_KEY } from "./app"; import { RESOURCE_URI_META_KEY } from "./app";
export { PostMessageTransport } from "./message-transport"; 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. * Extract UI resource URI from tool metadata.
* *
@ -206,10 +215,13 @@ export function buildAllowAttribute(
* *
* @property hostContext - Optional initial host context to provide to the view * @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 * @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure
*/ */
export type HostOptions = ProtocolOptions & { export type HostOptions = Omit<
ServerOptions,
"capabilities" | "supportedProtocolVersions"
> & {
hostContext?: McpUiHostContext; hostContext?: McpUiHostContext;
}; };
@ -224,15 +236,12 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION];
/** /**
* Extra metadata passed to request handlers. * Extra metadata passed to request handlers.
* *
* This type represents the additional context provided by the `Protocol` class * This type represents the additional context provided by the base MCP SDK
* when handling requests, including abort signals and session information. * `Server` when handling requests.
* It is extracted from the MCP SDK's request handler signature.
* *
* @internal * @internal
*/ */
type RequestHandlerExtra = Parameters< type RequestHandlerExtra = ServerContext;
Parameters<AppBridge["setRequestHandler"]>[1]
>[1];
/** /**
* Maps DOM-style event names to their notification `params` types. * Maps DOM-style event names to their notification `params` types.
@ -306,66 +315,53 @@ export type AppBridgeEventMap = {
* await bridge.connect(transport); * await bridge.connect(transport);
* ``` * ```
*/ */
export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult> { export class AppBridge extends Server {
private _appCapabilities?: McpUiAppCapabilities; private _appCapabilities?: McpUiAppCapabilities;
private _hostContext: McpUiHostContext = {}; private _hostContext: McpUiHostContext = {};
private _appInfo?: Implementation; private _appInfo?: Implementation;
private _initializedReceived = false; private _initializedReceived = false;
private readonly _registeredMethods = new Set<string>();
private readonly _registeredEvents = new Set<keyof AppBridgeEventMap>(); private readonly _registeredEvents = new Set<keyof AppBridgeEventMap>();
private readonly _events = new EventDispatcher<AppBridgeEventMap>(); private readonly _events = new EventDispatcher<AppBridgeEventMap>();
// ProtocolWithEvents previously owned these registration guards. Keep them
// on AppBridge to preserve the existing double-registration behavior while
// event fan-out moves to protocol-independent composition. Arrow-function
// fields let Protocol's constructor run before the guards initialize.
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);
};
/** /**
* Wrap every handler registered via `replaceRequestHandler` with a check * The base MCP SDK calls this hook for every standard and custom request
* that the View has sent `ui/notifications/initialized`. Warns (never * handler registration. AppBridge uses it to preserve the existing warning
* throws) so lenient hosts keep working while still surfacing the * when a View calls host methods before `ui/notifications/initialized`,
* misordering that leaves strict hosts with a permanently hidden iframe. * without adding overload-forwarding adapters around `setRequestHandler`.
* `ui/initialize` and `ping` use `setRequestHandler` directly and are * Chaining through `super._wrapHandler()` retains the base `Server`'s own
* intentionally exempt. * 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.
* *
* @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149}
*/ */
protected replaceRequestHandler: Protocol< protected override _wrapHandler(
AppRequest, method: string,
AppNotification, handler: (
AppResult request: JSONRPCRequest,
>["setRequestHandler"] = (schema, handler) => { context: ServerContext,
const method = (schema as MethodSchema).shape.method.value; ) => Promise<Result>,
this._registeredMethods.add(method); ): (request: JSONRPCRequest, context: ServerContext) => Promise<Result> {
super.setRequestHandler(schema, (request, extra) => { const wrapped = super._wrapHandler(method, handler);
if (
method === "initialize" ||
method === "ui/initialize" ||
method === "ping"
) {
return wrapped;
}
return async (request, context) => {
if (!this._initializedReceived) { if (!this._initializedReceived) {
console.warn( console.warn(
`[ext-apps] AppBridge received '${request.method}' before ` + `[ext-apps] AppBridge received '${method}' before ` +
`ui/notifications/initialized. The View is calling host ` + `ui/notifications/initialized. The View is calling host ` +
`methods before completing the handshake; it should await ` + `methods before completing the handshake; it should await ` +
`app.connect() first.`, `app.connect() first.`,
); );
} }
return handler(request, extra); return wrapped(request, context);
}); };
}; }
private readonly eventSchemas: { private readonly eventSchemas: {
[K in keyof AppBridgeEventMap]: MethodSchema; [K in keyof AppBridgeEventMap]: MethodSchema;
@ -406,17 +402,27 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
this._events.removeEventListener(event, handler); this._events.removeEventListener(event, handler);
} }
private _ensureEventSlot<K extends keyof AppBridgeEventMap>( private _ensureEventSlot<K extends keyof AppBridgeEventMap>(event: K): void {
event: K,
): void {
if (this._registeredEvents.has(event)) return; if (this._registeredEvents.has(event)) return;
const schema = this.eventSchemas[event]; const schema = this.eventSchemas[event];
if (!schema) throw new Error(`Unknown event: ${String(event)}`); if (!schema) throw new Error(`Unknown event: ${String(event)}`);
this.setNotificationHandler(schema, (notification) => {
const params = (notification as { params: AppBridgeEventMap[K] }).params; const dispatch = (params: AppBridgeEventMap[K]) => {
if (event === "initialized") this._initializedReceived = true; if (event === "initialized") this._initializedReceived = true;
this._events.dispatch(event, params); this._events.dispatch(event, params);
}); };
if (event === "loggingmessage") {
this.setNotificationHandler("notifications/message", (notification) => {
dispatch(notification.params as AppBridgeEventMap[K]);
});
} else {
this.setNotificationHandler(
schema.shape.method.value,
{ params: schema.shape.params },
(params) => dispatch(params as AppBridgeEventMap[K]),
);
}
this._registeredEvents.add(event); this._registeredEvents.add(event);
} }
@ -433,16 +439,12 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
} }
} }
private _assertMethodNotRegistered(schema: unknown, via: string): void { private ensureInnerCapabilities(capabilities: ServerCapabilities): void {
const method = (schema as MethodSchema).shape.method.value; const current = super.getCapabilities();
if (this._registeredMethods.has(method)) { const missing = Object.keys(capabilities).some(
throw new Error( (key) => current[key as keyof ServerCapabilities] === undefined,
`Handler for "${method}" already registered (via ${via}). ` + );
`Use addEventListener() to attach multiple listeners, ` + if (missing) this.registerCapabilities(capabilities);
`or the on* setter for replace semantics.`,
);
}
this._registeredMethods.add(method);
} }
/** /**
@ -453,8 +455,8 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
* between the View and the server. When `null`, you must register handlers * between the View and the server. When `null`, you must register handlers
* manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters. * manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters.
* @param _hostInfo - Host application identification (name and version) * @param _hostInfo - Host application identification (name and version)
* @param _capabilities - Features and capabilities the host supports * @param _hostCapabilities - Features and capabilities the host supports
* @param options - Configuration options (inherited from Protocol) * @param options - Configuration options (inherited from Server)
* *
* @example With MCP client (automatic forwarding) * @example With MCP client (automatic forwarding)
* ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient" * ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient"
@ -481,29 +483,61 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
constructor( constructor(
private _client: Client | null, private _client: Client | null,
private _hostInfo: Implementation, private _hostInfo: Implementation,
private _capabilities: McpUiHostCapabilities, private _hostCapabilities: McpUiHostCapabilities,
options?: HostOptions, options?: HostOptions,
) { ) {
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(_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),
});
this._ensureEventSlot("initialized"); this._ensureEventSlot("initialized");
this._hostContext = options?.hostContext || {}; this._hostContext = options?.hostContext || {};
this.setRequestHandler(McpUiInitializeRequestSchema, (request) => this.setRequestHandler(
this._oninitialize(request), "ui/initialize",
{
params: McpUiInitializeRequestSchema.shape.params,
result: McpUiInitializeResultSchema,
},
(params) => this._onAppsInitialize(params),
); );
this.setRequestHandler(PingRequestSchema, (request, extra) => { this.setRequestHandler("ping", (request, extra) => {
this.onping?.(request.params, extra); this.onping?.(request.params, extra);
return {}; return {};
}); });
// Default handler for requestDisplayMode - returns current mode from host context. // Default handler for requestDisplayMode - returns current mode from host context.
// Hosts can override this by setting bridge.onrequestdisplaymode = ... // Hosts can override this by setting bridge.onrequestdisplaymode = ...
this.replaceRequestHandler( this.setRequestHandler(
McpUiRequestDisplayModeRequestSchema, "ui/request-display-mode",
(request) => { {
params: McpUiRequestDisplayModeRequestSchema.shape.params,
result: McpUiRequestDisplayModeResultSchema,
},
() => {
const currentMode = this._hostContext.displayMode ?? "inline"; const currentMode = this._hostContext.displayMode ?? "inline";
return { mode: currentMode }; return { mode: currentMode };
}, },
@ -680,18 +714,9 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
* @see {@link sendToolInput `sendToolInput`} for sending tool arguments to the View * @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`}. * @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}.
*/ */
get oninitialized(): override oninitialized?: (
| ((params: McpUiInitializedNotification["params"]) => void) params?: McpUiInitializedNotification["params"],
| undefined { ) => void;
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. * Register a handler for message requests from the view.
@ -744,11 +769,15 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
) { ) {
this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback);
this._onmessage = callback; this._onmessage = callback;
this.replaceRequestHandler( this.setRequestHandler(
McpUiMessageRequestSchema, "ui/message",
async (request, extra) => { {
params: McpUiMessageRequestSchema.shape.params,
result: McpUiMessageResultSchema,
},
async (params, extra) => {
if (!this._onmessage) throw new Error("No onmessage handler set"); if (!this._onmessage) throw new Error("No onmessage handler set");
return this._onmessage(request.params, extra); return this._onmessage(params, extra);
}, },
); );
} }
@ -813,11 +842,15 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
) { ) {
this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback);
this._onopenlink = callback; this._onopenlink = callback;
this.replaceRequestHandler( this.setRequestHandler(
McpUiOpenLinkRequestSchema, "ui/open-link",
async (request, extra) => { {
params: McpUiOpenLinkRequestSchema.shape.params,
result: McpUiOpenLinkResultSchema,
},
async (params, extra) => {
if (!this._onopenlink) throw new Error("No onopenlink handler set"); if (!this._onopenlink) throw new Error("No onopenlink handler set");
return this._onopenlink(request.params, extra); return this._onopenlink(params, extra);
}, },
); );
} }
@ -885,12 +918,16 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
callback, callback,
); );
this._ondownloadfile = callback; this._ondownloadfile = callback;
this.replaceRequestHandler( this.setRequestHandler(
McpUiDownloadFileRequestSchema, "ui/download-file",
async (request, extra) => { {
params: McpUiDownloadFileRequestSchema.shape.params,
result: McpUiDownloadFileResultSchema,
},
async (params, extra) => {
if (!this._ondownloadfile) if (!this._ondownloadfile)
throw new Error("No ondownloadfile handler set"); throw new Error("No ondownloadfile handler set");
return this._ondownloadfile(request.params, extra); return this._ondownloadfile(params, extra);
}, },
); );
} }
@ -985,12 +1022,16 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
callback, callback,
); );
this._onrequestdisplaymode = callback; this._onrequestdisplaymode = callback;
this.replaceRequestHandler( this.setRequestHandler(
McpUiRequestDisplayModeRequestSchema, "ui/request-display-mode",
async (request, extra) => { {
params: McpUiRequestDisplayModeRequestSchema.shape.params,
result: McpUiRequestDisplayModeResultSchema,
},
async (params, extra) => {
if (!this._onrequestdisplaymode) if (!this._onrequestdisplaymode)
throw new Error("No onrequestdisplaymode handler set"); throw new Error("No onrequestdisplaymode handler set");
return this._onrequestdisplaymode(request.params, extra); return this._onrequestdisplaymode(params, extra);
}, },
); );
} }
@ -1082,12 +1123,16 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
callback, callback,
); );
this._onupdatemodelcontext = callback; this._onupdatemodelcontext = callback;
this.replaceRequestHandler( this.setRequestHandler(
McpUiUpdateModelContextRequestSchema, "ui/update-model-context",
async (request, extra) => { {
params: McpUiUpdateModelContextRequestSchema.shape.params,
result: EmptyResultSchema,
},
async (params, extra) => {
if (!this._onupdatemodelcontext) if (!this._onupdatemodelcontext)
throw new Error("No onupdatemodelcontext handler set"); throw new Error("No onupdatemodelcontext handler set");
return this._onupdatemodelcontext(request.params, extra); return this._onupdatemodelcontext(params, extra);
}, },
); );
} }
@ -1135,13 +1180,11 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
) { ) {
this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback);
this._oncalltool = callback; this._oncalltool = callback;
this.replaceRequestHandler( this.ensureInnerCapabilities({ tools: {} });
CallToolRequestSchema, this.setRequestHandler("tools/call", async (request, extra) => {
async (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set");
if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra);
return this._oncalltool(request.params, extra); });
},
);
} }
/** /**
@ -1179,12 +1222,9 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
extra: RequestHandlerExtra, extra: RequestHandlerExtra,
) => Promise<CreateMessageResult | CreateMessageResultWithTools>, ) => Promise<CreateMessageResult | CreateMessageResultWithTools>,
) { ) {
this.setRequestHandler( this.setRequestHandler("sampling/createMessage", async (request, extra) => {
CreateMessageRequestSchema, return callback(request.params, extra);
async (request, extra) => { });
return callback(request.params, extra);
},
);
} }
/** /**
@ -1260,14 +1300,12 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
callback, callback,
); );
this._onlistresources = callback; this._onlistresources = callback;
this.replaceRequestHandler( this.ensureInnerCapabilities({ resources: {} });
ListResourcesRequestSchema, this.setRequestHandler("resources/list", async (request, extra) => {
async (request, extra) => { if (!this._onlistresources)
if (!this._onlistresources) throw new Error("No onlistresources handler set");
throw new Error("No onlistresources handler set"); return this._onlistresources(request.params, extra);
return this._onlistresources(request.params, extra); });
},
);
} }
/** /**
@ -1317,8 +1355,9 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
callback, callback,
); );
this._onlistresourcetemplates = callback; this._onlistresourcetemplates = callback;
this.replaceRequestHandler( this.ensureInnerCapabilities({ resources: {} });
ListResourceTemplatesRequestSchema, this.setRequestHandler(
"resources/templates/list",
async (request, extra) => { async (request, extra) => {
if (!this._onlistresourcetemplates) if (!this._onlistresourcetemplates)
throw new Error("No onlistresourcetemplates handler set"); throw new Error("No onlistresourcetemplates handler set");
@ -1374,14 +1413,12 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
callback, callback,
); );
this._onreadresource = callback; this._onreadresource = callback;
this.replaceRequestHandler( this.ensureInnerCapabilities({ resources: {} });
ReadResourceRequestSchema, this.setRequestHandler("resources/read", async (request, extra) => {
async (request, extra) => { if (!this._onreadresource)
if (!this._onreadresource) throw new Error("No onreadresource handler set");
throw new Error("No onreadresource handler set"); return this._onreadresource(request.params, extra);
return this._onreadresource(request.params, extra); });
},
);
} }
/** /**
@ -1459,14 +1496,11 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
callback, callback,
); );
this._onlistprompts = callback; this._onlistprompts = callback;
this.replaceRequestHandler( this.ensureInnerCapabilities({ prompts: {} });
ListPromptsRequestSchema, this.setRequestHandler("prompts/list", async (request, extra) => {
async (request, extra) => { if (!this._onlistprompts) throw new Error("No onlistprompts handler set");
if (!this._onlistprompts) return this._onlistprompts(request.params, extra);
throw new Error("No onlistprompts handler set"); });
return this._onlistprompts(request.params, extra);
},
);
} }
/** /**
@ -1495,46 +1529,6 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
}); });
} }
/**
* 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. * Get the host capabilities passed to the constructor.
* *
@ -1542,18 +1536,18 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
* *
* @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure * @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure
*/ */
getCapabilities(): McpUiHostCapabilities { getHostCapabilities(): McpUiHostCapabilities {
return this._capabilities; return this._hostCapabilities;
} }
/** /**
* Handle the ui/initialize request from the guest. * Handle the ui/initialize request from the guest.
* @internal * @internal
*/ */
private async _oninitialize( private async _onAppsInitialize(
request: McpUiInitializeRequest, params: McpUiInitializeRequest["params"],
): Promise<McpUiInitializeResult> { ): Promise<McpUiInitializeResult> {
const requestedVersion = request.params.protocolVersion; const requestedVersion = params.protocolVersion;
if (this._appInfo !== undefined) { if (this._appInfo !== undefined) {
console.warn( console.warn(
@ -1564,8 +1558,8 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
); );
} }
this._appCapabilities = request.params.appCapabilities; this._appCapabilities = params.appCapabilities;
this._appInfo = request.params.appInfo; this._appInfo = params.appInfo;
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes( const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(
requestedVersion, requestedVersion,
@ -1575,7 +1569,7 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
return { return {
protocolVersion, protocolVersion,
hostCapabilities: this.getCapabilities(), hostCapabilities: this.getHostCapabilities(),
hostInfo: this._hostInfo, hostInfo: this._hostInfo,
hostContext: this._hostContext, hostContext: this._hostContext,
}; };
@ -1857,11 +1851,7 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
* @returns Promise resolving to the tool call result * @returns Promise resolving to the tool call result
*/ */
callTool(params: CallToolRequest["params"], options?: RequestOptions) { callTool(params: CallToolRequest["params"], options?: RequestOptions) {
return this.request( return this.request({ method: "tools/call", params }, options);
{ method: "tools/call", params },
CallToolResultSchema,
options,
);
} }
/** /**
@ -1874,11 +1864,7 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
* @returns Promise resolving to the list of tools * @returns Promise resolving to the list of tools
*/ */
listTools(params: ListToolsRequest["params"], options?: RequestOptions) { listTools(params: ListToolsRequest["params"], options?: RequestOptions) {
return this.request( return this.request({ method: "tools/list", params }, options);
{ method: "tools/list", params },
ListToolsResultSchema,
options,
);
} }
/** /**
@ -1949,18 +1935,18 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
if (!serverCapabilities) { if (!serverCapabilities) {
throw new Error("Client server capabilities not available"); throw new Error("Client server capabilities not available");
} }
this.registerCapabilities(proxiedInnerCapabilities(serverCapabilities));
if (serverCapabilities.tools) { if (serverCapabilities.tools) {
this.oncalltool = async (params, extra) => { this.oncalltool = async (params, extra) => {
return this._client!.request( return this._client!.request(
{ method: "tools/call", params }, { method: "tools/call", params },
CallToolResultSchema, { signal: extra.mcpReq.signal },
{ signal: extra.signal },
); );
}; };
if (serverCapabilities.tools.listChanged) { if (serverCapabilities.tools.listChanged) {
this._client.setNotificationHandler( this._client.setNotificationHandler(
ToolListChangedNotificationSchema, "notifications/tools/list_changed",
(n) => this.sendToolListChanged(n.params), (n) => this.sendToolListChanged(n.params),
); );
} }
@ -1969,27 +1955,24 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
this.onlistresources = async (params, extra) => { this.onlistresources = async (params, extra) => {
return this._client!.request( return this._client!.request(
{ method: "resources/list", params }, { method: "resources/list", params },
ListResourcesResultSchema, { signal: extra.mcpReq.signal },
{ signal: extra.signal },
); );
}; };
this.onlistresourcetemplates = async (params, extra) => { this.onlistresourcetemplates = async (params, extra) => {
return this._client!.request( return this._client!.request(
{ method: "resources/templates/list", params }, { method: "resources/templates/list", params },
ListResourceTemplatesResultSchema, { signal: extra.mcpReq.signal },
{ signal: extra.signal },
); );
}; };
this.onreadresource = async (params, extra) => { this.onreadresource = async (params, extra) => {
return this._client!.request( return this._client!.request(
{ method: "resources/read", params }, { method: "resources/read", params },
ReadResourceResultSchema, { signal: extra.mcpReq.signal },
{ signal: extra.signal },
); );
}; };
if (serverCapabilities.resources.listChanged) { if (serverCapabilities.resources.listChanged) {
this._client.setNotificationHandler( this._client.setNotificationHandler(
ResourceListChangedNotificationSchema, "notifications/resources/list_changed",
(n) => this.sendResourceListChanged(n.params), (n) => this.sendResourceListChanged(n.params),
); );
} }
@ -1998,22 +1981,20 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
this.onlistprompts = async (params, extra) => { this.onlistprompts = async (params, extra) => {
return this._client!.request( return this._client!.request(
{ method: "prompts/list", params }, { method: "prompts/list", params },
ListPromptsResultSchema, { signal: extra.mcpReq.signal },
{ signal: extra.signal },
); );
}; };
if (serverCapabilities.prompts.listChanged) { if (serverCapabilities.prompts.listChanged) {
this._client.setNotificationHandler( this._client.setNotificationHandler(
PromptListChangedNotificationSchema, "notifications/prompts/list_changed",
(n) => this.sendPromptListChanged(n.params), (n) => this.sendPromptListChanged(n.params),
); );
} }
} }
} }
// MCP-UI specific handlers are registered by the host component // The standard MCP handshake only establishes the temporary inner Server
// after the proxy is created. The standard MCP initialization // role. ui/notifications/initialized remains the public View-ready signal.
// (via oninitialized callback set in constructor) handles the ready signal.
return super.connect(transport); return super.connect(transport);
} }