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:
parent
296b5921fb
commit
c026b398a1
9 changed files with 195 additions and 5 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
47
src/app.ts
47
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<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.
|
||||
*
|
||||
|
|
|
|||
17
src/generated/schema.json
generated
17
src/generated/schema.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
10
src/generated/schema.test.ts
generated
10
src/generated/schema.test.ts
generated
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
13
src/generated/schema.ts
generated
13
src/generated/schema.ts
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"] =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue