Add ui/notifications/request-teardown notification for view-initiated termination (#215)

* Add ui/close-resource request for UI to initiate termination

* Add missing METHOD export

* Fix typo in remaining ui/request-close

* Align spec terminology to teardown/termination instead of close/clearnup

* Implement final comments

* Removing view behavior on initiation of request termination
This commit is contained in:
Frédéric Barthelet 2026-03-19 21:01:59 +01:00 • committed by GitHub
commit c026b398a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 195 additions and 5 deletions

View file

@ -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", () => {

View file

@ -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.
*

View file

@ -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<AppRequest, AppNotification, AppResult> {
);
}
/**
* 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(<McpUiRequestTeardownNotification>{
method: "ui/notifications/request-teardown",
params,
});
}
/**
* Request a change to the display mode.
*

View file

@ -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",

View file

@ -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<spec.McpUiSupportedContentBlockModalities>(
expectType<McpUiSupportedContentBlockModalitiesSchemaInferredType>(
{} as spec.McpUiSupportedContentBlockModalities,
);
expectType<spec.McpUiRequestTeardownNotification>(
{} as McpUiRequestTeardownNotificationSchemaInferredType,
);
expectType<McpUiRequestTeardownNotificationSchemaInferredType>(
{} as spec.McpUiRequestTeardownNotification,
);
expectType<spec.McpUiHostCapabilities>(
{} as McpUiHostCapabilitiesSchemaInferredType,
);

View file

@ -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

View file

@ -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"] =

View file

@ -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;
/**