From 1398aaf9df8d7090070d9e4b1bca20c768b970fe Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 21 Nov 2025 12:05:44 +0000 Subject: [PATCH] add AppHost --- src/app-host.ts | 219 ++++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 2 - 2 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 src/app-host.ts diff --git a/src/app-host.ts b/src/app-host.ts new file mode 100644 index 0000000..581d36c --- /dev/null +++ b/src/app-host.ts @@ -0,0 +1,219 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { ZodLiteral, ZodObject } from "zod"; + +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { + CallToolRequestSchema, + CallToolResultSchema, + Implementation, + ListPromptsRequestSchema, + ListPromptsResultSchema, + ListResourcesRequestSchema, + ListResourcesResultSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + Notification, + PingRequestSchema, + PromptListChangedNotificationSchema, + ResourceListChangedNotificationSchema, + Result, + ToolListChangedNotificationSchema, + Request, +} from "@modelcontextprotocol/sdk/types.js"; +import { + Protocol, + ProtocolOptions, + RequestOptions, +} from "@modelcontextprotocol/sdk/shared/protocol.js"; + +import { + type McpUiToolInputNotification, + type McpUiToolResultNotification, + type McpUiSandboxResourceReadyNotification, + type McpUiSizeChangeNotification, + LATEST_PROTOCOL_VERSION, + McpUiAppCapabilities, + McpUiHostCapabilities, + McpUiInitializedNotificationSchema, + McpUiInitializeRequest, + McpUiInitializeRequestSchema, + McpUiInitializeResult, + McpUiResourceTeardownRequest, + McpUiResourceTeardownResultSchema, +} from "./types"; +export { LATEST_PROTOCOL_VERSION } from "./types"; +export { PostMessageTransport } from "./message-transport"; + +type HostOptions = ProtocolOptions; + +export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION]; + +export class AppHost extends Protocol { + oninitialized?: () => void; + + private _appCapabilities?: McpUiAppCapabilities; + + constructor( + private _client: Client, + private _hostInfo: Implementation, + private _capabilities: McpUiHostCapabilities, + options?: HostOptions, + ) { + super(options); + + this.setRequestHandler(McpUiInitializeRequestSchema, (request) => + this._oninitialize(request), + ); + this.setNotificationHandler(McpUiInitializedNotificationSchema, () => + this.oninitialized?.(), + ); + + this.setRequestHandler(PingRequestSchema, (request) => { + console.log("Received ping:", request.params); + return {}; + }); + } + + assertCapabilityForMethod(method: Request["method"]): void { + // TODO + } + assertRequestHandlerCapability(method: Request["method"]): void { + // TODO + } + assertNotificationCapability(method: Notification["method"]): void { + // TODO + } + + getCapabilities(): McpUiHostCapabilities { + return this._capabilities; + } + + private async _oninitialize( + request: McpUiInitializeRequest, + ): Promise { + const requestedVersion = request.params.protocolVersion; + + this._appCapabilities = request.params.appCapabilities; + + const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes( + requestedVersion, + ) + ? requestedVersion + : LATEST_PROTOCOL_VERSION; + + return { + protocolVersion, + hostCapabilities: this.getCapabilities(), + hostInfo: this._hostInfo, + hostContext: { + // TODO + }, + }; + } + + sendSizeChange(params: McpUiSizeChangeNotification["params"]) { + return this.notification({ + method: "ui/notifications/size-change", + params, + }); + } + + sendToolInput(params: McpUiToolInputNotification["params"]) { + return this.notification({ + method: "ui/notifications/tool-inputs", + params, + }); + } + sendToolResult(params: McpUiToolResultNotification["params"]) { + return this.notification({ + method: "ui/notifications/tool-result", + params, + }); + } + + sendSandboxResourceReady( + params: McpUiSandboxResourceReadyNotification["params"], + ) { + return this.notification({ + method: "ui/notifications/sandbox-resource-ready", + params, + }); + } + + sendResourceTeardown( + params: McpUiResourceTeardownRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { + method: "ui/resource-teardown", + params, + }, + McpUiResourceTeardownResultSchema, + options, + ); + } + + private forwardRequest< + Req extends ZodObject<{ + method: ZodLiteral; + }>, + Res extends ZodObject<{}>, + >(requestSchema: Req, resultSchema: Res) { + this.setRequestHandler(requestSchema, async (request, extra) => { + console.log(`Forwarding request ${request.method} from MCP UI client`); + return this._client.request(request, resultSchema, { + signal: extra.signal, + }); + }); + } + private forwardNotification< + N extends ZodObject<{ method: ZodLiteral }>, + >(notificationSchema: N) { + this.setNotificationHandler(notificationSchema, async (notification) => { + console.log( + `Forwarding notification ${notification.method} from MCP UI client`, + ); + await this._client.notification(notification); + }); + } + async connect(transport: Transport) { + // Forward core available MCP features + const serverCapabilities = this._client.getServerCapabilities(); + if (!serverCapabilities) { + throw new Error("Client server capabilities not available"); + } + + if (serverCapabilities.tools) { + this.forwardRequest(CallToolRequestSchema, CallToolResultSchema); + if (serverCapabilities.tools.listChanged) { + this.forwardNotification(ToolListChangedNotificationSchema); + } + } + if (serverCapabilities.resources) { + this.forwardRequest( + ListResourcesRequestSchema, + ListResourcesResultSchema, + ); + this.forwardRequest( + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResultSchema, + ); + if (serverCapabilities.resources.listChanged) { + this.forwardNotification(ResourceListChangedNotificationSchema); + } + } + if (serverCapabilities.prompts) { + this.forwardRequest(ListPromptsRequestSchema, ListPromptsResultSchema); + if (serverCapabilities.prompts.listChanged) { + this.forwardNotification(PromptListChangedNotificationSchema); + } + } + + // 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. + + return super.connect(transport); + } +} diff --git a/src/types.ts b/src/types.ts index d216b31..a644113 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,10 +3,8 @@ import { ContentBlockSchema, EmptyResultSchema, ImplementationSchema, - RequestId, RequestIdSchema, RequestSchema, - Tool, ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod";