Initial commit
This commit is contained in:
commit
92f42cf533
7 changed files with 2534 additions and 0 deletions
209
src/app.ts
Normal file
209
src/app.ts
Normal 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
3
src/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { App } from "./app";
|
||||
export { PostMessageTransport } from "./message-transport";
|
||||
export * from "./types";
|
||||
58
src/message-transport.ts
Normal file
58
src/message-transport.ts
Normal 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
351
src/peer.ts_
Normal 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
70
src/react/useApp.tsx
Normal 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 };
|
||||
}
|
||||
20
src/react/useAutoResize.ts
Normal file
20
src/react/useAutoResize.ts
Normal 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
236
src/types.ts
Normal 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
|
||||
>;
|
||||
Loading…
Reference in a new issue