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:
Olivier Chafik 2025-12-01 22:18:31 +00:00 • committed by GitHub
commit cf4492df89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -12,18 +12,18 @@ import {
ListResourcesResultSchema,
ListResourceTemplatesRequestSchema,
ListResourceTemplatesResultSchema,
ReadResourceRequestSchema,
ReadResourceResultSchema,
LoggingMessageNotification,
LoggingMessageNotificationSchema,
Notification,
PingRequest,
PingRequestSchema,
PromptListChangedNotificationSchema,
ReadResourceRequestSchema,
ReadResourceResultSchema,
Request,
ResourceListChangedNotificationSchema,
Result,
ToolListChangedNotificationSchema,
Request,
} from "@modelcontextprotocol/sdk/types.js";
import {
Protocol,
@ -32,13 +32,16 @@ import {
} from "@modelcontextprotocol/sdk/shared/protocol.js";
import {
type McpUiToolInputNotification,
type McpUiToolResultNotification,
type McpUiSandboxResourceReadyNotification,
type McpUiSizeChangeNotification,
type McpUiToolInputNotification,
type McpUiToolInputPartialNotification,
type McpUiToolResultNotification,
LATEST_PROTOCOL_VERSION,
McpUiAppCapabilities,
McpUiHostCapabilities,
McpUiHostContext,
McpUiHostContextChangedNotification,
McpUiInitializedNotification,
McpUiInitializedNotificationSchema,
McpUiInitializeRequest,
@ -64,7 +67,9 @@ export { PostMessageTransport } from "./message-transport";
*
* @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.
@ -149,6 +154,7 @@ type RequestHandlerExtra = Parameters<
export class AppBridge extends Protocol<Request, Notification, Result> {
private _appCapabilities?: McpUiAppCapabilities;
private _appInfo?: Implementation;
private _hostContext: McpUiHostContext;
/**
* Create a new AppBridge instance.
@ -175,6 +181,8 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
) {
super(options);
this._hostContext = options?.hostContext || {};
this.setRequestHandler(McpUiInitializeRequestSchema, (request) =>
this._oninitialize(request),
);
@ -550,12 +558,64 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
protocolVersion,
hostCapabilities: this.getCapabilities(),
hostInfo: this._hostInfo,
hostContext: {
// TODO
},
hostContext: this._hostContext,
};
}
/**
* 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.
*
@ -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.
*
@ -780,3 +874,7 @@ export class AppBridge extends Protocol<Request, Notification, Result> {
return super.connect(transport);
}
}
function deepEqual(a: any, b: any): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}