From c6b937ab89fc31d57a16ff5c920fb3e293bf7b0a Mon Sep 17 00:00:00 2001 From: netanelavr Date: Sun, 15 Mar 2026 20:16:51 +0200 Subject: [PATCH] feat(spec): add renderTiming to McpUiToolMeta for deferred View rendering Add a new `renderTiming` field to `McpUiToolMeta` that lets servers declare when a View should appear in the conversation: - "inline" (default): render as soon as the tool returns - "end-of-turn": defer rendering until the agent's turn is complete This addresses a gap in the spec where hosts have no standardized way to know whether a View should be shown immediately or after the agent finishes its turn. Tools like "Apply to Site" need deferred rendering to prevent premature user interaction while the agent is still making additional tool calls. This is orthogonal to the existing visual `displayMode` (inline/fullscreen/pip) which controls layout, not timing. Changes: - spec.types.ts: add McpUiRenderTiming type and renderTiming field - types.ts: re-export new type and schema - specification/draft/apps.mdx: document Render Timing section and design decision - generated/schema.*: auto-regenerated from types Made-with: Cursor --- specification/draft/apps.mdx | 50 ++++++++++++++++++++++++++++++++++++ src/generated/schema.json | 27 +++++++++++++++++++ src/generated/schema.test.ts | 6 +++++ src/generated/schema.ts | 17 ++++++++++++ src/spec.types.ts | 11 ++++++++ src/types.ts | 2 ++ 6 files changed, 113 insertions(+) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index e7c343b8..97e77dca 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -348,6 +348,12 @@ interface McpUiToolMeta { * - "app": Tool callable by the app from this server only */ visibility?: Array<"model" | "app">; + /** + * When the host should render the View in the conversation. Default: "inline" + * - "inline": Render the View as soon as the tool returns + * - "end-of-turn": Defer rendering until the agent's turn is complete + */ + renderTiming?: "inline" | "end-of-turn"; } interface Tool { @@ -419,6 +425,32 @@ Example (app-only tool, hidden from model): - **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 +#### Render Timing: + +Some tools produce Views that should only be shown after the agent has finished its turn — for example, an "Apply to Site" action where the user should not interact with the View while the agent is still making additional tool calls. The `renderTiming` field controls this: + +- `renderTiming` defaults to `"inline"` if omitted +- `"inline"`: Host SHOULD render the View as soon as the tool returns its result +- `"end-of-turn"`: Host SHOULD defer rendering the View until the agent's turn is complete (no more tool calls or model output expected) +- Host MAY ignore `renderTiming` and render immediately if it does not support deferred rendering +- This field is orthogonal to `displayMode` (inline/fullscreen/pip), which controls the visual layout of the View + +Example (tool with deferred rendering): + +```json +{ + "name": "apply_changes", + "description": "Apply code changes to the user's site", + "inputSchema": { "type": "object" }, + "_meta": { + "ui": { + "resourceUri": "ui://editor/apply-to-site", + "renderTiming": "end-of-turn" + } + } +} +``` + #### Benefits: - **Performance:** Host can preload templates before tool execution @@ -1754,6 +1786,24 @@ This proposal synthesizes feedback from the UI CWG and MCP-UI community, host im - **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. +#### 6. Render Timing via Tool Metadata + +**Decision:** Use `_meta.ui.renderTiming` to let servers declare when a View should appear in the conversation. + +**Rationale:** + +- The server knows best whether its View requires user interaction during or after the agent's turn +- Orthogonal to the visual `displayMode` (inline/fullscreen/pip) — timing and layout are independent concerns +- Optional field with `"inline"` default preserves backward compatibility +- Addresses a real production need: tools like "Apply to Site" should not show interactive UI while the agent is still making additional tool calls +- Simple two-value enum (`"inline"` | `"end-of-turn"`) covers the observed use cases without over-engineering + +**Alternatives considered:** + +- **Host-side only:** Let hosts decide timing without server input. Rejected because the server has the domain knowledge about whether its View needs deferred rendering. +- **Boolean `deferRendering` flag:** Simpler but less extensible if future timing modes are needed (e.g., `"on-user-action"`). +- **Reuse `displayMode`:** Rejected because `displayMode` controls visual layout (inline/fullscreen/pip), not temporal presentation. Overloading it would create confusion. + ### Backward Compatibility The proposal builds on the existing core protocol. There are no incompatibilities. diff --git a/src/generated/schema.json b/src/generated/schema.json index 522f1592..23d60916 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -4013,6 +4013,20 @@ }, "additionalProperties": {} }, + "McpUiRenderTiming": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { + "type": "string", + "const": "inline" + }, + { + "type": "string", + "const": "end-of-turn" + } + ], + "description": "When the host should render the View relative to the agent's turn." + }, "McpUiRequestDisplayModeRequest": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -5251,6 +5265,19 @@ ], "description": "Tool visibility scope - who can access the tool." } + }, + "renderTiming": { + "description": "When the host should render the View in the conversation. Default: \"inline\"\n- \"inline\": Render the View as soon as the tool returns\n- \"end-of-turn\": Defer rendering until the agent's turn is complete (no more tool calls)", + "anyOf": [ + { + "type": "string", + "const": "inline" + }, + { + "type": "string", + "const": "end-of-turn" + } + ] } }, "additionalProperties": false diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index cebad70b..86d5c257 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -119,6 +119,10 @@ export type McpUiToolVisibilitySchemaInferredType = z.infer< typeof generated.McpUiToolVisibilitySchema >; +export type McpUiRenderTimingSchemaInferredType = z.infer< + typeof generated.McpUiRenderTimingSchema +>; + export type McpUiToolMetaSchemaInferredType = z.infer< typeof generated.McpUiToolMetaSchema >; @@ -293,6 +297,8 @@ expectType( expectType( {} as spec.McpUiToolVisibility, ); +expectType({} as McpUiRenderTimingSchemaInferredType); +expectType({} as spec.McpUiRenderTiming); expectType({} as McpUiToolMetaSchemaInferredType); expectType({} as spec.McpUiToolMeta); expectType( diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 8eb12b5a..6bcc3336 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -674,6 +674,15 @@ export const McpUiToolVisibilitySchema = z .union([z.literal("model"), z.literal("app")]) .describe("Tool visibility scope - who can access the tool."); +/** + * @description When the host should render the View relative to the agent's turn. + */ +export const McpUiRenderTimingSchema = z + .union([z.literal("inline"), z.literal("end-of-turn")]) + .describe( + "When the host should render the View relative to the agent's turn.", + ); + /** * @description UI-related metadata for tools. */ @@ -699,6 +708,14 @@ export const McpUiToolMetaSchema = z.object({ .describe( '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', ), + /** + * @description When the host should render the View in the conversation. Default: "inline" + * - "inline": Render the View as soon as the tool returns + * - "end-of-turn": Defer rendering until the agent's turn is complete (no more tool calls) + */ + renderTiming: McpUiRenderTimingSchema.optional().describe( + 'When the host should render the View in the conversation. Default: "inline"\n- "inline": Render the View as soon as the tool returns\n- "end-of-turn": Defer rendering until the agent\'s turn is complete (no more tool calls)', + ), }); /** diff --git a/src/spec.types.ts b/src/spec.types.ts index 8e0e2eb0..2f8fb4d7 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -742,6 +742,11 @@ export interface McpUiRequestDisplayModeResult { */ export type McpUiToolVisibility = "model" | "app"; +/** + * @description When the host should render the View relative to the agent's turn. + */ +export type McpUiRenderTiming = "inline" | "end-of-turn"; + /** * @description UI-related metadata for tools. */ @@ -762,6 +767,12 @@ export interface McpUiToolMeta { * - "app": Tool callable by the app from this server only */ visibility?: McpUiToolVisibility[]; + /** + * @description When the host should render the View in the conversation. Default: "inline" + * - "inline": Render the View as soon as the tool returns + * - "end-of-turn": Defer rendering until the agent's turn is complete (no more tool calls) + */ + renderTiming?: McpUiRenderTiming; } /** diff --git a/src/types.ts b/src/types.ts index 739da6fa..0e36d85c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -63,6 +63,7 @@ export { type McpUiRequestDisplayModeRequest, type McpUiRequestDisplayModeResult, type McpUiToolVisibility, + type McpUiRenderTiming, type McpUiToolMeta, type McpUiClientCapabilities, } from "./spec.types.js"; @@ -129,6 +130,7 @@ export { McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResultSchema, McpUiToolVisibilitySchema, + McpUiRenderTimingSchema, McpUiToolMetaSchema, } from "./generated/schema.js";