feat: implement ui/notifications/tool-cancelled + add missing App.onteardown (#120)
* feat: implement ui/notifications/tool-cancelled throughout SDK Wire the tool-cancelled notification that hosts MUST send when tool execution is cancelled (user action, timeout, classifier intervention, etc.): - Add McpUiToolCancelledNotification type in spec.types.ts - Generate Zod schema via ts-to-zod - Add AppBridge.sendToolCancelled() method for hosts - Add App.ontoolcancelled setter for guest UIs - Export type and schema from types.ts - Add tests for notification with/without reason 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add App.onteardown handler for graceful shutdown Wire the guest UI side of ui/resource-teardown request: - Add onteardown setter in App class for guest UIs to handle teardown - Allow ui/resource-teardown in assertRequestHandlerCapability - Import McpUiResourceTeardownRequest/Result types - Add tests for sync and async cleanup handlers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(example): wire tool-cancelled and teardown in basic-host Update basic-host example to properly use the new notifications: - Send sendToolCancelled when tool call promise rejects - Send sendResourceTeardown when React component unmounts - Store appBridge ref for cleanup access 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(example): implement graceful teardown per spec recommendation Address review feedback: the host now waits for guest's teardown response before unmounting, following the spec: "Host SHOULD wait for a response before tearing down the resource (to prevent data loss)." Changes: - Add close button to tool call panels - Two-phase unmount: isDestroying state dims panel and disables events - sendResourceTeardown() completes before component removal - Non-app tool results close immediately 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(examples): add onteardown handlers to demo apps Demonstrate graceful shutdown with simulated 500ms cleanup work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a31e48a11e
commit
70b393a7e6
13 changed files with 379 additions and 15 deletions
|
|
@ -186,11 +186,19 @@ export async function initializeApp(
|
|||
log.info("Sending tool call input to MCP App:", input);
|
||||
appBridge.sendToolInput({ arguments: input });
|
||||
|
||||
// Schedule tool call result to be sent to MCP App
|
||||
resultPromise.then((result) => {
|
||||
log.info("Sending tool call result to MCP App:", result);
|
||||
appBridge.sendToolResult(result);
|
||||
});
|
||||
// Schedule tool call result (or cancellation) to be sent to MCP App
|
||||
resultPromise.then(
|
||||
(result) => {
|
||||
log.info("Sending tool call result to MCP App:", result);
|
||||
appBridge.sendToolResult(result);
|
||||
},
|
||||
(error) => {
|
||||
log.error("Tool call failed, sending cancellation to MCP App:", error);
|
||||
appBridge.sendToolCancelled({
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -92,10 +92,32 @@
|
|||
flex-direction: column;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
position: relative;
|
||||
|
||||
.toolName {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #e0e0e0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #d0d0d0;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,17 +8,41 @@ import styles from "./index.module.css";
|
|||
interface HostProps {
|
||||
serversPromise: Promise<ServerInfo[]>;
|
||||
}
|
||||
|
||||
type ToolCallEntry = ToolCallInfo & { id: number };
|
||||
let nextToolCallId = 0;
|
||||
|
||||
function Host({ serversPromise }: HostProps) {
|
||||
const [toolCalls, setToolCalls] = useState<ToolCallInfo[]>([]);
|
||||
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
|
||||
const [destroyingIds, setDestroyingIds] = useState<Set<number>>(new Set());
|
||||
|
||||
const requestClose = (id: number) => {
|
||||
setDestroyingIds((s) => new Set(s).add(id));
|
||||
};
|
||||
|
||||
const completeClose = (id: number) => {
|
||||
setDestroyingIds((s) => {
|
||||
const next = new Set(s);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
setToolCalls((calls) => calls.filter((c) => c.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{toolCalls.map((info, i) => (
|
||||
<ToolCallInfoPanel key={i} toolCallInfo={info} />
|
||||
{toolCalls.map((info) => (
|
||||
<ToolCallInfoPanel
|
||||
key={info.id}
|
||||
toolCallInfo={info}
|
||||
isDestroying={destroyingIds.has(info.id)}
|
||||
onRequestClose={() => requestClose(info.id)}
|
||||
onCloseComplete={() => completeClose(info.id)}
|
||||
/>
|
||||
))}
|
||||
<CallToolPanel
|
||||
serversPromise={serversPromise}
|
||||
addToolCall={(info) => setToolCalls([...toolCalls, info])}
|
||||
addToolCall={(info) => setToolCalls([...toolCalls, { ...info, id: nextToolCallId++ }])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
@ -135,14 +159,38 @@ function ServerSelect({ serversPromise, onSelect }: ServerSelectProps) {
|
|||
|
||||
interface ToolCallInfoPanelProps {
|
||||
toolCallInfo: ToolCallInfo;
|
||||
isDestroying?: boolean;
|
||||
onRequestClose?: () => void;
|
||||
onCloseComplete?: () => void;
|
||||
}
|
||||
function ToolCallInfoPanel({ toolCallInfo }: ToolCallInfoPanelProps) {
|
||||
function ToolCallInfoPanel({ toolCallInfo, isDestroying, onRequestClose, onCloseComplete }: ToolCallInfoPanelProps) {
|
||||
const isApp = hasAppHtml(toolCallInfo);
|
||||
|
||||
// For non-app tool calls, close immediately when isDestroying becomes true
|
||||
useEffect(() => {
|
||||
if (isDestroying && !isApp) {
|
||||
onCloseComplete?.();
|
||||
}
|
||||
}, [isDestroying, isApp, onCloseComplete]);
|
||||
|
||||
return (
|
||||
<div className={styles.toolCallInfoPanel}>
|
||||
<div
|
||||
className={styles.toolCallInfoPanel}
|
||||
style={isDestroying ? { opacity: 0.5, pointerEvents: "none" } : undefined}
|
||||
>
|
||||
<div className={styles.inputInfoPanel}>
|
||||
<h2>
|
||||
<span>{toolCallInfo.serverInfo.name}</span>
|
||||
<span className={styles.toolName}>{toolCallInfo.tool.name}</span>
|
||||
{onRequestClose && !isDestroying && (
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={onRequestClose}
|
||||
title="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</h2>
|
||||
<JsonBlock value={toolCallInfo.input} />
|
||||
</div>
|
||||
|
|
@ -150,8 +198,12 @@ function ToolCallInfoPanel({ toolCallInfo }: ToolCallInfoPanelProps) {
|
|||
<ErrorBoundary>
|
||||
<Suspense fallback="Loading...">
|
||||
{
|
||||
hasAppHtml(toolCallInfo)
|
||||
? <AppIFramePanel toolCallInfo={toolCallInfo} />
|
||||
isApp
|
||||
? <AppIFramePanel
|
||||
toolCallInfo={toolCallInfo}
|
||||
isDestroying={isDestroying}
|
||||
onTeardownComplete={onCloseComplete}
|
||||
/>
|
||||
: <ToolResultPanel toolCallInfo={toolCallInfo} />
|
||||
}
|
||||
</Suspense>
|
||||
|
|
@ -173,9 +225,12 @@ function JsonBlock({ value }: { value: object }) {
|
|||
|
||||
interface AppIFramePanelProps {
|
||||
toolCallInfo: Required<ToolCallInfo>;
|
||||
isDestroying?: boolean;
|
||||
onTeardownComplete?: () => void;
|
||||
}
|
||||
function AppIFramePanel({ toolCallInfo }: AppIFramePanelProps) {
|
||||
function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppIFramePanelProps) {
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const appBridgeRef = useRef<ReturnType<typeof newAppBridge> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = iframeRef.current!;
|
||||
|
|
@ -186,11 +241,34 @@ function AppIFramePanel({ toolCallInfo }: AppIFramePanelProps) {
|
|||
// `toolCallInfo`.
|
||||
if (firstTime) {
|
||||
const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe);
|
||||
appBridgeRef.current = appBridge;
|
||||
initializeApp(iframe, appBridge, toolCallInfo);
|
||||
}
|
||||
});
|
||||
}, [toolCallInfo]);
|
||||
|
||||
// Graceful teardown: wait for guest to respond before unmounting
|
||||
// This follows the spec: "Host SHOULD wait for a response before tearing
|
||||
// down the resource (to prevent data loss)."
|
||||
useEffect(() => {
|
||||
if (!isDestroying) return;
|
||||
|
||||
if (!appBridgeRef.current) {
|
||||
// Bridge not ready yet (e.g., user closed before iframe loaded)
|
||||
onTeardownComplete?.();
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Sending teardown notification to MCP App");
|
||||
appBridgeRef.current.sendResourceTeardown({})
|
||||
.catch((err) => {
|
||||
log.warn("Teardown request failed (app may have already closed):", err);
|
||||
})
|
||||
.finally(() => {
|
||||
onTeardownComplete?.();
|
||||
});
|
||||
}, [isDestroying, onTeardownComplete]);
|
||||
|
||||
return (
|
||||
<div className={styles.appIframePanel}>
|
||||
<iframe ref={iframeRef} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @file App that demonstrates a few features using MCP Apps SDK + React.
|
||||
*/
|
||||
import type { App } from "@modelcontextprotocol/ext-apps";
|
||||
import type { App, McpUiResourceTeardownResult } from "@modelcontextprotocol/ext-apps";
|
||||
import { useApp } from "@modelcontextprotocol/ext-apps/react";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { StrictMode, useCallback, useEffect, useState } from "react";
|
||||
|
|
@ -35,6 +35,12 @@ function GetTimeApp() {
|
|||
appInfo: IMPLEMENTATION,
|
||||
capabilities: {},
|
||||
onAppCreated: (app) => {
|
||||
app.onteardown = async () => {
|
||||
log.info("App is being torn down");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate cleanup work
|
||||
log.info("App teardown complete");
|
||||
return {};
|
||||
};
|
||||
app.ontoolinput = async (input) => {
|
||||
log.info("Received tool call input:", input);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ const openLinkBtn = document.getElementById("open-link-btn")!;
|
|||
// Create app instance
|
||||
const app = new App({ name: "Get Time App", version: "1.0.0" });
|
||||
|
||||
app.onteardown = async () => {
|
||||
log.info("App is being torn down");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate cleanup work
|
||||
log.info("App teardown complete");
|
||||
return {};
|
||||
};
|
||||
|
||||
// Register handlers BEFORE connecting
|
||||
app.ontoolinput = (params) => {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,36 @@ describe("App <-> AppBridge integration", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("sendToolCancelled triggers app.ontoolcancelled", async () => {
|
||||
const receivedCancellations: unknown[] = [];
|
||||
app.ontoolcancelled = (params) => {
|
||||
receivedCancellations.push(params);
|
||||
};
|
||||
|
||||
await app.connect(appTransport);
|
||||
await bridge.sendToolCancelled({
|
||||
reason: "User cancelled the operation",
|
||||
});
|
||||
|
||||
expect(receivedCancellations).toHaveLength(1);
|
||||
expect(receivedCancellations[0]).toEqual({
|
||||
reason: "User cancelled the operation",
|
||||
});
|
||||
});
|
||||
|
||||
it("sendToolCancelled works without reason", async () => {
|
||||
const receivedCancellations: unknown[] = [];
|
||||
app.ontoolcancelled = (params) => {
|
||||
receivedCancellations.push(params);
|
||||
};
|
||||
|
||||
await app.connect(appTransport);
|
||||
await bridge.sendToolCancelled({});
|
||||
|
||||
expect(receivedCancellations).toHaveLength(1);
|
||||
expect(receivedCancellations[0]).toEqual({});
|
||||
});
|
||||
|
||||
it("setHostContext triggers app.onhostcontextchanged", async () => {
|
||||
const receivedContexts: unknown[] = [];
|
||||
app.onhostcontextchanged = (params) => {
|
||||
|
|
@ -173,6 +203,34 @@ describe("App <-> AppBridge integration", () => {
|
|||
{ theme: "light" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("sendResourceTeardown triggers app.onteardown", async () => {
|
||||
let teardownCalled = false;
|
||||
app.onteardown = async () => {
|
||||
teardownCalled = true;
|
||||
return {};
|
||||
};
|
||||
|
||||
await app.connect(appTransport);
|
||||
await bridge.sendResourceTeardown({});
|
||||
|
||||
expect(teardownCalled).toBe(true);
|
||||
});
|
||||
|
||||
it("sendResourceTeardown waits for async cleanup", async () => {
|
||||
const cleanupSteps: string[] = [];
|
||||
app.onteardown = async () => {
|
||||
cleanupSteps.push("start");
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
cleanupSteps.push("done");
|
||||
return {};
|
||||
};
|
||||
|
||||
await app.connect(appTransport);
|
||||
await bridge.sendResourceTeardown({});
|
||||
|
||||
expect(cleanupSteps).toEqual(["start", "done"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("App -> Host notifications", () => {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
import {
|
||||
type McpUiSandboxResourceReadyNotification,
|
||||
type McpUiSizeChangedNotification,
|
||||
type McpUiToolCancelledNotification,
|
||||
type McpUiToolInputNotification,
|
||||
type McpUiToolInputPartialNotification,
|
||||
type McpUiToolResultNotification,
|
||||
|
|
@ -737,6 +738,43 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the Guest UI that tool execution was cancelled.
|
||||
*
|
||||
* The host MUST send this notification if tool execution was cancelled for any
|
||||
* reason, including user action, sampling error, classifier intervention, or
|
||||
* any other interruption. This allows the Guest UI to update its state and
|
||||
* display appropriate feedback to the user.
|
||||
*
|
||||
* @param params - Optional cancellation details:
|
||||
* - `reason`: Human-readable explanation for why the tool was cancelled
|
||||
*
|
||||
* @example User-initiated cancellation
|
||||
* ```typescript
|
||||
* // User clicked "Cancel" button
|
||||
* bridge.sendToolCancelled({ reason: "User cancelled the operation" });
|
||||
* ```
|
||||
*
|
||||
* @example System-level cancellation
|
||||
* ```typescript
|
||||
* // Sampling error or timeout
|
||||
* bridge.sendToolCancelled({ reason: "Request timeout after 30 seconds" });
|
||||
*
|
||||
* // Classifier intervention
|
||||
* bridge.sendToolCancelled({ reason: "Content policy violation detected" });
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiToolCancelledNotification} for the notification type
|
||||
* @see {@link sendToolResult} for sending successful results
|
||||
* @see {@link sendToolInput} for sending tool arguments
|
||||
*/
|
||||
sendToolCancelled(params: McpUiToolCancelledNotification["params"]) {
|
||||
return this.notification(<McpUiToolCancelledNotification>{
|
||||
method: "ui/notifications/tool-cancelled",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send HTML resource to the sandbox proxy for secure loading.
|
||||
*
|
||||
|
|
|
|||
83
src/app.ts
83
src/app.ts
|
|
@ -31,7 +31,12 @@ import {
|
|||
McpUiMessageResultSchema,
|
||||
McpUiOpenLinkRequest,
|
||||
McpUiOpenLinkResultSchema,
|
||||
McpUiResourceTeardownRequest,
|
||||
McpUiResourceTeardownRequestSchema,
|
||||
McpUiResourceTeardownResult,
|
||||
McpUiSizeChangedNotification,
|
||||
McpUiToolCancelledNotification,
|
||||
McpUiToolCancelledNotificationSchema,
|
||||
McpUiToolInputNotification,
|
||||
McpUiToolInputNotificationSchema,
|
||||
McpUiToolInputPartialNotification,
|
||||
|
|
@ -388,6 +393,41 @@ export class App extends Protocol<Request, Notification, Result> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience handler for receiving tool cancellation notifications from the host.
|
||||
*
|
||||
* Set this property to register a handler that will be called when the host
|
||||
* notifies that tool execution was cancelled. This can occur for various reasons
|
||||
* including user action, sampling error, classifier intervention, or other
|
||||
* interruptions. Apps should update their state and display appropriate feedback.
|
||||
*
|
||||
* This setter is a convenience wrapper around `setNotificationHandler()` that
|
||||
* automatically handles the notification schema and extracts the params for you.
|
||||
*
|
||||
* Register handlers before calling {@link connect} to avoid missing notifications.
|
||||
*
|
||||
* @param callback - Function called when tool execution is cancelled
|
||||
*
|
||||
* @example Handle tool cancellation
|
||||
* ```typescript
|
||||
* app.ontoolcancelled = (params) => {
|
||||
* console.log("Tool cancelled:", params.reason);
|
||||
* showCancelledMessage(params.reason ?? "Operation was cancelled");
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link setNotificationHandler} for the underlying method
|
||||
* @see {@link McpUiToolCancelledNotification} for the notification structure
|
||||
* @see {@link ontoolresult} for successful tool completion
|
||||
*/
|
||||
set ontoolcancelled(
|
||||
callback: (params: McpUiToolCancelledNotification["params"]) => void,
|
||||
) {
|
||||
this.setNotificationHandler(McpUiToolCancelledNotificationSchema, (n) =>
|
||||
callback(n.params),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience handler for host context changes (theme, viewport, locale, etc.).
|
||||
*
|
||||
|
|
@ -427,6 +467,48 @@ export class App extends Protocol<Request, Notification, Result> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience handler for graceful shutdown requests from the host.
|
||||
*
|
||||
* Set this property to register a handler that will be called when the host
|
||||
* requests the app to prepare for teardown. This allows the app to perform
|
||||
* cleanup operations (save state, close connections, etc.) before being unmounted.
|
||||
*
|
||||
* The handler can be sync or async. The host will wait for the returned promise
|
||||
* to resolve before proceeding with teardown.
|
||||
*
|
||||
* This setter is a convenience wrapper around `setRequestHandler()` that
|
||||
* automatically handles the request schema.
|
||||
*
|
||||
* Register handlers before calling {@link connect} to avoid missing requests.
|
||||
*
|
||||
* @param callback - Function called when teardown is requested.
|
||||
* Can return void or a Promise that resolves when cleanup is complete.
|
||||
*
|
||||
* @example Perform cleanup before teardown
|
||||
* ```typescript
|
||||
* app.onteardown = async () => {
|
||||
* await saveState();
|
||||
* closeConnections();
|
||||
* console.log("App ready for teardown");
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @see {@link setRequestHandler} for the underlying method
|
||||
* @see {@link McpUiResourceTeardownRequest} for the request structure
|
||||
*/
|
||||
set onteardown(
|
||||
callback: (
|
||||
params: McpUiResourceTeardownRequest["params"],
|
||||
extra: RequestHandlerExtra,
|
||||
) => McpUiResourceTeardownResult | Promise<McpUiResourceTeardownResult>,
|
||||
) {
|
||||
this.setRequestHandler(
|
||||
McpUiResourceTeardownRequestSchema,
|
||||
(request, extra) => callback(request.params, extra),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience handler for tool call requests from the host.
|
||||
*
|
||||
|
|
@ -533,6 +615,7 @@ export class App extends Protocol<Request, Notification, Result> {
|
|||
}
|
||||
return;
|
||||
case "ping":
|
||||
case "ui/resource-teardown":
|
||||
return;
|
||||
default:
|
||||
throw new Error(`No handler for method ${method} registered`);
|
||||
|
|
|
|||
|
|
@ -1732,6 +1732,28 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"McpUiToolCancelledNotification": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"type": "string",
|
||||
"const": "ui/notifications/tool-cancelled"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"description": "Optional reason for the cancellation (e.g., \"user action\", \"timeout\").",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["method", "params"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"McpUiToolInputNotification": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ export type McpUiToolInputPartialNotificationSchemaInferredType = z.infer<
|
|||
typeof generated.McpUiToolInputPartialNotificationSchema
|
||||
>;
|
||||
|
||||
export type McpUiToolCancelledNotificationSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiToolCancelledNotificationSchema
|
||||
>;
|
||||
|
||||
export type McpUiResourceTeardownRequestSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiResourceTeardownRequestSchema
|
||||
>;
|
||||
|
|
@ -151,6 +155,12 @@ expectType<spec.McpUiToolInputPartialNotification>(
|
|||
expectType<McpUiToolInputPartialNotificationSchemaInferredType>(
|
||||
{} as spec.McpUiToolInputPartialNotification,
|
||||
);
|
||||
expectType<spec.McpUiToolCancelledNotification>(
|
||||
{} as McpUiToolCancelledNotificationSchemaInferredType,
|
||||
);
|
||||
expectType<McpUiToolCancelledNotificationSchemaInferredType>(
|
||||
{} as spec.McpUiToolCancelledNotification,
|
||||
);
|
||||
expectType<spec.McpUiResourceTeardownRequest>(
|
||||
{} as McpUiResourceTeardownRequestSchemaInferredType,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -160,6 +160,24 @@ export const McpUiToolInputPartialNotificationSchema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Notification that tool execution was cancelled (Host -> Guest UI).
|
||||
* Host MUST send this if tool execution was cancelled for any reason (user action,
|
||||
* sampling error, classifier intervention, etc.).
|
||||
*/
|
||||
export const McpUiToolCancelledNotificationSchema = z.object({
|
||||
method: z.literal("ui/notifications/tool-cancelled"),
|
||||
params: z.object({
|
||||
/** @description Optional reason for the cancellation (e.g., "user action", "timeout"). */
|
||||
reason: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional reason for the cancellation (e.g., "user action", "timeout").',
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Request for graceful shutdown of the Guest UI (Host -> Guest UI).
|
||||
* @see {@link app-bridge.AppBridge.sendResourceTeardown} for the host method that sends this
|
||||
|
|
|
|||
|
|
@ -167,6 +167,19 @@ export interface McpUiToolResultNotification {
|
|||
params: CallToolResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Notification that tool execution was cancelled (Host -> Guest UI).
|
||||
* Host MUST send this if tool execution was cancelled for any reason (user action,
|
||||
* sampling error, classifier intervention, etc.).
|
||||
*/
|
||||
export interface McpUiToolCancelledNotification {
|
||||
method: "ui/notifications/tool-cancelled";
|
||||
params: {
|
||||
/** @description Optional reason for the cancellation (e.g., "user action", "timeout"). */
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Rich context about the host environment provided to Guest UIs.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export {
|
|||
type McpUiToolInputNotification,
|
||||
type McpUiToolInputPartialNotification,
|
||||
type McpUiToolResultNotification,
|
||||
type McpUiToolCancelledNotification,
|
||||
type McpUiHostContext,
|
||||
type McpUiHostContextChangedNotification,
|
||||
type McpUiResourceTeardownRequest,
|
||||
|
|
@ -51,6 +52,7 @@ export {
|
|||
McpUiToolInputNotificationSchema,
|
||||
McpUiToolInputPartialNotificationSchema,
|
||||
McpUiToolResultNotificationSchema,
|
||||
McpUiToolCancelledNotificationSchema,
|
||||
McpUiHostContextSchema,
|
||||
McpUiHostContextChangedNotificationSchema,
|
||||
McpUiResourceTeardownRequestSchema,
|
||||
|
|
|
|||
Loading…
Reference in a new issue