From 1b67a0d12f44f907777cae8c612a65dbe4e1326e Mon Sep 17 00:00:00 2001 From: martinalong Date: Tue, 23 Dec 2025 12:00:34 -0800 Subject: [PATCH 1/4] Send follow up message --- src/app.ts | 44 +++++ src/generated/schema.json | 352 +++++++++++++++++++++++++++++++++++ src/generated/schema.test.ts | 20 ++ src/generated/schema.ts | 44 +++++ src/spec.types.ts | 35 ++++ src/types.ts | 10 +- 6 files changed, 504 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 31885302..6311ee44 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,6 +20,8 @@ import { PostMessageTransport } from "./message-transport"; import { LATEST_PROTOCOL_VERSION, McpUiAppCapabilities, + McpUiFollowUpMessageRequest, + McpUiFollowUpMessageResultSchema, McpUiHostCapabilities, McpUiHostContext, McpUiHostContextChangedNotification, @@ -783,6 +785,48 @@ export class App extends Protocol { ); } + /** + * Send a follow-up message to the host's chat. + * + * Use this to continue the conversation based on user interaction with the app. + * For example, when a user clicks on a data point, the app can send a follow-up + * message asking for more details about that item. + * + * @param params - Message role and content + * @param options - Request options (timeout, etc.) + * @returns Result indicating success or error + * + * @throws {Error} If the host rejects the request + * @throws {Error} If the request times out or the connection is lost + * + * @example Send a follow-up question based on user interaction + * ```typescript + * try { + * await app.sendFollowUpMessage({ + * role: "user", + * content: [{ type: "text", text: "Tell me more about this item" }] + * }); + * } catch (error) { + * console.error("Failed to send message:", error); + * } + * ``` + * + * @see {@link McpUiFollowUpMessageRequest} for request structure + */ + sendFollowUpMessage( + params: McpUiFollowUpMessageRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { + method: "ui/follow-up-message", + params, + }, + McpUiFollowUpMessageResultSchema, + options, + ); + } + /** * Send log messages to the host for debugging and telemetry. * diff --git a/src/generated/schema.json b/src/generated/schema.json index e8359865..e646092e 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -46,6 +46,358 @@ } ] }, + "McpUiFollowUpMessageRequest": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "method": { + "type": "string", + "const": "ui/follow-up-message" + }, + "params": { + "type": "object", + "properties": { + "role": { + "description": "Message role, currently only \"user\" is supported.", + "type": "string", + "const": "user" + }, + "content": { + "description": "Message content blocks (text, image, etc.).", + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "text": { + "type": "string" + }, + "annotations": { + "type": "object", + "properties": { + "audience": { + "type": "array", + "items": { + "type": "string", + "enum": ["user", "assistant"] + } + }, + "priority": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "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)))$" + } + }, + "additionalProperties": false + }, + "_meta": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["type", "text"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "annotations": { + "type": "object", + "properties": { + "audience": { + "type": "array", + "items": { + "type": "string", + "enum": ["user", "assistant"] + } + }, + "priority": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "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)))$" + } + }, + "additionalProperties": false + }, + "_meta": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["type", "data", "mimeType"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "annotations": { + "type": "object", + "properties": { + "audience": { + "type": "array", + "items": { + "type": "string", + "enum": ["user", "assistant"] + } + }, + "priority": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "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)))$" + } + }, + "additionalProperties": false + }, + "_meta": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["type", "data", "mimeType"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "icons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "sizes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["src"], + "additionalProperties": false + } + }, + "uri": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "annotations": { + "type": "object", + "properties": { + "audience": { + "type": "array", + "items": { + "type": "string", + "enum": ["user", "assistant"] + } + }, + "priority": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "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)))$" + } + }, + "additionalProperties": false + }, + "_meta": { + "type": "object", + "properties": {}, + "additionalProperties": {} + }, + "type": { + "type": "string", + "const": "resource_link" + } + }, + "required": ["name", "uri", "type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource" + }, + "resource": { + "anyOf": [ + { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "_meta": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "text": { + "type": "string" + } + }, + "required": ["uri", "text"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "_meta": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "blob": { + "type": "string" + } + }, + "required": ["uri", "blob"], + "additionalProperties": false + } + ] + }, + "annotations": { + "type": "object", + "properties": { + "audience": { + "type": "array", + "items": { + "type": "string", + "enum": ["user", "assistant"] + } + }, + "priority": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "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)))$" + } + }, + "additionalProperties": false + }, + "_meta": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["type", "resource"], + "additionalProperties": false + } + ] + } + } + }, + "required": ["role", "content"], + "additionalProperties": false + } + }, + "required": ["method", "params"], + "additionalProperties": false + }, + "McpUiFollowUpMessageResult": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "isError": { + "description": "True if the host rejected or failed to send the message.", + "type": "boolean" + }, + "errorMessage": { + "description": "Error message explaining why the request failed. Only present when isError is true.", + "type": "string" + } + }, + "additionalProperties": {} + }, "McpUiHostCapabilities": { "$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..411db39d 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -39,6 +39,10 @@ export type McpUiMessageResultSchemaInferredType = z.infer< typeof generated.McpUiMessageResultSchema >; +export type McpUiFollowUpMessageResultSchemaInferredType = z.infer< + typeof generated.McpUiFollowUpMessageResultSchema +>; + export type McpUiSandboxProxyReadyNotificationSchemaInferredType = z.infer< typeof generated.McpUiSandboxProxyReadyNotificationSchema >; @@ -119,6 +123,10 @@ export type McpUiMessageRequestSchemaInferredType = z.infer< typeof generated.McpUiMessageRequestSchema >; +export type McpUiFollowUpMessageRequestSchemaInferredType = z.infer< + typeof generated.McpUiFollowUpMessageRequestSchema +>; + export type McpUiToolResultNotificationSchemaInferredType = z.infer< typeof generated.McpUiToolResultNotificationSchema >; @@ -165,6 +173,12 @@ expectType( ); expectType({} as McpUiMessageResultSchemaInferredType); expectType({} as spec.McpUiMessageResult); +expectType( + {} as McpUiFollowUpMessageResultSchemaInferredType, +); +expectType( + {} as spec.McpUiFollowUpMessageResult, +); expectType( {} as McpUiSandboxProxyReadyNotificationSchemaInferredType, ); @@ -265,6 +279,12 @@ expectType( expectType( {} as spec.McpUiMessageRequest, ); +expectType( + {} as McpUiFollowUpMessageRequestSchemaInferredType, +); +expectType( + {} as spec.McpUiFollowUpMessageRequest, +); expectType( {} as McpUiToolResultNotificationSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 9e6120a5..62005e91 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -174,6 +174,27 @@ export const McpUiMessageResultSchema = z }) .passthrough(); +/** + * @description Result from sending a follow-up message. + * @see {@link McpUiFollowUpMessageRequest} + */ +export const McpUiFollowUpMessageResultSchema = z + .object({ + /** @description True if the host rejected or failed to send the message. */ + isError: z + .boolean() + .optional() + .describe("True if the host rejected or failed to send the message."), + /** @description Error message explaining why the request failed. Only present when isError is true. */ + errorMessage: z + .string() + .optional() + .describe( + "Error message explaining why the request failed. Only present when isError is true.", + ), + }) + .passthrough(); + /** * @description Notification that the sandbox proxy iframe is ready to receive content. * @internal @@ -520,6 +541,29 @@ export const McpUiMessageRequestSchema = z.object({ }), }); +/** + * @description Request to send a follow-up message to the host's chat. + * + * Use this to continue the conversation based on user interaction with the app. + * For example, when a user clicks on a data point, the app can send a follow-up + * message asking for more details about that item. + * + * @see {@link app.App.sendFollowUpMessage} for the method that sends this request + */ +export const McpUiFollowUpMessageRequestSchema = z.object({ + method: z.literal("ui/follow-up-message"), + params: z.object({ + /** @description Message role, currently only "user" is supported. */ + role: z + .literal("user") + .describe('Message role, currently only "user" is supported.'), + /** @description Message content blocks (text, image, etc.). */ + content: z + .array(ContentBlockSchema) + .describe("Message content blocks (text, image, etc.)."), + }), +}); + /** * @description Notification containing tool execution result (Host -> Guest UI). */ diff --git a/src/spec.types.ts b/src/spec.types.ts index f97a0a6f..59d60ce5 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -195,6 +195,41 @@ export interface McpUiMessageResult { [key: string]: unknown; } +/** + * @description Request to send a follow-up message to the host's chat. + * + * Use this to continue the conversation based on user interaction with the app. + * For example, when a user clicks on a data point, the app can send a follow-up + * message asking for more details about that item. + * + * @see {@link app.App.sendFollowUpMessage} for the method that sends this request + */ +export interface McpUiFollowUpMessageRequest { + method: "ui/follow-up-message"; + params: { + /** @description Message role, currently only "user" is supported. */ + role: "user"; + /** @description Message content blocks (text, image, etc.). */ + content: ContentBlock[]; + }; +} + +/** + * @description Result from sending a follow-up message. + * @see {@link McpUiFollowUpMessageRequest} + */ +export interface McpUiFollowUpMessageResult { + /** @description True if the host rejected or failed to send the message. */ + isError?: boolean; + /** @description Error message explaining why the request failed. Only present when isError is true. */ + errorMessage?: string; + /** + * Index signature required for MCP SDK `Protocol` class compatibility. + * Note: The schema intentionally omits this to enforce strict validation. + */ + [key: string]: unknown; +} + /** * @description Notification that the sandbox proxy iframe is ready to receive content. * @internal diff --git a/src/types.ts b/src/types.ts index d710ad35..897e9cd9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,6 +22,8 @@ export { type McpUiOpenLinkResult, type McpUiMessageRequest, type McpUiMessageResult, + type McpUiFollowUpMessageRequest, + type McpUiFollowUpMessageResult, type McpUiSandboxProxyReadyNotification, type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, @@ -51,6 +53,7 @@ import type { McpUiInitializeRequest, McpUiOpenLinkRequest, McpUiMessageRequest, + McpUiFollowUpMessageRequest, McpUiResourceTeardownRequest, McpUiRequestDisplayModeRequest, McpUiHostContextChangedNotification, @@ -65,6 +68,7 @@ import type { McpUiInitializeResult, McpUiOpenLinkResult, McpUiMessageResult, + McpUiFollowUpMessageResult, McpUiResourceTeardownResult, McpUiRequestDisplayModeResult, } from "./spec.types.js"; @@ -79,6 +83,8 @@ export { McpUiOpenLinkResultSchema, McpUiMessageRequestSchema, McpUiMessageResultSchema, + McpUiFollowUpMessageRequestSchema, + McpUiFollowUpMessageResultSchema, McpUiSandboxProxyReadyNotificationSchema, McpUiSandboxResourceReadyNotificationSchema, McpUiSizeChangedNotificationSchema, @@ -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, follow-up-message, resource-teardown, request-display-mode) * - MCP server requests forwarded from the app (tools/call, resources/*, prompts/list) * - Protocol requests (ping) */ @@ -137,6 +143,7 @@ export type AppRequest = | McpUiInitializeRequest | McpUiOpenLinkRequest | McpUiMessageRequest + | McpUiFollowUpMessageRequest | McpUiResourceTeardownRequest | McpUiRequestDisplayModeRequest | CallToolRequest @@ -184,6 +191,7 @@ export type AppResult = | McpUiInitializeResult | McpUiOpenLinkResult | McpUiMessageResult + | McpUiFollowUpMessageResult | McpUiResourceTeardownResult | McpUiRequestDisplayModeResult | CallToolResult From 155e1fe9fd4763d481b62d86ec3a52ab919d9914 Mon Sep 17 00:00:00 2001 From: martinalong Date: Tue, 23 Dec 2025 12:38:43 -0800 Subject: [PATCH 2/4] Remove 'role' field --- src/generated/schema.json | 7 +------ src/generated/schema.ts | 4 ---- src/spec.types.ts | 2 -- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index e646092e..90ecde99 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -57,11 +57,6 @@ "params": { "type": "object", "properties": { - "role": { - "description": "Message role, currently only \"user\" is supported.", - "type": "string", - "const": "user" - }, "content": { "description": "Message content blocks (text, image, etc.).", "type": "array", @@ -376,7 +371,7 @@ } } }, - "required": ["role", "content"], + "required": ["content"], "additionalProperties": false } }, diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 62005e91..0bd1ff34 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -553,10 +553,6 @@ export const McpUiMessageRequestSchema = z.object({ export const McpUiFollowUpMessageRequestSchema = z.object({ method: z.literal("ui/follow-up-message"), params: z.object({ - /** @description Message role, currently only "user" is supported. */ - role: z - .literal("user") - .describe('Message role, currently only "user" is supported.'), /** @description Message content blocks (text, image, etc.). */ content: z .array(ContentBlockSchema) diff --git a/src/spec.types.ts b/src/spec.types.ts index 59d60ce5..b730c550 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -207,8 +207,6 @@ export interface McpUiMessageResult { export interface McpUiFollowUpMessageRequest { method: "ui/follow-up-message"; params: { - /** @description Message role, currently only "user" is supported. */ - role: "user"; /** @description Message content blocks (text, image, etc.). */ content: ContentBlock[]; }; From d752ae6d0006baedf4d1834d9b7697e7bcafd3d0 Mon Sep 17 00:00:00 2001 From: martinalong Date: Tue, 23 Dec 2025 12:41:09 -0800 Subject: [PATCH 3/4] Remove 'errorMessage' --- src/generated/schema.json | 4 ---- src/generated/schema.ts | 7 ------- src/spec.types.ts | 2 -- 3 files changed, 13 deletions(-) diff --git a/src/generated/schema.json b/src/generated/schema.json index 90ecde99..69ec3459 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -385,10 +385,6 @@ "isError": { "description": "True if the host rejected or failed to send the message.", "type": "boolean" - }, - "errorMessage": { - "description": "Error message explaining why the request failed. Only present when isError is true.", - "type": "string" } }, "additionalProperties": {} diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 0bd1ff34..34e2ef40 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -185,13 +185,6 @@ export const McpUiFollowUpMessageResultSchema = z .boolean() .optional() .describe("True if the host rejected or failed to send the message."), - /** @description Error message explaining why the request failed. Only present when isError is true. */ - errorMessage: z - .string() - .optional() - .describe( - "Error message explaining why the request failed. Only present when isError is true.", - ), }) .passthrough(); diff --git a/src/spec.types.ts b/src/spec.types.ts index b730c550..699c1cdf 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -219,8 +219,6 @@ export interface McpUiFollowUpMessageRequest { export interface McpUiFollowUpMessageResult { /** @description True if the host rejected or failed to send the message. */ isError?: boolean; - /** @description Error message explaining why the request failed. Only present when isError is true. */ - errorMessage?: string; /** * Index signature required for MCP SDK `Protocol` class compatibility. * Note: The schema intentionally omits this to enforce strict validation. From 74ea3b2bd1ba41285b65edc96f5d1017ceb2d2b2 Mon Sep 17 00:00:00 2001 From: martinalong Date: Tue, 23 Dec 2025 12:48:59 -0800 Subject: [PATCH 4/4] Add documentation to spec --- specification/draft/apps.mdx | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 6e33f813..c81e4f79 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -767,6 +767,43 @@ Host behavior: * Host SHOULD add the message to the conversation context, preserving the specified role. * Host MAY request user consent. +`ui/follow-up-message` - Send a follow-up message to continue the conversation + +```typescript +// Request +{ + jsonrpc: "2.0", + id: 3, + method: "ui/follow-up-message", + params: { + content: ContentBlock[] // Message content blocks (text, image, etc.) + } +} + +// Success Response +{ + jsonrpc: "2.0", + id: 3, + result: {} // Empty result on success +} + +// Error Response (if denied or failed) +{ + jsonrpc: "2.0", + id: 3, + result: { + isError: true + } +} +``` + +Use this to continue the conversation based on user interaction with the app or prompt a follow up app. For example, when a user clicks on a data point, the app can send a follow-up message asking for an analysis of that data point, or to display a new app with more details about that data point. + +Host behavior: +* Host SHOULD add the message to the conversation context as a user message. +* Host MAY request user consent before sending. +* Host MAY allow user to review and edit the message before sending. + `ui/request-display-mode` - Request host to change display mode ```typescript