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 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..69ec3459 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -46,6 +46,349 @@ } ] }, + "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": { + "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": ["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" + } + }, + "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..34e2ef40 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -174,6 +174,20 @@ 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."), + }) + .passthrough(); + /** * @description Notification that the sandbox proxy iframe is ready to receive content. * @internal @@ -520,6 +534,25 @@ 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 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..699c1cdf 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -195,6 +195,37 @@ 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 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; + /** + * 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