diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 1be80e76..c71d512f 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -1214,6 +1214,33 @@ Host SHOULD wait for a response before tearing down the resource (to prevent dat The View SHOULD send this notification when rendered content body size changes (e.g. using ResizeObserver API to report up to date size). +#### Notifications (View → Host) + +`ui/notifications/request-close` - View requests host to close it + +```typescript +{ + jsonrpc: "2.0", + method: "ui/notifications/request-close", + params: {} +} +``` + +The View MAY send this notification to request that the host close it. This enables View-initiated close flows (e.g., user clicks a "Done" button in the View). + +**Host behavior:** +- Host decides whether to proceed with the close +- If approved, Host MUST send `ui/resource-teardown` to allow the View to perform cleanup +- Host MUST wait for the View's teardown response before unmounting the iframe +- Host MAY deny or defer the close request (e.g., if there are unsaved changes elsewhere) + +**View behavior:** +- View SHOULD NOT perform cleanup before sending this notification +- View SHOULD handle cleanup in its `ui/resource-teardown` handler +- This ensures the View has a single cleanup procedure regardless of whether the close was initiated by the View or the Host + +This piggybacks on the existing `ui/resource-teardown` mechanism, ensuring Views only need one shutdown procedure. + `ui/notifications/host-context-changed` - Host context has changed ```typescript @@ -1368,17 +1395,23 @@ sequenceDiagram #### 4. Cleanup +Cleanup can be initiated by either the Host or the View. In both cases, the Host sends `ui/resource-teardown` to allow the View to perform cleanup before unmounting. + ```mermaid sequenceDiagram participant H as Host participant UI as View (iframe) + opt View-initiated close + UI ->> H: ui/notifications/request-close + note right of H: Host decides whether to close + end H ->> UI: ui/resource-teardown UI --> UI: Graceful termination UI -->> H: ui/resource-teardown response H -x H: Tear down iframe and listeners ``` -Note: Cleanup may be triggered at any point in the lifecycle following View initialization. +Note: Cleanup may be triggered at any point in the lifecycle following View initialization. If the View sends `ui/notifications/request-close`, the Host MAY deny or defer the request. #### Key Differences from Pre-SEP MCP-UI: diff --git a/src/app-bridge.ts b/src/app-bridge.ts index d8f7dee1..1869a370 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -71,6 +71,8 @@ import { McpUiOpenLinkResult, McpUiResourceTeardownRequest, McpUiResourceTeardownResultSchema, + McpUiRequestCloseNotification, + McpUiRequestCloseNotificationSchema, McpUiSandboxProxyReadyNotification, McpUiSandboxProxyReadyNotificationSchema, McpUiSizeChangedNotificationSchema, @@ -585,6 +587,41 @@ export class AppBridge extends Protocol< ); } + /** + * Register a handler for app-initiated close request notifications from the view. + * + * The view sends `ui/request-close` when it wants the host to close it. + * If the host decides to proceed with the close, it should send + * `ui/resource-teardown` (via {@link teardownResource `teardownResource`}) to allow + * the view to perform cleanup, then unmount the iframe after the view responds. + * + * @param callback - Handler that receives close request params + * - params - Empty object (reserved for future use) + * + * @example + * ```typescript + * bridge.onrequestclose = async (params) => { + * console.log("App requested close"); + * // Initiate teardown to allow the app to clean up + * // Alternatively, the callback can early return to prevent teardown + * await bridge.teardownResource({}); + * // Now safe to unmount the iframe + * iframe.remove(); + * }; + * ``` + * + * @see {@link McpUiRequestCloseNotification `McpUiRequestCloseNotification`} for the notification type + * @see {@link teardownResource `teardownResource`} for initiating teardown + */ + set onrequestclose( + callback: (params: McpUiRequestCloseNotification["params"]) => void, + ) { + this.setNotificationHandler( + McpUiRequestCloseNotificationSchema, + (request) => callback(request.params), + ); + } + /** * Register a handler for display mode change requests from the view. * diff --git a/src/app.ts b/src/app.ts index f813f44c..11b1df33 100644 --- a/src/app.ts +++ b/src/app.ts @@ -36,6 +36,7 @@ import { McpUiResourceTeardownRequest, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResult, + McpUiRequestCloseNotification, McpUiSizeChangedNotification, McpUiToolCancelledNotification, McpUiToolCancelledNotificationSchema, @@ -924,6 +925,50 @@ export class App extends Protocol { /** @deprecated Use {@link openLink `openLink`} instead */ sendOpenLink: App["openLink"] = this.openLink; + /** + * Request the host to close this app. + * + * Apps call this method to request that the host close them. The host decides + * whether to proceed with the close - if approved, the host will send + * `ui/resource-teardown` to allow the app to perform cleanup before being + * unmounted. This piggybacks on the existing teardown mechanism, ensuring + * the app only needs a single shutdown procedure (via {@link onteardown `onteardown`}) + * regardless of whether the close was initiated by the app or the host. + * + * This is a fire-and-forget notification - no response is expected. + * If the host approves the close, the app will receive a `ui/resource-teardown` + * request via the {@link onteardown `onteardown`} handler to perform cleanup. + * + * @param params - Empty params object (reserved for future use) + * @returns Promise that resolves when the notification is sent + * + * @example App-initiated close after user action + * ```typescript + * // User clicks "Done" button in the app + * async function handleDoneClick() { + * // Request the host to close the app + * await app.requestClose(); + * // If host approves, onteardown handler will be called for cleanup + * } + * + * // Set up teardown handler (called for both app-initiated and host-initiated close) + * app.onteardown = async () => { + * await saveState(); + * closeConnections(); + * return {}; + * }; + * ``` + * + * @see {@link McpUiRequestCloseNotification `McpUiRequestCloseNotification`} for notification structure + * @see {@link onteardown `onteardown`} for the cleanup handler + */ + requestClose(params: McpUiRequestCloseNotification["params"] = {}) { + return this.notification({ + method: "ui/notifications/request-close", + params, + }); + } + /** * Request a change to the display mode. * diff --git a/src/generated/schema.json b/src/generated/schema.json index bdbb058e..ab8c41ae 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -3808,6 +3808,23 @@ }, "additionalProperties": {} }, + "McpUiRequestCloseNotification": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "method": { + "type": "string", + "const": "ui/notifications/request-close" + }, + "params": { + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "required": ["method"], + "additionalProperties": false + }, "McpUiRequestDisplayModeRequest": { "$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 95ec2f21..bc2c5a78 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -87,6 +87,10 @@ export type McpUiSupportedContentBlockModalitiesSchemaInferredType = z.infer< typeof generated.McpUiSupportedContentBlockModalitiesSchema >; +export type McpUiRequestCloseNotificationSchemaInferredType = z.infer< + typeof generated.McpUiRequestCloseNotificationSchema +>; + export type McpUiHostCapabilitiesSchemaInferredType = z.infer< typeof generated.McpUiHostCapabilitiesSchema >; @@ -241,6 +245,12 @@ expectType( expectType( {} as spec.McpUiSupportedContentBlockModalities, ); +expectType( + {} as McpUiRequestCloseNotificationSchemaInferredType, +); +expectType( + {} as spec.McpUiRequestCloseNotification, +); expectType( {} as McpUiHostCapabilitiesSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 4719ada3..89d9393f 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -385,6 +385,21 @@ export const McpUiSupportedContentBlockModalitiesSchema = z.object({ .describe("Host supports structured content."), }); +/** + * @description Notification for app-initiated close request (View -> Host). + * Views send this to request that the host close them. The host decides + * whether to proceed with the close - if approved, the host will send + * `ui/resource-teardown` to allow the view to perform cleanup before being + * unmounted. This piggybacks on the existing teardown mechanism, ensuring + * the view only needs a single shutdown procedure regardless of whether + * the close was initiated by the view or the host. + * @see {@link app.App.requestClose} for the app method that sends this + */ +export const McpUiRequestCloseNotificationSchema = z.object({ + method: z.literal("ui/notifications/request-close"), + params: z.object({}).optional(), +}); + /** * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities diff --git a/src/spec.types.ts b/src/spec.types.ts index 7cea3daf..011fd3fe 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -441,6 +441,21 @@ export interface McpUiSupportedContentBlockModalities { structuredContent?: {}; } +/** + * @description Notification for app-initiated close request (View -> Host). + * Views send this to request that the host close them. The host decides + * whether to proceed with the close - if approved, the host will send + * `ui/resource-teardown` to allow the view to perform cleanup before being + * unmounted. This piggybacks on the existing teardown mechanism, ensuring + * the view only needs a single shutdown procedure regardless of whether + * the close was initiated by the view or the host. + * @see {@link app.App.requestClose} for the app method that sends this + */ +export interface McpUiRequestCloseNotification { + method: "ui/notifications/request-close"; + params?: {}; +} + /** * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities diff --git a/src/types.ts b/src/types.ts index a4770fda..b3d562ac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,6 +49,7 @@ export { type McpUiHostContextChangedNotification, type McpUiResourceTeardownRequest, type McpUiResourceTeardownResult, + type McpUiRequestCloseNotification, type McpUiHostCapabilities, type McpUiAppCapabilities, type McpUiInitializeRequest, @@ -81,6 +82,7 @@ import type { McpUiInitializedNotification, McpUiSizeChangedNotification, McpUiSandboxProxyReadyNotification, + McpUiRequestCloseNotification, McpUiInitializeResult, McpUiOpenLinkResult, McpUiMessageResult, @@ -111,6 +113,7 @@ export { McpUiHostContextChangedNotificationSchema, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResultSchema, + McpUiRequestCloseNotificationSchema, McpUiHostCapabilitiesSchema, McpUiAppCapabilitiesSchema, McpUiInitializeRequestSchema, @@ -181,7 +184,7 @@ export type AppRequest = * - Sandbox resource ready * * App to host: - * - Initialized, size-changed, sandbox-proxy-ready + * - Initialized, size-changed, sandbox-proxy-ready, request-close * - Logging messages */ export type AppNotification = @@ -199,6 +202,7 @@ export type AppNotification = | McpUiInitializedNotification | McpUiSizeChangedNotification | McpUiSandboxProxyReadyNotification + | McpUiRequestCloseNotification | LoggingMessageNotification; /**