refactor: migrate AppBridge to SDK v2 server
This commit is contained in:
parent
59e6ffc6d8
commit
d19a1fb83a
2 changed files with 546 additions and 317 deletions
|
|
@ -1,14 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
|
||||
import {
|
||||
EmptyResultSchema,
|
||||
ListPromptsResultSchema,
|
||||
ListResourcesResultSchema,
|
||||
ListResourceTemplatesResultSchema,
|
||||
ReadResourceResultSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Server, type ServerCapabilities } from "@modelcontextprotocol/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { App } from "./app";
|
||||
|
|
@ -19,6 +11,7 @@ import {
|
|||
getToolUiResourceUri,
|
||||
isToolVisibilityModelOnly,
|
||||
isToolVisibilityAppOnly,
|
||||
McpUiOpenLinkResultSchema,
|
||||
type McpUiHostCapabilities,
|
||||
} from "./app-bridge";
|
||||
|
||||
|
|
@ -31,11 +24,18 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|||
*/
|
||||
function createMockClient(
|
||||
serverCapabilities: ServerCapabilities = {},
|
||||
): Pick<Client, "getServerCapabilities" | "request" | "notification"> {
|
||||
): Pick<
|
||||
Client,
|
||||
| "getServerCapabilities"
|
||||
| "request"
|
||||
| "notification"
|
||||
| "setNotificationHandler"
|
||||
> {
|
||||
return {
|
||||
getServerCapabilities: () => serverCapabilities,
|
||||
request: async () => ({}) as never,
|
||||
notification: async () => {},
|
||||
setNotificationHandler: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +82,58 @@ describe("App <-> AppBridge integration", () => {
|
|||
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 () => {
|
||||
await bridge.connect(bridgeTransport);
|
||||
await app.connect(appTransport);
|
||||
|
|
@ -91,6 +143,8 @@ describe("App <-> AppBridge integration", () => {
|
|||
|
||||
const hostCaps = app.getHostCapabilities();
|
||||
expect(hostCaps).toEqual(testHostCapabilities);
|
||||
expect(bridge.getHostCapabilities()).toEqual(testHostCapabilities);
|
||||
expect(bridge.getCapabilities()).toEqual({});
|
||||
});
|
||||
|
||||
it("Bridge receives app info and capabilities after initialization", async () => {
|
||||
|
|
@ -611,6 +665,30 @@ describe("App <-> AppBridge integration", () => {
|
|||
|
||||
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", () => {
|
||||
|
|
@ -680,10 +758,7 @@ describe("App <-> AppBridge integration", () => {
|
|||
await app.connect(appTransport);
|
||||
|
||||
// Bridge can send ping via the protocol's request method
|
||||
const result = await bridge.request(
|
||||
{ method: "ping", params: {} },
|
||||
EmptyResultSchema,
|
||||
);
|
||||
const result = await bridge.request({ method: "ping", params: {} });
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
|
@ -798,7 +873,7 @@ describe("App <-> AppBridge integration", () => {
|
|||
});
|
||||
await bridge.callTool({ name: "noargs", arguments: {} });
|
||||
expect(receivedExtra).toBeDefined();
|
||||
expect(receivedExtra.signal).toBeInstanceOf(AbortSignal);
|
||||
expect(receivedExtra.mcpReq.signal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
|
||||
it("isError result skips output schema validation", async () => {
|
||||
|
|
@ -2180,10 +2255,10 @@ describe("App <-> AppBridge integration", () => {
|
|||
await app.connect(appTransport);
|
||||
|
||||
// App sends resources/list request via the protocol's request method
|
||||
const result = await app.request(
|
||||
{ method: "resources/list", params: requestParams },
|
||||
ListResourcesResultSchema,
|
||||
);
|
||||
const result = await app.request({
|
||||
method: "resources/list",
|
||||
params: requestParams,
|
||||
});
|
||||
|
||||
expect(receivedRequests).toHaveLength(1);
|
||||
expect(receivedRequests[0]).toMatchObject(requestParams);
|
||||
|
|
@ -2224,10 +2299,10 @@ describe("App <-> AppBridge integration", () => {
|
|||
await bridge.connect(bridgeTransport);
|
||||
await app.connect(appTransport);
|
||||
|
||||
const result = await app.request(
|
||||
{ method: "resources/read", params: requestParams },
|
||||
ReadResourceResultSchema,
|
||||
);
|
||||
const result = await app.request({
|
||||
method: "resources/read",
|
||||
params: requestParams,
|
||||
});
|
||||
|
||||
expect(receivedRequests).toHaveLength(1);
|
||||
expect(receivedRequests[0]).toMatchObject(requestParams);
|
||||
|
|
@ -2275,10 +2350,10 @@ describe("App <-> AppBridge integration", () => {
|
|||
await bridge.connect(bridgeTransport);
|
||||
await app.connect(appTransport);
|
||||
|
||||
const result = await app.request(
|
||||
{ method: "resources/templates/list", params: requestParams },
|
||||
ListResourceTemplatesResultSchema,
|
||||
);
|
||||
const result = await app.request({
|
||||
method: "resources/templates/list",
|
||||
params: requestParams,
|
||||
});
|
||||
|
||||
expect(receivedRequests).toHaveLength(1);
|
||||
expect(receivedRequests[0]).toMatchObject(requestParams);
|
||||
|
|
@ -2298,10 +2373,10 @@ describe("App <-> AppBridge integration", () => {
|
|||
await bridge.connect(bridgeTransport);
|
||||
await app.connect(appTransport);
|
||||
|
||||
const result = await app.request(
|
||||
{ method: "prompts/list", params: requestParams },
|
||||
ListPromptsResultSchema,
|
||||
);
|
||||
const result = await app.request({
|
||||
method: "prompts/list",
|
||||
params: requestParams,
|
||||
});
|
||||
|
||||
expect(receivedRequests).toHaveLength(1);
|
||||
expect(receivedRequests[0]).toMatchObject(requestParams);
|
||||
|
|
@ -2310,6 +2385,7 @@ describe("App <-> AppBridge integration", () => {
|
|||
|
||||
it("sendToolListChanged sends notification to app", async () => {
|
||||
const receivedNotifications: unknown[] = [];
|
||||
bridge.oncalltool = async () => ({ content: [] });
|
||||
app.setNotificationHandler(
|
||||
"notifications/tools/list_changed",
|
||||
(notification) => {
|
||||
|
|
@ -2328,6 +2404,7 @@ describe("App <-> AppBridge integration", () => {
|
|||
|
||||
it("sendResourceListChanged sends notification to app", async () => {
|
||||
const receivedNotifications: unknown[] = [];
|
||||
bridge.onlistresources = async () => ({ resources: [] });
|
||||
app.setNotificationHandler(
|
||||
"notifications/resources/list_changed",
|
||||
(notification) => {
|
||||
|
|
@ -2346,6 +2423,7 @@ describe("App <-> AppBridge integration", () => {
|
|||
|
||||
it("sendPromptListChanged sends notification to app", async () => {
|
||||
const receivedNotifications: unknown[] = [];
|
||||
bridge.onlistprompts = async () => ({ prompts: [] });
|
||||
app.setNotificationHandler(
|
||||
"notifications/prompts/list_changed",
|
||||
(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("new nested format (_meta.ui.resourceUri)", () => {
|
||||
it("extracts resourceUri from _meta.ui.resourceUri", () => {
|
||||
|
|
@ -2758,24 +3014,16 @@ describe("isToolVisibilityAppOnly", () => {
|
|||
expect(app.onteardown).toBe(handler);
|
||||
});
|
||||
|
||||
it("direct setRequestHandler throws when called twice", () => {
|
||||
it("direct setRequestHandler uses base SDK replacement semantics", () => {
|
||||
const bridge2 = new AppBridge(
|
||||
createMockClient() as Client,
|
||||
testHostInfo,
|
||||
testHostCapabilities,
|
||||
);
|
||||
bridge2.setRequestHandler(
|
||||
// @ts-expect-error — exercising throw path with raw schema
|
||||
{ shape: { method: { value: "test/method" } } },
|
||||
() => ({}),
|
||||
);
|
||||
bridge2.setRequestHandler("ping", () => ({}));
|
||||
expect(() => {
|
||||
bridge2.setRequestHandler(
|
||||
// @ts-expect-error — exercising throw path with raw schema
|
||||
{ shape: { method: { value: "test/method" } } },
|
||||
() => ({}),
|
||||
);
|
||||
}).toThrow(/already registered/);
|
||||
bridge2.setRequestHandler("ping", () => ({}));
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,61 +1,51 @@
|
|||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
||||
import { type Client } from "@modelcontextprotocol/client";
|
||||
import {
|
||||
CallToolRequest,
|
||||
CallToolRequestSchema,
|
||||
CallToolResult,
|
||||
CallToolResultSchema,
|
||||
CreateMessageRequest,
|
||||
CreateMessageRequestSchema,
|
||||
CreateMessageResult,
|
||||
CreateMessageResultWithTools,
|
||||
EmptyResult,
|
||||
Implementation,
|
||||
ListPromptsRequest,
|
||||
ListPromptsRequestSchema,
|
||||
ListPromptsResult,
|
||||
ListPromptsResultSchema,
|
||||
ListResourcesRequest,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourcesResult,
|
||||
ListResourcesResultSchema,
|
||||
ListResourceTemplatesRequest,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListResourceTemplatesResult,
|
||||
ListResourceTemplatesResultSchema,
|
||||
ListToolsRequest,
|
||||
ListToolsRequestSchema,
|
||||
ListToolsResultSchema,
|
||||
LoggingMessageNotification,
|
||||
Server,
|
||||
type CallToolRequest,
|
||||
type CallToolResult,
|
||||
type CreateMessageRequest,
|
||||
type CreateMessageResult,
|
||||
type CreateMessageResultWithTools,
|
||||
type EmptyResult,
|
||||
type Implementation,
|
||||
type JSONRPCRequest,
|
||||
type ListPromptsRequest,
|
||||
type ListPromptsResult,
|
||||
type ListResourcesRequest,
|
||||
type ListResourcesResult,
|
||||
type ListResourceTemplatesRequest,
|
||||
type ListResourceTemplatesResult,
|
||||
type ListToolsRequest,
|
||||
type LoggingMessageNotification,
|
||||
type PingRequest,
|
||||
type PromptListChangedNotification,
|
||||
type ReadResourceRequest,
|
||||
type ReadResourceResult,
|
||||
type RequestOptions,
|
||||
type ResourceListChangedNotification,
|
||||
type Result,
|
||||
type ServerCapabilities,
|
||||
type ServerContext,
|
||||
type ServerOptions,
|
||||
type Tool,
|
||||
type ToolListChangedNotification,
|
||||
type Transport,
|
||||
} from "@modelcontextprotocol/server";
|
||||
import {
|
||||
EmptyResultSchema,
|
||||
LoggingMessageNotificationSchema,
|
||||
PingRequest,
|
||||
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";
|
||||
} from "@modelcontextprotocol/core";
|
||||
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 {
|
||||
type AppNotification,
|
||||
type AppRequest,
|
||||
type AppResult,
|
||||
type McpUiSandboxResourceReadyNotification,
|
||||
type McpUiSizeChangedNotification,
|
||||
type McpUiToolCancelledNotification,
|
||||
|
|
@ -74,15 +64,19 @@ import {
|
|||
McpUiInitializeRequest,
|
||||
McpUiInitializeRequestSchema,
|
||||
McpUiInitializeResult,
|
||||
McpUiInitializeResultSchema,
|
||||
McpUiMessageRequest,
|
||||
McpUiMessageRequestSchema,
|
||||
McpUiMessageResult,
|
||||
McpUiMessageResultSchema,
|
||||
McpUiOpenLinkRequest,
|
||||
McpUiOpenLinkRequestSchema,
|
||||
McpUiOpenLinkResult,
|
||||
McpUiOpenLinkResultSchema,
|
||||
McpUiDownloadFileRequest,
|
||||
McpUiDownloadFileRequestSchema,
|
||||
McpUiDownloadFileResult,
|
||||
McpUiDownloadFileResultSchema,
|
||||
McpUiResourceTeardownRequest,
|
||||
McpUiResourceTeardownResultSchema,
|
||||
McpUiRequestTeardownNotification,
|
||||
|
|
@ -93,6 +87,7 @@ import {
|
|||
McpUiRequestDisplayModeRequest,
|
||||
McpUiRequestDisplayModeRequestSchema,
|
||||
McpUiRequestDisplayModeResult,
|
||||
McpUiRequestDisplayModeResultSchema,
|
||||
McpUiResourcePermissions,
|
||||
McpUiToolMeta,
|
||||
} from "./types";
|
||||
|
|
@ -101,6 +96,20 @@ 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.
|
||||
*
|
||||
|
|
@ -206,10 +215,13 @@ export function buildAllowAttribute(
|
|||
*
|
||||
* @property hostContext - Optional initial host context to provide to the view
|
||||
*
|
||||
* @see `ProtocolOptions` from @modelcontextprotocol/sdk for available options
|
||||
* @see `ServerOptions` from @modelcontextprotocol/server for available options
|
||||
* @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure
|
||||
*/
|
||||
export type HostOptions = ProtocolOptions & {
|
||||
export type HostOptions = Omit<
|
||||
ServerOptions,
|
||||
"capabilities" | "supportedProtocolVersions"
|
||||
> & {
|
||||
hostContext?: McpUiHostContext;
|
||||
};
|
||||
|
||||
|
|
@ -224,15 +236,12 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION];
|
|||
/**
|
||||
* Extra metadata passed to request handlers.
|
||||
*
|
||||
* This type represents the additional context provided by the `Protocol` class
|
||||
* when handling requests, including abort signals and session information.
|
||||
* It is extracted from the MCP SDK's request handler signature.
|
||||
* This type represents the additional context provided by the base MCP SDK
|
||||
* `Server` when handling requests.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
type RequestHandlerExtra = Parameters<
|
||||
Parameters<AppBridge["setRequestHandler"]>[1]
|
||||
>[1];
|
||||
type RequestHandlerExtra = ServerContext;
|
||||
|
||||
/**
|
||||
* Maps DOM-style event names to their notification `params` types.
|
||||
|
|
@ -306,66 +315,53 @@ export type AppBridgeEventMap = {
|
|||
* await bridge.connect(transport);
|
||||
* ```
|
||||
*/
|
||||
export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult> {
|
||||
export class AppBridge extends Server {
|
||||
private _appCapabilities?: McpUiAppCapabilities;
|
||||
private _hostContext: McpUiHostContext = {};
|
||||
private _appInfo?: Implementation;
|
||||
private _initializedReceived = false;
|
||||
private readonly _registeredMethods = new Set<string>();
|
||||
private readonly _registeredEvents = new Set<keyof 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
|
||||
* that the View has sent `ui/notifications/initialized`. Warns (never
|
||||
* throws) so lenient hosts keep working while still surfacing the
|
||||
* misordering that leaves strict hosts with a permanently hidden iframe.
|
||||
* `ui/initialize` and `ping` use `setRequestHandler` directly and are
|
||||
* intentionally exempt.
|
||||
* The base MCP SDK calls this hook for every standard and custom request
|
||||
* 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.
|
||||
*
|
||||
* @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149}
|
||||
*/
|
||||
protected replaceRequestHandler: Protocol<
|
||||
AppRequest,
|
||||
AppNotification,
|
||||
AppResult
|
||||
>["setRequestHandler"] = (schema, handler) => {
|
||||
const method = (schema as MethodSchema).shape.method.value;
|
||||
this._registeredMethods.add(method);
|
||||
super.setRequestHandler(schema, (request, extra) => {
|
||||
protected override _wrapHandler(
|
||||
method: string,
|
||||
handler: (
|
||||
request: JSONRPCRequest,
|
||||
context: ServerContext,
|
||||
) => Promise<Result>,
|
||||
): (request: JSONRPCRequest, context: ServerContext) => Promise<Result> {
|
||||
const wrapped = super._wrapHandler(method, handler);
|
||||
if (
|
||||
method === "initialize" ||
|
||||
method === "ui/initialize" ||
|
||||
method === "ping"
|
||||
) {
|
||||
return wrapped;
|
||||
}
|
||||
return async (request, context) => {
|
||||
if (!this._initializedReceived) {
|
||||
console.warn(
|
||||
`[ext-apps] AppBridge received '${request.method}' before ` +
|
||||
`[ext-apps] AppBridge received '${method}' before ` +
|
||||
`ui/notifications/initialized. The View is calling host ` +
|
||||
`methods before completing the handshake; it should await ` +
|
||||
`app.connect() first.`,
|
||||
);
|
||||
}
|
||||
return handler(request, extra);
|
||||
});
|
||||
};
|
||||
return wrapped(request, context);
|
||||
};
|
||||
}
|
||||
|
||||
private readonly eventSchemas: {
|
||||
[K in keyof AppBridgeEventMap]: MethodSchema;
|
||||
|
|
@ -406,17 +402,27 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
this._events.removeEventListener(event, handler);
|
||||
}
|
||||
|
||||
private _ensureEventSlot<K extends keyof AppBridgeEventMap>(
|
||||
event: K,
|
||||
): void {
|
||||
private _ensureEventSlot<K extends keyof AppBridgeEventMap>(event: K): void {
|
||||
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: AppBridgeEventMap[K] }).params;
|
||||
|
||||
const dispatch = (params: AppBridgeEventMap[K]) => {
|
||||
if (event === "initialized") this._initializedReceived = true;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -433,16 +439,12 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -453,8 +455,8 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
* between the View and the server. When `null`, you must register handlers
|
||||
* manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters.
|
||||
* @param _hostInfo - Host application identification (name and version)
|
||||
* @param _capabilities - Features and capabilities the host supports
|
||||
* @param options - Configuration options (inherited from Protocol)
|
||||
* @param _hostCapabilities - Features and capabilities the host supports
|
||||
* @param options - Configuration options (inherited from Server)
|
||||
*
|
||||
* @example With MCP client (automatic forwarding)
|
||||
* ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient"
|
||||
|
|
@ -481,29 +483,61 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
constructor(
|
||||
private _client: Client | null,
|
||||
private _hostInfo: Implementation,
|
||||
private _capabilities: McpUiHostCapabilities,
|
||||
private _hostCapabilities: McpUiHostCapabilities,
|
||||
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._hostContext = options?.hostContext || {};
|
||||
|
||||
this.setRequestHandler(McpUiInitializeRequestSchema, (request) =>
|
||||
this._oninitialize(request),
|
||||
this.setRequestHandler(
|
||||
"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);
|
||||
return {};
|
||||
});
|
||||
|
||||
// Default handler for requestDisplayMode - returns current mode from host context.
|
||||
// Hosts can override this by setting bridge.onrequestdisplaymode = ...
|
||||
this.replaceRequestHandler(
|
||||
McpUiRequestDisplayModeRequestSchema,
|
||||
(request) => {
|
||||
this.setRequestHandler(
|
||||
"ui/request-display-mode",
|
||||
{
|
||||
params: McpUiRequestDisplayModeRequestSchema.shape.params,
|
||||
result: McpUiRequestDisplayModeResultSchema,
|
||||
},
|
||||
() => {
|
||||
const currentMode = this._hostContext.displayMode ?? "inline";
|
||||
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
|
||||
* @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}.
|
||||
*/
|
||||
get oninitialized():
|
||||
| ((params: McpUiInitializedNotification["params"]) => void)
|
||||
| undefined {
|
||||
return this.getEventHandler("initialized");
|
||||
}
|
||||
set oninitialized(
|
||||
callback:
|
||||
| ((params: McpUiInitializedNotification["params"]) => void)
|
||||
| undefined,
|
||||
) {
|
||||
this.setEventHandler("initialized", callback);
|
||||
}
|
||||
override oninitialized?: (
|
||||
params?: McpUiInitializedNotification["params"],
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* 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._onmessage = callback;
|
||||
this.replaceRequestHandler(
|
||||
McpUiMessageRequestSchema,
|
||||
async (request, extra) => {
|
||||
this.setRequestHandler(
|
||||
"ui/message",
|
||||
{
|
||||
params: McpUiMessageRequestSchema.shape.params,
|
||||
result: McpUiMessageResultSchema,
|
||||
},
|
||||
async (params, extra) => {
|
||||
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._onopenlink = callback;
|
||||
this.replaceRequestHandler(
|
||||
McpUiOpenLinkRequestSchema,
|
||||
async (request, extra) => {
|
||||
this.setRequestHandler(
|
||||
"ui/open-link",
|
||||
{
|
||||
params: McpUiOpenLinkRequestSchema.shape.params,
|
||||
result: McpUiOpenLinkResultSchema,
|
||||
},
|
||||
async (params, extra) => {
|
||||
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,
|
||||
);
|
||||
this._ondownloadfile = callback;
|
||||
this.replaceRequestHandler(
|
||||
McpUiDownloadFileRequestSchema,
|
||||
async (request, extra) => {
|
||||
this.setRequestHandler(
|
||||
"ui/download-file",
|
||||
{
|
||||
params: McpUiDownloadFileRequestSchema.shape.params,
|
||||
result: McpUiDownloadFileResultSchema,
|
||||
},
|
||||
async (params, extra) => {
|
||||
if (!this._ondownloadfile)
|
||||
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,
|
||||
);
|
||||
this._onrequestdisplaymode = callback;
|
||||
this.replaceRequestHandler(
|
||||
McpUiRequestDisplayModeRequestSchema,
|
||||
async (request, extra) => {
|
||||
this.setRequestHandler(
|
||||
"ui/request-display-mode",
|
||||
{
|
||||
params: McpUiRequestDisplayModeRequestSchema.shape.params,
|
||||
result: McpUiRequestDisplayModeResultSchema,
|
||||
},
|
||||
async (params, extra) => {
|
||||
if (!this._onrequestdisplaymode)
|
||||
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,
|
||||
);
|
||||
this._onupdatemodelcontext = callback;
|
||||
this.replaceRequestHandler(
|
||||
McpUiUpdateModelContextRequestSchema,
|
||||
async (request, extra) => {
|
||||
this.setRequestHandler(
|
||||
"ui/update-model-context",
|
||||
{
|
||||
params: McpUiUpdateModelContextRequestSchema.shape.params,
|
||||
result: EmptyResultSchema,
|
||||
},
|
||||
async (params, extra) => {
|
||||
if (!this._onupdatemodelcontext)
|
||||
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._oncalltool = callback;
|
||||
this.replaceRequestHandler(
|
||||
CallToolRequestSchema,
|
||||
async (request, extra) => {
|
||||
if (!this._oncalltool) throw new Error("No oncalltool handler set");
|
||||
return this._oncalltool(request.params, extra);
|
||||
},
|
||||
);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1179,12 +1222,9 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
extra: RequestHandlerExtra,
|
||||
) => Promise<CreateMessageResult | CreateMessageResultWithTools>,
|
||||
) {
|
||||
this.setRequestHandler(
|
||||
CreateMessageRequestSchema,
|
||||
async (request, extra) => {
|
||||
return callback(request.params, extra);
|
||||
},
|
||||
);
|
||||
this.setRequestHandler("sampling/createMessage", async (request, extra) => {
|
||||
return callback(request.params, extra);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1260,14 +1300,12 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
callback,
|
||||
);
|
||||
this._onlistresources = callback;
|
||||
this.replaceRequestHandler(
|
||||
ListResourcesRequestSchema,
|
||||
async (request, extra) => {
|
||||
if (!this._onlistresources)
|
||||
throw new Error("No onlistresources handler set");
|
||||
return this._onlistresources(request.params, extra);
|
||||
},
|
||||
);
|
||||
this.ensureInnerCapabilities({ resources: {} });
|
||||
this.setRequestHandler("resources/list", async (request, extra) => {
|
||||
if (!this._onlistresources)
|
||||
throw new Error("No onlistresources handler set");
|
||||
return this._onlistresources(request.params, extra);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1317,8 +1355,9 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
callback,
|
||||
);
|
||||
this._onlistresourcetemplates = callback;
|
||||
this.replaceRequestHandler(
|
||||
ListResourceTemplatesRequestSchema,
|
||||
this.ensureInnerCapabilities({ resources: {} });
|
||||
this.setRequestHandler(
|
||||
"resources/templates/list",
|
||||
async (request, extra) => {
|
||||
if (!this._onlistresourcetemplates)
|
||||
throw new Error("No onlistresourcetemplates handler set");
|
||||
|
|
@ -1374,14 +1413,12 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
callback,
|
||||
);
|
||||
this._onreadresource = callback;
|
||||
this.replaceRequestHandler(
|
||||
ReadResourceRequestSchema,
|
||||
async (request, extra) => {
|
||||
if (!this._onreadresource)
|
||||
throw new Error("No onreadresource handler set");
|
||||
return this._onreadresource(request.params, extra);
|
||||
},
|
||||
);
|
||||
this.ensureInnerCapabilities({ resources: {} });
|
||||
this.setRequestHandler("resources/read", async (request, extra) => {
|
||||
if (!this._onreadresource)
|
||||
throw new Error("No onreadresource handler set");
|
||||
return this._onreadresource(request.params, extra);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1459,14 +1496,11 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
callback,
|
||||
);
|
||||
this._onlistprompts = callback;
|
||||
this.replaceRequestHandler(
|
||||
ListPromptsRequestSchema,
|
||||
async (request, extra) => {
|
||||
if (!this._onlistprompts)
|
||||
throw new Error("No onlistprompts handler set");
|
||||
return this._onlistprompts(request.params, extra);
|
||||
},
|
||||
);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
@ -1542,18 +1536,18 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
*
|
||||
* @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure
|
||||
*/
|
||||
getCapabilities(): McpUiHostCapabilities {
|
||||
return this._capabilities;
|
||||
getHostCapabilities(): McpUiHostCapabilities {
|
||||
return this._hostCapabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ui/initialize request from the guest.
|
||||
* @internal
|
||||
*/
|
||||
private async _oninitialize(
|
||||
request: McpUiInitializeRequest,
|
||||
private async _onAppsInitialize(
|
||||
params: McpUiInitializeRequest["params"],
|
||||
): Promise<McpUiInitializeResult> {
|
||||
const requestedVersion = request.params.protocolVersion;
|
||||
const requestedVersion = params.protocolVersion;
|
||||
|
||||
if (this._appInfo !== undefined) {
|
||||
console.warn(
|
||||
|
|
@ -1564,8 +1558,8 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
);
|
||||
}
|
||||
|
||||
this._appCapabilities = request.params.appCapabilities;
|
||||
this._appInfo = request.params.appInfo;
|
||||
this._appCapabilities = params.appCapabilities;
|
||||
this._appInfo = params.appInfo;
|
||||
|
||||
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(
|
||||
requestedVersion,
|
||||
|
|
@ -1575,7 +1569,7 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
|
||||
return {
|
||||
protocolVersion,
|
||||
hostCapabilities: this.getCapabilities(),
|
||||
hostCapabilities: this.getHostCapabilities(),
|
||||
hostInfo: this._hostInfo,
|
||||
hostContext: this._hostContext,
|
||||
};
|
||||
|
|
@ -1857,11 +1851,7 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
* @returns Promise resolving to the tool call result
|
||||
*/
|
||||
callTool(params: CallToolRequest["params"], options?: RequestOptions) {
|
||||
return this.request(
|
||||
{ method: "tools/call", params },
|
||||
CallToolResultSchema,
|
||||
options,
|
||||
);
|
||||
return this.request({ method: "tools/call", params }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1874,11 +1864,7 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
* @returns Promise resolving to the list of tools
|
||||
*/
|
||||
listTools(params: ListToolsRequest["params"], options?: RequestOptions) {
|
||||
return this.request(
|
||||
{ method: "tools/list", params },
|
||||
ListToolsResultSchema,
|
||||
options,
|
||||
);
|
||||
return this.request({ method: "tools/list", params }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1949,18 +1935,18 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
if (!serverCapabilities) {
|
||||
throw new Error("Client server capabilities not available");
|
||||
}
|
||||
this.registerCapabilities(proxiedInnerCapabilities(serverCapabilities));
|
||||
|
||||
if (serverCapabilities.tools) {
|
||||
this.oncalltool = async (params, extra) => {
|
||||
return this._client!.request(
|
||||
{ method: "tools/call", params },
|
||||
CallToolResultSchema,
|
||||
{ signal: extra.signal },
|
||||
{ signal: extra.mcpReq.signal },
|
||||
);
|
||||
};
|
||||
if (serverCapabilities.tools.listChanged) {
|
||||
this._client.setNotificationHandler(
|
||||
ToolListChangedNotificationSchema,
|
||||
"notifications/tools/list_changed",
|
||||
(n) => this.sendToolListChanged(n.params),
|
||||
);
|
||||
}
|
||||
|
|
@ -1969,27 +1955,24 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
this.onlistresources = async (params, extra) => {
|
||||
return this._client!.request(
|
||||
{ method: "resources/list", params },
|
||||
ListResourcesResultSchema,
|
||||
{ signal: extra.signal },
|
||||
{ signal: extra.mcpReq.signal },
|
||||
);
|
||||
};
|
||||
this.onlistresourcetemplates = async (params, extra) => {
|
||||
return this._client!.request(
|
||||
{ method: "resources/templates/list", params },
|
||||
ListResourceTemplatesResultSchema,
|
||||
{ signal: extra.signal },
|
||||
{ signal: extra.mcpReq.signal },
|
||||
);
|
||||
};
|
||||
this.onreadresource = async (params, extra) => {
|
||||
return this._client!.request(
|
||||
{ method: "resources/read", params },
|
||||
ReadResourceResultSchema,
|
||||
{ signal: extra.signal },
|
||||
{ signal: extra.mcpReq.signal },
|
||||
);
|
||||
};
|
||||
if (serverCapabilities.resources.listChanged) {
|
||||
this._client.setNotificationHandler(
|
||||
ResourceListChangedNotificationSchema,
|
||||
"notifications/resources/list_changed",
|
||||
(n) => this.sendResourceListChanged(n.params),
|
||||
);
|
||||
}
|
||||
|
|
@ -1998,22 +1981,20 @@ export class AppBridge extends Protocol<AppRequest, AppNotification, AppResult>
|
|||
this.onlistprompts = async (params, extra) => {
|
||||
return this._client!.request(
|
||||
{ method: "prompts/list", params },
|
||||
ListPromptsResultSchema,
|
||||
{ signal: extra.signal },
|
||||
{ signal: extra.mcpReq.signal },
|
||||
);
|
||||
};
|
||||
if (serverCapabilities.prompts.listChanged) {
|
||||
this._client.setNotificationHandler(
|
||||
PromptListChangedNotificationSchema,
|
||||
"notifications/prompts/list_changed",
|
||||
(n) => this.sendPromptListChanged(n.params),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MCP-UI specific handlers are registered by the host component
|
||||
// after the proxy is created. The standard MCP initialization
|
||||
// (via oninitialized callback set in constructor) handles the ready signal.
|
||||
// The standard MCP handshake only establishes the temporary inner Server
|
||||
// role. ui/notifications/initialized remains the public View-ready signal.
|
||||
|
||||
return super.connect(transport);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue