From 83860d7c31989c6a44553e5970a5dc2eb9b0493e Mon Sep 17 00:00:00 2001 From: martinalong Date: Sun, 14 Dec 2025 18:52:55 -0800 Subject: [PATCH 1/2] Have apps request display mode instead --- specification/draft/apps.mdx | 33 +++++++++++++++++++ src/app-bridge.ts | 56 +++++++++++++++++++++++++++++++++ src/app.ts | 40 +++++++++++++++++++++++ src/generated/schema.json | 61 ++++++++++++++++++++++++++++++++++++ src/generated/schema.test.ts | 20 ++++++++++++ src/generated/schema.ts | 25 +++++++++++++++ src/spec.types.ts | 28 +++++++++++++++++ src/types.ts | 10 +++++- 8 files changed, 272 insertions(+), 1 deletion(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index f4a77c5b..d3f74958 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -537,6 +537,39 @@ Host behavior: * Host SHOULD add the message to the conversation context, preserving the specified role. * Host MAY request user consent. +`ui/request-display-mode` - Request host to change display mode + +```typescript +// Request +{ + jsonrpc: "2.0", + id: 3, + method: "ui/request-display-mode", + params: { + mode: "inline" | "fullscreen" | "pip" // Requested display mode + } +} + +// Success Response +{ + jsonrpc: "2.0", + id: 3, + result: { + mode: "inline" | "fullscreen" | "pip" // Actual display mode set + } +} +``` + +Host behavior: +* Host SHOULD check if the requested mode is in `availableDisplayModes` from host context. +* If the requested mode is available, Host SHOULD switch to that mode and return it in the response. +* If the requested mode is not available, Host SHOULD return the current display mode in the response. +* Host MAY coerce modes on certain platforms (e.g., "pip" to "fullscreen" on mobile). + +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. + #### 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-bridge.ts b/src/app-bridge.ts index bed0a75d..6841acee 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -71,6 +71,9 @@ import { McpUiSandboxProxyReadyNotification, McpUiSandboxProxyReadyNotificationSchema, McpUiSizeChangedNotificationSchema, + McpUiRequestDisplayModeRequest, + McpUiRequestDisplayModeRequestSchema, + McpUiRequestDisplayModeResult, } from "./types"; export * from "./types"; export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; @@ -222,6 +225,13 @@ export class AppBridge extends Protocol< this.onping?.(request.params, extra); return {}; }); + + // Default handler for requestDisplayMode - returns current mode from host context. + // Hosts can override this by setting bridge.onrequestdisplaymode = ... + this.setRequestHandler(McpUiRequestDisplayModeRequestSchema, (request) => { + const currentMode = this._hostContext.displayMode ?? "inline"; + return { mode: currentMode }; + }); } /** @@ -494,6 +504,52 @@ export class AppBridge extends Protocol< ); } + /** + * Register a handler for display mode change requests from the Guest UI. + * + * The Guest UI sends `ui/request-display-mode` requests when it wants to change + * its display mode (e.g., from "inline" to "fullscreen"). The handler should + * check if the requested mode is in `availableDisplayModes` from the host context, + * update the display mode if supported, and return the actual mode that was set. + * + * If the requested mode is not available, the handler should return the current + * display mode instead. + * + * @param callback - Handler that receives the requested mode and returns the actual mode set + * - params.mode - The display mode being requested ("inline" | "fullscreen" | "pip") + * - extra - Request metadata (abort signal, session info) + * - Returns: Promise with the actual mode set + * + * @example + * ```typescript + * bridge.onrequestdisplaymode = async ({ mode }, extra) => { + * const availableModes = hostContext.availableDisplayModes ?? ["inline"]; + * if (availableModes.includes(mode)) { + * setDisplayMode(mode); + * return { mode }; + * } + * // Return current mode if requested mode not available + * return { mode: currentDisplayMode }; + * }; + * ``` + * + * @see {@link McpUiRequestDisplayModeRequest} for the request type + * @see {@link McpUiRequestDisplayModeResult} for the result type + */ + set onrequestdisplaymode( + callback: ( + params: McpUiRequestDisplayModeRequest["params"], + extra: RequestHandlerExtra, + ) => Promise, + ) { + this.setRequestHandler( + McpUiRequestDisplayModeRequestSchema, + async (request, extra) => { + return callback(request.params, extra); + }, + ); + } + /** * Register a handler for logging messages from the Guest UI. * diff --git a/src/app.ts b/src/app.ts index c4970fcd..168f10bf 100644 --- a/src/app.ts +++ b/src/app.ts @@ -42,6 +42,8 @@ import { McpUiToolInputPartialNotificationSchema, McpUiToolResultNotification, McpUiToolResultNotificationSchema, + McpUiRequestDisplayModeRequest, + McpUiRequestDisplayModeResultSchema, } from "./types"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; @@ -839,6 +841,44 @@ export class App extends Protocol { ); } + /** + * Request a change to the display mode. + * + * Requests the host to change the UI container to the specified display mode + * (e.g., "inline", "fullscreen", "pip"). The host will respond with the actual + * display mode that was set, which may differ from the requested mode if + * the requested mode is not available (check `availableDisplayModes` in host context). + * + * @param params - The display mode being requested + * @param options - Request options (timeout, etc.) + * @returns Result containing the actual display mode that was set + * + * @example Request fullscreen mode + * ```typescript + * const context = app.getHostContext(); + * if (context?.availableDisplayModes?.includes("fullscreen")) { + * const result = await app.requestDisplayMode({ mode: "fullscreen" }); + * console.log("Display mode set to:", result.mode); + * } + * ``` + * + * @see {@link McpUiRequestDisplayModeRequest} for request structure + * @see {@link McpUiHostContext} for checking availableDisplayModes + */ + requestDisplayMode( + params: McpUiRequestDisplayModeRequest["params"], + options?: RequestOptions, + ) { + return this.request( + { + method: "ui/request-display-mode", + params, + }, + McpUiRequestDisplayModeResultSchema, + options, + ); + } + /** * Notify the host of UI size changes. * diff --git a/src/generated/schema.json b/src/generated/schema.json index f70b60f9..396cfdbf 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -1545,6 +1545,67 @@ }, "additionalProperties": {} }, + "McpUiRequestDisplayModeRequest": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "method": { + "type": "string", + "const": "ui/request-display-mode" + }, + "params": { + "type": "object", + "properties": { + "mode": { + "description": "The display mode being requested.", + "anyOf": [ + { + "type": "string", + "const": "inline" + }, + { + "type": "string", + "const": "fullscreen" + }, + { + "type": "string", + "const": "pip" + } + ] + } + }, + "required": ["mode"], + "additionalProperties": false + } + }, + "required": ["method", "params"], + "additionalProperties": false + }, + "McpUiRequestDisplayModeResult": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "mode": { + "description": "The display mode that was actually set. May differ from requested if not supported.", + "anyOf": [ + { + "type": "string", + "const": "inline" + }, + { + "type": "string", + "const": "fullscreen" + }, + { + "type": "string", + "const": "pip" + } + ] + } + }, + "required": ["mode"], + "additionalProperties": {} + }, "McpUiResourceCsp": { "$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 051ac8e5..6d1ce156 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -83,6 +83,14 @@ export type McpUiResourceMetaSchemaInferredType = z.infer< typeof generated.McpUiResourceMetaSchema >; +export type McpUiRequestDisplayModeRequestSchemaInferredType = z.infer< + typeof generated.McpUiRequestDisplayModeRequestSchema +>; + +export type McpUiRequestDisplayModeResultSchemaInferredType = z.infer< + typeof generated.McpUiRequestDisplayModeResultSchema +>; + export type McpUiMessageRequestSchemaInferredType = z.infer< typeof generated.McpUiMessageRequestSchema >; @@ -195,6 +203,18 @@ expectType({} as McpUiResourceCspSchemaInferredType); expectType({} as spec.McpUiResourceCsp); expectType({} as McpUiResourceMetaSchemaInferredType); expectType({} as spec.McpUiResourceMeta); +expectType( + {} as McpUiRequestDisplayModeRequestSchemaInferredType, +); +expectType( + {} as spec.McpUiRequestDisplayModeRequest, +); +expectType( + {} as McpUiRequestDisplayModeResultSchemaInferredType, +); +expectType( + {} as spec.McpUiRequestDisplayModeResult, +); expectType( {} as McpUiMessageRequestSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index baff9658..42e44422 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -307,6 +307,31 @@ export const McpUiResourceMetaSchema = z.object({ ), }); +/** + * @description Request to change the display mode of the UI. + * The host will respond with the actual display mode that was set, + * which may differ from the requested mode if not supported. + * @see {@link app.App.requestDisplayMode} for the method that sends this request + */ +export const McpUiRequestDisplayModeRequestSchema = z.object({ + method: z.literal("ui/request-display-mode"), + params: z.object({ + /** @description The display mode being requested. */ + mode: McpUiDisplayModeSchema.describe("The display mode being requested."), + }), +}); + +/** + * @description Result from requesting a display mode change. + * @see {@link McpUiRequestDisplayModeRequest} + */ +export const McpUiRequestDisplayModeResultSchema = z.looseObject({ + /** @description The display mode that was actually set. May differ from requested if not supported. */ + mode: McpUiDisplayModeSchema.describe( + "The display mode that was actually set. May differ from requested if not supported.", + ), +}); + /** * @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 01cc4713..2a781a0f 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -371,3 +371,31 @@ export interface McpUiResourceMeta { /** @description Visual boundary preference - true if UI prefers a visible border. */ prefersBorder?: boolean; } + +/** + * @description Request to change the display mode of the UI. + * The host will respond with the actual display mode that was set, + * which may differ from the requested mode if not supported. + * @see {@link app.App.requestDisplayMode} for the method that sends this request + */ +export interface McpUiRequestDisplayModeRequest { + method: "ui/request-display-mode"; + params: { + /** @description The display mode being requested. */ + mode: McpUiDisplayMode; + }; +} + +/** + * @description Result from requesting a display mode change. + * @see {@link McpUiRequestDisplayModeRequest} + */ +export interface McpUiRequestDisplayModeResult { + /** @description The display mode that was actually set. May differ from requested if not supported. */ + mode: McpUiDisplayMode; + /** + * 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 8544b27e..92ce88de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,6 +36,8 @@ export { type McpUiInitializedNotification, type McpUiResourceCsp, type McpUiResourceMeta, + type McpUiRequestDisplayModeRequest, + type McpUiRequestDisplayModeResult, } from "./spec.types.js"; // Import types needed for protocol type unions (not re-exported, just used internally) @@ -44,6 +46,7 @@ import type { McpUiOpenLinkRequest, McpUiMessageRequest, McpUiResourceTeardownRequest, + McpUiRequestDisplayModeRequest, McpUiHostContextChangedNotification, McpUiToolInputNotification, McpUiToolInputPartialNotification, @@ -57,6 +60,7 @@ import type { McpUiOpenLinkResult, McpUiMessageResult, McpUiResourceTeardownResult, + McpUiRequestDisplayModeResult, } from "./spec.types.js"; // Re-export all schemas from generated/schema.ts (already PascalCase) @@ -85,6 +89,8 @@ export { McpUiInitializedNotificationSchema, McpUiResourceCspSchema, McpUiResourceMetaSchema, + McpUiRequestDisplayModeRequestSchema, + McpUiRequestDisplayModeResultSchema, } from "./generated/schema.js"; // Re-export SDK types used in protocol type unions @@ -113,7 +119,7 @@ import { * All request types in the MCP Apps protocol. * * Includes: - * - MCP UI requests (initialize, open-link, message, resource-teardown) + * - MCP UI requests (initialize, open-link, message, resource-teardown, request-display-mode) * - MCP server requests forwarded from the app (tools/call, resources/*, prompts/list) * - Protocol requests (ping) */ @@ -122,6 +128,7 @@ export type AppRequest = | McpUiOpenLinkRequest | McpUiMessageRequest | McpUiResourceTeardownRequest + | McpUiRequestDisplayModeRequest | CallToolRequest | ListToolsRequest | ListResourcesRequest @@ -168,6 +175,7 @@ export type AppResult = | McpUiOpenLinkResult | McpUiMessageResult | McpUiResourceTeardownResult + | McpUiRequestDisplayModeResult | CallToolResult | ListToolsResult | ListResourcesResult From 46d38f44a7b12df329c791d1ef6b3be93461eb4b Mon Sep 17 00:00:00 2001 From: martinalong Date: Mon, 15 Dec 2025 16:05:06 -0800 Subject: [PATCH 2/2] Update spec wording --- specification/draft/apps.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index d3f74958..c9d77a9d 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -561,8 +561,8 @@ Host behavior: ``` Host behavior: -* Host SHOULD check if the requested mode is in `availableDisplayModes` from host context. -* If the requested mode is available, Host SHOULD switch to that mode and return it in the response. +* App MUST check if the requested mode is in `availableDisplayModes` from host context. +* It is up to the host whether it switches to the requested mode, but the host MUST return the resulting mode (whether updated or not) in the response. * If the requested mode is not available, Host SHOULD return the current display mode in the response. * Host MAY coerce modes on certain platforms (e.g., "pip" to "fullscreen" on mobile).