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).
|
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
|
`ui/notifications/size-changed` - View's size changed
|
||||||
|
|
||||||
```typescript
|
```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).
|
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
|
`ui/notifications/host-context-changed` - Host context has changed
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -1455,19 +1473,24 @@ sequenceDiagram
|
||||||
end
|
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
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant H as Host
|
participant H as Host
|
||||||
participant UI as View (iframe)
|
participant UI as View (iframe)
|
||||||
|
opt View-initiated teardown
|
||||||
|
UI ->> H: ui/notifications/request-teardown
|
||||||
|
end
|
||||||
H ->> UI: ui/resource-teardown
|
H ->> UI: ui/resource-teardown
|
||||||
UI --> UI: Graceful termination
|
UI --> UI: Graceful termination
|
||||||
UI -->> H: ui/resource-teardown response
|
UI -->> H: ui/resource-teardown response
|
||||||
H -x H: Tear down iframe and listeners
|
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:
|
#### Key Differences from Pre-SEP MCP-UI:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -520,6 +520,31 @@ describe("App <-> AppBridge integration", () => {
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow("Context update failed");
|
).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", () => {
|
describe("App -> Host requests", () => {
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ import {
|
||||||
McpUiDownloadFileResult,
|
McpUiDownloadFileResult,
|
||||||
McpUiResourceTeardownRequest,
|
McpUiResourceTeardownRequest,
|
||||||
McpUiResourceTeardownResultSchema,
|
McpUiResourceTeardownResultSchema,
|
||||||
|
McpUiRequestTeardownNotification,
|
||||||
|
McpUiRequestTeardownNotificationSchema,
|
||||||
McpUiSandboxProxyReadyNotification,
|
McpUiSandboxProxyReadyNotification,
|
||||||
McpUiSandboxProxyReadyNotificationSchema,
|
McpUiSandboxProxyReadyNotificationSchema,
|
||||||
McpUiSizeChangedNotificationSchema,
|
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.
|
* 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,
|
McpUiResourceTeardownRequest,
|
||||||
McpUiResourceTeardownRequestSchema,
|
McpUiResourceTeardownRequestSchema,
|
||||||
McpUiResourceTeardownResult,
|
McpUiResourceTeardownResult,
|
||||||
|
McpUiRequestTeardownNotification,
|
||||||
McpUiSizeChangedNotification,
|
McpUiSizeChangedNotification,
|
||||||
McpUiToolCancelledNotification,
|
McpUiToolCancelledNotification,
|
||||||
McpUiToolCancelledNotificationSchema,
|
McpUiToolCancelledNotificationSchema,
|
||||||
|
|
@ -177,7 +178,7 @@ type RequestHandlerExtra = Parameters<
|
||||||
* 1. **Create**: Instantiate App with info and capabilities
|
* 1. **Create**: Instantiate App with info and capabilities
|
||||||
* 2. **Connect**: Call `connect()` to establish transport and perform handshake
|
* 2. **Connect**: Call `connect()` to establish transport and perform handshake
|
||||||
* 3. **Interactive**: Send requests, receive notifications, call tools
|
* 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
|
* ## 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.
|
* 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"],
|
"required": ["mode"],
|
||||||
"additionalProperties": {}
|
"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": {
|
"McpUiResourceCsp": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"type": "object",
|
"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
|
typeof generated.McpUiSupportedContentBlockModalitiesSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type McpUiRequestTeardownNotificationSchemaInferredType = z.infer<
|
||||||
|
typeof generated.McpUiRequestTeardownNotificationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export type McpUiHostCapabilitiesSchemaInferredType = z.infer<
|
export type McpUiHostCapabilitiesSchemaInferredType = z.infer<
|
||||||
typeof generated.McpUiHostCapabilitiesSchema
|
typeof generated.McpUiHostCapabilitiesSchema
|
||||||
>;
|
>;
|
||||||
|
|
@ -255,6 +259,12 @@ expectType<spec.McpUiSupportedContentBlockModalities>(
|
||||||
expectType<McpUiSupportedContentBlockModalitiesSchemaInferredType>(
|
expectType<McpUiSupportedContentBlockModalitiesSchemaInferredType>(
|
||||||
{} as spec.McpUiSupportedContentBlockModalities,
|
{} as spec.McpUiSupportedContentBlockModalities,
|
||||||
);
|
);
|
||||||
|
expectType<spec.McpUiRequestTeardownNotification>(
|
||||||
|
{} as McpUiRequestTeardownNotificationSchemaInferredType,
|
||||||
|
);
|
||||||
|
expectType<McpUiRequestTeardownNotificationSchemaInferredType>(
|
||||||
|
{} as spec.McpUiRequestTeardownNotification,
|
||||||
|
);
|
||||||
expectType<spec.McpUiHostCapabilities>(
|
expectType<spec.McpUiHostCapabilities>(
|
||||||
{} as McpUiHostCapabilitiesSchemaInferredType,
|
{} 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."),
|
.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.
|
* @description Capabilities supported by the host application.
|
||||||
* @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities
|
* @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities
|
||||||
|
|
|
||||||
|
|
@ -474,6 +474,19 @@ export interface McpUiSupportedContentBlockModalities {
|
||||||
structuredContent?: {};
|
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.
|
* @description Capabilities supported by the host application.
|
||||||
* @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities
|
* @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";
|
"ui/notifications/tool-cancelled";
|
||||||
export const HOST_CONTEXT_CHANGED_METHOD: McpUiHostContextChangedNotification["method"] =
|
export const HOST_CONTEXT_CHANGED_METHOD: McpUiHostContextChangedNotification["method"] =
|
||||||
"ui/notifications/host-context-changed";
|
"ui/notifications/host-context-changed";
|
||||||
|
export const REQUEST_TEARDOWN_METHOD: McpUiRequestTeardownNotification["method"] =
|
||||||
|
"ui/notifications/request-teardown";
|
||||||
export const RESOURCE_TEARDOWN_METHOD: McpUiResourceTeardownRequest["method"] =
|
export const RESOURCE_TEARDOWN_METHOD: McpUiResourceTeardownRequest["method"] =
|
||||||
"ui/resource-teardown";
|
"ui/resource-teardown";
|
||||||
export const INITIALIZE_METHOD: McpUiInitializeRequest["method"] =
|
export const INITIALIZE_METHOD: McpUiInitializeRequest["method"] =
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export {
|
||||||
TOOL_RESULT_METHOD,
|
TOOL_RESULT_METHOD,
|
||||||
TOOL_CANCELLED_METHOD,
|
TOOL_CANCELLED_METHOD,
|
||||||
HOST_CONTEXT_CHANGED_METHOD,
|
HOST_CONTEXT_CHANGED_METHOD,
|
||||||
|
REQUEST_TEARDOWN_METHOD,
|
||||||
RESOURCE_TEARDOWN_METHOD,
|
RESOURCE_TEARDOWN_METHOD,
|
||||||
INITIALIZE_METHOD,
|
INITIALIZE_METHOD,
|
||||||
INITIALIZED_METHOD,
|
INITIALIZED_METHOD,
|
||||||
|
|
@ -52,6 +53,7 @@ export {
|
||||||
type McpUiHostContextChangedNotification,
|
type McpUiHostContextChangedNotification,
|
||||||
type McpUiResourceTeardownRequest,
|
type McpUiResourceTeardownRequest,
|
||||||
type McpUiResourceTeardownResult,
|
type McpUiResourceTeardownResult,
|
||||||
|
type McpUiRequestTeardownNotification,
|
||||||
type McpUiHostCapabilities,
|
type McpUiHostCapabilities,
|
||||||
type McpUiAppCapabilities,
|
type McpUiAppCapabilities,
|
||||||
type McpUiInitializeRequest,
|
type McpUiInitializeRequest,
|
||||||
|
|
@ -85,6 +87,7 @@ import type {
|
||||||
McpUiInitializedNotification,
|
McpUiInitializedNotification,
|
||||||
McpUiSizeChangedNotification,
|
McpUiSizeChangedNotification,
|
||||||
McpUiSandboxProxyReadyNotification,
|
McpUiSandboxProxyReadyNotification,
|
||||||
|
McpUiRequestTeardownNotification,
|
||||||
McpUiInitializeResult,
|
McpUiInitializeResult,
|
||||||
McpUiOpenLinkResult,
|
McpUiOpenLinkResult,
|
||||||
McpUiDownloadFileResult,
|
McpUiDownloadFileResult,
|
||||||
|
|
@ -118,6 +121,7 @@ export {
|
||||||
McpUiHostContextChangedNotificationSchema,
|
McpUiHostContextChangedNotificationSchema,
|
||||||
McpUiResourceTeardownRequestSchema,
|
McpUiResourceTeardownRequestSchema,
|
||||||
McpUiResourceTeardownResultSchema,
|
McpUiResourceTeardownResultSchema,
|
||||||
|
McpUiRequestTeardownNotificationSchema,
|
||||||
McpUiHostCapabilitiesSchema,
|
McpUiHostCapabilitiesSchema,
|
||||||
McpUiAppCapabilitiesSchema,
|
McpUiAppCapabilitiesSchema,
|
||||||
McpUiInitializeRequestSchema,
|
McpUiInitializeRequestSchema,
|
||||||
|
|
@ -189,7 +193,7 @@ export type AppRequest =
|
||||||
* - Sandbox resource ready
|
* - Sandbox resource ready
|
||||||
*
|
*
|
||||||
* App to host:
|
* App to host:
|
||||||
* - Initialized, size-changed, sandbox-proxy-ready
|
* - Initialized, size-changed, sandbox-proxy-ready, request-teardown
|
||||||
* - Logging messages
|
* - Logging messages
|
||||||
*/
|
*/
|
||||||
export type AppNotification =
|
export type AppNotification =
|
||||||
|
|
@ -207,6 +211,7 @@ export type AppNotification =
|
||||||
| McpUiInitializedNotification
|
| McpUiInitializedNotification
|
||||||
| McpUiSizeChangedNotification
|
| McpUiSizeChangedNotification
|
||||||
| McpUiSandboxProxyReadyNotification
|
| McpUiSandboxProxyReadyNotification
|
||||||
|
| McpUiRequestTeardownNotification
|
||||||
| LoggingMessageNotification;
|
| LoggingMessageNotification;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue