From 67ce38c3ce66b75376e3a3dfe0f11d9317406013 Mon Sep 17 00:00:00 2001 From: martinalong Date: Tue, 23 Dec 2025 14:09:42 -0800 Subject: [PATCH 1/2] Set widget state --- specification/draft/apps.mdx | 52 ++++++++++++++++++++++++++++++++++++ src/app.ts | 49 +++++++++++++++++++++++++++++++++ src/generated/schema.json | 42 +++++++++++++++++++++++++++++ src/generated/schema.test.ts | 20 ++++++++++++++ src/generated/schema.ts | 42 +++++++++++++++++++++++++++++ src/spec.types.ts | 32 ++++++++++++++++++++++ src/types.ts | 10 ++++++- 7 files changed, 246 insertions(+), 1 deletion(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 6e33f813..e0f74aef 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -800,6 +800,58 @@ Guest UI behavior: * Guest UI SHOULD check `availableDisplayModes` in host context before requesting a mode change. * Guest UI MUST handle the response mode differing from the requested mode. +`ui/set-widget-state` - Persist widget state for inclusion in future prompts + +```typescript +// Request +{ + jsonrpc: "2.0", + id: 4, + method: "ui/set-widget-state", + params: { + toolId: string, // Unique identifier for this widget/tool instance + toolName: string, // Human-readable name of the tool (shown in prompts) + content: string // JSON string containing the widget's current state + } +} + +// Success Response +{ + jsonrpc: "2.0", + id: 4, + result: {} // Empty result on success +} + +// Error Response (if failed) +{ + jsonrpc: "2.0", + id: 4, + error: { + code: -32000, // Implementation-defined error + message: "Failed to store widget state" + } +} +``` + +This request allows apps to persist state that the model can see in subsequent messages. The state is stored per-conversation and sent with each completion request, enabling the model to be aware of the current widget configuration and user selections. + +Host behavior: +* Host MUST store the widget state associated with the current conversation. +* Host MUST include all stored widget states in subsequent completion requests to the model. +* Host SHOULD treat widget state content as untrusted third-party data and wrap it appropriately when presenting to the model. +* Host MAY implement size limits on widget state content. + +Guest UI behavior: +* Guest UI SHOULD call this method whenever its state changes meaningfully in a way that the model should be aware of. +* Guest UI SHOULD use a consistent `toolId` for the same widget instance. +* Guest UI SHOULD serialize state as a JSON string in the `content` field. +* Guest UI SHOULD include relevant context such as selected items, current view configuration, and user choices. + +Example use cases: +* A chart widget reports which data point the user has selected +* A form widget reports current field values before submission +* A data explorer reports active filters and sort order + #### Notifications (Host → UI) `ui/notifications/tool-input` - Host MUST send this notification with the complete tool arguments after the Guest UI's initialize request completes. diff --git a/src/app.ts b/src/app.ts index 31885302..40519684 100644 --- a/src/app.ts +++ b/src/app.ts @@ -45,6 +45,8 @@ import { McpUiToolResultNotificationSchema, McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeResultSchema, + McpUiSetWidgetStateRequest, + McpUiSetWidgetStateResultSchema, } from "./types"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; @@ -886,6 +888,53 @@ export class App extends Protocol { ); } + /** + * Set widget state that will be included in future prompts. + * + * This allows apps to persist state that Claude can see in subsequent messages. + * The state is treated as untrusted third-party content and will be wrapped + * in a document block with explicit trust metadata on the backend. + * + * Call this method whenever your widget's state changes meaningfully in a way + * that Claude should be aware of. The state is stored per-conversation and + * sent with each completion request. + * + * @param params - Widget state parameters + * @param params.toolId - Unique identifier for this widget/tool instance + * @param params.toolName - Human-readable name of the tool (shown in prompts) + * @param params.content - JSON string containing the widget's current state + * @param options - Request options (timeout, etc.) + * @returns Result indicating success or error + * + * @example Persist chart configuration + * ```typescript + * await app.setWidgetState({ + * toolId: "customer-segmentation-chart", + * toolName: "Customer Segmentation", + * content: JSON.stringify({ + * xAxis: "revenue", + * yAxis: "engagement", + * hiddenSegments: ["inactive"] + * }) + * }); + * ``` + * + * @see {@link McpUiSetWidgetStateRequest} for request structure + */ + setWidgetState( + params: McpUiSetWidgetStateRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { + method: "ui/set-widget-state", + params, + }, + McpUiSetWidgetStateResultSchema, + options, + ); + } + /** * Notify the host of UI size changes. * diff --git a/src/generated/schema.json b/src/generated/schema.json index e8359865..edc27c1e 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -3124,6 +3124,48 @@ "required": ["method", "params"], "additionalProperties": false }, + "McpUiSetWidgetStateRequest": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "method": { + "type": "string", + "const": "ui/set-widget-state" + }, + "params": { + "type": "object", + "properties": { + "toolId": { + "description": "Unique identifier for this widget/tool instance. Typically the app_id or tool name.", + "type": "string" + }, + "toolName": { + "description": "Human-readable name of the tool (shown in prompts).", + "type": "string" + }, + "content": { + "description": "JSON string containing the widget's current state. This will be included in future completion requests.", + "type": "string" + } + }, + "required": ["toolId", "toolName", "content"], + "additionalProperties": false + } + }, + "required": ["method", "params"], + "additionalProperties": false + }, + "McpUiSetWidgetStateResult": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "isError": { + "description": "True if the host failed to store the widget state.", + "type": "boolean" + } + }, + "additionalProperties": {} + }, "McpUiSizeChangedNotification": { "$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 e9a57981..6c2b1659 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -115,6 +115,14 @@ export type McpUiToolMetaSchemaInferredType = z.infer< typeof generated.McpUiToolMetaSchema >; +export type McpUiSetWidgetStateRequestSchemaInferredType = z.infer< + typeof generated.McpUiSetWidgetStateRequestSchema +>; + +export type McpUiSetWidgetStateResultSchemaInferredType = z.infer< + typeof generated.McpUiSetWidgetStateResultSchema +>; + export type McpUiMessageRequestSchemaInferredType = z.infer< typeof generated.McpUiMessageRequestSchema >; @@ -259,6 +267,18 @@ expectType( ); expectType({} as McpUiToolMetaSchemaInferredType); expectType({} as spec.McpUiToolMeta); +expectType( + {} as McpUiSetWidgetStateRequestSchemaInferredType, +); +expectType( + {} as spec.McpUiSetWidgetStateRequest, +); +expectType( + {} as McpUiSetWidgetStateResultSchemaInferredType, +); +expectType( + {} as spec.McpUiSetWidgetStateResult, +); expectType( {} as McpUiMessageRequestSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 9e6120a5..6becb774 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -502,6 +502,48 @@ export const McpUiToolMetaSchema = z.object({ ), }); +/** + * @description Request to set widget state that will be included in future prompts. + * The state is treated as untrusted third-party content and will be wrapped + * in a document block with explicit trust metadata. + * @see {@link app.App.setWidgetState} for the method that sends this request + */ +export const McpUiSetWidgetStateRequestSchema = z.object({ + method: z.literal("ui/set-widget-state"), + params: z.object({ + /** @description Unique identifier for this widget/tool instance. Typically the app_id or tool name. */ + toolId: z + .string() + .describe( + "Unique identifier for this widget/tool instance. Typically the app_id or tool name.", + ), + /** @description Human-readable name of the tool (shown in prompts). */ + toolName: z + .string() + .describe("Human-readable name of the tool (shown in prompts)."), + /** @description JSON string containing the widget's current state. This will be included in future completion requests. */ + content: z + .string() + .describe( + "JSON string containing the widget's current state. This will be included in future completion requests.", + ), + }), +}); + +/** + * @description Result from setting widget state. + * @see {@link McpUiSetWidgetStateRequest} + */ +export const McpUiSetWidgetStateResultSchema = z + .object({ + /** @description True if the host failed to store the widget state. */ + isError: z + .boolean() + .optional() + .describe("True if the host failed to store the widget state."), + }) + .passthrough(); + /** * @description Request to send a message to the host's chat interface. * @see {@link app.App.sendMessage} for the method that sends this request diff --git a/src/spec.types.ts b/src/spec.types.ts index f97a0a6f..d8ec18e2 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -548,3 +548,35 @@ export interface McpUiToolMeta { */ visibility?: McpUiToolVisibility[]; } + +/** + * @description Request to set widget state that will be included in future prompts. + * The state is treated as untrusted third-party content and will be wrapped + * in a document block with explicit trust metadata. + * @see {@link app.App.setWidgetState} for the method that sends this request + */ +export interface McpUiSetWidgetStateRequest { + method: "ui/set-widget-state"; + params: { + /** @description Unique identifier for this widget/tool instance. Typically the app_id or tool name. */ + toolId: string; + /** @description Human-readable name of the tool (shown in prompts). */ + toolName: string; + /** @description JSON string containing the widget's current state. This will be included in future completion requests. */ + content: string; + }; +} + +/** + * @description Result from setting widget state. + * @see {@link McpUiSetWidgetStateRequest} + */ +export interface McpUiSetWidgetStateResult { + /** @description True if the host failed to store the widget state. */ + isError?: boolean; + /** + * Index signature required for MCP SDK `Protocol` class compatibility. + * Note: The schema intentionally omits this to enforce strict validation. + */ + [key: string]: unknown; +} diff --git a/src/types.ts b/src/types.ts index d710ad35..2e35e836 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,6 +44,8 @@ export { type McpUiRequestDisplayModeResult, type McpUiToolVisibility, type McpUiToolMeta, + type McpUiSetWidgetStateRequest, + type McpUiSetWidgetStateResult, } from "./spec.types.js"; // Import types needed for protocol type unions (not re-exported, just used internally) @@ -53,6 +55,7 @@ import type { McpUiMessageRequest, McpUiResourceTeardownRequest, McpUiRequestDisplayModeRequest, + McpUiSetWidgetStateRequest, McpUiHostContextChangedNotification, McpUiToolInputNotification, McpUiToolInputPartialNotification, @@ -67,6 +70,7 @@ import type { McpUiMessageResult, McpUiResourceTeardownResult, McpUiRequestDisplayModeResult, + McpUiSetWidgetStateResult, } from "./spec.types.js"; // Re-export all schemas from generated/schema.ts (already PascalCase) @@ -101,6 +105,8 @@ export { McpUiRequestDisplayModeResultSchema, McpUiToolVisibilitySchema, McpUiToolMetaSchema, + McpUiSetWidgetStateRequestSchema, + McpUiSetWidgetStateResultSchema, } from "./generated/schema.js"; // Re-export SDK types used in protocol type unions @@ -129,7 +135,7 @@ import { * All request types in the MCP Apps protocol. * * Includes: - * - MCP UI requests (initialize, open-link, message, resource-teardown, request-display-mode) + * - MCP UI requests (initialize, open-link, message, resource-teardown, request-display-mode, set-widget-state) * - MCP server requests forwarded from the app (tools/call, resources/*, prompts/list) * - Protocol requests (ping) */ @@ -139,6 +145,7 @@ export type AppRequest = | McpUiMessageRequest | McpUiResourceTeardownRequest | McpUiRequestDisplayModeRequest + | McpUiSetWidgetStateRequest | CallToolRequest | ListToolsRequest | ListResourcesRequest @@ -186,6 +193,7 @@ export type AppResult = | McpUiMessageResult | McpUiResourceTeardownResult | McpUiRequestDisplayModeResult + | McpUiSetWidgetStateResult | CallToolResult | ListToolsResult | ListResourcesResult From fb5cf6aa65224379b7ee2b215270702a01c7363c Mon Sep 17 00:00:00 2001 From: martinalong Date: Tue, 23 Dec 2025 14:30:28 -0800 Subject: [PATCH 2/2] Update spec --- specification/draft/apps.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index e0f74aef..62002de9 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -835,6 +835,8 @@ Guest UI behavior: This request allows apps to persist state that the model can see in subsequent messages. The state is stored per-conversation and sent with each completion request, enabling the model to be aware of the current widget configuration and user selections. +**Important:** Widget state is ephemeral and not persisted across browser sessions or page reloads. Apps SHOULD send their initial state after receiving the `ui/notifications/tool-input` notification to ensure the model has context from the start of each session. + Host behavior: * Host MUST store the widget state associated with the current conversation. * Host MUST include all stored widget states in subsequent completion requests to the model.