Add App <-> AppBridge tests, setHostContext & sendToolInputPartial (#55)
* Add App <-> AppBridge integration tests - Add minimalistic tests validating communication between App (guest) and AppBridge (host) - Test initialization handshake flow - Test Host -> App notifications (toolInput, toolInputPartial, toolResult, hostContext) - Test App -> Host notifications (sizeChange, log) - Test App -> Host requests (sendMessage, sendOpenLink) - Test ping functionality - Add setHostContext and sendToolInputPartial to AppBridge - Add test script to package.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add JSDoc for setHostContext and sendToolInputPartial 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: add @types/bun for test type checking - Add @types/bun devDependency for bun:test types - Fix mock client request return type 🤖 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
a7d73c72e0
commit
cf4492df89
3 changed files with 406 additions and 9 deletions
|
|
@ -30,6 +30,7 @@
|
||||||
"start:example-mcp-server": "cd examples/simple-server && npm start",
|
"start:example-mcp-server": "cd examples/simple-server && npm start",
|
||||||
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:example-host' 'npm run start:example-mcp-server'",
|
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:example-host' 'npm run start:example-mcp-server'",
|
||||||
"build": "bun build.bun.ts",
|
"build": "bun build.bun.ts",
|
||||||
|
"test": "bun test",
|
||||||
"prepare": "npm run build && husky",
|
"prepare": "npm run build && husky",
|
||||||
"docs": "typedoc",
|
"docs": "typedoc",
|
||||||
"docs:watch": "typedoc --watch",
|
"docs:watch": "typedoc --watch",
|
||||||
|
|
@ -39,6 +40,7 @@
|
||||||
},
|
},
|
||||||
"author": "Olivier Chafik",
|
"author": "Olivier Chafik",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.3.2",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
|
|
|
||||||
297
src/app-bridge.test.ts
Normal file
297
src/app-bridge.test.ts
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||||
|
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { EmptyResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
import { App } from "./app";
|
||||||
|
import { AppBridge, type McpUiHostCapabilities } from "./app-bridge";
|
||||||
|
|
||||||
|
/** Wait for pending microtasks to complete */
|
||||||
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a minimal mock MCP client for testing AppBridge.
|
||||||
|
* Only implements methods that AppBridge calls.
|
||||||
|
*/
|
||||||
|
function createMockClient(
|
||||||
|
serverCapabilities: ServerCapabilities = {},
|
||||||
|
): Pick<Client, "getServerCapabilities" | "request" | "notification"> {
|
||||||
|
return {
|
||||||
|
getServerCapabilities: () => serverCapabilities,
|
||||||
|
request: async () => ({}) as never,
|
||||||
|
notification: async () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const testHostInfo = { name: "TestHost", version: "1.0.0" };
|
||||||
|
const testAppInfo = { name: "TestApp", version: "1.0.0" };
|
||||||
|
const testHostCapabilities: McpUiHostCapabilities = {
|
||||||
|
openLinks: {},
|
||||||
|
serverTools: {},
|
||||||
|
logging: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("App <-> AppBridge integration", () => {
|
||||||
|
let app: App;
|
||||||
|
let bridge: AppBridge;
|
||||||
|
let appTransport: InMemoryTransport;
|
||||||
|
let bridgeTransport: InMemoryTransport;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
[appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair();
|
||||||
|
app = new App(testAppInfo, {}, { autoResize: false });
|
||||||
|
bridge = new AppBridge(
|
||||||
|
createMockClient() as Client,
|
||||||
|
testHostInfo,
|
||||||
|
testHostCapabilities,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await appTransport.close();
|
||||||
|
await bridgeTransport.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("initialization handshake", () => {
|
||||||
|
it("App.connect() triggers bridge.oninitialized", async () => {
|
||||||
|
let initializedFired = false;
|
||||||
|
|
||||||
|
bridge.oninitialized = () => {
|
||||||
|
initializedFired = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
await bridge.connect(bridgeTransport);
|
||||||
|
await app.connect(appTransport);
|
||||||
|
|
||||||
|
expect(initializedFired).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("App receives host info and capabilities after connect", async () => {
|
||||||
|
await bridge.connect(bridgeTransport);
|
||||||
|
await app.connect(appTransport);
|
||||||
|
|
||||||
|
const hostInfo = app.getHostVersion();
|
||||||
|
expect(hostInfo).toEqual(testHostInfo);
|
||||||
|
|
||||||
|
const hostCaps = app.getHostCapabilities();
|
||||||
|
expect(hostCaps).toEqual(testHostCapabilities);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Bridge receives app info and capabilities after initialization", async () => {
|
||||||
|
const appCapabilities = { tools: { listChanged: true } };
|
||||||
|
app = new App(testAppInfo, appCapabilities, { autoResize: false });
|
||||||
|
|
||||||
|
await bridge.connect(bridgeTransport);
|
||||||
|
await app.connect(appTransport);
|
||||||
|
|
||||||
|
const appInfo = bridge.getAppVersion();
|
||||||
|
expect(appInfo).toEqual(testAppInfo);
|
||||||
|
|
||||||
|
const appCaps = bridge.getAppCapabilities();
|
||||||
|
expect(appCaps).toEqual(appCapabilities);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Host -> App notifications", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await bridge.connect(bridgeTransport);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sendToolInput triggers app.ontoolinput", async () => {
|
||||||
|
const receivedArgs: unknown[] = [];
|
||||||
|
app.ontoolinput = (params) => {
|
||||||
|
receivedArgs.push(params.arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
await bridge.sendToolInput({ arguments: { location: "NYC" } });
|
||||||
|
|
||||||
|
expect(receivedArgs).toEqual([{ location: "NYC" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sendToolInputPartial triggers app.ontoolinputpartial", async () => {
|
||||||
|
const receivedArgs: unknown[] = [];
|
||||||
|
app.ontoolinputpartial = (params) => {
|
||||||
|
receivedArgs.push(params.arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
await bridge.sendToolInputPartial({ arguments: { loc: "N" } });
|
||||||
|
await bridge.sendToolInputPartial({ arguments: { location: "NYC" } });
|
||||||
|
|
||||||
|
expect(receivedArgs).toEqual([{ loc: "N" }, { location: "NYC" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sendToolResult triggers app.ontoolresult", async () => {
|
||||||
|
const receivedResults: unknown[] = [];
|
||||||
|
app.ontoolresult = (params) => {
|
||||||
|
receivedResults.push(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
await bridge.sendToolResult({
|
||||||
|
content: [{ type: "text", text: "Weather: Sunny" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(receivedResults).toHaveLength(1);
|
||||||
|
expect(receivedResults[0]).toEqual({
|
||||||
|
content: [{ type: "text", text: "Weather: Sunny" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setHostContext triggers app.onhostcontextchanged", async () => {
|
||||||
|
const receivedContexts: unknown[] = [];
|
||||||
|
app.onhostcontextchanged = (params) => {
|
||||||
|
receivedContexts.push(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
bridge.setHostContext({ theme: "dark" });
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(receivedContexts).toEqual([{ theme: "dark" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("setHostContext only sends changed values", async () => {
|
||||||
|
const receivedContexts: unknown[] = [];
|
||||||
|
app.onhostcontextchanged = (params) => {
|
||||||
|
receivedContexts.push(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
|
||||||
|
bridge.setHostContext({ theme: "dark", locale: "en-US" });
|
||||||
|
await flush();
|
||||||
|
bridge.setHostContext({ theme: "dark", locale: "en-US" }); // No change
|
||||||
|
await flush();
|
||||||
|
bridge.setHostContext({ theme: "light", locale: "en-US" }); // Only theme changed
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
expect(receivedContexts).toEqual([
|
||||||
|
{ theme: "dark", locale: "en-US" },
|
||||||
|
{ theme: "light" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("App -> Host notifications", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await bridge.connect(bridgeTransport);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("app.sendSizeChange triggers bridge.onsizechange", async () => {
|
||||||
|
const receivedSizes: unknown[] = [];
|
||||||
|
bridge.onsizechange = (params) => {
|
||||||
|
receivedSizes.push(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
await app.sendSizeChange({ width: 400, height: 600 });
|
||||||
|
|
||||||
|
expect(receivedSizes).toEqual([{ width: 400, height: 600 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("app.sendLog triggers bridge.onloggingmessage", async () => {
|
||||||
|
const receivedLogs: unknown[] = [];
|
||||||
|
bridge.onloggingmessage = (params) => {
|
||||||
|
receivedLogs.push(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
await app.sendLog({
|
||||||
|
level: "info",
|
||||||
|
data: "Test log message",
|
||||||
|
logger: "TestApp",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(receivedLogs).toHaveLength(1);
|
||||||
|
expect(receivedLogs[0]).toMatchObject({
|
||||||
|
level: "info",
|
||||||
|
data: "Test log message",
|
||||||
|
logger: "TestApp",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("App -> Host requests", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await bridge.connect(bridgeTransport);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("app.sendMessage triggers bridge.onmessage and returns result", async () => {
|
||||||
|
const receivedMessages: unknown[] = [];
|
||||||
|
bridge.onmessage = async (params) => {
|
||||||
|
receivedMessages.push(params);
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
const result = await app.sendMessage({
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Hello from app" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(receivedMessages).toHaveLength(1);
|
||||||
|
expect(receivedMessages[0]).toMatchObject({
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Hello from app" }],
|
||||||
|
});
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("app.sendMessage returns error result when handler indicates error", async () => {
|
||||||
|
bridge.onmessage = async () => {
|
||||||
|
return { isError: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
const result = await app.sendMessage({
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "Test" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("app.sendOpenLink triggers bridge.onopenlink and returns result", async () => {
|
||||||
|
const receivedLinks: string[] = [];
|
||||||
|
bridge.onopenlink = async (params) => {
|
||||||
|
receivedLinks.push(params.url);
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
const result = await app.sendOpenLink({ url: "https://example.com" });
|
||||||
|
|
||||||
|
expect(receivedLinks).toEqual(["https://example.com"]);
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("app.sendOpenLink returns error when host denies", async () => {
|
||||||
|
bridge.onopenlink = async () => {
|
||||||
|
return { isError: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
await app.connect(appTransport);
|
||||||
|
const result = await app.sendOpenLink({ url: "https://blocked.com" });
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ping", () => {
|
||||||
|
it("App responds to ping from bridge", async () => {
|
||||||
|
await bridge.connect(bridgeTransport);
|
||||||
|
await app.connect(appTransport);
|
||||||
|
|
||||||
|
// Bridge can send ping via the protocol's request method
|
||||||
|
const result = await bridge.request(
|
||||||
|
{ method: "ping", params: {} },
|
||||||
|
EmptyResultSchema,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -12,18 +12,18 @@ import {
|
||||||
ListResourcesResultSchema,
|
ListResourcesResultSchema,
|
||||||
ListResourceTemplatesRequestSchema,
|
ListResourceTemplatesRequestSchema,
|
||||||
ListResourceTemplatesResultSchema,
|
ListResourceTemplatesResultSchema,
|
||||||
ReadResourceRequestSchema,
|
|
||||||
ReadResourceResultSchema,
|
|
||||||
LoggingMessageNotification,
|
LoggingMessageNotification,
|
||||||
LoggingMessageNotificationSchema,
|
LoggingMessageNotificationSchema,
|
||||||
Notification,
|
Notification,
|
||||||
PingRequest,
|
PingRequest,
|
||||||
PingRequestSchema,
|
PingRequestSchema,
|
||||||
PromptListChangedNotificationSchema,
|
PromptListChangedNotificationSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
ReadResourceResultSchema,
|
||||||
|
Request,
|
||||||
ResourceListChangedNotificationSchema,
|
ResourceListChangedNotificationSchema,
|
||||||
Result,
|
Result,
|
||||||
ToolListChangedNotificationSchema,
|
ToolListChangedNotificationSchema,
|
||||||
Request,
|
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import {
|
import {
|
||||||
Protocol,
|
Protocol,
|
||||||
|
|
@ -32,13 +32,16 @@ import {
|
||||||
} from "@modelcontextprotocol/sdk/shared/protocol.js";
|
} from "@modelcontextprotocol/sdk/shared/protocol.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type McpUiToolInputNotification,
|
|
||||||
type McpUiToolResultNotification,
|
|
||||||
type McpUiSandboxResourceReadyNotification,
|
type McpUiSandboxResourceReadyNotification,
|
||||||
type McpUiSizeChangeNotification,
|
type McpUiSizeChangeNotification,
|
||||||
|
type McpUiToolInputNotification,
|
||||||
|
type McpUiToolInputPartialNotification,
|
||||||
|
type McpUiToolResultNotification,
|
||||||
LATEST_PROTOCOL_VERSION,
|
LATEST_PROTOCOL_VERSION,
|
||||||
McpUiAppCapabilities,
|
McpUiAppCapabilities,
|
||||||
McpUiHostCapabilities,
|
McpUiHostCapabilities,
|
||||||
|
McpUiHostContext,
|
||||||
|
McpUiHostContextChangedNotification,
|
||||||
McpUiInitializedNotification,
|
McpUiInitializedNotification,
|
||||||
McpUiInitializedNotificationSchema,
|
McpUiInitializedNotificationSchema,
|
||||||
McpUiInitializeRequest,
|
McpUiInitializeRequest,
|
||||||
|
|
@ -64,7 +67,9 @@ export { PostMessageTransport } from "./message-transport";
|
||||||
*
|
*
|
||||||
* @see ProtocolOptions from @modelcontextprotocol/sdk for available options
|
* @see ProtocolOptions from @modelcontextprotocol/sdk for available options
|
||||||
*/
|
*/
|
||||||
export type HostOptions = ProtocolOptions;
|
export type HostOptions = ProtocolOptions & {
|
||||||
|
hostContext?: McpUiHostContext;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Protocol versions supported by this AppBridge implementation.
|
* Protocol versions supported by this AppBridge implementation.
|
||||||
|
|
@ -149,6 +154,7 @@ type RequestHandlerExtra = Parameters<
|
||||||
export class AppBridge extends Protocol<Request, Notification, Result> {
|
export class AppBridge extends Protocol<Request, Notification, Result> {
|
||||||
private _appCapabilities?: McpUiAppCapabilities;
|
private _appCapabilities?: McpUiAppCapabilities;
|
||||||
private _appInfo?: Implementation;
|
private _appInfo?: Implementation;
|
||||||
|
private _hostContext: McpUiHostContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new AppBridge instance.
|
* Create a new AppBridge instance.
|
||||||
|
|
@ -175,6 +181,8 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
|
||||||
) {
|
) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
|
this._hostContext = options?.hostContext || {};
|
||||||
|
|
||||||
this.setRequestHandler(McpUiInitializeRequestSchema, (request) =>
|
this.setRequestHandler(McpUiInitializeRequestSchema, (request) =>
|
||||||
this._oninitialize(request),
|
this._oninitialize(request),
|
||||||
);
|
);
|
||||||
|
|
@ -550,12 +558,64 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
|
||||||
protocolVersion,
|
protocolVersion,
|
||||||
hostCapabilities: this.getCapabilities(),
|
hostCapabilities: this.getCapabilities(),
|
||||||
hostInfo: this._hostInfo,
|
hostInfo: this._hostInfo,
|
||||||
hostContext: {
|
hostContext: this._hostContext,
|
||||||
// TODO
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the host context and notify the Guest UI of changes.
|
||||||
|
*
|
||||||
|
* Compares the new context with the current context and sends a
|
||||||
|
* `ui/notifications/host-context-changed` notification containing only the
|
||||||
|
* fields that have changed. If no fields have changed, no notification is sent.
|
||||||
|
*
|
||||||
|
* Common use cases include notifying the Guest UI when:
|
||||||
|
* - Theme changes (light/dark mode toggle)
|
||||||
|
* - Viewport size changes (window resize)
|
||||||
|
* - Display mode changes (inline/fullscreen)
|
||||||
|
* - Locale or timezone changes
|
||||||
|
*
|
||||||
|
* @param hostContext - The complete new host context state
|
||||||
|
*
|
||||||
|
* @example Update theme when user toggles dark mode
|
||||||
|
* ```typescript
|
||||||
|
* bridge.setHostContext({ theme: "dark" });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example Update multiple context fields
|
||||||
|
* ```typescript
|
||||||
|
* bridge.setHostContext({
|
||||||
|
* theme: "dark",
|
||||||
|
* viewport: { width: 800, height: 600 }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @see {@link McpUiHostContext} for the context structure
|
||||||
|
* @see {@link McpUiHostContextChangedNotification} for the notification type
|
||||||
|
*/
|
||||||
|
setHostContext(hostContext: McpUiHostContext) {
|
||||||
|
const changes: McpUiHostContext = {};
|
||||||
|
let hasChanges = false;
|
||||||
|
for (const key of Object.keys(hostContext) as Array<
|
||||||
|
keyof McpUiHostContext
|
||||||
|
>) {
|
||||||
|
const oldValue = this._hostContext[key];
|
||||||
|
const newValue = hostContext[key];
|
||||||
|
if (deepEqual(oldValue, newValue)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changes[key] = newValue as any;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
if (hasChanges) {
|
||||||
|
this._hostContext = hostContext;
|
||||||
|
this.notification((<McpUiHostContextChangedNotification>{
|
||||||
|
method: "ui/notifications/host-context-changed",
|
||||||
|
params: changes,
|
||||||
|
}) as Notification); // Cast needed because McpUiHostContext is a params type that doesn't allow arbitrary keys.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send complete tool arguments to the Guest UI.
|
* Send complete tool arguments to the Guest UI.
|
||||||
*
|
*
|
||||||
|
|
@ -585,6 +645,40 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send streaming partial tool arguments to the Guest UI.
|
||||||
|
*
|
||||||
|
* The host MAY send this notification zero or more times while tool arguments
|
||||||
|
* are being streamed, before {@link sendToolInput} is called with complete
|
||||||
|
* arguments. This enables progressive rendering of tool arguments in the
|
||||||
|
* Guest UI.
|
||||||
|
*
|
||||||
|
* The arguments represent best-effort recovery of incomplete JSON. Guest UIs
|
||||||
|
* SHOULD handle missing or changing fields gracefully between notifications.
|
||||||
|
*
|
||||||
|
* @param params - Partial tool call arguments (may be incomplete)
|
||||||
|
*
|
||||||
|
* @example Stream partial arguments as they arrive
|
||||||
|
* ```typescript
|
||||||
|
* // As streaming progresses...
|
||||||
|
* bridge.sendToolInputPartial({ arguments: { loc: "N" } });
|
||||||
|
* bridge.sendToolInputPartial({ arguments: { location: "New" } });
|
||||||
|
* bridge.sendToolInputPartial({ arguments: { location: "New York" } });
|
||||||
|
*
|
||||||
|
* // When complete, send final input
|
||||||
|
* bridge.sendToolInput({ arguments: { location: "New York", units: "metric" } });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @see {@link McpUiToolInputPartialNotification} for the notification type
|
||||||
|
* @see {@link sendToolInput} for sending complete arguments
|
||||||
|
*/
|
||||||
|
sendToolInputPartial(params: McpUiToolInputPartialNotification["params"]) {
|
||||||
|
return this.notification(<McpUiToolInputPartialNotification>{
|
||||||
|
method: "ui/notifications/tool-input-partial",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send tool execution result to the Guest UI.
|
* Send tool execution result to the Guest UI.
|
||||||
*
|
*
|
||||||
|
|
@ -780,3 +874,7 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
|
||||||
return super.connect(transport);
|
return super.connect(transport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deepEqual(a: any, b: any): boolean {
|
||||||
|
return JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue