From 5ff40d0269ec560913862889642ac9f8c266c50d Mon Sep 17 00:00:00 2001 From: Anton Pidkuiko MacBook Date: Thu, 11 Dec 2025 13:19:24 +0000 Subject: [PATCH 01/14] docs: add tool visibility and restructure _meta.ui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Restructure tool metadata: `_meta["ui/resourceUri"]` → `_meta.ui.resourceUri` - Add `visibility` array field: ["model"], ["apps"], or ["model", "apps"] - Default ["model"] preserves standard MCP behavior - ["apps"] enables widget-only tools hidden from agent - Add McpUiToolMeta interface for type safety - Add Design Decision #4 explaining approach vs OpenAI's two-field model 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- specification/draft/apps.mdx | 78 +++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 2619964c0..767b7c180 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -245,21 +245,31 @@ Example: ### Resource Discovery -Tools are associated with UI resources through the `_meta` field: +Tools are associated with UI resources through the `_meta.ui` field: ```typescript +interface McpUiToolMeta { + /** URI of UI resource for rendering tool results */ + resourceUri?: string; + /** + * Who can access this tool. Default: ["model"] + * - "model": Tool visible to and callable by the agent + * - "apps": Tool callable by ui apps from this server + */ + visibility?: Array<"model" | "apps">; +} + interface Tool { name: string; description: string; inputSchema: object; _meta?: { - // Required: URI of the UI resource to use for rendering - "ui/resourceUri"?: string; + ui?: McpUiToolMeta; }; } ``` -Example: +Example (tool visible to both model and apps): ```json { @@ -272,20 +282,48 @@ Example: } }, "_meta": { - "ui/resourceUri": "ui://weather-server/dashboard-template" + "ui": { + "resourceUri": "ui://weather-server/dashboard-template", + "visibility": ["model", "apps"] + } + } +} +``` + +Example (app-only tool, hidden from model): + +```json +{ + "name": "refresh_dashboard", + "description": "Refresh dashboard data", + "inputSchema": { "type": "object" }, + "_meta": { + "ui": { + "resourceUri": "ui://weather-server/dashboard-template", + "visibility": ["apps"] + } } } ``` #### Behavior: -- If `ui/resourceUri` is present and host supports MCP Apps, host renders tool results using the specified UI resource +- If `ui.resourceUri` is present and host supports MCP Apps, host renders tool results using the specified UI resource - If host does not support MCP Apps, tool behaves as standard tool (text-only fallback) - Resource MUST exist on the server -- Host MUST use `resources/read` to fetch the referenced resource URI. +- Host MUST use `resources/read` to fetch the referenced resource URI - Host MAY prefetch and cache UI resource content for performance optimization - Since UI resources are primarily discovered through tool metadata, Servers MAY omit UI-only resources from `resources/list` and `notifications/resources/list_changed` +#### Visibility: + +- `visibility` defaults to `["model"]` if omitted (standard MCP behavior) +- `"model"`: Tool is visible to and callable by the agent +- `"apps"`: Tool is callable by apps from the same server connection only +- Host MUST NOT include tools with `visibility: ["apps"]` in the agent's tool list +- Host MUST reject `tools/call` requests from apps for tools that don't include `"apps"` in visibility +- Cross-server tool calls are always blocked for app-only tools + #### Benefits: - **Performance:** Host can preload templates before tool execution @@ -879,7 +917,7 @@ sequenceDiagram autonumber S -->> H: resources/list (includes ui:// resources) - S -->> H: tools/list (includes tools with ui/resourceUri metadata) + S -->> H: tools/list (includes tools with _meta.ui metadata) ``` #### 2. UI Initialization (Desktop/Native Hosts) @@ -893,7 +931,7 @@ sequenceDiagram autonumber par UI Tool call - H ->> S: tools/call to Tool with ui/resourceUri metadata + H ->> S: tools/call to Tool with _meta.ui metadata and UI initialization alt Desktop/Native hosts H ->> H: Render Guest UI in an iframe (HTML from the ui:// resource) @@ -1079,7 +1117,7 @@ await client.callTool("get_weather", { location: "New York" }); This pattern enables interactive, self-updating widgets. -Note: The called tool may not appear in `tools/list` responses. MCP servers MAY expose private tools specifically designed for UI interaction that are not visible to the agent. UI implementations SHOULD attempt to call tools by name regardless of discoverability. The specification for Private Tools will be covered in a future SEP. +Note: Tools with `visibility: ["apps"]` are hidden from the agent but remain callable by apps via `tools/call`. This enables UI-only interactions (refresh buttons, form submissions) without exposing implementation details to the model. See the Visibility section under Resource Discovery for details. ### Client\<\>Server Capability Negotiation @@ -1132,7 +1170,7 @@ if (hasUISupport) { description: "Get weather with interactive dashboard", inputSchema: { /* ... */ }, _meta: { - "ui/resourceUri": "ui://weather-server/dashboard" + ui: { resourceUri: "ui://weather-server/dashboard" } } }); } else { @@ -1241,6 +1279,24 @@ This proposal synthesizes feedback from the UI CWG and MCP-UI community, host im - **Inline styles in tool results:** Rejected; separating theming from data enables caching and updates - **CSS-in-JS injection:** Rejected; framework-specific and security concerns with injected code +#### 5. Tool Visibility via Metadata + +**Decision:** Use `_meta.ui.visibility` array to control tool accessibility between model and app. + +**Rationale:** + +- Nested `_meta.ui` structure groups all UI-related metadata cleanly +- Array format (`["model", "app"]`) allows flexible combinations +- Default `["model", "app"]` allows both agent and app to access tools +- `"app"` scope is per-server, preventing cross-server tool calls +- Cleaner than OpenAI's two-field approach (`widgetAccessible` + `visibility`) + +**Alternatives considered:** + +- **Two separate fields:** OpenAI uses `widgetAccessible` and `visibility` separately. Rejected as redundant; single `visibility` array covers all cases. +- **Boolean `private` flag:** Simpler but less flexible; doesn't express model-only tools. +- **Flat `ui/visibility` key:** Rejected in favor of nested structure for consistency with future `_meta.ui` fields. + ### Backward Compatibility The proposal builds on the existing core protocol. There are no incompatibilities. From 8fe8977801b7548aa9b2bf8577ebc02843d02731 Mon Sep 17 00:00:00 2001 From: Anton Pidkuiko MacBook Date: Sat, 13 Dec 2025 13:53:14 +0000 Subject: [PATCH 02/14] address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change default visibility to ["model", "apps"] - Add deprecation notice for flat ui/resourceUri format - Add McpUiToolMeta and McpUiToolVisibility to spec.types.ts - Improve tools/list and tools/call behavior wording 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- specification/draft/apps.mdx | 24 ++++++++++++++---------- src/spec.types.ts | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 767b7c180..1166cad71 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -252,11 +252,11 @@ interface McpUiToolMeta { /** URI of UI resource for rendering tool results */ resourceUri?: string; /** - * Who can access this tool. Default: ["model"] + * Who can access this tool. Default: ["model", "app"] * - "model": Tool visible to and callable by the agent - * - "apps": Tool callable by ui apps from this server + * - "app": Tool callable by the app from this server only */ - visibility?: Array<"model" | "apps">; + visibility?: Array<"model" | "app">; } interface Tool { @@ -265,10 +265,14 @@ interface Tool { inputSchema: object; _meta?: { ui?: McpUiToolMeta; + /** @deprecated Use `ui.resourceUri` instead. Will be removed before GA. */ + "ui/resourceUri"?: string; }; } ``` +> **Deprecation notice:** The flat `_meta["ui/resourceUri"]` format is deprecated. Use `_meta.ui.resourceUri` instead. The deprecated format will be removed before GA. + Example (tool visible to both model and apps): ```json @@ -284,7 +288,7 @@ Example (tool visible to both model and apps): "_meta": { "ui": { "resourceUri": "ui://weather-server/dashboard-template", - "visibility": ["model", "apps"] + "visibility": ["model", "app"] } } } @@ -300,7 +304,7 @@ Example (app-only tool, hidden from model): "_meta": { "ui": { "resourceUri": "ui://weather-server/dashboard-template", - "visibility": ["apps"] + "visibility": ["app"] } } } @@ -317,11 +321,11 @@ Example (app-only tool, hidden from model): #### Visibility: -- `visibility` defaults to `["model"]` if omitted (standard MCP behavior) +- `visibility` defaults to `["model", "app"]` if omitted - `"model"`: Tool is visible to and callable by the agent -- `"apps"`: Tool is callable by apps from the same server connection only -- Host MUST NOT include tools with `visibility: ["apps"]` in the agent's tool list -- Host MUST reject `tools/call` requests from apps for tools that don't include `"apps"` in visibility +- `"app"`: Tool is callable by the app from the same server connection only +- **tools/list behavior:** Host MUST NOT include tools in the agent's tool list when their visibility does not include `"model"` (e.g., `visibility: ["app"]`) +- **tools/call behavior:** Host MUST reject `tools/call` requests from apps for tools that don't include `"app"` in visibility - Cross-server tool calls are always blocked for app-only tools #### Benefits: @@ -1117,7 +1121,7 @@ await client.callTool("get_weather", { location: "New York" }); This pattern enables interactive, self-updating widgets. -Note: Tools with `visibility: ["apps"]` are hidden from the agent but remain callable by apps via `tools/call`. This enables UI-only interactions (refresh buttons, form submissions) without exposing implementation details to the model. See the Visibility section under Resource Discovery for details. +Note: Tools with `visibility: ["app"]` are hidden from the agent but remain callable by apps via `tools/call`. This enables UI-only interactions (refresh buttons, form submissions) without exposing implementation details to the model. See the Visibility section under Resource Discovery for details. ### Client\<\>Server Capability Negotiation diff --git a/src/spec.types.ts b/src/spec.types.ts index a371cc4d4..436f1ecb0 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -513,3 +513,22 @@ export interface McpUiRequestDisplayModeResult { */ [key: string]: unknown; } + +/** + * @description Tool visibility scope - who can access the tool. + */ +export type McpUiToolVisibility = "model" | "app"; + +/** + * @description UI-related metadata for tools. + */ +export interface McpUiToolMeta { + /** @description URI of UI resource for rendering tool results. */ + resourceUri?: string; + /** + * @description Who can access this tool. Default: ["model", "app"] + * - "model": Tool visible to and callable by the agent + * - "app": Tool callable by the app from this server only + */ + visibility?: McpUiToolVisibility[]; +} From 0b4b13c3ae228421002cff51d99ce8d4cc55d558 Mon Sep 17 00:00:00 2001 From: Anton Pidkuiko MacBook Date: Sat, 13 Dec 2025 14:23:23 +0000 Subject: [PATCH 03/14] chore: regenerate schemas for McpUiToolMeta types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/generated/schema.json | 42 ++++++++++++++++++++++++++++++++++++ src/generated/schema.test.ts | 24 +++++++++------------ src/generated/schema.ts | 40 ++++++++++++++++++---------------- 3 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 5867796b3..42722d6c4 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -3796,6 +3796,34 @@ "required": ["method", "params"], "additionalProperties": false }, + "McpUiToolMeta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "resourceUri": { + "description": "URI of UI resource for rendering tool results.", + "type": "string" + }, + "visibility": { + "description": "Who can access this tool. Default: [\"model\", \"apps\"]\n- \"model\": Tool visible to and callable by the agent\n- \"apps\": Tool callable by apps from this server only", + "type": "array", + "items": { + "description": "Tool visibility scope - who can access the tool.", + "anyOf": [ + { + "type": "string", + "const": "model" + }, + { + "type": "string", + "const": "apps" + } + ] + } + } + }, + "additionalProperties": false + }, "McpUiToolResultNotification": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -4154,6 +4182,20 @@ }, "required": ["method", "params"], "additionalProperties": false + }, + "McpUiToolVisibility": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Tool visibility scope - who can access the tool.", + "anyOf": [ + { + "type": "string", + "const": "model" + }, + { + "type": "string", + "const": "apps" + } + ] } } } diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 702d441e4..65ffb6aa1 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -95,12 +95,12 @@ export type McpUiResourceMetaSchemaInferredType = z.infer< typeof generated.McpUiResourceMetaSchema >; -export type McpUiRequestDisplayModeRequestSchemaInferredType = z.infer< - typeof generated.McpUiRequestDisplayModeRequestSchema +export type McpUiToolVisibilitySchemaInferredType = z.infer< + typeof generated.McpUiToolVisibilitySchema >; -export type McpUiRequestDisplayModeResultSchemaInferredType = z.infer< - typeof generated.McpUiRequestDisplayModeResultSchema +export type McpUiToolMetaSchemaInferredType = z.infer< + typeof generated.McpUiToolMetaSchema >; export type McpUiMessageRequestSchemaInferredType = z.infer< @@ -225,18 +225,14 @@ expectType({} as McpUiResourceCspSchemaInferredType); expectType({} as spec.McpUiResourceCsp); expectType({} as McpUiResourceMetaSchemaInferredType); expectType({} as spec.McpUiResourceMeta); -expectType( - {} as McpUiRequestDisplayModeRequestSchemaInferredType, +expectType( + {} as McpUiToolVisibilitySchemaInferredType, ); -expectType( - {} as spec.McpUiRequestDisplayModeRequest, -); -expectType( - {} as McpUiRequestDisplayModeResultSchemaInferredType, -); -expectType( - {} as spec.McpUiRequestDisplayModeResult, +expectType( + {} as spec.McpUiToolVisibility, ); +expectType({} as McpUiToolMetaSchemaInferredType); +expectType({} as spec.McpUiToolMeta); expectType( {} as McpUiMessageRequestSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index e5e04d6c1..9d5f85618 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -425,28 +425,32 @@ export const McpUiResourceMetaSchema = z.object({ }); /** - * @description Request to change the display mode of the UI. - * The host will respond with the actual display mode that was set, - * which may differ from the requested mode if not supported. - * @see {@link app.App.requestDisplayMode} for the method that sends this request + * @description Tool visibility scope - who can access the tool. */ -export const McpUiRequestDisplayModeRequestSchema = z.object({ - method: z.literal("ui/request-display-mode"), - params: z.object({ - /** @description The display mode being requested. */ - mode: McpUiDisplayModeSchema.describe("The display mode being requested."), - }), -}); +export const McpUiToolVisibilitySchema = z + .union([z.literal("model"), z.literal("apps")]) + .describe("Tool visibility scope - who can access the tool."); /** - * @description Result from requesting a display mode change. - * @see {@link McpUiRequestDisplayModeRequest} + * @description UI-related metadata for tools. */ -export const McpUiRequestDisplayModeResultSchema = z.looseObject({ - /** @description The display mode that was actually set. May differ from requested if not supported. */ - mode: McpUiDisplayModeSchema.describe( - "The display mode that was actually set. May differ from requested if not supported.", - ), +export const McpUiToolMetaSchema = z.object({ + /** @description URI of UI resource for rendering tool results. */ + resourceUri: z + .string() + .optional() + .describe("URI of UI resource for rendering tool results."), + /** + * @description Who can access this tool. Default: ["model", "apps"] + * - "model": Tool visible to and callable by the agent + * - "apps": Tool callable by apps from this server only + */ + visibility: z + .array(McpUiToolVisibilitySchema) + .optional() + .describe( + 'Who can access this tool. Default: ["model", "apps"]\n- "model": Tool visible to and callable by the agent\n- "apps": Tool callable by apps from this server only', + ), }); /** From 61cf2a4800e416d502f1c29435732d5b9c82eda0 Mon Sep 17 00:00:00 2001 From: Anton Pidkuiko MacBook Date: Mon, 15 Dec 2025 22:06:30 +0000 Subject: [PATCH 04/14] feat: tool visibility and nested _meta.ui format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename visibility "apps" → "app" in McpUiToolVisibility type - Add _meta.ui.resourceUri nested format (deprecate flat format) - Add getToolUiResourceUri() utility with backward compatibility - Add visibility demo to system-monitor-server: - get-system-stats: visibility ["model"] with resourceUri - refresh-stats: visibility ["app"] (app-only polling) - Update all example servers to use new _meta.ui format - Add 11 unit tests for getToolUiResourceUri() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/quickstart.md | 4 +- examples/basic-host/src/implementation.ts | 14 +-- examples/basic-server-react/server.ts | 4 +- examples/basic-server-vanillajs/server.ts | 4 +- examples/budget-allocator-server/README.md | 2 +- examples/budget-allocator-server/server.ts | 4 +- examples/cohort-heatmap-server/server.ts | 4 +- .../customer-segmentation-server/README.md | 2 +- .../customer-segmentation-server/server.ts | 4 +- examples/scenario-modeler-server/README.md | 2 +- examples/scenario-modeler-server/server.ts | 4 +- examples/system-monitor-server/README.md | 2 +- examples/system-monitor-server/server.ts | 81 ++++++++----- examples/system-monitor-server/src/mcp-app.ts | 2 +- examples/threejs-server/server.ts | 4 +- examples/wiki-explorer-server/server.ts | 4 +- specification/draft/apps.mdx | 2 +- src/app-bridge.test.ts | 110 +++++++++++++++++- src/app-bridge.ts | 45 +++++++ src/generated/schema.json | 26 ++--- src/generated/schema.test.ts | 20 ++++ src/generated/schema.ts | 33 +++++- 22 files changed, 295 insertions(+), 82 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 320a274a9..68251a542 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -97,7 +97,7 @@ Create `server.ts`: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps"; +import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps"; import cors from "cors"; import express from "express"; import fs from "node:fs/promises"; @@ -119,7 +119,7 @@ server.registerTool( description: "Returns the current server time.", inputSchema: {}, outputSchema: { time: z.string() }, - _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, // Links tool to UI + _meta: { ui: { resourceUri } }, // Links tool to UI }, async () => { const time = new Date().toISOString(); diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 9ab575911..703ab6a5a 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -1,4 +1,4 @@ -import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge"; +import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js"; @@ -77,7 +77,7 @@ export function callTool( const toolCallInfo: ToolCallInfo = { serverInfo, tool, input, resultPromise }; - const uiResourceUri = getUiResourceUri(tool); + const uiResourceUri = getToolUiResourceUri(tool); if (uiResourceUri) { toolCallInfo.appResourcePromise = getUiResource(serverInfo, uiResourceUri); } @@ -86,16 +86,6 @@ export function callTool( } -function getUiResourceUri(tool: Tool): string | undefined { - const uri = tool._meta?.[RESOURCE_URI_META_KEY]; - if (typeof uri === "string" && uri.startsWith("ui://")) { - return uri; - } else if (uri !== undefined) { - throw new Error(`Invalid UI resource URI: ${JSON.stringify(uri)}`); - } -} - - async function getUiResource(serverInfo: ServerInfo, uri: string): Promise { log.info("Reading UI resource:", uri); const resource = await serverInfo.client.readResource({ uri }); diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 24a07f2ad..8163e5e8f 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 { RESOURCE_MIME_TYPE } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -28,7 +28,7 @@ function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, + _meta: { ui: { resourceUri: RESOURCE_URI } }, }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 0c596955b..1204190b6 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 { RESOURCE_MIME_TYPE } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -28,7 +28,7 @@ function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, + _meta: { ui: { resourceUri: RESOURCE_URI } }, }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/budget-allocator-server/README.md b/examples/budget-allocator-server/README.md index f2f05e85a..b81d2e187 100644 --- a/examples/budget-allocator-server/README.md +++ b/examples/budget-allocator-server/README.md @@ -47,7 +47,7 @@ Exposes a single `get-budget-data` tool that returns: - Historical data (~120 data points) - 24 months of allocation history per category - Industry benchmarks (~60 data points) - Aggregated percentile data by company stage -The tool is linked to a UI resource via `_meta[RESOURCE_URI_META_KEY]`. +The tool is linked to a UI resource via `_meta.ui.resourceUri`. ### App (`src/mcp-app.ts`) diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index ed09e1c00..19205ea74 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -13,7 +13,7 @@ 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 } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -242,7 +242,7 @@ function createServer(): McpServer { description: "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage", inputSchema: {}, - _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + _meta: { ui: { resourceUri } }, }, async (): Promise => { const response: BudgetDataResponse = { diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index ca212ed63..1ab85783c 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -4,7 +4,7 @@ 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 } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -163,7 +163,7 @@ function createServer(): McpServer { description: "Returns cohort retention heatmap data showing customer retention over time by signup month", inputSchema: GetCohortDataInputSchema.shape, - _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + _meta: { ui: { resourceUri } }, }, async ({ metric, periodType, cohortCount, maxPeriods }) => { const data = generateCohortData( diff --git a/examples/customer-segmentation-server/README.md b/examples/customer-segmentation-server/README.md index ceec50a7d..73d3b4b48 100644 --- a/examples/customer-segmentation-server/README.md +++ b/examples/customer-segmentation-server/README.md @@ -48,7 +48,7 @@ Exposes a single `get-customer-data` tool that returns: - Segment summary with counts and colors for each group - Optional segment filter parameter -The tool is linked to a UI resource via `_meta[RESOURCE_URI_META_KEY]`. +The tool is linked to a UI resource via `_meta.ui.resourceUri`. ### App (`src/mcp-app.ts`) diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index c75378224..e4f58d142 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -7,7 +7,7 @@ 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 } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; import { generateCustomers, @@ -72,7 +72,7 @@ function createServer(): McpServer { description: "Returns customer data with segment information for visualization. Optionally filter by segment.", inputSchema: GetCustomerDataInputSchema.shape, - _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + _meta: { ui: { resourceUri } }, }, async ({ segment }): Promise => { const data = getCustomerData(segment); diff --git a/examples/scenario-modeler-server/README.md b/examples/scenario-modeler-server/README.md index ba50fbfc4..c5252a733 100644 --- a/examples/scenario-modeler-server/README.md +++ b/examples/scenario-modeler-server/README.md @@ -47,7 +47,7 @@ Exposes a single `get-scenario-data` tool that returns: - Default input values for the sliders - Optionally computes custom projections when `customInputs` are provided -The tool is linked to a UI resource via `_meta[RESOURCE_URI_META_KEY]`. +The tool is linked to a UI resource via `_meta.ui.resourceUri`. ### App (`src/`) diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index 61b705b0b..b56388093 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -7,7 +7,7 @@ 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 } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -263,7 +263,7 @@ function createServer(): McpServer { description: "Returns SaaS scenario templates and optionally computes custom projections for given inputs", inputSchema: GetScenarioDataInputSchema.shape, - _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + _meta: { ui: { resourceUri } }, }, async (args: { customInputs?: ScenarioInputs; diff --git a/examples/system-monitor-server/README.md b/examples/system-monitor-server/README.md index ca885d46a..6e343866b 100644 --- a/examples/system-monitor-server/README.md +++ b/examples/system-monitor-server/README.md @@ -46,7 +46,7 @@ Exposes a single `get-system-stats` tool that returns: - Memory usage (used/total/percentage) - System info (hostname, platform, uptime) -The tool is linked to a UI resource via `_meta[RESOURCE_URI_META_KEY]`. +The tool is linked to a UI resource via `_meta.ui.resourceUri`. ### App (`src/mcp-app.ts`) diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 30687edb5..71d5006f0 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -9,7 +9,7 @@ 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 } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; // Schemas - types are derived from these using z.infer @@ -108,9 +108,39 @@ function createServer(): McpServer { version: "1.0.0", }); - // Register the get-system-stats tool and its associated UI resource + // VISIBILITY DEMO: + // - get-system-stats: visibility ["model"] - model calls this, returns widget + // - refresh-stats: visibility ["app"] - hidden from model, app calls for polling const resourceUri = "ui://system-monitor/mcp-app.html"; + async function getStats(): Promise { + const cpuSnapshots = getCpuSnapshots(); + const cpuInfo = os.cpus()[0]; + const memory = await getMemoryStats(); + const uptimeSeconds = os.uptime(); + + const stats: SystemStats = { + cpu: { + cores: cpuSnapshots, + model: cpuInfo?.model ?? "Unknown", + count: os.cpus().length, + }, + memory, + system: { + hostname: os.hostname(), + platform: `${os.platform()} ${os.arch()}`, + arch: os.arch(), + uptime: uptimeSeconds, + uptimeFormatted: formatUptime(uptimeSeconds), + }, + timestamp: new Date().toISOString(), + }; + + return { + content: [{ type: "text", text: JSON.stringify(stats) }], + }; + } + server.registerTool( "get-system-stats", { @@ -118,35 +148,30 @@ function createServer(): McpServer { description: "Returns current system statistics including per-core CPU usage, memory, and system info.", inputSchema: {}, - _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, - }, - async (): Promise => { - const cpuSnapshots = getCpuSnapshots(); - const cpuInfo = os.cpus()[0]; - const memory = await getMemoryStats(); - const uptimeSeconds = os.uptime(); - - const stats: SystemStats = { - cpu: { - cores: cpuSnapshots, - model: cpuInfo?.model ?? "Unknown", - count: os.cpus().length, - }, - memory, - system: { - hostname: os.hostname(), - platform: `${os.platform()} ${os.arch()}`, - arch: os.arch(), - uptime: uptimeSeconds, - uptimeFormatted: formatUptime(uptimeSeconds), + _meta: { + ui: { + resourceUri, + visibility: ["model"], }, - timestamp: new Date().toISOString(), - }; + }, + }, + getStats, + ); - return { - content: [{ type: "text", text: JSON.stringify(stats) }], - }; + // App-only tool for polling - hidden from model's tool list + server.registerTool( + "refresh-stats", + { + title: "Refresh Stats", + description: "Refresh system statistics (app-only, for polling)", + inputSchema: {}, + _meta: { + ui: { + visibility: ["app"], + }, + }, }, + getStats, ); server.registerResource( diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 19e53c6aa..f857eefbb 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -260,7 +260,7 @@ const app = new App({ name: "System Monitor", version: "1.0.0" }); async function fetchStats(): Promise { try { const result = await app.callServerTool({ - name: "get-system-stats", + name: "refresh-stats", // Use app-only tool for polling arguments: {}, }); diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index 72f19b437..332015318 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -9,7 +9,7 @@ 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 } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -160,7 +160,7 @@ function createServer(): McpServer { .default(400) .describe("Height in pixels"), }, - _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + _meta: { ui: { resourceUri } }, }, async ({ code, height }) => { return { diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index 4ee56225a..4bc4bda1d 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -8,7 +8,7 @@ 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 } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -89,7 +89,7 @@ function createServer(): McpServer { .default("https://en.wikipedia.org/wiki/Model_Context_Protocol") .describe("Wikipedia page URL"), }), - _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, + _meta: { ui: { resourceUri } }, }, async ({ url }): Promise => { let title = url; diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 1166cad71..dda528f7f 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -273,7 +273,7 @@ interface Tool { > **Deprecation notice:** The flat `_meta["ui/resourceUri"]` format is deprecated. Use `_meta.ui.resourceUri` instead. The deprecated format will be removed before GA. -Example (tool visible to both model and apps): +Example (tool visible to both model and app): ```json { diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index c82f8b144..2bf54f531 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -14,7 +14,11 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import { App } from "./app"; -import { AppBridge, type McpUiHostCapabilities } from "./app-bridge"; +import { + AppBridge, + getToolUiResourceUri, + type McpUiHostCapabilities, +} from "./app-bridge"; /** Wait for pending microtasks to complete */ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); @@ -707,3 +711,107 @@ describe("App <-> AppBridge integration", () => { }); }); }); + +describe("getToolUiResourceUri", () => { + describe("new nested format (_meta.ui.resourceUri)", () => { + it("extracts resourceUri from _meta.ui.resourceUri", () => { + const tool = { + name: "test-tool", + _meta: { + ui: { resourceUri: "ui://server/app.html" }, + }, + }; + expect(getToolUiResourceUri(tool)).toBe("ui://server/app.html"); + }); + + it("extracts resourceUri when visibility is also present", () => { + const tool = { + name: "test-tool", + _meta: { + ui: { + resourceUri: "ui://server/app.html", + visibility: ["model"], + }, + }, + }; + expect(getToolUiResourceUri(tool)).toBe("ui://server/app.html"); + }); + }); + + describe("deprecated flat format (_meta['ui/resourceUri'])", () => { + it("extracts resourceUri from deprecated format", () => { + const tool = { + name: "test-tool", + _meta: { "ui/resourceUri": "ui://server/app.html" }, + }; + expect(getToolUiResourceUri(tool)).toBe("ui://server/app.html"); + }); + }); + + describe("format precedence", () => { + it("prefers new nested format over deprecated format", () => { + const tool = { + name: "test-tool", + _meta: { + ui: { resourceUri: "ui://server/new.html" }, + "ui/resourceUri": "ui://server/old.html", + }, + }; + expect(getToolUiResourceUri(tool)).toBe("ui://server/new.html"); + }); + }); + + describe("missing resourceUri", () => { + it("returns undefined when no resourceUri in empty _meta", () => { + const tool = { name: "test-tool", _meta: {} }; + expect(getToolUiResourceUri(tool)).toBeUndefined(); + }); + + it("returns undefined when _meta is missing", () => { + const tool = {} as { _meta?: Record }; + expect(getToolUiResourceUri(tool)).toBeUndefined(); + }); + + it("returns undefined for app-only tools with visibility but no resourceUri", () => { + const tool = { + name: "refresh-stats", + _meta: { + ui: { visibility: ["app"] }, + }, + }; + expect(getToolUiResourceUri(tool)).toBeUndefined(); + }); + }); + + describe("validation", () => { + it("throws for invalid URI (not starting with ui://)", () => { + const tool = { + name: "test-tool", + _meta: { ui: { resourceUri: "https://example.com" } }, + }; + expect(() => getToolUiResourceUri(tool)).toThrow( + "Invalid UI resource URI", + ); + }); + + it("throws for non-string resourceUri", () => { + const tool = { + name: "test-tool", + _meta: { ui: { resourceUri: 123 } }, + }; + expect(() => getToolUiResourceUri(tool)).toThrow( + "Invalid UI resource URI", + ); + }); + + it("throws for null resourceUri", () => { + const tool = { + name: "test-tool", + _meta: { ui: { resourceUri: null } }, + }; + expect(() => getToolUiResourceUri(tool)).toThrow( + "Invalid UI resource URI", + ); + }); + }); +}); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 6841aceeb..291cec47b 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -77,8 +77,53 @@ import { } from "./types"; export * from "./types"; export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; +import { RESOURCE_URI_META_KEY } from "./app"; export { PostMessageTransport } from "./message-transport"; +/** + * Extract UI resource URI from tool metadata. + * + * Supports both the new nested format (`_meta.ui.resourceUri`) and the + * deprecated flat format (`_meta["ui/resourceUri"]`). The new nested format + * takes precedence if both are present. + * + * @param tool - A tool object with optional `_meta` property + * @returns The UI resource URI if valid, undefined if not present + * @throws Error if resourceUri is present but invalid (not starting with "ui://") + * + * @example + * ```typescript + * // New nested format (preferred) + * const uri = getToolUiResourceUri({ + * _meta: { ui: { resourceUri: "ui://server/app.html" } } + * }); + * + * // Deprecated flat format (still supported) + * const uri = getToolUiResourceUri({ + * _meta: { "ui/resourceUri": "ui://server/app.html" } + * }); + * ``` + */ +export function getToolUiResourceUri(tool: { + _meta?: Record; +}): string | undefined { + // Try new nested format first: _meta.ui.resourceUri + const uiMeta = tool._meta?.ui as { resourceUri?: unknown } | undefined; + let uri: unknown = uiMeta?.resourceUri; + + // Fall back to deprecated flat format: _meta["ui/resourceUri"] + if (uri === undefined) { + uri = tool._meta?.[RESOURCE_URI_META_KEY]; + } + + if (typeof uri === "string" && uri.startsWith("ui://")) { + return uri; + } else if (uri !== undefined) { + throw new Error(`Invalid UI resource URI: ${JSON.stringify(uri)}`); + } + return undefined; +} + /** * Options for configuring AppBridge behavior. * diff --git a/src/generated/schema.json b/src/generated/schema.json index 42722d6c4..8d72a45dd 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -2512,7 +2512,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2559,7 +2559,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2606,7 +2606,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2680,7 +2680,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -2773,7 +2773,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -3805,7 +3805,7 @@ "type": "string" }, "visibility": { - "description": "Who can access this tool. Default: [\"model\", \"apps\"]\n- \"model\": Tool visible to and callable by the agent\n- \"apps\": Tool callable by apps from this server only", + "description": "Who can access this tool. Default: [\"model\", \"app\"]\n- \"model\": Tool visible to and callable by the agent\n- \"app\": Tool callable by the app from this server only", "type": "array", "items": { "description": "Tool visibility scope - who can access the tool.", @@ -3816,7 +3816,7 @@ }, { "type": "string", - "const": "apps" + "const": "app" } ] } @@ -3885,7 +3885,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -3932,7 +3932,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -3979,7 +3979,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -4053,7 +4053,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -4146,7 +4146,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" } }, "additionalProperties": false @@ -4193,7 +4193,7 @@ }, { "type": "string", - "const": "apps" + "const": "app" } ] } diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 65ffb6aa1..3710cf7a7 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -95,6 +95,14 @@ export type McpUiResourceMetaSchemaInferredType = z.infer< typeof generated.McpUiResourceMetaSchema >; +export type McpUiRequestDisplayModeRequestSchemaInferredType = z.infer< + typeof generated.McpUiRequestDisplayModeRequestSchema +>; + +export type McpUiRequestDisplayModeResultSchemaInferredType = z.infer< + typeof generated.McpUiRequestDisplayModeResultSchema +>; + export type McpUiToolVisibilitySchemaInferredType = z.infer< typeof generated.McpUiToolVisibilitySchema >; @@ -225,6 +233,18 @@ expectType({} as McpUiResourceCspSchemaInferredType); expectType({} as spec.McpUiResourceCsp); expectType({} as McpUiResourceMetaSchemaInferredType); expectType({} as spec.McpUiResourceMeta); +expectType( + {} as McpUiRequestDisplayModeRequestSchemaInferredType, +); +expectType( + {} as spec.McpUiRequestDisplayModeRequest, +); +expectType( + {} as McpUiRequestDisplayModeResultSchemaInferredType, +); +expectType( + {} as spec.McpUiRequestDisplayModeResult, +); expectType( {} as McpUiToolVisibilitySchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 9d5f85618..b146eed9e 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -424,11 +424,36 @@ export const McpUiResourceMetaSchema = z.object({ ), }); +/** + * @description Request to change the display mode of the UI. + * The host will respond with the actual display mode that was set, + * which may differ from the requested mode if not supported. + * @see {@link app.App.requestDisplayMode} for the method that sends this request + */ +export const McpUiRequestDisplayModeRequestSchema = z.object({ + method: z.literal("ui/request-display-mode"), + params: z.object({ + /** @description The display mode being requested. */ + mode: McpUiDisplayModeSchema.describe("The display mode being requested."), + }), +}); + +/** + * @description Result from requesting a display mode change. + * @see {@link McpUiRequestDisplayModeRequest} + */ +export const McpUiRequestDisplayModeResultSchema = z.looseObject({ + /** @description The display mode that was actually set. May differ from requested if not supported. */ + mode: McpUiDisplayModeSchema.describe( + "The display mode that was actually set. May differ from requested if not supported.", + ), +}); + /** * @description Tool visibility scope - who can access the tool. */ export const McpUiToolVisibilitySchema = z - .union([z.literal("model"), z.literal("apps")]) + .union([z.literal("model"), z.literal("app")]) .describe("Tool visibility scope - who can access the tool."); /** @@ -441,15 +466,15 @@ export const McpUiToolMetaSchema = z.object({ .optional() .describe("URI of UI resource for rendering tool results."), /** - * @description Who can access this tool. Default: ["model", "apps"] + * @description Who can access this tool. Default: ["model", "app"] * - "model": Tool visible to and callable by the agent - * - "apps": Tool callable by apps from this server only + * - "app": Tool callable by the app from this server only */ visibility: z .array(McpUiToolVisibilitySchema) .optional() .describe( - 'Who can access this tool. Default: ["model", "apps"]\n- "model": Tool visible to and callable by the agent\n- "apps": Tool callable by apps from this server only', + 'Who can access this tool. Default: ["model", "app"]\n- "model": Tool visible to and callable by the agent\n- "app": Tool callable by the app from this server only', ), }); From 34324ba2db9b0059126c1b78046cfedf34535aac Mon Sep 17 00:00:00 2001 From: Anton Pidkuiko MacBook Date: Tue, 16 Dec 2025 13:43:00 +0000 Subject: [PATCH 05/14] chore: regenerate schemas with updated dependencies --- src/generated/schema.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 8d72a45dd..22dc63c23 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -2512,7 +2512,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -2559,7 +2559,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -2606,7 +2606,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -2680,7 +2680,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -2773,7 +2773,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -3885,7 +3885,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -3932,7 +3932,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -3979,7 +3979,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -4053,7 +4053,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false @@ -4146,7 +4146,7 @@ "lastModified": { "type": "string", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-]\\d{2}:\\d{2})))$" + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "additionalProperties": false From 3d97a00ec2fd6b6dbdeb2cbc10c0994901870c8e Mon Sep 17 00:00:00 2001 From: Anton Pidkuiko MacBook Date: Tue, 16 Dec 2025 16:50:19 +0000 Subject: [PATCH 06/14] feat: export McpUiToolMeta type and add type annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export McpUiToolMeta and McpUiToolVisibility types from types.ts - Export corresponding Zod schemas (McpUiToolMetaSchema, McpUiToolVisibilitySchema) - Add `as McpUiToolMeta` type annotations to all example servers - Update docs/quickstart.md with proper typing Ensures type safety for `_meta.ui` tool metadata across the codebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/quickstart.md | 7 +++++-- examples/basic-server-react/server.ts | 4 ++-- examples/basic-server-vanillajs/server.ts | 4 ++-- examples/budget-allocator-server/server.ts | 4 ++-- examples/cohort-heatmap-server/server.ts | 4 ++-- examples/customer-segmentation-server/server.ts | 4 ++-- examples/scenario-modeler-server/server.ts | 4 ++-- examples/system-monitor-server/server.ts | 6 +++--- examples/threejs-server/server.ts | 4 ++-- examples/wiki-explorer-server/server.ts | 4 ++-- src/types.ts | 4 ++++ 11 files changed, 28 insertions(+), 21 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 68251a542..eff4c9b45 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -97,7 +97,10 @@ Create `server.ts`: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps"; +import { + RESOURCE_MIME_TYPE, + type McpUiToolMeta, +} from "@modelcontextprotocol/ext-apps"; import cors from "cors"; import express from "express"; import fs from "node:fs/promises"; @@ -119,7 +122,7 @@ server.registerTool( description: "Returns the current server time.", inputSchema: {}, outputSchema: { time: z.string() }, - _meta: { ui: { resourceUri } }, // Links tool to UI + _meta: { ui: { resourceUri } as McpUiToolMeta }, // Links tool to UI }, async () => { const time = new Date().toISOString(); diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 8163e5e8f..16b311f32 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 } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -28,7 +28,7 @@ function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { ui: { resourceUri: RESOURCE_URI } }, + _meta: { ui: { resourceUri: RESOURCE_URI } as McpUiToolMeta }, }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 1204190b6..c13a5f455 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 } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -28,7 +28,7 @@ function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, - _meta: { ui: { resourceUri: RESOURCE_URI } }, + _meta: { ui: { resourceUri: RESOURCE_URI } as McpUiToolMeta }, }, async (): Promise => { const time = new Date().toISOString(); diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index 19205ea74..03aa582cb 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -13,7 +13,7 @@ import type { import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -242,7 +242,7 @@ function createServer(): McpServer { description: "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage", inputSchema: {}, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } as McpUiToolMeta }, }, async (): Promise => { const response: BudgetDataResponse = { diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index 1ab85783c..cea3408cf 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -4,7 +4,7 @@ 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 } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -163,7 +163,7 @@ function createServer(): McpServer { description: "Returns cohort retention heatmap data showing customer retention over time by signup month", inputSchema: GetCohortDataInputSchema.shape, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } as McpUiToolMeta }, }, async ({ metric, periodType, cohortCount, maxPeriods }) => { const data = generateCohortData( diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index e4f58d142..e45699b8f 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -7,7 +7,7 @@ import type { import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; import { generateCustomers, @@ -72,7 +72,7 @@ function createServer(): McpServer { description: "Returns customer data with segment information for visualization. Optionally filter by segment.", inputSchema: GetCustomerDataInputSchema.shape, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } as McpUiToolMeta }, }, async ({ segment }): Promise => { const data = getCustomerData(segment); diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index b56388093..884842eba 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -7,7 +7,7 @@ import type { import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -263,7 +263,7 @@ function createServer(): McpServer { description: "Returns SaaS scenario templates and optionally computes custom projections for given inputs", inputSchema: GetScenarioDataInputSchema.shape, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } as McpUiToolMeta }, }, async (args: { customInputs?: ScenarioInputs; diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 71d5006f0..c709848d2 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -9,7 +9,7 @@ import os from "node:os"; import path from "node:path"; import si from "systeminformation"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; // Schemas - types are derived from these using z.infer @@ -152,7 +152,7 @@ function createServer(): McpServer { ui: { resourceUri, visibility: ["model"], - }, + } as McpUiToolMeta, }, }, getStats, @@ -168,7 +168,7 @@ function createServer(): McpServer { _meta: { ui: { visibility: ["app"], - }, + } as McpUiToolMeta, }, }, getStats, diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index 332015318..938ec82d7 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -9,7 +9,7 @@ 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 } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -160,7 +160,7 @@ function createServer(): McpServer { .default(400) .describe("Height in pixels"), }, - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } as McpUiToolMeta }, }, async ({ code, height }) => { return { diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index 4bc4bda1d..adeef6c67 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -8,7 +8,7 @@ import * as cheerio from "cheerio"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; -import { RESOURCE_MIME_TYPE } from "../../dist/src/app"; +import { RESOURCE_MIME_TYPE, type McpUiToolMeta } from "../../dist/src/app"; import { startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -89,7 +89,7 @@ function createServer(): McpServer { .default("https://en.wikipedia.org/wiki/Model_Context_Protocol") .describe("Wikipedia page URL"), }), - _meta: { ui: { resourceUri } }, + _meta: { ui: { resourceUri } as McpUiToolMeta }, }, async ({ url }): Promise => { let title = url; diff --git a/src/types.ts b/src/types.ts index b6209daeb..ba8cde111 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,8 @@ export { type McpUiResourceMeta, type McpUiRequestDisplayModeRequest, type McpUiRequestDisplayModeResult, + type McpUiToolVisibility, + type McpUiToolMeta, } from "./spec.types.js"; // Import types needed for protocol type unions (not re-exported, just used internally) @@ -95,6 +97,8 @@ export { McpUiResourceMetaSchema, McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResultSchema, + McpUiToolVisibilitySchema, + McpUiToolMetaSchema, } from "./generated/schema.js"; // Re-export SDK types used in protocol type unions From f2bbb7729f0641fcf13337b5d1b6bffddf5aad53 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 20:14:25 +0000 Subject: [PATCH 07/14] feat: add server helpers and optional connect() transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `src/server/` with convenience functions for registering MCP App tools and resources: - `registerAppTool(server, name, config, handler)` - `registerAppResource(server, name, uri, config, callback)` The `transport` parameter in `App.connect()` is now optional, defaulting to `PostMessageTransport(window.parent)`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/basic-server-react/server.ts | 6 +- examples/basic-server-vanillajs/README.md | 2 +- examples/basic-server-vanillajs/server.ts | 6 +- .../basic-server-vanillajs/src/mcp-app.ts | 4 +- examples/budget-allocator-server/server.ts | 11 +- .../budget-allocator-server/src/mcp-app.ts | 4 +- examples/cohort-heatmap-server/server.ts | 11 +- .../customer-segmentation-server/server.ts | 11 +- .../src/mcp-app.ts | 2 +- examples/qr-server/widget.html | 2 +- examples/scenario-modeler-server/server.ts | 11 +- examples/system-monitor-server/server.ts | 11 +- examples/system-monitor-server/src/mcp-app.ts | 4 +- examples/threejs-server/server.ts | 14 +- examples/wiki-explorer-server/server.ts | 11 +- examples/wiki-explorer-server/src/mcp-app.ts | 4 +- package.json | 4 + src/app.ts | 3 +- src/server/index.test.ts | 179 ++++++++++++++++++ src/server/index.ts | 148 +++++++++++++++ 20 files changed, 408 insertions(+), 40 deletions(-) create mode 100644 src/server/index.test.ts create mode 100644 src/server/index.ts diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 24a07f2ad..abb855c63 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 8d227266d..0d7cb6192 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 0c596955b..d3baad232 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 7bfa6d698..544425205 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 ed09e1c00..fd7618edd 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 723dc0607..9cddc8ad6 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 ca212ed63..9d3c9cb3b 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 c75378224..0bbbbba85 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 77e348de0..39221584d 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 e2ff4cb0a..9275ff68d 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 61b705b0b..c3824939a 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 30687edb5..b439c5ac5 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 19e53c6aa..b5751f2e1 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 72f19b437..e9208542e 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 4ee56225a..15c93c263 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 8f9026c76..2afa81fc8 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 ada7844a9..cd3085fc0 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 b8a923887..4a0780102 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 000000000..2b7b45ea1 --- /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 000000000..ef80a9d88 --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,148 @@ +/** + * Server Helpers for MCP Apps. + * + * @module server-helpers + */ + +import { + RESOURCE_URI_META_KEY as _RESOURCE_URI_META_KEY, + McpUiResourceMeta, + RESOURCE_MIME_TYPE, +} 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-export SDK types for convenience +export type { ResourceMetadata, ToolCallback, ReadResourceCallback }; + +// Re-export for convenience +export const RESOURCE_URI_META_KEY = _RESOURCE_URI_META_KEY; +export { RESOURCE_MIME_TYPE }; + +/** + * 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; + }; +} + +/** + * MCP App Resource configuration for `registerAppResource`. + */ +export interface McpUiAppResourceConfig extends ResourceMetadata { + _meta: { + ui: McpUiResourceMeta; + }; +} + +/** + * 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, + ); +} From c5fc9278ffd4fa175a488d068147ba63ef1a6c25 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 20:58:06 +0000 Subject: [PATCH 08/14] Update src/server/index.ts Co-authored-by: Jonathan Hefner --- src/server/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/index.ts b/src/server/index.ts index ef80a9d88..c76be740c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -50,6 +50,7 @@ export interface McpUiAppToolConfig extends ToolConfig { * @example "ui://weather/widget.html" */ [RESOURCE_URI_META_KEY]?: string; + [key: string]: unknown; }; } @@ -59,6 +60,7 @@ export interface McpUiAppToolConfig extends ToolConfig { export interface McpUiAppResourceConfig extends ResourceMetadata { _meta: { ui: McpUiResourceMeta; + [key: string]: unknown; }; } From 729caac638250f8d5fc50872400591bc022b3615 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 21:30:11 +0000 Subject: [PATCH 09/14] feat: update McpUiAppToolConfig to support both _meta.ui and flat formats --- src/generated/schema.json | 1 + src/generated/schema.ts | 1 - src/server/index.ts | 10 ++++++---- src/spec.types.ts | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 22dc63c23..98f3a76bd 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -3822,6 +3822,7 @@ } } }, + "required": ["resourceUri"], "additionalProperties": false }, "McpUiToolResultNotification": { diff --git a/src/generated/schema.ts b/src/generated/schema.ts index b146eed9e..ebae5d7cc 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -463,7 +463,6 @@ export const McpUiToolMetaSchema = z.object({ /** @description URI of UI resource for rendering tool results. */ resourceUri: z .string() - .optional() .describe("URI of UI resource for rendering tool results."), /** * @description Who can access this tool. Default: ["model", "app"] diff --git a/src/server/index.ts b/src/server/index.ts index c76be740c..1eb47caa1 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -7,6 +7,7 @@ import { RESOURCE_URI_META_KEY as _RESOURCE_URI_META_KEY, McpUiResourceMeta, + McpUiToolMeta, RESOURCE_MIME_TYPE, } from "../app.js"; import type { @@ -41,8 +42,10 @@ export interface ToolConfig { */ export interface McpUiAppToolConfig extends ToolConfig { _meta: { - // Soon: `ui: McpUiToolMeta;` (https://github.com/modelcontextprotocol/ext-apps/pull/131) - + [key: string]: unknown; + } & ({ + ui: McpUiToolMeta; + } | { /** * URI of the UI resource to display for this tool. * This is converted to `_meta["ui/resourceUri"]`. @@ -50,8 +53,7 @@ export interface McpUiAppToolConfig extends ToolConfig { * @example "ui://weather/widget.html" */ [RESOURCE_URI_META_KEY]?: string; - [key: string]: unknown; - }; + }); } /** diff --git a/src/spec.types.ts b/src/spec.types.ts index 436f1ecb0..7401fdeba 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -524,7 +524,7 @@ export type McpUiToolVisibility = "model" | "app"; */ export interface McpUiToolMeta { /** @description URI of UI resource for rendering tool results. */ - resourceUri?: string; + resourceUri: string; /** * @description Who can access this tool. Default: ["model", "app"] * - "model": Tool visible to and callable by the agent From df11e5e0ec12e6fa082d3e34d23dd516063ef5ea Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 21:30:40 +0000 Subject: [PATCH 10/14] refactor: simplify server helper imports --- src/server/index.ts | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 1eb47caa1..7a0dabf67 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -5,10 +5,10 @@ */ import { - RESOURCE_URI_META_KEY as _RESOURCE_URI_META_KEY, + RESOURCE_URI_META_KEY, + RESOURCE_MIME_TYPE, McpUiResourceMeta, McpUiToolMeta, - RESOURCE_MIME_TYPE, } from "../app.js"; import type { McpServer, @@ -19,13 +19,10 @@ import type { import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js"; import type { ZodRawShape } from "zod"; -// Re-export SDK types for convenience +// Re-exports for convenience +export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; export type { ResourceMetadata, ToolCallback, ReadResourceCallback }; -// Re-export for convenience -export const RESOURCE_URI_META_KEY = _RESOURCE_URI_META_KEY; -export { RESOURCE_MIME_TYPE }; - /** * Tool configuration (same as McpServer.registerTool). */ @@ -43,17 +40,20 @@ export interface ToolConfig { 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" - */ - [RESOURCE_URI_META_KEY]?: string; - }); + } & ( + | { + ui: McpUiToolMeta; + } + | { + /** + * 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; + } + ); } /** From 0cc7515771e03b9aa5c19a97170386ae31063b9c Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 21:32:13 +0000 Subject: [PATCH 11/14] docs: add deprecation notice and improve resourceUri documentation --- src/generated/schema.json | 1 - src/generated/schema.ts | 11 +++++++---- src/server/index.ts | 4 +++- src/spec.types.ts | 7 ++++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 98f3a76bd..5af29fe35 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -3801,7 +3801,6 @@ "type": "object", "properties": { "resourceUri": { - "description": "URI of UI resource for rendering tool results.", "type": "string" }, "visibility": { diff --git a/src/generated/schema.ts b/src/generated/schema.ts index ebae5d7cc..d0abe8ae2 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -460,10 +460,13 @@ export const McpUiToolVisibilitySchema = z * @description UI-related metadata for tools. */ export const McpUiToolMetaSchema = z.object({ - /** @description URI of UI resource for rendering tool results. */ - resourceUri: z - .string() - .describe("URI of UI resource for rendering tool results."), + /** + * URI of the UI resource to display for this tool. + * This is converted to `_meta["ui/resourceUri"]`. + * + * @example "ui://weather/widget.html" + */ + resourceUri: z.string(), /** * @description Who can access this tool. Default: ["model", "app"] * - "model": Tool visible to and callable by the agent diff --git a/src/server/index.ts b/src/server/index.ts index 7a0dabf67..6a48f671b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -48,8 +48,10 @@ export interface McpUiAppToolConfig extends ToolConfig { /** * 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; } diff --git a/src/spec.types.ts b/src/spec.types.ts index 7401fdeba..77596b18c 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -523,7 +523,12 @@ export type McpUiToolVisibility = "model" | "app"; * @description UI-related metadata for tools. */ export interface McpUiToolMeta { - /** @description URI of UI resource for rendering tool results. */ + /** + * URI of the UI resource to display for this tool. + * This is converted to `_meta["ui/resourceUri"]`. + * + * @example "ui://weather/widget.html" + */ resourceUri: string; /** * @description Who can access this tool. Default: ["model", "app"] From 6a22f0def32e4b3a28b3949dfbb530a0f47042a3 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 21:38:20 +0000 Subject: [PATCH 12/14] feat: add backward compat normalization in registerAppTool - If _meta.ui.resourceUri is set, also set legacy flat key - If legacy flat key is set, also set _meta.ui.resourceUri - Preserves existing visibility when merging - Does not overwrite if both formats already set --- src/server/index.test.ts | 125 +++++++++++++++++++++++++++++++++++++++ src/server/index.ts | 15 +++++ 2 files changed, 140 insertions(+) diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 2b7b45ea1..06c7f1450 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -52,6 +52,131 @@ describe("registerAppTool", () => { ).toBe("ui://test/widget.html"); expect(capturedHandler).toBe(handler); }); + + describe("backward compatibility", () => { + it("should set legacy key when _meta.ui.resourceUri is provided", () => { + let capturedConfig: Record | undefined; + + const mockServer = { + registerTool: mock( + (_name: string, config: Record, _handler: unknown) => { + capturedConfig = config; + }, + ), + }; + + registerAppTool( + mockServer as unknown as Pick, + "my-tool", + { + _meta: { + ui: { resourceUri: "ui://test/widget.html" }, + }, + }, + async () => ({ content: [{ type: "text" as const, text: "ok" }] }), + ); + + const meta = capturedConfig?._meta as Record; + // New format should be preserved + expect((meta.ui as { resourceUri: string }).resourceUri).toBe( + "ui://test/widget.html", + ); + // Legacy key should also be set + expect(meta[RESOURCE_URI_META_KEY]).toBe("ui://test/widget.html"); + }); + + it("should set _meta.ui.resourceUri when legacy key is provided", () => { + let capturedConfig: Record | undefined; + + const mockServer = { + registerTool: mock( + (_name: string, config: Record, _handler: unknown) => { + capturedConfig = config; + }, + ), + }; + + registerAppTool( + mockServer as unknown as Pick, + "my-tool", + { + _meta: { + [RESOURCE_URI_META_KEY]: "ui://test/widget.html", + }, + }, + async () => ({ content: [{ type: "text" as const, text: "ok" }] }), + ); + + const meta = capturedConfig?._meta as Record; + // Legacy key should be preserved + expect(meta[RESOURCE_URI_META_KEY]).toBe("ui://test/widget.html"); + // New format should also be set + expect((meta.ui as { resourceUri: string }).resourceUri).toBe( + "ui://test/widget.html", + ); + }); + + it("should preserve visibility when converting from legacy format", () => { + let capturedConfig: Record | undefined; + + const mockServer = { + registerTool: mock( + (_name: string, config: Record, _handler: unknown) => { + capturedConfig = config; + }, + ), + }; + + registerAppTool( + mockServer as unknown as Pick, + "my-tool", + { + _meta: { + ui: { visibility: ["app"] }, + [RESOURCE_URI_META_KEY]: "ui://test/widget.html", + }, + } as any, + async () => ({ content: [{ type: "text" as const, text: "ok" }] }), + ); + + const meta = capturedConfig?._meta as Record; + const ui = meta.ui as { resourceUri: string; visibility: string[] }; + // Should have merged resourceUri into existing ui object + expect(ui.resourceUri).toBe("ui://test/widget.html"); + expect(ui.visibility).toEqual(["app"]); + }); + + it("should not overwrite if both formats are already set", () => { + let capturedConfig: Record | undefined; + + const mockServer = { + registerTool: mock( + (_name: string, config: Record, _handler: unknown) => { + capturedConfig = config; + }, + ), + }; + + registerAppTool( + mockServer as unknown as Pick, + "my-tool", + { + _meta: { + ui: { resourceUri: "ui://new/widget.html" }, + [RESOURCE_URI_META_KEY]: "ui://old/widget.html", + }, + } as any, + async () => ({ content: [{ type: "text" as const, text: "ok" }] }), + ); + + const meta = capturedConfig?._meta as Record; + // Both should remain unchanged + expect((meta.ui as { resourceUri: string }).resourceUri).toBe( + "ui://new/widget.html", + ); + expect(meta[RESOURCE_URI_META_KEY]).toBe("ui://old/widget.html"); + }); + }); }); describe("registerAppResource", () => { diff --git a/src/server/index.ts b/src/server/index.ts index 6a48f671b..8ac3055cf 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -102,6 +102,21 @@ export function registerAppTool( config: McpUiAppToolConfig, handler: ToolCallback, ): void { + // 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; + + if (uiMeta?.resourceUri && !legacyUri) { + // New format -> also set legacy key + meta[RESOURCE_URI_META_KEY] = uiMeta.resourceUri; + } else if (legacyUri && !uiMeta?.resourceUri) { + // Legacy format -> also set new format + meta.ui = { ...uiMeta, resourceUri: legacyUri }; + } + server.registerTool(name, config, handler); } From 157849fc06da947a7d04f1fa218d77f14be08076 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 21:39:47 +0000 Subject: [PATCH 13/14] refactor: avoid mutating config arg in registerAppTool --- src/server/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 8ac3055cf..2191a72f9 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -48,7 +48,7 @@ export interface McpUiAppToolConfig extends ToolConfig { /** * 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. @@ -109,15 +109,16 @@ export function registerAppTool( 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 - meta[RESOURCE_URI_META_KEY] = uiMeta.resourceUri; + normalizedMeta = { ...meta, [RESOURCE_URI_META_KEY]: uiMeta.resourceUri }; } else if (legacyUri && !uiMeta?.resourceUri) { // Legacy format -> also set new format - meta.ui = { ...uiMeta, resourceUri: legacyUri }; + normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - server.registerTool(name, config, handler); + server.registerTool(name, { ...config, _meta: normalizedMeta }, handler); } /** From 551ca92509cbf6ace4294f53fa5a0ebcc89f132c Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 21:46:22 +0000 Subject: [PATCH 14/14] style: format with prettier --- src/server/index.test.ts | 24 ++++++++++++++++++++---- src/spec.types.ts | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 06c7f1450..d5e0a80ac 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -59,7 +59,11 @@ describe("registerAppTool", () => { const mockServer = { registerTool: mock( - (_name: string, config: Record, _handler: unknown) => { + ( + _name: string, + config: Record, + _handler: unknown, + ) => { capturedConfig = config; }, ), @@ -90,7 +94,11 @@ describe("registerAppTool", () => { const mockServer = { registerTool: mock( - (_name: string, config: Record, _handler: unknown) => { + ( + _name: string, + config: Record, + _handler: unknown, + ) => { capturedConfig = config; }, ), @@ -121,7 +129,11 @@ describe("registerAppTool", () => { const mockServer = { registerTool: mock( - (_name: string, config: Record, _handler: unknown) => { + ( + _name: string, + config: Record, + _handler: unknown, + ) => { capturedConfig = config; }, ), @@ -151,7 +163,11 @@ describe("registerAppTool", () => { const mockServer = { registerTool: mock( - (_name: string, config: Record, _handler: unknown) => { + ( + _name: string, + config: Record, + _handler: unknown, + ) => { capturedConfig = config; }, ), diff --git a/src/spec.types.ts b/src/spec.types.ts index 77596b18c..47cb638cf 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -526,7 +526,7 @@ export interface McpUiToolMeta { /** * URI of the UI resource to display for this tool. * This is converted to `_meta["ui/resourceUri"]`. - * + * * @example "ui://weather/widget.html" */ resourceUri: string;