diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 24a07f2a..abb855c6 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -3,7 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; import path from "node:path"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -22,7 +22,7 @@ function createServer(): McpServer { // MCP Apps require two-part registration: a tool (what the LLM calls) and a // resource (the UI it renders). The `_meta` field on the tool links to the // resource URI, telling hosts which UI to display when the tool executes. - server.registerTool( + registerAppTool(server, "get-time", { title: "Get Time", @@ -38,7 +38,7 @@ function createServer(): McpServer { }, ); - server.registerResource( + registerAppResource(server, RESOURCE_URI, RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE }, diff --git a/examples/basic-server-vanillajs/README.md b/examples/basic-server-vanillajs/README.md index 8d227266..0d7cb619 100644 --- a/examples/basic-server-vanillajs/README.md +++ b/examples/basic-server-vanillajs/README.md @@ -52,5 +52,5 @@ button.addEventListener("click", () => { }); // Connect to host -app.connect(new PostMessageTransport(window.parent)); +app.connect(); ``` diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 0c596955..d3baad23 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -3,7 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; import path from "node:path"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -22,7 +22,7 @@ function createServer(): McpServer { // MCP Apps require two-part registration: a tool (what the LLM calls) and a // resource (the UI it renders). The `_meta` field on the tool links to the // resource URI, telling hosts which UI to display when the tool executes. - server.registerTool( + registerAppTool(server, "get-time", { title: "Get Time", @@ -38,7 +38,7 @@ function createServer(): McpServer { }, ); - server.registerResource( + registerAppResource(server, RESOURCE_URI, RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE }, diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index 7bfa6d69..54442520 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -1,7 +1,7 @@ /** * @file App that demonstrates a few features using MCP Apps SDK with vanilla JS. */ -import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps"; +import { App } from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import "./global.css"; import "./mcp-app.css"; @@ -98,4 +98,4 @@ openLinkBtn.addEventListener("click", async () => { // Connect to host -app.connect(new PostMessageTransport(window.parent)); +app.connect(); diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index ed09e1c0..fd7618ed 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -13,7 +13,10 @@ import type { import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, +} from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -235,7 +238,8 @@ function createServer(): McpServer { version: "1.0.0", }); - server.registerTool( + registerAppTool( + server, "get-budget-data", { title: "Get Budget Data", @@ -277,7 +281,8 @@ function createServer(): McpServer { }, ); - server.registerResource( + registerAppResource( + server, resourceUri, resourceUri, { diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index 723dc060..9cddc8ad 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -1,7 +1,7 @@ /** * Budget Allocator App - Interactive budget allocation with real-time visualization */ -import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps"; +import { App } from "@modelcontextprotocol/ext-apps"; import { Chart, registerables } from "chart.js"; import "./global.css"; import "./mcp-app.css"; @@ -631,4 +631,4 @@ window }); // Connect to host -app.connect(new PostMessageTransport(window.parent)); +app.connect(); diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index ca212ed6..9d3c9cb3 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -4,7 +4,10 @@ import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, +} from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -156,7 +159,8 @@ function createServer(): McpServer { // Register tool and resource const resourceUri = "ui://get-cohort-data/mcp-app.html"; - server.registerTool( + registerAppTool( + server, "get-cohort-data", { title: "Get Cohort Retention Data", @@ -179,7 +183,8 @@ function createServer(): McpServer { }, ); - server.registerResource( + registerAppResource( + server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index c7537822..0bbbbba8 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -7,7 +7,10 @@ import type { import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, +} from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; import { generateCustomers, @@ -65,7 +68,8 @@ function createServer(): McpServer { { const resourceUri = "ui://customer-segmentation/mcp-app.html"; - server.registerTool( + registerAppTool( + server, "get-customer-data", { title: "Get Customer Data", @@ -83,7 +87,8 @@ function createServer(): McpServer { }, ); - server.registerResource( + registerAppResource( + server, resourceUri, resourceUri, { diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index 77e348de..39221584 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -463,7 +463,7 @@ app.onhostcontextchanged = (params) => { } }; -app.connect(new PostMessageTransport(window.parent)).then(() => { +app.connect().then(() => { // Apply initial host context after connection const ctx = app.getHostContext(); if (ctx?.theme) { diff --git a/examples/qr-server/widget.html b/examples/qr-server/widget.html index e2ff4cb0..9275ff68 100644 --- a/examples/qr-server/widget.html +++ b/examples/qr-server/widget.html @@ -47,7 +47,7 @@ } }; - await app.connect(new PostMessageTransport(window.parent)); + await app.connect(); diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index 61b705b0..c3824939 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -7,7 +7,10 @@ import type { import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, +} from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -256,7 +259,8 @@ function createServer(): McpServer { { const resourceUri = "ui://scenario-modeler/mcp-app.html"; - server.registerTool( + registerAppTool( + server, "get-scenario-data", { title: "Get Scenario Data", @@ -288,7 +292,8 @@ function createServer(): McpServer { }, ); - server.registerResource( + registerAppResource( + server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE, description: "SaaS Scenario Modeler UI" }, diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 30687edb..b439c5ac 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -9,7 +9,10 @@ import os from "node:os"; import path from "node:path"; import si from "systeminformation"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, +} from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; // Schemas - types are derived from these using z.infer @@ -111,7 +114,8 @@ function createServer(): McpServer { // Register the get-system-stats tool and its associated UI resource const resourceUri = "ui://system-monitor/mcp-app.html"; - server.registerTool( + registerAppTool( + server, "get-system-stats", { title: "Get System Stats", @@ -149,7 +153,8 @@ function createServer(): McpServer { }, ); - server.registerResource( + registerAppResource( + server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE, description: "System Monitor UI" }, diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 19e53c6a..b5751f2e 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -1,7 +1,7 @@ /** * @file System Monitor App - displays real-time OS metrics with Chart.js */ -import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps"; +import { App } from "@modelcontextprotocol/ext-apps"; import { Chart, registerables } from "chart.js"; import "./global.css"; import "./mcp-app.css"; @@ -360,7 +360,7 @@ window // Register handlers and connect app.onerror = log.error; -app.connect(new PostMessageTransport(window.parent)); +app.connect(); // Auto-start polling after a short delay setTimeout(startPolling, 500); diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index 72f19b43..e9208542 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -9,7 +9,10 @@ import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, +} from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -142,7 +145,8 @@ function createServer(): McpServer { }); // Tool 1: show_threejs_scene - server.registerTool( + registerAppTool( + server, "show_threejs_scene", { title: "Show Three.js Scene", @@ -175,7 +179,8 @@ function createServer(): McpServer { ); // Tool 2: learn_threejs - server.registerTool( + registerAppTool( + server, "learn_threejs", { title: "Learn Three.js", @@ -191,7 +196,8 @@ function createServer(): McpServer { ); // Resource registration - server.registerResource( + registerAppResource( + server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE, description: "Three.js Widget UI" }, diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index 4ee56225..15c93c26 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -8,7 +8,10 @@ import * as cheerio from "cheerio"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; +import { + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, +} from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -76,7 +79,8 @@ function createServer(): McpServer { // Register the get-first-degree-links tool and its associated UI resource const resourceUri = "ui://wiki-explorer/mcp-app.html"; - server.registerTool( + registerAppTool( + server, "get-first-degree-links", { title: "Get First-Degree Links", @@ -124,7 +128,8 @@ function createServer(): McpServer { }, ); - server.registerResource( + registerAppResource( + server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 8f9026c7..2afa81fc 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -1,7 +1,7 @@ /** * Wiki Explorer - Force-directed graph visualization of Wikipedia link networks */ -import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps"; +import { App } from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { forceCenter, @@ -367,4 +367,4 @@ app.onerror = (err) => { }; // Connect to host -app.connect(new PostMessageTransport(window.parent)); +app.connect(); diff --git a/package.json b/package.json index ada7844a..cd3085fc 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,10 @@ "types": "./dist/src/app-bridge.d.ts", "default": "./dist/src/app-bridge.js" }, + "./server": { + "types": "./dist/src/server/index.d.ts", + "default": "./dist/src/server/index.js" + }, "./schema.json": "./dist/src/generated/schema.json" }, "files": [ diff --git a/src/app.ts b/src/app.ts index b8a92388..4a078010 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,6 +16,7 @@ import { PingRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { AppNotification, AppRequest, AppResult } from "./types"; +import { PostMessageTransport } from "./message-transport"; import { LATEST_PROTOCOL_VERSION, McpUiAppCapabilities, @@ -1025,7 +1026,7 @@ export class App extends Protocol { * @see {@link PostMessageTransport} for the typical transport implementation */ override async connect( - transport: Transport, + transport: Transport = new PostMessageTransport(window.parent), options?: RequestOptions, ): Promise { await super.connect(transport); diff --git a/src/server/index.test.ts b/src/server/index.test.ts new file mode 100644 index 00000000..2b7b45ea --- /dev/null +++ b/src/server/index.test.ts @@ -0,0 +1,179 @@ +import { describe, it, expect, mock } from "bun:test"; +import { + registerAppTool, + registerAppResource, + RESOURCE_URI_META_KEY, + RESOURCE_MIME_TYPE, +} from "./index"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +describe("registerAppTool", () => { + it("should pass through config to server.registerTool", () => { + let capturedName: string | undefined; + let capturedConfig: Record | undefined; + let capturedHandler: unknown; + + const mockServer = { + registerTool: mock( + (name: string, config: Record, handler: unknown) => { + capturedName = name; + capturedConfig = config; + capturedHandler = handler; + }, + ), + registerResource: mock(() => {}), + }; + + const handler = async () => ({ + content: [{ type: "text" as const, text: "ok" }], + }); + + registerAppTool( + mockServer as unknown as Pick, + "my-tool", + { + title: "My Tool", + description: "A test tool", + _meta: { + [RESOURCE_URI_META_KEY]: "ui://test/widget.html", + }, + }, + handler, + ); + + expect(mockServer.registerTool).toHaveBeenCalledTimes(1); + expect(capturedName).toBe("my-tool"); + expect(capturedConfig?.title).toBe("My Tool"); + expect(capturedConfig?.description).toBe("A test tool"); + expect( + (capturedConfig?._meta as Record)?.[ + RESOURCE_URI_META_KEY + ], + ).toBe("ui://test/widget.html"); + expect(capturedHandler).toBe(handler); + }); +}); + +describe("registerAppResource", () => { + it("should register a resource with default MIME type", () => { + let capturedName: string | undefined; + let capturedUri: string | undefined; + let capturedConfig: Record | undefined; + + const mockServer = { + registerTool: mock(() => {}), + registerResource: mock( + (name: string, uri: string, config: Record) => { + capturedName = name; + capturedUri = uri; + capturedConfig = config; + }, + ), + }; + + const callback = async () => ({ + contents: [ + { + uri: "ui://test/widget.html", + mimeType: RESOURCE_MIME_TYPE, + text: "", + }, + ], + }); + + registerAppResource( + mockServer as unknown as Pick, + "My Resource", + "ui://test/widget.html", + { + description: "A test resource", + _meta: { ui: {} }, + }, + callback, + ); + + expect(mockServer.registerResource).toHaveBeenCalledTimes(1); + expect(capturedName).toBe("My Resource"); + expect(capturedUri).toBe("ui://test/widget.html"); + expect(capturedConfig?.mimeType).toBe(RESOURCE_MIME_TYPE); + expect(capturedConfig?.description).toBe("A test resource"); + }); + + it("should allow custom MIME type to override default", () => { + let capturedConfig: Record | undefined; + + const mockServer = { + registerTool: mock(() => {}), + registerResource: mock( + (_name: string, _uri: string, config: Record) => { + capturedConfig = config; + }, + ), + }; + + registerAppResource( + mockServer as unknown as Pick, + "My Resource", + "ui://test/widget.html", + { + mimeType: "text/html", + _meta: { ui: {} }, + }, + async () => ({ + contents: [ + { + uri: "ui://test/widget.html", + mimeType: "text/html", + text: "", + }, + ], + }), + ); + + // Custom mimeType should override the default + expect(capturedConfig?.mimeType).toBe("text/html"); + }); + + it("should call the callback when handler is invoked", async () => { + let capturedHandler: (() => Promise) | undefined; + + const mockServer = { + registerTool: mock(() => {}), + registerResource: mock( + ( + _name: string, + _uri: string, + _config: unknown, + handler: () => Promise, + ) => { + capturedHandler = handler; + }, + ), + }; + + const expectedResult = { + contents: [ + { + uri: "ui://test/widget.html", + mimeType: RESOURCE_MIME_TYPE, + text: "content", + }, + ], + }; + const callback = mock(async () => expectedResult); + + registerAppResource( + mockServer as unknown as Pick, + "My Resource", + "ui://test/widget.html", + { _meta: { ui: {} } }, + callback, + ); + + expect(capturedHandler).toBeDefined(); + const result = await capturedHandler!(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(result).toEqual(expectedResult); + }); +}); diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 00000000..900ae777 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,147 @@ +/** + * Server Helpers for MCP Apps. + * + * @module server-helpers + */ + +import { + RESOURCE_URI_META_KEY, + RESOURCE_MIME_TYPE, + McpUiResourceMeta, +} from "../app.js"; +import type { + McpServer, + ResourceMetadata, + ToolCallback, + ReadResourceCallback, +} from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js"; +import type { ZodRawShape } from "zod"; + +// Re-exports for convenience +export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; +export type { ResourceMetadata, ToolCallback, ReadResourceCallback }; + +/** + * Tool configuration (same as McpServer.registerTool). + */ +export interface ToolConfig { + title?: string; + description?: string; + inputSchema?: ZodRawShape; + annotations?: ToolAnnotations; + _meta?: Record; +} + +/** + * MCP App Tool configuration for `registerAppTool`. + */ +export interface McpUiAppToolConfig extends ToolConfig { + _meta: { + // Soon: `ui: McpUiToolMeta;` (https://github.com/modelcontextprotocol/ext-apps/pull/131) + + /** + * URI of the UI resource to display for this tool. + * This is converted to `_meta["ui/resourceUri"]`. + * + * @example "ui://weather/widget.html" + */ + [RESOURCE_URI_META_KEY]: string; + [key: string]: unknown; + }; +} + +/** + * MCP App Resource configuration for `registerAppResource`. + */ +export interface McpUiAppResourceConfig extends ResourceMetadata { + _meta?: { + ui?: McpUiResourceMeta; + [key: string]: unknown; + }; +} + +/** + * Register an app tool with the MCP server. + * + * This is a convenience wrapper around `server.registerTool` that will allow more backwards-compatibility. + * + * @param server - The MCP server instance + * @param name - Tool name/identifier + * @param config - Tool configuration with required `ui` field + * @param handler - Tool handler function + * + * @example + * ```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: { + * [RESOURCE_URI_META_KEY]: "ui://weather/widget.html", + * }, + * }, async (args) => { + * const weather = await fetchWeather(args.location); + * return { content: [{ type: "text", text: JSON.stringify(weather) }] }; + * }); + * ``` + */ +export function registerAppTool( + server: Pick, + name: string, + config: McpUiAppToolConfig, + handler: ToolCallback, +): void { + server.registerTool(name, config, handler); +} + +/** + * Register an app resource with the MCP server. + * + * This is a convenience wrapper around `server.registerResource` that: + * - Defaults the MIME type to "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 `ui` field in tool config) + * @param config - Resource configuration + * @param readCallback - Callback that returns the resource contents + * + * @example + * ```typescript + * import { registerAppResource } from '@modelcontextprotocol/ext-apps/server'; + * + * registerAppResource(server, "Weather Widget", "ui://weather/widget.html", { + * description: "Interactive weather display", + * mimeType: RESOURCE_MIME_TYPE, + * }, async () => ({ + * contents: [{ + * uri: "ui://weather/widget.html", + * mimeType: RESOURCE_MIME_TYPE, + * text: await fs.readFile("dist/widget.html", "utf-8"), + * }], + * })); + * ``` + */ +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, + ); +}