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:
Olivier Chafik 2025-12-11 20:31:17 +01:00 • committed by GitHub
commit 70b393a7e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 379 additions and 15 deletions

View file

@ -186,11 +186,19 @@ export async function initializeApp(
log.info("Sending tool call input to MCP App:", input); log.info("Sending tool call input to MCP App:", input);
appBridge.sendToolInput({ arguments: input }); appBridge.sendToolInput({ arguments: input });
// Schedule tool call result to be sent to MCP App // Schedule tool call result (or cancellation) to be sent to MCP App
resultPromise.then((result) => { resultPromise.then(
log.info("Sending tool call result to MCP App:", result); (result) => {
appBridge.sendToolResult(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),
});
},
);
} }
/** /**

View file

@ -92,10 +92,32 @@
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.5rem;
position: relative;
.toolName { .toolName {
font-family: monospace; 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;
}
}
} }
} }

View file

@ -8,17 +8,41 @@ import styles from "./index.module.css";
interface HostProps { interface HostProps {
serversPromise: Promise<ServerInfo[]>; serversPromise: Promise<ServerInfo[]>;
} }
type ToolCallEntry = ToolCallInfo & { id: number };
let nextToolCallId = 0;
function Host({ serversPromise }: HostProps) { 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 ( return (
<> <>
{toolCalls.map((info, i) => ( {toolCalls.map((info) => (
<ToolCallInfoPanel key={i} toolCallInfo={info} /> <ToolCallInfoPanel
key={info.id}
toolCallInfo={info}
isDestroying={destroyingIds.has(info.id)}
onRequestClose={() => requestClose(info.id)}
onCloseComplete={() => completeClose(info.id)}
/>
))} ))}
<CallToolPanel <CallToolPanel
serversPromise={serversPromise} 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 { interface ToolCallInfoPanelProps {
toolCallInfo: ToolCallInfo; 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 ( return (
<div className={styles.toolCallInfoPanel}> <div
className={styles.toolCallInfoPanel}
style={isDestroying ? { opacity: 0.5, pointerEvents: "none" } : undefined}
>
<div className={styles.inputInfoPanel}> <div className={styles.inputInfoPanel}>
<h2> <h2>
<span>{toolCallInfo.serverInfo.name}</span> <span>{toolCallInfo.serverInfo.name}</span>
<span className={styles.toolName}>{toolCallInfo.tool.name}</span> <span className={styles.toolName}>{toolCallInfo.tool.name}</span>
{onRequestClose && !isDestroying && (
<button
className={styles.closeButton}
onClick={onRequestClose}
title="Close"
>
×
</button>
)}
</h2> </h2>
<JsonBlock value={toolCallInfo.input} /> <JsonBlock value={toolCallInfo.input} />
</div> </div>
@ -150,8 +198,12 @@ function ToolCallInfoPanel({ toolCallInfo }: ToolCallInfoPanelProps) {
<ErrorBoundary> <ErrorBoundary>
<Suspense fallback="Loading..."> <Suspense fallback="Loading...">
{ {
hasAppHtml(toolCallInfo) isApp
? <AppIFramePanel toolCallInfo={toolCallInfo} /> ? <AppIFramePanel
toolCallInfo={toolCallInfo}
isDestroying={isDestroying}
onTeardownComplete={onCloseComplete}
/>
: <ToolResultPanel toolCallInfo={toolCallInfo} /> : <ToolResultPanel toolCallInfo={toolCallInfo} />
} }
</Suspense> </Suspense>
@ -173,9 +225,12 @@ function JsonBlock({ value }: { value: object }) {
interface AppIFramePanelProps { interface AppIFramePanelProps {
toolCallInfo: Required<ToolCallInfo>; toolCallInfo: Required<ToolCallInfo>;
isDestroying?: boolean;
onTeardownComplete?: () => void;
} }
function AppIFramePanel({ toolCallInfo }: AppIFramePanelProps) { function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppIFramePanelProps) {
const iframeRef = useRef<HTMLIFrameElement | null>(null); const iframeRef = useRef<HTMLIFrameElement | null>(null);
const appBridgeRef = useRef<ReturnType<typeof newAppBridge> | null>(null);
useEffect(() => { useEffect(() => {
const iframe = iframeRef.current!; const iframe = iframeRef.current!;
@ -186,11 +241,34 @@ function AppIFramePanel({ toolCallInfo }: AppIFramePanelProps) {
// `toolCallInfo`. // `toolCallInfo`.
if (firstTime) { if (firstTime) {
const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe); const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe);
appBridgeRef.current = appBridge;
initializeApp(iframe, appBridge, toolCallInfo); initializeApp(iframe, appBridge, toolCallInfo);
} }
}); });
}, [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 ( return (
<div className={styles.appIframePanel}> <div className={styles.appIframePanel}>
<iframe ref={iframeRef} /> <iframe ref={iframeRef} />

View file

@ -1,7 +1,7 @@
/** /**
* @file App that demonstrates a few features using MCP Apps SDK + React. * @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 { useApp } from "@modelcontextprotocol/ext-apps/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useCallback, useEffect, useState } from "react"; import { StrictMode, useCallback, useEffect, useState } from "react";
@ -35,6 +35,12 @@ function GetTimeApp() {
appInfo: IMPLEMENTATION, appInfo: IMPLEMENTATION,
capabilities: {}, capabilities: {},
onAppCreated: (app) => { 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) => { app.ontoolinput = async (input) => {
log.info("Received tool call input:", input); log.info("Received tool call input:", input);
}; };

View file

@ -38,6 +38,12 @@ const openLinkBtn = document.getElementById("open-link-btn")!;
// Create app instance // Create app instance
const app = new App({ name: "Get Time App", version: "1.0.0" }); 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 // Register handlers BEFORE connecting
app.ontoolinput = (params) => { app.ontoolinput = (params) => {

View file

@ -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 () => { it("setHostContext triggers app.onhostcontextchanged", async () => {
const receivedContexts: unknown[] = []; const receivedContexts: unknown[] = [];
app.onhostcontextchanged = (params) => { app.onhostcontextchanged = (params) => {
@ -173,6 +203,34 @@ describe("App <-> AppBridge integration", () => {
{ theme: "light" }, { 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", () => { describe("App -> Host notifications", () => {

View file

@ -34,6 +34,7 @@ import {
import { import {
type McpUiSandboxResourceReadyNotification, type McpUiSandboxResourceReadyNotification,
type McpUiSizeChangedNotification, type McpUiSizeChangedNotification,
type McpUiToolCancelledNotification,
type McpUiToolInputNotification, type McpUiToolInputNotification,
type McpUiToolInputPartialNotification, type McpUiToolInputPartialNotification,
type McpUiToolResultNotification, 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. * Send HTML resource to the sandbox proxy for secure loading.
* *

View file

@ -31,7 +31,12 @@ import {
McpUiMessageResultSchema, McpUiMessageResultSchema,
McpUiOpenLinkRequest, McpUiOpenLinkRequest,
McpUiOpenLinkResultSchema, McpUiOpenLinkResultSchema,
McpUiResourceTeardownRequest,
McpUiResourceTeardownRequestSchema,
McpUiResourceTeardownResult,
McpUiSizeChangedNotification, McpUiSizeChangedNotification,
McpUiToolCancelledNotification,
McpUiToolCancelledNotificationSchema,
McpUiToolInputNotification, McpUiToolInputNotification,
McpUiToolInputNotificationSchema, McpUiToolInputNotificationSchema,
McpUiToolInputPartialNotification, 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.). * 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. * Convenience handler for tool call requests from the host.
* *
@ -533,6 +615,7 @@ export class App extends Protocol<Request, Notification, Result> {
} }
return; return;
case "ping": case "ping":
case "ui/resource-teardown":
return; return;
default: default:
throw new Error(`No handler for method ${method} registered`); throw new Error(`No handler for method ${method} registered`);

View file

@ -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": { "McpUiToolInputNotification": {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object", "type": "object",

View file

@ -51,6 +51,10 @@ export type McpUiToolInputPartialNotificationSchemaInferredType = z.infer<
typeof generated.McpUiToolInputPartialNotificationSchema typeof generated.McpUiToolInputPartialNotificationSchema
>; >;
export type McpUiToolCancelledNotificationSchemaInferredType = z.infer<
typeof generated.McpUiToolCancelledNotificationSchema
>;
export type McpUiResourceTeardownRequestSchemaInferredType = z.infer< export type McpUiResourceTeardownRequestSchemaInferredType = z.infer<
typeof generated.McpUiResourceTeardownRequestSchema typeof generated.McpUiResourceTeardownRequestSchema
>; >;
@ -151,6 +155,12 @@ expectType<spec.McpUiToolInputPartialNotification>(
expectType<McpUiToolInputPartialNotificationSchemaInferredType>( expectType<McpUiToolInputPartialNotificationSchemaInferredType>(
{} as spec.McpUiToolInputPartialNotification, {} as spec.McpUiToolInputPartialNotification,
); );
expectType<spec.McpUiToolCancelledNotification>(
{} as McpUiToolCancelledNotificationSchemaInferredType,
);
expectType<McpUiToolCancelledNotificationSchemaInferredType>(
{} as spec.McpUiToolCancelledNotification,
);
expectType<spec.McpUiResourceTeardownRequest>( expectType<spec.McpUiResourceTeardownRequest>(
{} as McpUiResourceTeardownRequestSchemaInferredType, {} as McpUiResourceTeardownRequestSchemaInferredType,
); );

View file

@ -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). * @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 * @see {@link app-bridge.AppBridge.sendResourceTeardown} for the host method that sends this

View file

@ -167,6 +167,19 @@ export interface McpUiToolResultNotification {
params: CallToolResult; 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. * @description Rich context about the host environment provided to Guest UIs.
*/ */

View file

@ -24,6 +24,7 @@ export {
type McpUiToolInputNotification, type McpUiToolInputNotification,
type McpUiToolInputPartialNotification, type McpUiToolInputPartialNotification,
type McpUiToolResultNotification, type McpUiToolResultNotification,
type McpUiToolCancelledNotification,
type McpUiHostContext, type McpUiHostContext,
type McpUiHostContextChangedNotification, type McpUiHostContextChangedNotification,
type McpUiResourceTeardownRequest, type McpUiResourceTeardownRequest,
@ -51,6 +52,7 @@ export {
McpUiToolInputNotificationSchema, McpUiToolInputNotificationSchema,
McpUiToolInputPartialNotificationSchema, McpUiToolInputPartialNotificationSchema,
McpUiToolResultNotificationSchema, McpUiToolResultNotificationSchema,
McpUiToolCancelledNotificationSchema,
McpUiHostContextSchema, McpUiHostContextSchema,
McpUiHostContextChangedNotificationSchema, McpUiHostContextChangedNotificationSchema,
McpUiResourceTeardownRequestSchema, McpUiResourceTeardownRequestSchema,