Initial commit

This commit is contained in:
Olivier Chafik 2025-11-21 11:56:27 +00:00
commit 92f42cf533
No known key found for this signature in database
7 changed files with 2534 additions and 0 deletions

209
src/app.ts Normal file
View file

@ -0,0 +1,209 @@
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<Request, Notification, Result> {
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<CallToolResult> {
return await this.request(
{ method: "tools/call", params },
CallToolResultSchema,
options,
);
}
sendMessage(params: McpUiMessageRequest["params"], options?: RequestOptions) {
return this.request(
<McpUiMessageRequest>{
method: "ui/message",
params,
},
McpUiMessageResultSchema,
options,
);
}
sendLog(params: LoggingMessageNotification["params"]) {
return this.notification(<LoggingMessageNotification>{
method: "notifications/message",
params,
});
}
sendOpenLink(
params: McpUiOpenLinkRequest["params"],
options?: RequestOptions,
) {
return this.request(
<McpUiOpenLinkRequest>{
method: "ui/open-link",
params,
},
McpUiOpenLinkResultSchema,
options,
);
}
sendSizeChange(params: McpUiSizeChangeNotification["params"]) {
return this.notification(<McpUiSizeChangeNotification>{
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<void> {
await super.connect(transport);
try {
const result = await this.request(
<McpUiInitializeRequest>{
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(<McpUiInitializedNotification>{
method: "ui/notifications/initialized",
});
if (this.options?.autoResize) {
this.setupSizeChangeNotifications();
}
} catch (error) {
// Disconnect if initialization fails.
void this.close();
throw error;
}
}
}

3
src/index.ts Normal file
View file

@ -0,0 +1,3 @@
export { App } from "./app";
export { PostMessageTransport } from "./message-transport";
export * from "./types";

58
src/message-transport.ts Normal file
View file

@ -0,0 +1,58 @@
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;
}

351
src/peer.ts_ Normal file
View file

@ -0,0 +1,351 @@
// import {
// z,
// ZodRawShape,
// ZodLiteral,
// ZodObject,
// ZodString,
// ZodTypeAny,
// ZodType,
// ZodTypeDef,
// ZodOptional,
// } from "zod";
// import { Client } from "@modelcontextprotocol/sdk/client/index.js";
// import {
// // ToolCallback,
// RegisteredTool,
// } from "@modelcontextprotocol/sdk/server/mcp.js";
// import { Server } from "@modelcontextprotocol/sdk/server/index.js";
// import {
// Protocol,
// type RequestOptions,
// type RequestHandlerExtra,
// } from "@modelcontextprotocol/sdk/shared/protocol.js";
// import {
// CallToolRequestSchema,
// CallToolResult,
// CallToolResultSchema,
// CancelledNotificationSchema,
// ClientCapabilities,
// ClientCapabilitiesSchema,
// Request,
// Implementation,
// JSONRPCError,
// JSONRPCMessage,
// JSONRPCMessageSchema,
// JSONRPCNotification,
// JSONRPCResponse,
// ListPromptsRequestSchema,
// ListPromptsResultSchema,
// ListResourcesRequestSchema,
// ListResourcesResultSchema,
// ListResourceTemplatesRequestSchema,
// ListResourceTemplatesResultSchema,
// LoggingMessageNotification,
// LoggingMessageNotificationSchema,
// MessageExtraInfo,
// Notification,
// PromptListChangedNotificationSchema,
// RequestId,
// ResourceListChangedNotificationSchema,
// Result,
// ServerCapabilities,
// ServerCapabilitiesSchema,
// ServerNotification,
// ServerRequest,
// ToolAnnotations,
// ToolListChangedNotificationSchema,
// } from "@modelcontextprotocol/sdk/types.js";
// import {
// Transport,
// TransportSendOptions,
// } from "@modelcontextprotocol/sdk/shared/transport.js";
// import {
// McpUiToolInputNotification,
// McpUiToolInputNotificationSchema,
// McpUiToolResultNotification,
// McpUiToolResultNotificationSchema,
// McpUiHostCapabilities,
// McpUiHostInfo,
// McpUiInitializedNotification,
// McpUiInitializeRequest,
// McpUiInitializeResultSchema,
// McpUiIntentRequest,
// McpUiIntentRequestSchema,
// McpUiIntentResultSchema,
// McpUiRenderDataRequest,
// McpUiRenderDataRequestSchema,
// McpUiMessageRequest,
// McpUiMessageRequestSchema,
// McpUiMessageResult,
// McpUiMessageResultSchema,
// McpUiOpenLinkRequest,
// McpUiOpenLinkRequestSchema,
// McpUiOpenLinkResultSchema,
// McpUiRenderDataResultSchema,
// McpUiSizeChangeNotification,
// McpUiSizeChangeNotificationSchema,
// } from "./types";
// export { PostMessageTransport } from "./message-transport.js";
// if (false) {
// let server: Server;
// let client: Client;
// }
// const McpUiInitializeRequestSchema = z.object({
// method: z.literal("ui/initialize"),
// params: z.object({
// name: z.string(),
// version: z.string(),
// capabilities: ServerCapabilitiesSchema.extend(
// ClientCapabilitiesSchema.shape,
// ),
// }),
// });
// /**
// * Converts a provided Zod schema to a Zod object if it is a ZodRawShape,
// * otherwise returns the schema as is.
// */
// function getZodSchemaObject(
// schema: ZodRawShape | ZodType<object> | undefined,
// ): ZodType<object> | undefined {
// if (!schema) {
// return undefined;
// }
// if (isZodRawShape(schema)) {
// return z.object(schema);
// }
// return schema;
// }
// export class McpUiGuest extends Protocol<Request, Notification, Result> {
// private _registeredHostTools: Record<string, RegisteredTool> = {};
// assertCapabilityForMethod(method: Request["method"]): void {}
// assertRequestHandlerCapability(method: Request["method"]): void {}
// assertNotificationCapability(method: Notification["method"]): void {}
// async callServerTool(
// name: string,
// argsSchema: ZodTypeAny,
// args: unknown,
// options?: RequestOptions,
// ) {
// //: Promise<CallToolResult> {
// }
// async addUiToChat(
// resourceUri: string,
// state: Record<string, unknown>,
// ): Promise<void> {}
// // In future, could expose callHostTool
// registerHostTool<
// InputArgs extends ZodRawShape | ZodType<object>,
// OutputArgs extends ZodRawShape | ZodType<object>,
// >(
// name: string,
// config: {
// title?: string;
// description?: string;
// inputSchema?: InputArgs;
// outputSchema?: OutputArgs;
// annotations?: ToolAnnotations;
// _meta?: Record<string, unknown>;
// },
// cb: ToolCallback<InputArgs>,
// ): RegisteredTool {
// if (this._registeredHostTools[name]) {
// throw new Error(`Tool ${name} is already registered on host`);
// }
// const {
// title,
// description,
// inputSchema,
// outputSchema,
// annotations,
// _meta,
// } = config;
// return this._createRegisteredTool(
// name,
// title,
// description,
// inputSchema,
// outputSchema,
// annotations,
// _meta,
// cb as ToolCallback<ZodRawShape | undefined>,
// );
// }
// private _createRegisteredTool(
// name: string,
// title: string | undefined,
// description: string | undefined,
// inputSchema: ZodRawShape | ZodType<object> | undefined,
// outputSchema: ZodRawShape | ZodType<object> | undefined,
// annotations: ToolAnnotations | undefined,
// _meta: Record<string, unknown> | undefined,
// callback: ToolCallback<ZodRawShape | undefined>,
// ): RegisteredTool {
// const registeredTool: RegisteredTool = {
// title,
// description,
// inputSchema: getZodSchemaObject(inputSchema) as any,
// outputSchema: getZodSchemaObject(outputSchema) as any,
// annotations,
// _meta,
// callback,
// enabled: true,
// disable: () => registeredTool.update({ enabled: false }),
// enable: () => registeredTool.update({ enabled: true }),
// remove: () => registeredTool.update({ name: null }),
// update: (updates) => {
// if (typeof updates.name !== "undefined" && updates.name !== name) {
// delete this._registeredHostTools[name];
// if (updates.name)
// this._registeredHostTools[updates.name] = registeredTool;
// }
// if (typeof updates.title !== "undefined")
// registeredTool.title = updates.title;
// if (typeof updates.description !== "undefined")
// registeredTool.description = updates.description;
// if (typeof updates.paramsSchema !== "undefined")
// registeredTool.inputSchema = z.object(updates.paramsSchema);
// if (typeof updates.callback !== "undefined")
// registeredTool.callback = updates.callback;
// if (typeof updates.annotations !== "undefined")
// registeredTool.annotations = updates.annotations;
// if (typeof updates._meta !== "undefined")
// registeredTool._meta = updates._meta;
// if (typeof updates.enabled !== "undefined")
// registeredTool.enabled = updates.enabled;
// // this.sendToolListChanged();
// },
// };
// this._registeredHostTools[name] = registeredTool;
// return registeredTool;
// }
// sendMessage(params: McpUiMessageRequest["params"], options?: RequestOptions) {
// return this.request(
// <McpUiMessageRequest>{
// method: "ui/message",
// params,
// },
// McpUiMessageResultSchema,
// options,
// );
// }
// sendLog(params: LoggingMessageNotification["params"]) {
// return this.notification(<LoggingMessageNotification>{
// method: "notifications/message",
// params,
// });
// }
// sendOpenLink(
// params: McpUiOpenLinkRequest["params"],
// options?: RequestOptions,
// ) {
// return this.request(
// <McpUiOpenLinkRequest>{
// method: "ui/open-link",
// params,
// },
// McpUiOpenLinkResultSchema,
// options,
// );
// }
// sendSizeChange(params: McpUiSizeChangeNotification["params"]) {
// return this.notification(<McpUiSizeChangeNotification>{
// method: "ui/size-change",
// params,
// });
// }
// private _hostCapabilities?: McpUiHostCapabilities;
// private _hostInfo?: McpUiHostInfo;
// override async connect(
// transport: Transport,
// options?: RequestOptions,
// ): Promise<void> {
// await super.connect(transport);
// try {
// const result = await this.request(
// <McpUiInitializeRequest>{
// method: "ui/initialize",
// params: {
// // TODO: 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(<McpUiInitializedNotification>{
// method: "ui/notifications/initialized",
// });
// } catch (error) {
// // Disconnect if initialization fails.
// void this.close();
// throw error;
// }
// }
// }
// export type ToolCallback<
// Args extends undefined | ZodRawShape | ZodType<object> = undefined,
// > = Args extends ZodRawShape
// ? (
// args: z.objectOutputType<Args, ZodTypeAny>,
// extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
// ) => CallToolResult | Promise<CallToolResult>
// : Args extends ZodType<infer T>
// ? (
// args: T,
// extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
// ) => CallToolResult | Promise<CallToolResult>
// : (
// extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
// ) => CallToolResult | Promise<CallToolResult>;
// // Helper to check if an object is a Zod schema (ZodRawShape)
// function isZodRawShape(obj: unknown): obj is ZodRawShape {
// if (typeof obj !== "object" || obj === null) return false;
// const isEmptyObject = Object.keys(obj).length === 0;
// // Check if object is empty or at least one property is a ZodType instance
// // Note: use heuristic check to avoid instanceof failure across different Zod versions
// return isEmptyObject || Object.values(obj as object).some(isZodTypeLike);
// }
// function isZodTypeLike(value: unknown): value is ZodType {
// return (
// value !== null &&
// typeof value === "object" &&
// "parse" in value &&
// typeof value.parse === "function" &&
// "safeParse" in value &&
// typeof value.safeParse === "function"
// );
// }

70
src/react/useApp.tsx Normal file
View file

@ -0,0 +1,70 @@
import { useEffect, useState } from "react";
import { Implementation } from "@modelcontextprotocol/sdk/types.js";
import { Client } from "@modelcontextprotocol/sdk/client";
import { App, McpUiAppCapabilities, PostMessageTransport } from "../app";
export * from "../app";
export interface UseAppOptions {
appInfo: Implementation;
capabilities: McpUiAppCapabilities;
/**
* Called after client is created but before connection.
* Use this to register request/notification handlers via
* client.setRequestHandler() and client.setNotificationHandler().
*/
onAppCreated?: (app: App) => void;
}
export interface AppState {
app: App | null;
isConnected: boolean;
error: Error | null;
}
export function useApp({
appInfo,
capabilities,
onAppCreated,
}: UseAppOptions): AppState {
const [app, setApp] = useState<App | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let mounted = true;
async function connect() {
try {
const transport = new PostMessageTransport(window.parent);
const app = new App(appInfo, capabilities);
// Register handlers BEFORE connecting
onAppCreated?.(app);
await app.connect(transport);
if (mounted) {
setApp(app);
setIsConnected(true);
setError(null);
}
} catch (error) {
if (mounted) {
setApp(null);
setIsConnected(false);
setError(
error instanceof Error ? error : new Error("Failed to connect"),
);
}
}
}
connect();
return () => {
mounted = false;
};
}, []); // Intentionally not including options to avoid reconnection
return { app, isConnected, error };
}

View file

@ -0,0 +1,20 @@
import { useEffect, RefObject } from "react";
import { App } from "../app";
/**
* Custom hook that automatically reports size changes to the parent window.
*
* @param client - MCP UI client for sending size notifications
*/
export function useAutoResize(
app: App | null,
elementRef?: RefObject<HTMLElement | null>,
) {
useEffect(() => {
if (!app) {
return;
}
return app.setupSizeChangeNotifications();
}, [app, elementRef]);
}

236
src/types.ts Normal file
View file

@ -0,0 +1,236 @@
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<typeof McpUiOpenLinkRequestSchema>;
export const McpUiOpenLinkResultSchema = z.object({
isError: z.boolean(),
});
export type McpUiOpenLinkResult = z.infer<typeof McpUiOpenLinkResultSchema>;
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<typeof McpUiMessageRequestSchema>;
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<typeof McpUiMessageResultSchema>;
// 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-inputs"),
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-inputs-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<typeof McpUiHostContextSchema>;
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<typeof McpUiHostCapabilitiesSchema>;
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<typeof McpUiAppCapabilitiesSchema>;
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<typeof McpUiHostInfoSchema>;
export const McpUiInitializeResultSchema = z.object({
protocolVersion: z.string(),
hostInfo: McpUiHostInfoSchema,
hostCapabilities: McpUiHostCapabilitiesSchema,
hostContext: McpUiHostContextSchema,
});
export type McpUiInitializeResult = z.infer<typeof McpUiInitializeResultSchema>;
export const McpUiInitializedNotificationSchema = z.object({
method: z.literal("ui/notifications/initialized"),
params: z.object({}),
});
export type McpUiInitializedNotification = z.infer<
typeof McpUiInitializedNotificationSchema
>;