/** * Utilities for MCP servers to register tools and resources that display interactive UIs. * * Use these helpers instead of the base SDK's `registerTool` and `registerResource` when * your tool should render an {@link app!App} in the client. They handle UI metadata normalization * and provide sensible defaults for the MCP Apps MIME type ({@link RESOURCE_MIME_TYPE}). * * @module server-helpers * * @example * ```typescript * import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; * * // Register a tool that displays a widget * registerAppTool(server, "weather", { * description: "Get weather forecast", * _meta: { ui: { resourceUri: "ui://weather/widget.html" } }, * }, handler); * * // Register the HTML resource the tool references * registerAppResource(server, "Weather Widget", "ui://weather/widget.html", {}, readCallback); * ``` */ import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE, McpUiResourceMeta, McpUiToolMeta, } from "../app.js"; import type { BaseToolCallback, McpServer, RegisteredTool, ResourceMetadata, ToolCallback, ReadResourceCallback, } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { AnySchema, ZodRawShapeCompat, } from "@modelcontextprotocol/sdk/server/zod-compat.js"; import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js"; // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; export type { ResourceMetadata, ToolCallback, ReadResourceCallback }; /** * Base tool configuration matching the standard MCP server tool options. * Extended by {@link McpUiAppToolConfig} to add UI metadata requirements. */ export interface ToolConfig { title?: string; description?: string; inputSchema?: ZodRawShapeCompat | AnySchema; outputSchema?: ZodRawShapeCompat | AnySchema; annotations?: ToolAnnotations; _meta?: Record; } /** * Configuration for tools that render an interactive UI. * * Extends {@link ToolConfig} with a required `_meta` field that specifies UI metadata. * The UI resource can be specified in two ways: * - `_meta.ui.resourceUri` (preferred) * - `_meta["ui/resourceUri"]` (deprecated, for backward compatibility) * * @see {@link registerAppTool} for the recommended way to register app tools */ export interface McpUiAppToolConfig extends ToolConfig { _meta: { [key: string]: unknown; } & ( | { ui: McpUiToolMeta; } | { /** * URI of the UI resource to display for this tool. * This is converted to `_meta["ui/resourceUri"]`. * * @example "ui://weather/widget.html" * * @deprecated Use `_meta.ui.resourceUri` instead. */ [RESOURCE_URI_META_KEY]?: string; } ); } /** * MCP App Resource configuration for {@link registerAppResource}. * * Extends the base MCP SDK `ResourceMetadata` with optional UI metadata * for configuring security policies and rendering preferences. * * @see {@link registerAppResource} for usage */ export interface McpUiAppResourceConfig extends ResourceMetadata { /** * Optional UI metadata for the resource. * Used to configure security policies (CSP) and rendering preferences. */ _meta?: { /** * UI-specific metadata including CSP configuration and rendering preferences. */ ui?: McpUiResourceMeta; // Allow additional metadata properties for extensibility. [key: string]: unknown; }; } /** * Register an app tool with the MCP server. * * This is a convenience wrapper around `server.registerTool` that normalizes * UI metadata: if `_meta.ui.resourceUri` is set, the legacy `_meta["ui/resourceUri"]` * key is also populated (and vice versa) for compatibility with older hosts. * * @param server - The MCP server instance * @param name - Tool name/identifier * @param config - Tool configuration with `_meta` field containing UI metadata * @param cb - Tool handler function * * @example Basic usage * ```typescript * import { registerAppTool } from '@modelcontextprotocol/ext-apps/server'; * import { z } from 'zod'; * * registerAppTool(server, "get-weather", { * title: "Get Weather", * description: "Get current weather for a location", * inputSchema: { location: z.string() }, * _meta: { * ui: { resourceUri: "ui://weather/widget.html" }, * }, * }, async (args) => { * const weather = await fetchWeather(args.location); * return { content: [{ type: "text", text: JSON.stringify(weather) }] }; * }); * ``` * * @example Tool visibility - create app-only tools for UI actions * ```typescript * import { registerAppTool } from '@modelcontextprotocol/ext-apps/server'; * import { z } from 'zod'; * * // Main tool - visible to both model and app (default) * registerAppTool(server, "show-cart", { * description: "Display the user's shopping cart", * _meta: { * ui: { * resourceUri: "ui://shop/cart.html", * visibility: ["model", "app"], * }, * }, * }, async () => { * const cart = await getCart(); * return { content: [{ type: "text", text: JSON.stringify(cart) }] }; * }); * * // App-only tool - hidden from the model, only callable by the UI * registerAppTool(server, "update-quantity", { * description: "Update item quantity in cart", * inputSchema: { itemId: z.string(), quantity: z.number() }, * _meta: { * ui: { * resourceUri: "ui://shop/cart.html", * visibility: ["app"], * }, * }, * }, async ({ itemId, quantity }) => { * const cart = await updateCartItem(itemId, quantity); * return { content: [{ type: "text", text: JSON.stringify(cart) }] }; * }); * ``` * * @see {@link registerAppResource} to register the HTML resource referenced by the tool */ export function registerAppTool< OutputArgs extends ZodRawShapeCompat | AnySchema, InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined, >( server: Pick, name: string, config: McpUiAppToolConfig & { inputSchema?: InputArgs; outputSchema?: OutputArgs; }, cb: ToolCallback, ): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key // - If the legacy flat key is set, also set _meta.ui.resourceUri const meta = config._meta; const uiMeta = meta.ui as McpUiToolMeta | undefined; const legacyUri = meta[RESOURCE_URI_META_KEY] as string | undefined; let normalizedMeta = meta; if (uiMeta?.resourceUri && !legacyUri) { // New format -> also set legacy key normalizedMeta = { ...meta, [RESOURCE_URI_META_KEY]: uiMeta.resourceUri }; } else if (legacyUri && !uiMeta?.resourceUri) { // Legacy format -> also set new format normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); } /** * Register an app resource with the MCP server. * * This is a convenience wrapper around `server.registerResource` that: * - Defaults the MIME type to {@link RESOURCE_MIME_TYPE} (`"text/html;profile=mcp-app"`) * - Provides a cleaner API matching the SDK's callback signature * * @param server - The MCP server instance * @param name - Human-readable resource name * @param uri - Resource URI (should match the `_meta.ui` field in tool config) * @param config - Resource configuration * @param readCallback - Callback that returns the resource contents * * @example Basic usage * ```typescript * import { registerAppResource } from '@modelcontextprotocol/ext-apps/server'; * * registerAppResource(server, "Weather Widget", "ui://weather/widget.html", { * description: "Interactive weather display", * }, async () => ({ * contents: [{ * uri: "ui://weather/widget.html", * mimeType: RESOURCE_MIME_TYPE, * text: await fs.readFile("dist/widget.html", "utf-8"), * }], * })); * ``` * * @example With CSP configuration for external domains * ```typescript * registerAppResource(server, "Music Player", "ui://music/player.html", { * description: "Audio player with external soundfonts", * _meta: { * ui: { * csp: { * connectDomains: ["https://api.example.com"], // For fetch/WebSocket * resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images * }, * }, * }, * }, readCallback); * ``` * * @see {@link registerAppTool} to register tools that reference this resource */ export function registerAppResource( server: Pick, name: string, uri: string, config: McpUiAppResourceConfig, readCallback: ReadResourceCallback, ): void { server.registerResource( name, uri, { // Default MIME type for MCP App UI resources (can still be overridden by config below) mimeType: RESOURCE_MIME_TYPE, ...config, }, readCallback, ); }