diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index f2523f25e..1d2aa4d3d 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -340,9 +340,13 @@ Example: Tools are associated with UI resources through the `_meta.ui` field: ```typescript +type McpUiToolPreload = "optional" | "disabled"; + interface McpUiToolMeta { /** URI of UI resource for rendering tool results */ resourceUri?: string; + /** Whether the Host may preload the UI resource. Default: "optional" */ + preload?: McpUiToolPreload; /** * Who can access this tool. Default: ["model", "app"] * - "model": Tool visible to and callable by the agent @@ -402,6 +406,22 @@ Example (app-only tool, hidden from model): } ``` +Example (defer loading the View until the tool result is available): + +```json +{ + "name": "search_orders", + "description": "Search for matching orders", + "inputSchema": { "type": "object" }, + "_meta": { + "ui": { + "resourceUri": "ui://orders/search-results", + "preload": "disabled" + } + } +} +``` + #### Behavior: - If `ui.resourceUri` is present and host supports MCP Apps, host renders tool results using the specified UI resource @@ -411,6 +431,43 @@ Example (app-only tool, hidden from model): - 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` +#### Preloading: + +Hosts that recognize the `preload` field apply the following behavior: + +- `preload` defaults to `"optional"` if omitted +- `"optional"`: Host MAY fetch the UI resource and initialize or display its View before tool execution completes +- `"disabled"`: Host MUST wait for the tool result before fetching the UI resource for that invocation or initializing or displaying its View +- `"disabled"` does not require a Host to evict a UI resource that is already cached from another invocation +- After receiving a result without `_meta["ui/close"]: true`, Host MAY load and display the View normally +- Hosts that do not recognize `preload` MAY ignore it and preserve their existing behavior + +#### Result-driven View closure: + +A Server MAY indicate that the View declared for a tool is not needed for a particular invocation by setting `_meta["ui/close"]` on its `CallToolResult`: + +```json +{ + "content": [ + { + "type": "text", + "text": "Found one matching order." + } + ], + "_meta": { + "ui/close": true + } +} +``` + +Hosts that recognize the `ui/close` signal apply the following behavior: + +- The signal applies only to the invocation that produced the result +- If its View has not been initialized or displayed, Host MUST suppress it +- If its View has been initialized or displayed, Host MUST initiate graceful teardown using `ui/resource-teardown` and SHOULD wait for a response before unmounting it +- The signal has no effect when it is omitted, set to `false`, or no View is associated with the invocation +- Hosts that do not recognize `ui/close` MAY ignore it; Servers SHOULD provide meaningful non-UI content for those Hosts + #### Visibility: - `visibility` defaults to `["model", "app"]` if omitted diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index d327c95fe..88ec034b2 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -19,9 +19,12 @@ import { LATEST_PROTOCOL_VERSION } from "./types"; import { AppBridge, buildAllowAttribute, + getToolUiPreload, getToolUiResourceUri, isToolVisibilityModelOnly, isToolVisibilityAppOnly, + shouldCloseToolUi, + UI_CLOSE_META_KEY, type McpUiHostCapabilities, } from "./app-bridge"; @@ -2470,6 +2473,49 @@ describe("getToolUiResourceUri", () => { }); }); +describe("getToolUiPreload", () => { + it("defaults to optional when preload is omitted", () => { + expect(getToolUiPreload({})).toBe("optional"); + expect(getToolUiPreload({ _meta: { ui: {} } })).toBe("optional"); + }); + + it("returns an explicitly declared mode", () => { + expect(getToolUiPreload({ _meta: { ui: { preload: "optional" } } })).toBe( + "optional", + ); + expect(getToolUiPreload({ _meta: { ui: { preload: "disabled" } } })).toBe( + "disabled", + ); + }); + + it("ignores an unrecognized mode for forward compatibility", () => { + expect(getToolUiPreload({ _meta: { ui: { preload: "required" } } })).toBe( + "optional", + ); + }); +}); + +describe("shouldCloseToolUi", () => { + it("returns true for the ui/close signal", () => { + expect(shouldCloseToolUi({ _meta: { [UI_CLOSE_META_KEY]: true } })).toBe( + true, + ); + }); + + it("returns false when the signal is absent or false", () => { + expect(shouldCloseToolUi({})).toBe(false); + expect(shouldCloseToolUi({ _meta: { [UI_CLOSE_META_KEY]: false } })).toBe( + false, + ); + }); + + it("ignores non-boolean truthy values", () => { + expect(shouldCloseToolUi({ _meta: { [UI_CLOSE_META_KEY]: "true" } })).toBe( + false, + ); + }); +}); + describe("isToolVisibilityModelOnly", () => { describe("returns true", () => { it("when visibility is exactly ['model']", () => { diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 23383c40f..cd5725060 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -91,11 +91,17 @@ import { McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResult, McpUiResourcePermissions, + McpUiToolPreload, McpUiToolMeta, + McpUiToolResultMeta, } from "./types"; export * from "./types"; -export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; -import { RESOURCE_URI_META_KEY } from "./app"; +export { + RESOURCE_URI_META_KEY, + RESOURCE_MIME_TYPE, + UI_CLOSE_META_KEY, +} from "./app"; +import { RESOURCE_URI_META_KEY, UI_CLOSE_META_KEY } from "./app"; export { PostMessageTransport } from "./message-transport"; /** @@ -140,6 +146,33 @@ export function getToolUiResourceUri(tool: Partial): string | undefined { return undefined; } +/** + * Get a tool's UI preload mode. + * + * @param tool - Tool object with optional UI metadata + * @returns The declared preload mode, or `"optional"` when omitted or unrecognized + */ +export function getToolUiPreload(tool: Partial): McpUiToolPreload { + const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined; + return uiMeta?.preload === "disabled" ? "disabled" : "optional"; +} + +/** + * Check whether a tool result requests closure of its associated View. + * + * The signal is scoped to the invocation that produced the result and is only + * active when `_meta["ui/close"]` is the literal boolean `true`. + * + * @param result - MCP tool execution result + * @returns True when the associated View should be suppressed or closed + */ +export function shouldCloseToolUi( + result: Pick, +): boolean { + const meta = result._meta as McpUiToolResultMeta | undefined; + return meta?.[UI_CLOSE_META_KEY] === true; +} + /** * Check if a tool is visible to the model only. * diff --git a/src/app.ts b/src/app.ts index adfad5c77..12c696ed7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -148,6 +148,17 @@ export { */ export const RESOURCE_URI_META_KEY = "ui/resourceUri"; +/** + * Metadata key for suppressing or closing the View associated with a tool + * invocation. + * + * A server sets this key to `true` in `CallToolResult._meta` when the result can + * be presented without the tool's declared UI resource. Hosts should suppress + * a pending View or gracefully tear down an initialized View for that specific + * invocation. + */ +export const UI_CLOSE_META_KEY = "ui/close"; + /** * MIME type for MCP UI resources. * diff --git a/src/generated/schema.json b/src/generated/schema.json index 6b66482c4..90e2a2448 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -5320,6 +5320,19 @@ "resourceUri": { "type": "string" }, + "preload": { + "description": "Whether the host may preload the UI resource. Default: \"optional\"\n- \"optional\": Host may preload or defer loading the UI resource\n- \"disabled\": Host waits for the tool result before loading the UI resource", + "anyOf": [ + { + "type": "string", + "const": "optional" + }, + { + "type": "string", + "const": "disabled" + } + ] + }, "visibility": { "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", @@ -5346,6 +5359,31 @@ }, "additionalProperties": false }, + "McpUiToolPreload": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { + "type": "string", + "const": "optional" + }, + { + "type": "string", + "const": "disabled" + } + ], + "description": "Controls whether the host may preload a tool's UI resource." + }, + "McpUiToolResultMeta": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "ui/close": { + "description": "Whether the host should close or suppress the View associated\nwith this tool invocation. Default: false", + "type": "boolean" + } + }, + "additionalProperties": false + }, "McpUiToolResultNotification": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 57d989fd0..cd01fc56a 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -123,10 +123,18 @@ export type McpUiToolVisibilitySchemaInferredType = z.infer< typeof generated.McpUiToolVisibilitySchema >; +export type McpUiToolPreloadSchemaInferredType = z.infer< + typeof generated.McpUiToolPreloadSchema +>; + export type McpUiToolMetaSchemaInferredType = z.infer< typeof generated.McpUiToolMetaSchema >; +export type McpUiToolResultMetaSchemaInferredType = z.infer< + typeof generated.McpUiToolResultMetaSchema +>; + export type McpUiClientCapabilitiesSchemaInferredType = z.infer< typeof generated.McpUiClientCapabilitiesSchema >; @@ -303,8 +311,16 @@ expectType( expectType( {} as spec.McpUiToolVisibility, ); +expectType({} as McpUiToolPreloadSchemaInferredType); +expectType({} as spec.McpUiToolPreload); expectType({} as McpUiToolMetaSchemaInferredType); expectType({} as spec.McpUiToolMeta); +expectType( + {} as McpUiToolResultMetaSchemaInferredType, +); +expectType( + {} as spec.McpUiToolResultMeta, +); expectType( {} as McpUiClientCapabilitiesSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index a8bc1b6f5..ec664c193 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -715,6 +715,13 @@ export const McpUiToolVisibilitySchema = z .union([z.literal("model"), z.literal("app")]) .describe("Tool visibility scope - who can access the tool."); +/** + * @description Controls whether the host may preload a tool's UI resource. + */ +export const McpUiToolPreloadSchema = z + .union([z.literal("optional"), z.literal("disabled")]) + .describe("Controls whether the host may preload a tool's UI resource."); + /** * @description UI-related metadata for tools. */ @@ -728,6 +735,14 @@ export const McpUiToolMetaSchema = z.object({ * ``` */ resourceUri: z.string().optional(), + /** + * @description Whether the host may preload the UI resource. Default: "optional" + * - "optional": Host may preload or defer loading the UI resource + * - "disabled": Host waits for the tool result before loading the UI resource + */ + preload: McpUiToolPreloadSchema.optional().describe( + 'Whether the host may preload the UI resource. Default: "optional"\n- "optional": Host may preload or defer loading the UI resource\n- "disabled": Host waits for the tool result before loading the UI resource', + ), /** * @description Who can access this tool. Default: ["model", "app"] * - "model": Tool visible to and callable by the agent @@ -752,6 +767,22 @@ export const McpUiToolMetaSchema = z.object({ permissions: z.never().optional(), }); +/** + * @description MCP Apps metadata for a tool execution result. + */ +export const McpUiToolResultMetaSchema = z.object({ + /** + * @description Whether the host should close or suppress the View associated + * with this tool invocation. Default: false + */ + "ui/close": z + .boolean() + .optional() + .describe( + "Whether the host should close or suppress the View associated\nwith this tool invocation. Default: false", + ), +}); + /** * @description MCP Apps capability settings advertised by clients to servers. * diff --git a/src/server/index.ts b/src/server/index.ts index c90514acd..137df7cf6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -34,6 +34,7 @@ import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE, + UI_CLOSE_META_KEY, McpUiResourceCsp, McpUiResourceMeta, McpUiToolMeta, @@ -60,7 +61,7 @@ import type { } from "@modelcontextprotocol/sdk/types.js"; // Re-exports for convenience -export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; +export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE, UI_CLOSE_META_KEY }; export type { ResourceMetadata, ToolCallback }; /** diff --git a/src/spec.types.ts b/src/spec.types.ts index bedc1f5c6..52b8129f0 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -763,6 +763,11 @@ export interface McpUiRequestDisplayModeResult { */ export type McpUiToolVisibility = "model" | "app"; +/** + * @description Controls whether the host may preload a tool's UI resource. + */ +export type McpUiToolPreload = "optional" | "disabled"; + /** * @description UI-related metadata for tools. */ @@ -776,6 +781,12 @@ export interface McpUiToolMeta { * ``` */ resourceUri?: string; + /** + * @description Whether the host may preload the UI resource. Default: "optional" + * - "optional": Host may preload or defer loading the UI resource + * - "disabled": Host waits for the tool result before loading the UI resource + */ + preload?: McpUiToolPreload; /** * @description Who can access this tool. Default: ["model", "app"] * - "model": Tool visible to and callable by the agent @@ -795,6 +806,17 @@ export interface McpUiToolMeta { permissions?: never; } +/** + * @description MCP Apps metadata for a tool execution result. + */ +export interface McpUiToolResultMeta { + /** + * @description Whether the host should close or suppress the View associated + * with this tool invocation. Default: false + */ + "ui/close"?: boolean; +} + /** * Method string constants for MCP Apps protocol messages. * diff --git a/src/types.ts b/src/types.ts index 7fc6b7188..2dbca2488 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,8 +64,10 @@ export { type McpUiResourceMeta, type McpUiRequestDisplayModeRequest, type McpUiRequestDisplayModeResult, + type McpUiToolPreload, type McpUiToolVisibility, type McpUiToolMeta, + type McpUiToolResultMeta, type McpUiClientCapabilities, } from "./spec.types.js"; @@ -132,8 +134,10 @@ export { McpUiResourceMetaSchema, McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResultSchema, + McpUiToolPreloadSchema, McpUiToolVisibilitySchema, McpUiToolMetaSchema, + McpUiToolResultMetaSchema, } from "./generated/schema.js"; // Re-export SDK types used in protocol type unions