From 873075e690f88b86b4c6568c3e30c12cff974181 Mon Sep 17 00:00:00 2001 From: ochafik Date: Fri, 21 Nov 2025 21:54:03 +0000 Subject: [PATCH] ditch McpUiHostInfoSchema in favour of ImplementationSchema --- app.ts | 209 -------------------------------------- message-transport.ts | 58 ----------- src/app.ts | 3 +- src/types.ts | 8 +- types.ts | 236 ------------------------------------------- 5 files changed, 2 insertions(+), 512 deletions(-) delete mode 100644 app.ts delete mode 100644 message-transport.ts delete mode 100644 types.ts diff --git a/app.ts b/app.ts deleted file mode 100644 index d02458d..0000000 --- a/app.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { - type RequestOptions, - Protocol, - ProtocolOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; - -import { - CallToolRequest, - CallToolResult, - CallToolResultSchema, - Implementation, - LoggingMessageNotification, - Notification, - PingRequestSchema, - Request, - Result, -} from "@modelcontextprotocol/sdk/types.js"; -import { - LATEST_PROTOCOL_VERSION, - McpUiAppCapabilities, - McpUiHostCapabilities, - McpUiHostInfo, - McpUiInitializedNotification, - McpUiInitializeRequest, - McpUiInitializeResultSchema, - McpUiMessageRequest, - McpUiMessageResultSchema, - McpUiOpenLinkRequest, - McpUiOpenLinkResultSchema, - McpUiSizeChangeNotification, -} from "./types"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; - -export { PostMessageTransport } from "./message-transport.js"; -export * from "./types"; - -type AppOptions = ProtocolOptions & { - autoResize?: boolean; -}; - -export class App extends Protocol { - private _hostCapabilities?: McpUiHostCapabilities; - private _hostInfo?: McpUiHostInfo; - - constructor( - private _appInfo: Implementation, - private _capabilities: McpUiAppCapabilities = {}, - private options: AppOptions = { autoResize: true }, - ) { - super(options); - - this.setRequestHandler(PingRequestSchema, (request) => { - console.log("Received ping:", request.params); - return {}; - }); - } - - assertCapabilityForMethod(method: Request["method"]): void { - // TODO - } - assertRequestHandlerCapability(method: Request["method"]): void { - switch (method) { - case "tools/call": - case "tools/list": - if (!this._capabilities.tools) { - throw new Error( - `Client does not support tool capability (required for ${method})`, - ); - } - return; - case "ping": - return; - default: - throw new Error(`No handler for method ${method} registered`); - } - } - assertNotificationCapability(method: Notification["method"]): void { - // TODO - } - - async callServerTool( - params: CallToolRequest["params"], - options?: RequestOptions, - ): Promise { - return await this.request( - { method: "tools/call", params }, - CallToolResultSchema, - options, - ); - } - - sendMessage(params: McpUiMessageRequest["params"], options?: RequestOptions) { - return this.request( - { - method: "ui/message", - params, - }, - McpUiMessageResultSchema, - options, - ); - } - - sendLog(params: LoggingMessageNotification["params"]) { - return this.notification({ - method: "notifications/message", - params, - }); - } - - sendOpenLink( - params: McpUiOpenLinkRequest["params"], - options?: RequestOptions, - ) { - return this.request( - { - method: "ui/open-link", - params, - }, - McpUiOpenLinkResultSchema, - options, - ); - } - - sendSizeChange(params: McpUiSizeChangeNotification["params"]) { - return this.notification({ - method: "ui/notifications/size-change", - params, - }); - } - - setupSizeChangeNotifications() { - const sendBodySizeChange = () => { - let rafId: number | null = null; - - // Debounce using requestAnimationFrame to avoid duplicate messages - // when both documentElement and body fire resize events - if (rafId !== null) { - cancelAnimationFrame(rafId); - } - rafId = requestAnimationFrame(() => { - const { body, documentElement: html } = document; - - const bodyStyle = getComputedStyle(body); - const htmlStyle = getComputedStyle(html); - - const width = body.scrollWidth; - const height = - body.scrollHeight + - (parseFloat(bodyStyle.borderTop) || 0) + - (parseFloat(bodyStyle.borderBottom) || 0) + - (parseFloat(htmlStyle.borderTop) || 0) + - (parseFloat(htmlStyle.borderBottom) || 0); - - this.sendSizeChange({ width, height }); - rafId = null; - }); - }; - - sendBodySizeChange(); - - const resizeObserver = new ResizeObserver(sendBodySizeChange); - // Observe both html and body to catch all size changes - resizeObserver.observe(document.documentElement); - resizeObserver.observe(document.body); - - return () => resizeObserver.disconnect(); - } - - override async connect( - transport: Transport, - options?: RequestOptions, - ): Promise { - await super.connect(transport); - - try { - const result = await this.request( - { - method: "ui/initialize", - params: { - appCapabilities: this._capabilities, - appInfo: this._appInfo, - protocolVersion: LATEST_PROTOCOL_VERSION, - }, - }, - McpUiInitializeResultSchema, - options, - ); - - if (result === undefined) { - throw new Error(`Server sent invalid initialize result: ${result}`); - } - - this._hostCapabilities = result.hostCapabilities; - this._hostInfo = result.hostInfo; - - await this.notification({ - method: "ui/notifications/initialized", - }); - - if (this.options?.autoResize) { - this.setupSizeChangeNotifications(); - } - } catch (error) { - // Disconnect if initialization fails. - void this.close(); - throw error; - } - } -} diff --git a/message-transport.ts b/message-transport.ts deleted file mode 100644 index 90b8e21..0000000 --- a/message-transport.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - JSONRPCMessage, - JSONRPCMessageSchema, - MessageExtraInfo, -} from "@modelcontextprotocol/sdk/types.js"; -import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { - Transport, - TransportSendOptions, -} from "@modelcontextprotocol/sdk/shared/transport.js"; -import { z } from "zod"; - -export class PostMessageTransport implements Transport { - private messageListener: ( - this: Window, - ev: WindowEventMap["message"], - ) => any | undefined; - - constructor( - private eventTarget: Window = window.parent, - private eventSource?: MessageEventSource, - ) { - this.messageListener = (event) => { - if (eventSource && event.source !== this.eventSource) { - console.error("Ignoring message from unknown source", event); - return; - } - const parsed = JSONRPCMessageSchema.safeParse(event.data); - if (parsed.success) { - console.info("[host] Parsed message", parsed.data); - this.onmessage?.(parsed.data); - } else { - console.error("Failed to parse message", parsed.error.message, event); - this.onerror?.( - new Error( - "Invalid JSON-RPC message received: " + parsed.error.message, - ), - ); - } - }; - } - async start() { - window.addEventListener("message", this.messageListener); - } - async send(message: JSONRPCMessage, options?: TransportSendOptions) { - console.info("[host] Sending message", message); - this.eventTarget.postMessage(message, "*"); - } - async close() { - window.removeEventListener("message", this.messageListener); - this.onclose?.(); - } - onclose?: () => void; - onerror?: (error: Error) => void; - onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; - sessionId?: string; - setProtocolVersion?: (version: string) => void; -} diff --git a/src/app.ts b/src/app.ts index d02458d..e2f3bfd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,7 +19,6 @@ import { LATEST_PROTOCOL_VERSION, McpUiAppCapabilities, McpUiHostCapabilities, - McpUiHostInfo, McpUiInitializedNotification, McpUiInitializeRequest, McpUiInitializeResultSchema, @@ -40,7 +39,7 @@ type AppOptions = ProtocolOptions & { export class App extends Protocol { private _hostCapabilities?: McpUiHostCapabilities; - private _hostInfo?: McpUiHostInfo; + private _hostInfo?: Implementation; constructor( private _appInfo: Implementation, diff --git a/src/types.ts b/src/types.ts index 90ad336..35445ab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -211,15 +211,9 @@ export type McpUiInitializeRequest = z.infer< typeof McpUiInitializeRequestSchema >; -export const McpUiHostInfoSchema = z.object({ - name: z.string(), - version: z.string(), -}); -export type McpUiHostInfo = z.infer; - export const McpUiInitializeResultSchema = z.object({ protocolVersion: z.string(), - hostInfo: McpUiHostInfoSchema, + hostInfo: ImplementationSchema, hostCapabilities: McpUiHostCapabilitiesSchema, hostContext: McpUiHostContextSchema, }); diff --git a/types.ts b/types.ts deleted file mode 100644 index 9aeb33d..0000000 --- a/types.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { - CallToolResultSchema, - ContentBlockSchema, - EmptyResultSchema, - ImplementationSchema, - RequestId, - RequestIdSchema, - RequestSchema, - Tool, - ToolSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; - -export const LATEST_PROTOCOL_VERSION = "2025-11-21"; - -export const McpUiOpenLinkRequestSchema = RequestSchema.extend({ - method: z.literal("ui/open-link"), - params: z.object({ - url: z.string().url(), - }), -}); -export type McpUiOpenLinkRequest = z.infer; - -export const McpUiOpenLinkResultSchema = z.object({ - isError: z.boolean(), -}); -export type McpUiOpenLinkResult = z.infer; - -export const McpUiMessageRequestSchema = RequestSchema.extend({ - method: z.literal("ui/message"), - params: z.object({ - role: z.literal("user"), - content: z.array(ContentBlockSchema), - }), -}); -export type McpUiMessageRequest = z.infer; - -export const McpUiMessageResultSchema = z.object({ - // Note: we don't return the result from follow up messages as they might leak info from the chat. - // We do tell the caller if it errored, though. - isError: z.boolean(), -}); -export type McpUiMessageResult = z.infer; - -// McpUiIframeReadyNotification removed - replaced by standard MCP initialization -// The SDK's oninitialized callback now handles the ready signal - -export const McpUiSandboxProxyReadyNotificationSchema = z.object({ - method: z.literal("ui/notifications/sandbox-proxy-ready"), - params: z.object({}), -}); -export type McpUiSandboxProxyReadyNotification = z.infer< - typeof McpUiSandboxProxyReadyNotificationSchema ->; - -export const McpUiSandboxResourceReadyNotificationSchema = z.object({ - method: z.literal("ui/notifications/sandbox-resource-ready"), - params: z.object({ - html: z.string(), //ReadResourceResultSchema, - sandbox: z.string().optional(), - }), -}); -export type McpUiSandboxResourceReadyNotification = z.infer< - typeof McpUiSandboxResourceReadyNotificationSchema ->; - -// Fired by the iframe when its body changes, and by the host when the viewport size changes. -export const McpUiSizeChangeNotificationSchema = z.object({ - method: z.literal("ui/notifications/size-change"), - params: z.object({ - width: z.number().optional(), - height: z.number().optional(), - }), -}); -export type McpUiSizeChangeNotification = z.infer< - typeof McpUiSizeChangeNotificationSchema ->; - -export const McpUiToolInputNotificationSchema = z.object({ - method: z.literal("ui/notifications/tool-input"), - params: z.object({ - arguments: z.record(z.unknown()).optional(), - }), -}); -export type McpUiToolInputNotification = z.infer< - typeof McpUiToolInputNotificationSchema ->; - -export const McpUiToolInputPartialNotificationSchema = z.object({ - method: z.literal("ui/notifications/tool-input-partial"), - params: z.object({ - arguments: z.record(z.unknown()).optional(), - }), -}); -export type McpUiToolInputPartialNotification = z.infer< - typeof McpUiToolInputPartialNotificationSchema ->; - -// Fired once both tool call returned *AND* host received ui/ui-lifecycle-iframe-ready. -export const McpUiToolResultNotificationSchema = z.object({ - method: z.literal("ui/notifications/tool-result"), - params: CallToolResultSchema, -}); -export type McpUiToolResultNotification = z.infer< - typeof McpUiToolResultNotificationSchema ->; - -export const McpUiHostContextSchema = z.object({ - toolInfo: z - .object({ - // Metadata of the tool call that instantiated the App - id: RequestIdSchema, // JSON-RPC id of the tools/call request - tool: ToolSchema, // contains name, inputSchema, etc… - }) - .optional(), - theme: z.enum(["light", "dark", "system"]).optional(), - displayMode: z.enum(["inline", "fullscreen", "pip", "carousel"]).optional(), - availableDisplayModes: z.array(z.string()).optional(), - viewport: z - .object({ - width: z.number(), - height: z.number(), - maxHeight: z.number().optional(), - maxWidth: z.number().optional(), - }) - .optional(), - locale: z.string().optional(), // BCP 47, e.g., "en-US" - timeZone: z.string().optional(), // IANA, e.g., "America/New_York" - userAgent: z.string().optional(), - platform: z.enum(["web", "desktop", "mobile"]).optional(), - deviceCapabilities: z - .object({ - touch: z.boolean().optional(), - hover: z.boolean().optional(), - }) - .optional(), - safeAreaInsets: z - .object({ - top: z.number(), - right: z.number(), - bottom: z.number(), - left: z.number(), - }) - .optional(), -}); -export type McpUiHostContext = z.infer; - -export const McpUiHostContextChangedNotificationSchema = z.object({ - method: z.literal("ui/notifications/host-context-changed"), - params: McpUiHostContextSchema, -}); -export type McpUiHostContextChangedNotification = z.infer< - typeof McpUiHostContextChangedNotificationSchema ->; - -export const McpUiResourceTeardownRequestSchema = RequestSchema.extend({ - method: z.literal("ui/resource-teardown"), - params: z.object({}), -}); -export type McpUiResourceTeardownRequest = z.infer< - typeof McpUiResourceTeardownRequestSchema ->; - -export const McpUiResourceTeardownResultSchema = EmptyResultSchema; -export type McpUiResourceTeardownResult = z.infer< - typeof McpUiResourceTeardownResultSchema ->; - -export const McpUiHostCapabilitiesSchema = z.object({ - experimental: z.object({}).optional(), - - openLinks: z.object({}).optional(), - - serverTools: z - .object({ - listChanged: z.boolean().optional(), - }) - .optional(), - - serverResources: z - .object({ - listChanged: z.boolean().optional(), - }) - .optional(), - - logging: z.object({}).optional(), - - // TODO: elicitation, sampling... -}); -export type McpUiHostCapabilities = z.infer; - -export const McpUiAppCapabilitiesSchema = z.object({ - experimental: z.object({}).optional(), - - // WebMCP-style tools exposed by the app to the host - tools: z - .object({ - listChanged: z.boolean().optional(), - }) - .optional(), -}); -export type McpUiAppCapabilities = z.infer; - -export const McpUiInitializeRequestSchema = RequestSchema.extend({ - method: z.literal("ui/initialize"), - params: z.object({ - appInfo: ImplementationSchema, - appCapabilities: McpUiAppCapabilitiesSchema, - protocolVersion: z.string(), - }), -}); -export type McpUiInitializeRequest = z.infer< - typeof McpUiInitializeRequestSchema ->; - -export const McpUiHostInfoSchema = z.object({ - name: z.string(), - version: z.string(), -}); -export type McpUiHostInfo = z.infer; - -export const McpUiInitializeResultSchema = z.object({ - protocolVersion: z.string(), - hostInfo: McpUiHostInfoSchema, - hostCapabilities: McpUiHostCapabilitiesSchema, - hostContext: McpUiHostContextSchema, -}); -export type McpUiInitializeResult = z.infer; - -export const McpUiInitializedNotificationSchema = z.object({ - method: z.literal("ui/notifications/initialized"), - params: z.object({}), -}); -export type McpUiInitializedNotification = z.infer< - typeof McpUiInitializedNotificationSchema ->;