feat(basic-host): improve host UI with collapsible panels and SDK types (#244)

* refactor(basic-host): use SDK types for ModelContext and AppMessage

- Import McpUiUpdateModelContextRequest and McpUiMessageRequest from SDK
- Replace custom ModelContext with McpUiUpdateModelContextRequest['params']
- Replace custom AppMessage with McpUiMessageRequest['params']
- Pass full message params instead of extracting text
- Format messages by extracting text from content blocks in display

* feat(basic-host): handle non-text content blocks in display

- Add formatContentBlock helper for text, image, audio, resource types
- Show placeholders like <image: image/png> for non-text content
- Apply to both messages and model context display

* test: update shadertoy screenshot

* test: update screenshots from CI (linux x64)
This commit is contained in:
Olivier Chafik 2026-01-12 19:42:11 +00:00 • committed by GitHub
commit 741b1d3925
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,4 +1,4 @@
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions } from "@modelcontextprotocol/ext-apps/app-bridge";
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
@ -122,12 +122,19 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiRes
export function loadSandboxProxy(
iframe: HTMLIFrameElement,
csp?: McpUiResourceCsp,
permissions?: McpUiResourcePermissions,
): Promise<boolean> {
// Prevent reload
if (iframe.src) return Promise.resolve(false);
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
// Set Permission Policy allow attribute based on requested permissions
const allowAttribute = buildAllowAttribute(permissions);
if (allowAttribute) {
iframe.setAttribute("allow", allowAttribute);
}
const readyNotification: McpUiSandboxProxyReadyNotification["method"] =
"ui/notifications/sandbox-proxy-ready";
@ -215,12 +222,26 @@ function hookInitializedCallback(appBridge: AppBridge): Promise<void> {
}
export function newAppBridge(serverInfo: ServerInfo, iframe: HTMLIFrameElement): AppBridge {
export type ModelContext = McpUiUpdateModelContextRequest["params"];
export type AppMessage = McpUiMessageRequest["params"];
export interface AppBridgeCallbacks {
onContextUpdate?: (context: ModelContext | null) => void;
onMessage?: (message: AppMessage) => void;
}
export function newAppBridge(
serverInfo: ServerInfo,
iframe: HTMLIFrameElement,
callbacks?: AppBridgeCallbacks,
): AppBridge {
const serverCapabilities = serverInfo.client.getServerCapabilities();
const appBridge = new AppBridge(serverInfo.client, IMPLEMENTATION, {
openLinks: {},
serverTools: serverCapabilities?.tools,
serverResources: serverCapabilities?.resources,
// Declare support for model context updates
updateModelContext: { text: {} },
});
// Register all handlers before calling connect(). The Guest UI can start
@ -229,6 +250,7 @@ export function newAppBridge(serverInfo: ServerInfo, iframe: HTMLIFrameElement):
appBridge.onmessage = async (params, _extra) => {
log.info("Message from MCP App:", params);
callbacks?.onMessage?.(params);
return {};
};
@ -242,6 +264,15 @@ export function newAppBridge(serverInfo: ServerInfo, iframe: HTMLIFrameElement):
log.info("Log message from MCP App:", params);
};
appBridge.onupdatemodelcontext = async (params) => {
log.info("Model context update from MCP App:", params);
// Normalize: empty content array means clear context
const hasContent = params.content && params.content.length > 0;
const hasStructured = params.structuredContent && Object.keys(params.structuredContent).length > 0;
callbacks?.onContextUpdate?.(hasContent || hasStructured ? params : null);
return {};
};
appBridge.onsizechange = async ({ width, height }) => {
// The MCP App has requested a `width` and `height`, but if
// `box-sizing: border-box` is applied to the outer iframe element, then we