diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index e7c343b8..6c810d63 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -1285,9 +1285,11 @@ Host MUST send this notification if the tool execution was cancelled, for any re } ``` -Host MUST send this notification before tearing down the UI resource, for any reason, including user action, resource re-allocation, etc. The Host MAY specify the reason. +Host MUST send this notification before tearing down the UI resource, for any reason, including user action, view-initiated teardown, resource re-allocation, etc. The Host MAY specify the reason. Host SHOULD wait for a response before tearing down the resource (to prevent data loss). +#### Notifications (View → Host) + `ui/notifications/size-changed` - View's size changed ```typescript @@ -1303,6 +1305,22 @@ 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). +`ui/notifications/request-teardown` - View requests host to tear it down + +```typescript +{ + jsonrpc: "2.0", + method: "ui/notifications/request-teardown", + params: {} +} +``` + +The View MAY send this notification to request that the host tear it down. This enables View-initiated teardown flows (e.g., user clicks a "Done" button in the View). + +**Host behavior:** +- Host MAY defer or ignore the teardown request. +- If the Host accepts the request, it MUST follow the graceful termination process by sending `ui/resource-teardown` to the View. The Host SHOULD wait for a response before tearing down the resource (to prevent data loss). + `ui/notifications/host-context-changed` - Host context has changed ```typescript @@ -1455,19 +1473,24 @@ sequenceDiagram end ``` -#### 4. Cleanup +#### 4. Teardown + +The Host can tear down Views. Views may request teardown by sending `ui/notifications/request-teardown` to the Host. In any case, the Host MUST send `ui/resource-teardown` to allow the View to terminate gracefully. ```mermaid sequenceDiagram participant H as Host participant UI as View (iframe) + opt View-initiated teardown + UI ->> H: ui/notifications/request-teardown + 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: Teardown may be triggered at any point in the lifecycle following View initialization. #### Key Differences from Pre-SEP MCP-UI: diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index adbe62cd..c1f4612c 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -520,6 +520,31 @@ describe("App <-> AppBridge integration", () => { }), ).rejects.toThrow("Context update failed"); }); + + it("app.requestTeardown allows host to initiate teardown flow", async () => { + const events: string[] = []; + + bridge.onrequestteardown = async () => { + events.push("teardown-requested"); + await bridge.teardownResource({}); + events.push("teardown-complete"); + }; + + app.onteardown = async () => { + events.push("persist-unsaved-state"); + return {}; + }; + + await app.connect(appTransport); + await app.requestTeardown(); + await flush(); + + expect(events).toEqual([ + "teardown-requested", + "persist-unsaved-state", + "teardown-complete", + ]); + }); }); describe("App -> Host requests", () => { diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 014420e8..d409fc06 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -74,6 +74,8 @@ import { McpUiDownloadFileResult, McpUiResourceTeardownRequest, McpUiResourceTeardownResultSchema, + McpUiRequestTeardownNotification, + McpUiRequestTeardownNotificationSchema, McpUiSandboxProxyReadyNotification, McpUiSandboxProxyReadyNotificationSchema, McpUiSizeChangedNotificationSchema, @@ -673,6 +675,41 @@ export class AppBridge extends Protocol< ); } + /** + * Register a handler for app-initiated teardown request notifications from the view. + * + * The view sends `ui/notifications/request-teardown` when it wants the host to tear it down. + * If the host decides to proceed, it should send + * `ui/resource-teardown` (via {@link teardownResource `teardownResource`}) to allow + * the view to perform gracefull termination, then unmount the iframe after the view responds. + * + * @param callback - Handler that receives teardown request params + * - params - Empty object (reserved for future use) + * + * @example + * ```typescript + * bridge.onrequestteardown = async (params) => { + * console.log("App requested teardown"); + * // Initiate teardown to allow the app to persist unsaved state + * // Alternatively, the callback can early return to prevent teardown + * await bridge.teardownResource({}); + * // Now safe to unmount the iframe + * iframe.remove(); + * }; + * ``` + * + * @see {@link McpUiRequestTeardownNotification `McpUiRequestTeardownNotification`} for the notification type + * @see {@link teardownResource `teardownResource`} for initiating teardown + */ + set onrequestteardown( + callback: (params: McpUiRequestTeardownNotification["params"]) => void, + ) { + this.setNotificationHandler( + McpUiRequestTeardownNotificationSchema, + (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 92db4810..3f2c9e43 100644 --- a/src/app.ts +++ b/src/app.ts @@ -44,6 +44,7 @@ import { McpUiResourceTeardownRequest, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResult, + McpUiRequestTeardownNotification, McpUiSizeChangedNotification, McpUiToolCancelledNotification, McpUiToolCancelledNotificationSchema, @@ -177,7 +178,7 @@ type RequestHandlerExtra = Parameters< * 1. **Create**: Instantiate App with info and capabilities * 2. **Connect**: Call `connect()` to establish transport and perform handshake * 3. **Interactive**: Send requests, receive notifications, call tools - * 4. **Cleanup**: Host sends teardown request before unmounting + * 4. **Teardown**: Host sends teardown request before unmounting * * ## Inherited Methods * @@ -1134,6 +1135,50 @@ export class App extends Protocol { ); } + /** + * Request the host to tear down this app. + * + * Apps call this method to request that the host tear them down. The host + * decides whether to proceed - if approved, the host will send + * `ui/resource-teardown` to allow the app to perform gracefull termination 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 teardown was initiated by the app or the host. + * + * This is a fire-and-forget notification - no response is expected. + * If the host approves, the app will receive a `ui/resource-teardown` + * request via the {@link onteardown `onteardown`} handler to persist unsaved state. + * + * @param params - Empty params object (reserved for future use) + * @returns Promise that resolves when the notification is sent + * + * @example App-initiated teardown after user action + * ```typescript + * // User clicks "Done" button in the app + * async function handleDoneClick() { + * // Request the host to tear down the app + * await app.requestTeardown(); + * // If host approves, onteardown handler will be called for termination + * } + * + * // Set up teardown handler (called for both app-initiated and host-initiated teardown) + * app.onteardown = async () => { + * await saveState(); + * closeConnections(); + * return {}; + * }; + * ``` + * + * @see {@link McpUiRequestTeardownNotification `McpUiRequestTeardownNotification`} for notification structure + * @see {@link onteardown `onteardown`} for the graceful termination handler + */ + requestTeardown(params: McpUiRequestTeardownNotification["params"] = {}) { + return this.notification({ + method: "ui/notifications/request-teardown", + params, + }); + } + /** * Request a change to the display mode. * diff --git a/src/generated/schema.json b/src/generated/schema.json index 522f1592..eb2a71f2 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -4074,6 +4074,23 @@ "required": ["mode"], "additionalProperties": {} }, + "McpUiRequestTeardownNotification": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "method": { + "type": "string", + "const": "ui/notifications/request-teardown" + }, + "params": { + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "required": ["method"], + "additionalProperties": false + }, "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 cebad70b..57d989fd 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -91,6 +91,10 @@ export type McpUiSupportedContentBlockModalitiesSchemaInferredType = z.infer< typeof generated.McpUiSupportedContentBlockModalitiesSchema >; +export type McpUiRequestTeardownNotificationSchemaInferredType = z.infer< + typeof generated.McpUiRequestTeardownNotificationSchema +>; + export type McpUiHostCapabilitiesSchemaInferredType = z.infer< typeof generated.McpUiHostCapabilitiesSchema >; @@ -255,6 +259,12 @@ expectType( expectType( {} as spec.McpUiSupportedContentBlockModalities, ); +expectType( + {} as McpUiRequestTeardownNotificationSchemaInferredType, +); +expectType( + {} as spec.McpUiRequestTeardownNotification, +); expectType( {} as McpUiHostCapabilitiesSchemaInferredType, ); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 8eb12b5a..aed1f965 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -478,6 +478,19 @@ export const McpUiSupportedContentBlockModalitiesSchema = z.object({ .describe("Host supports structured content."), }); +/** + * @description Notification for app-initiated teardown request (View -> Host). + * Views send this to request that the host tear them down. The host decides + * whether to proceed - if approved, the host will send + * `ui/resource-teardown` to allow the view to perform cleanup before being + * unmounted. + * @see {@link app.App.requestTeardown} for the app method that sends this + */ +export const McpUiRequestTeardownNotificationSchema = z.object({ + method: z.literal("ui/notifications/request-teardown"), + 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 8e0e2eb0..9f83fa97 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -474,6 +474,19 @@ export interface McpUiSupportedContentBlockModalities { structuredContent?: {}; } +/** + * @description Notification for app-initiated teardown request (View -> Host). + * Views send this to request that the host tear them down. The host decides + * whether to proceed - if approved, the host will send + * `ui/resource-teardown` to allow the view to perform cleanup before being + * unmounted. + * @see {@link app.App.requestTeardown} for the app method that sends this + */ +export interface McpUiRequestTeardownNotification { + method: "ui/notifications/request-teardown"; + params?: {}; +} + /** * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities @@ -800,6 +813,8 @@ export const TOOL_CANCELLED_METHOD: McpUiToolCancelledNotification["method"] = "ui/notifications/tool-cancelled"; export const HOST_CONTEXT_CHANGED_METHOD: McpUiHostContextChangedNotification["method"] = "ui/notifications/host-context-changed"; +export const REQUEST_TEARDOWN_METHOD: McpUiRequestTeardownNotification["method"] = + "ui/notifications/request-teardown"; export const RESOURCE_TEARDOWN_METHOD: McpUiResourceTeardownRequest["method"] = "ui/resource-teardown"; export const INITIALIZE_METHOD: McpUiInitializeRequest["method"] = diff --git a/src/types.ts b/src/types.ts index 739da6fa..24bffece 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,7 @@ export { TOOL_RESULT_METHOD, TOOL_CANCELLED_METHOD, HOST_CONTEXT_CHANGED_METHOD, + REQUEST_TEARDOWN_METHOD, RESOURCE_TEARDOWN_METHOD, INITIALIZE_METHOD, INITIALIZED_METHOD, @@ -52,6 +53,7 @@ export { type McpUiHostContextChangedNotification, type McpUiResourceTeardownRequest, type McpUiResourceTeardownResult, + type McpUiRequestTeardownNotification, type McpUiHostCapabilities, type McpUiAppCapabilities, type McpUiInitializeRequest, @@ -85,6 +87,7 @@ import type { McpUiInitializedNotification, McpUiSizeChangedNotification, McpUiSandboxProxyReadyNotification, + McpUiRequestTeardownNotification, McpUiInitializeResult, McpUiOpenLinkResult, McpUiDownloadFileResult, @@ -118,6 +121,7 @@ export { McpUiHostContextChangedNotificationSchema, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResultSchema, + McpUiRequestTeardownNotificationSchema, McpUiHostCapabilitiesSchema, McpUiAppCapabilitiesSchema, McpUiInitializeRequestSchema, @@ -189,7 +193,7 @@ export type AppRequest = * - Sandbox resource ready * * App to host: - * - Initialized, size-changed, sandbox-proxy-ready + * - Initialized, size-changed, sandbox-proxy-ready, request-teardown * - Logging messages */ export type AppNotification = @@ -207,6 +211,7 @@ export type AppNotification = | McpUiInitializedNotification | McpUiSizeChangedNotification | McpUiSandboxProxyReadyNotification + | McpUiRequestTeardownNotification | LoggingMessageNotification; /**