Skip to content

Commit c026b39

Browse files
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
1 parent 296b592 commit c026b39

File tree

9 files changed

+195
-5
lines changed

9 files changed

+195
-5
lines changed

specification/draft/apps.mdx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,9 +1285,11 @@ Host MUST send this notification if the tool execution was cancelled, for any re
12851285
}
12861286
```
12871287

1288-
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.
1288+
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.
12891289
Host SHOULD wait for a response before tearing down the resource (to prevent data loss).
12901290

1291+
#### Notifications (View → Host)
1292+
12911293
`ui/notifications/size-changed` - View's size changed
12921294

12931295
```typescript
@@ -1303,6 +1305,22 @@ Host SHOULD wait for a response before tearing down the resource (to prevent dat
13031305

13041306
The View SHOULD send this notification when rendered content body size changes (e.g. using ResizeObserver API to report up to date size).
13051307

1308+
`ui/notifications/request-teardown` - View requests host to tear it down
1309+
1310+
```typescript
1311+
{
1312+
jsonrpc: "2.0",
1313+
method: "ui/notifications/request-teardown",
1314+
params: {}
1315+
}
1316+
```
1317+
1318+
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).
1319+
1320+
**Host behavior:**
1321+
- Host MAY defer or ignore the teardown request.
1322+
- 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).
1323+
13061324
`ui/notifications/host-context-changed` - Host context has changed
13071325

13081326
```typescript
@@ -1455,19 +1473,24 @@ sequenceDiagram
14551473
end
14561474
```
14571475

1458-
#### 4. Cleanup
1476+
#### 4. Teardown
1477+
1478+
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.
14591479

14601480
```mermaid
14611481
sequenceDiagram
14621482
participant H as Host
14631483
participant UI as View (iframe)
1484+
opt View-initiated teardown
1485+
UI ->> H: ui/notifications/request-teardown
1486+
end
14641487
H ->> UI: ui/resource-teardown
14651488
UI --> UI: Graceful termination
14661489
UI -->> H: ui/resource-teardown response
14671490
H -x H: Tear down iframe and listeners
14681491
```
14691492

1470-
Note: Cleanup may be triggered at any point in the lifecycle following View initialization.
1493+
Note: Teardown may be triggered at any point in the lifecycle following View initialization.
14711494

14721495
#### Key Differences from Pre-SEP MCP-UI:
14731496

src/app-bridge.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,31 @@ describe("App <-> AppBridge integration", () => {
520520
}),
521521
).rejects.toThrow("Context update failed");
522522
});
523+
524+
it("app.requestTeardown allows host to initiate teardown flow", async () => {
525+
const events: string[] = [];
526+
527+
bridge.onrequestteardown = async () => {
528+
events.push("teardown-requested");
529+
await bridge.teardownResource({});
530+
events.push("teardown-complete");
531+
};
532+
533+
app.onteardown = async () => {
534+
events.push("persist-unsaved-state");
535+
return {};
536+
};
537+
538+
await app.connect(appTransport);
539+
await app.requestTeardown();
540+
await flush();
541+
542+
expect(events).toEqual([
543+
"teardown-requested",
544+
"persist-unsaved-state",
545+
"teardown-complete",
546+
]);
547+
});
523548
});
524549

525550
describe("App -> Host requests", () => {

src/app-bridge.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ import {
7474
McpUiDownloadFileResult,
7575
McpUiResourceTeardownRequest,
7676
McpUiResourceTeardownResultSchema,
77+
McpUiRequestTeardownNotification,
78+
McpUiRequestTeardownNotificationSchema,
7779
McpUiSandboxProxyReadyNotification,
7880
McpUiSandboxProxyReadyNotificationSchema,
7981
McpUiSizeChangedNotificationSchema,
@@ -673,6 +675,41 @@ export class AppBridge extends Protocol<
673675
);
674676
}
675677

678+
/**
679+
* Register a handler for app-initiated teardown request notifications from the view.
680+
*
681+
* The view sends `ui/notifications/request-teardown` when it wants the host to tear it down.
682+
* If the host decides to proceed, it should send
683+
* `ui/resource-teardown` (via {@link teardownResource `teardownResource`}) to allow
684+
* the view to perform gracefull termination, then unmount the iframe after the view responds.
685+
*
686+
* @param callback - Handler that receives teardown request params
687+
* - params - Empty object (reserved for future use)
688+
*
689+
* @example
690+
* ```typescript
691+
* bridge.onrequestteardown = async (params) => {
692+
* console.log("App requested teardown");
693+
* // Initiate teardown to allow the app to persist unsaved state
694+
* // Alternatively, the callback can early return to prevent teardown
695+
* await bridge.teardownResource({});
696+
* // Now safe to unmount the iframe
697+
* iframe.remove();
698+
* };
699+
* ```
700+
*
701+
* @see {@link McpUiRequestTeardownNotification `McpUiRequestTeardownNotification`} for the notification type
702+
* @see {@link teardownResource `teardownResource`} for initiating teardown
703+
*/
704+
set onrequestteardown(
705+
callback: (params: McpUiRequestTeardownNotification["params"]) => void,
706+
) {
707+
this.setNotificationHandler(
708+
McpUiRequestTeardownNotificationSchema,
709+
(request) => callback(request.params),
710+
);
711+
}
712+
676713
/**
677714
* Register a handler for display mode change requests from the view.
678715
*

src/app.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
McpUiResourceTeardownRequest,
4545
McpUiResourceTeardownRequestSchema,
4646
McpUiResourceTeardownResult,
47+
McpUiRequestTeardownNotification,
4748
McpUiSizeChangedNotification,
4849
McpUiToolCancelledNotification,
4950
McpUiToolCancelledNotificationSchema,
@@ -177,7 +178,7 @@ type RequestHandlerExtra = Parameters<
177178
* 1. **Create**: Instantiate App with info and capabilities
178179
* 2. **Connect**: Call `connect()` to establish transport and perform handshake
179180
* 3. **Interactive**: Send requests, receive notifications, call tools
180-
* 4. **Cleanup**: Host sends teardown request before unmounting
181+
* 4. **Teardown**: Host sends teardown request before unmounting
181182
*
182183
* ## Inherited Methods
183184
*
@@ -1134,6 +1135,50 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
11341135
);
11351136
}
11361137

1138+
/**
1139+
* Request the host to tear down this app.
1140+
*
1141+
* Apps call this method to request that the host tear them down. The host
1142+
* decides whether to proceed - if approved, the host will send
1143+
* `ui/resource-teardown` to allow the app to perform gracefull termination before being
1144+
* unmounted. This piggybacks on the existing teardown mechanism, ensuring
1145+
* the app only needs a single shutdown procedure (via {@link onteardown `onteardown`})
1146+
* regardless of whether the teardown was initiated by the app or the host.
1147+
*
1148+
* This is a fire-and-forget notification - no response is expected.
1149+
* If the host approves, the app will receive a `ui/resource-teardown`
1150+
* request via the {@link onteardown `onteardown`} handler to persist unsaved state.
1151+
*
1152+
* @param params - Empty params object (reserved for future use)
1153+
* @returns Promise that resolves when the notification is sent
1154+
*
1155+
* @example App-initiated teardown after user action
1156+
* ```typescript
1157+
* // User clicks "Done" button in the app
1158+
* async function handleDoneClick() {
1159+
* // Request the host to tear down the app
1160+
* await app.requestTeardown();
1161+
* // If host approves, onteardown handler will be called for termination
1162+
* }
1163+
*
1164+
* // Set up teardown handler (called for both app-initiated and host-initiated teardown)
1165+
* app.onteardown = async () => {
1166+
* await saveState();
1167+
* closeConnections();
1168+
* return {};
1169+
* };
1170+
* ```
1171+
*
1172+
* @see {@link McpUiRequestTeardownNotification `McpUiRequestTeardownNotification`} for notification structure
1173+
* @see {@link onteardown `onteardown`} for the graceful termination handler
1174+
*/
1175+
requestTeardown(params: McpUiRequestTeardownNotification["params"] = {}) {
1176+
return this.notification(<McpUiRequestTeardownNotification>{
1177+
method: "ui/notifications/request-teardown",
1178+
params,
1179+
});
1180+
}
1181+
11371182
/**
11381183
* Request a change to the display mode.
11391184
*

src/generated/schema.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/generated/schema.test.ts

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/generated/schema.ts

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/spec.types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,19 @@ export interface McpUiSupportedContentBlockModalities {
474474
structuredContent?: {};
475475
}
476476

477+
/**
478+
* @description Notification for app-initiated teardown request (View -> Host).
479+
* Views send this to request that the host tear them down. The host decides
480+
* whether to proceed - if approved, the host will send
481+
* `ui/resource-teardown` to allow the view to perform cleanup before being
482+
* unmounted.
483+
* @see {@link app.App.requestTeardown} for the app method that sends this
484+
*/
485+
export interface McpUiRequestTeardownNotification {
486+
method: "ui/notifications/request-teardown";
487+
params?: {};
488+
}
489+
477490
/**
478491
* @description Capabilities supported by the host application.
479492
* @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities
@@ -800,6 +813,8 @@ export const TOOL_CANCELLED_METHOD: McpUiToolCancelledNotification["method"] =
800813
"ui/notifications/tool-cancelled";
801814
export const HOST_CONTEXT_CHANGED_METHOD: McpUiHostContextChangedNotification["method"] =
802815
"ui/notifications/host-context-changed";
816+
export const REQUEST_TEARDOWN_METHOD: McpUiRequestTeardownNotification["method"] =
817+
"ui/notifications/request-teardown";
803818
export const RESOURCE_TEARDOWN_METHOD: McpUiResourceTeardownRequest["method"] =
804819
"ui/resource-teardown";
805820
export const INITIALIZE_METHOD: McpUiInitializeRequest["method"] =

src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export {
2323
TOOL_RESULT_METHOD,
2424
TOOL_CANCELLED_METHOD,
2525
HOST_CONTEXT_CHANGED_METHOD,
26+
REQUEST_TEARDOWN_METHOD,
2627
RESOURCE_TEARDOWN_METHOD,
2728
INITIALIZE_METHOD,
2829
INITIALIZED_METHOD,
@@ -52,6 +53,7 @@ export {
5253
type McpUiHostContextChangedNotification,
5354
type McpUiResourceTeardownRequest,
5455
type McpUiResourceTeardownResult,
56+
type McpUiRequestTeardownNotification,
5557
type McpUiHostCapabilities,
5658
type McpUiAppCapabilities,
5759
type McpUiInitializeRequest,
@@ -85,6 +87,7 @@ import type {
8587
McpUiInitializedNotification,
8688
McpUiSizeChangedNotification,
8789
McpUiSandboxProxyReadyNotification,
90+
McpUiRequestTeardownNotification,
8891
McpUiInitializeResult,
8992
McpUiOpenLinkResult,
9093
McpUiDownloadFileResult,
@@ -118,6 +121,7 @@ export {
118121
McpUiHostContextChangedNotificationSchema,
119122
McpUiResourceTeardownRequestSchema,
120123
McpUiResourceTeardownResultSchema,
124+
McpUiRequestTeardownNotificationSchema,
121125
McpUiHostCapabilitiesSchema,
122126
McpUiAppCapabilitiesSchema,
123127
McpUiInitializeRequestSchema,
@@ -189,7 +193,7 @@ export type AppRequest =
189193
* - Sandbox resource ready
190194
*
191195
* App to host:
192-
* - Initialized, size-changed, sandbox-proxy-ready
196+
* - Initialized, size-changed, sandbox-proxy-ready, request-teardown
193197
* - Logging messages
194198
*/
195199
export type AppNotification =
@@ -207,6 +211,7 @@ export type AppNotification =
207211
| McpUiInitializedNotification
208212
| McpUiSizeChangedNotification
209213
| McpUiSandboxProxyReadyNotification
214+
| McpUiRequestTeardownNotification
210215
| LoggingMessageNotification;
211216

212217
/**

0 commit comments

Comments
 (0)