Skip to content

Commit 179843f

Browse files
Add ui/close-resource request for UI to initiate termination
1 parent 6028cf2 commit 179843f

File tree

9 files changed

+200
-2
lines changed

9 files changed

+200
-2
lines changed

specification/draft/apps.mdx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,31 @@ Host SHOULD wait for a response before tearing down the resource (to prevent dat
12141214

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

1217+
#### Notifications (View → Host)
1218+
1219+
`ui/notifications/request-close` - View requests host to close it
1220+
1221+
```typescript
1222+
{
1223+
jsonrpc: "2.0",
1224+
method: "ui/notifications/request-close",
1225+
params: {}
1226+
}
1227+
```
1228+
1229+
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).
1230+
1231+
**Host behavior:**
1232+
- Host decides whether to proceed with the close
1233+
- If approved, Host MUST send `ui/resource-teardown` to allow the View to perform cleanup
1234+
- Host MUST wait for the View's teardown response before unmounting the iframe
1235+
- Host MAY deny or defer the close request (e.g., if there are unsaved changes elsewhere)
1236+
1237+
**View behavior:**
1238+
- View SHOULD NOT perform cleanup before sending this notification
1239+
- View SHOULD handle cleanup in its `ui/resource-teardown` handler
1240+
- This ensures the View has a single cleanup procedure regardless of whether the close was initiated by the View or the Host
1241+
12171242
`ui/notifications/host-context-changed` - Host context has changed
12181243

12191244
```typescript
@@ -1368,17 +1393,22 @@ sequenceDiagram
13681393

13691394
#### 4. Cleanup
13701395

1396+
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.
1397+
13711398
```mermaid
13721399
sequenceDiagram
13731400
participant H as Host
13741401
participant UI as View (iframe)
1402+
opt View-initiated close
1403+
UI ->> H: ui/notifications/request-close
1404+
end
13751405
H ->> UI: ui/resource-teardown
13761406
UI --> UI: Graceful termination
13771407
UI -->> H: ui/resource-teardown response
13781408
H -x H: Tear down iframe and listeners
13791409
```
13801410

1381-
Note: Cleanup may be triggered at any point in the lifecycle following View initialization.
1411+
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.
13821412

13831413
#### Key Differences from Pre-SEP MCP-UI:
13841414

src/app-bridge.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,35 @@ describe("App <-> AppBridge integration", () => {
518518
}),
519519
).rejects.toThrow("Context update failed");
520520
});
521+
522+
it("app.requestClose allows host to initiate teardown flow", async () => {
523+
const events: string[] = [];
524+
525+
// Host handles close request by initiating teardown
526+
bridge.onrequestclose = async () => {
527+
events.push("close-requested");
528+
// Host decides to proceed with close - initiate teardown
529+
await bridge.teardownResource({});
530+
events.push("teardown-complete");
531+
};
532+
533+
// App handles teardown (cleanup before unmount)
534+
app.onteardown = async () => {
535+
events.push("app-cleanup");
536+
return {};
537+
};
538+
539+
await app.connect(appTransport);
540+
await app.requestClose();
541+
await flush();
542+
543+
// Verify the full flow: request → teardown → cleanup
544+
expect(events).toEqual([
545+
"close-requested",
546+
"app-cleanup",
547+
"teardown-complete",
548+
]);
549+
});
521550
});
522551

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

src/app-bridge.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import {
7171
McpUiOpenLinkResult,
7272
McpUiResourceTeardownRequest,
7373
McpUiResourceTeardownResultSchema,
74+
McpUiRequestCloseNotification,
75+
McpUiRequestCloseNotificationSchema,
7476
McpUiSandboxProxyReadyNotification,
7577
McpUiSandboxProxyReadyNotificationSchema,
7678
McpUiSizeChangedNotificationSchema,
@@ -585,6 +587,41 @@ export class AppBridge extends Protocol<
585587
);
586588
}
587589

590+
/**
591+
* Register a handler for app-initiated close request notifications from the view.
592+
*
593+
* The view sends `ui/request-close` when it wants the host to close it.
594+
* If the host decides to proceed with the close, it should send
595+
* `ui/resource-teardown` (via {@link teardownResource `teardownResource`}) to allow
596+
* the view to perform cleanup, then unmount the iframe after the view responds.
597+
*
598+
* @param callback - Handler that receives close request params
599+
* - params - Empty object (reserved for future use)
600+
*
601+
* @example
602+
* ```typescript
603+
* bridge.onrequestclose = async (params) => {
604+
* console.log("App requested close");
605+
* // Initiate teardown to allow the app to clean up
606+
* // Alternatively, the callback can early return to prevent teardown
607+
* await bridge.teardownResource({});
608+
* // Now safe to unmount the iframe
609+
* iframe.remove();
610+
* };
611+
* ```
612+
*
613+
* @see {@link McpUiRequestCloseNotification `McpUiRequestCloseNotification`} for the notification type
614+
* @see {@link teardownResource `teardownResource`} for initiating teardown
615+
*/
616+
set onrequestclose(
617+
callback: (params: McpUiRequestCloseNotification["params"]) => void,
618+
) {
619+
this.setNotificationHandler(
620+
McpUiRequestCloseNotificationSchema,
621+
(request) => callback(request.params),
622+
);
623+
}
624+
588625
/**
589626
* Register a handler for display mode change requests from the view.
590627
*

src/app.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
McpUiResourceTeardownRequest,
3737
McpUiResourceTeardownRequestSchema,
3838
McpUiResourceTeardownResult,
39+
McpUiRequestCloseNotification,
3940
McpUiSizeChangedNotification,
4041
McpUiToolCancelledNotification,
4142
McpUiToolCancelledNotificationSchema,
@@ -924,6 +925,50 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
924925
/** @deprecated Use {@link openLink `openLink`} instead */
925926
sendOpenLink: App["openLink"] = this.openLink;
926927

928+
/**
929+
* Request the host to close this app.
930+
*
931+
* Apps call this method to request that the host close them. The host decides
932+
* whether to proceed with the close - if approved, the host will send
933+
* `ui/resource-teardown` to allow the app to perform cleanup before being
934+
* unmounted. This piggybacks on the existing teardown mechanism, ensuring
935+
* the app only needs a single shutdown procedure (via {@link onteardown `onteardown`})
936+
* regardless of whether the close was initiated by the app or the host.
937+
*
938+
* This is a fire-and-forget notification - no response is expected.
939+
* If the host approves the close, the app will receive a `ui/resource-teardown`
940+
* request via the {@link onteardown `onteardown`} handler to perform cleanup.
941+
*
942+
* @param params - Empty params object (reserved for future use)
943+
* @returns Promise that resolves when the notification is sent
944+
*
945+
* @example App-initiated close after user action
946+
* ```typescript
947+
* // User clicks "Done" button in the app
948+
* async function handleDoneClick() {
949+
* // Request the host to close the app
950+
* await app.requestClose();
951+
* // If host approves, onteardown handler will be called for cleanup
952+
* }
953+
*
954+
* // Set up teardown handler (called for both app-initiated and host-initiated close)
955+
* app.onteardown = async () => {
956+
* await saveState();
957+
* closeConnections();
958+
* return {};
959+
* };
960+
* ```
961+
*
962+
* @see {@link McpUiRequestCloseNotification `McpUiRequestCloseNotification`} for notification structure
963+
* @see {@link onteardown `onteardown`} for the cleanup handler
964+
*/
965+
requestClose(params: McpUiRequestCloseNotification["params"] = {}) {
966+
return this.notification(<McpUiRequestCloseNotification>{
967+
method: "ui/notifications/request-close",
968+
params,
969+
});
970+
}
971+
927972
/**
928973
* Request a change to the display mode.
929974
*

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,19 @@ export interface McpUiSupportedContentBlockModalities {
441441
structuredContent?: {};
442442
}
443443

444+
/**
445+
* @description Notification for app-initiated close request (View -> Host).
446+
* Views send this to request that the host close them. The host decides
447+
* whether to proceed with the close - if approved, the host will send
448+
* `ui/resource-teardown` to allow the view to perform cleanup before being
449+
* unmounted.
450+
* @see {@link app.App.requestClose} for the app method that sends this
451+
*/
452+
export interface McpUiRequestCloseNotification {
453+
method: "ui/notifications/request-close";
454+
params?: {};
455+
}
456+
444457
/**
445458
* @description Capabilities supported by the host application.
446459
* @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities

src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export {
4949
type McpUiHostContextChangedNotification,
5050
type McpUiResourceTeardownRequest,
5151
type McpUiResourceTeardownResult,
52+
type McpUiRequestCloseNotification,
5253
type McpUiHostCapabilities,
5354
type McpUiAppCapabilities,
5455
type McpUiInitializeRequest,
@@ -81,6 +82,7 @@ import type {
8182
McpUiInitializedNotification,
8283
McpUiSizeChangedNotification,
8384
McpUiSandboxProxyReadyNotification,
85+
McpUiRequestCloseNotification,
8486
McpUiInitializeResult,
8587
McpUiOpenLinkResult,
8688
McpUiMessageResult,
@@ -111,6 +113,7 @@ export {
111113
McpUiHostContextChangedNotificationSchema,
112114
McpUiResourceTeardownRequestSchema,
113115
McpUiResourceTeardownResultSchema,
116+
McpUiRequestCloseNotificationSchema,
114117
McpUiHostCapabilitiesSchema,
115118
McpUiAppCapabilitiesSchema,
116119
McpUiInitializeRequestSchema,
@@ -181,7 +184,7 @@ export type AppRequest =
181184
* - Sandbox resource ready
182185
*
183186
* App to host:
184-
* - Initialized, size-changed, sandbox-proxy-ready
187+
* - Initialized, size-changed, sandbox-proxy-ready, request-close
185188
* - Logging messages
186189
*/
187190
export type AppNotification =
@@ -199,6 +202,7 @@ export type AppNotification =
199202
| McpUiInitializedNotification
200203
| McpUiSizeChangedNotification
201204
| McpUiSandboxProxyReadyNotification
205+
| McpUiRequestCloseNotification
202206
| LoggingMessageNotification;
203207

204208
/**

0 commit comments

Comments
 (0)