From 14a4805875daf2e36238856112e0104f1ec9c2f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:16:37 +0000 Subject: [PATCH 1/6] Initial plan From 72bc1f4068507b5bdef5a0f0e41e4f61e6b9b92e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:21:10 +0000 Subject: [PATCH 2/6] feat: add hostInfo and hostCapabilities props to AppRenderer Co-authored-by: idosal <18148989+idosal@users.noreply.github.com> --- .../client/src/components/AppRenderer.tsx | 37 +++++--- .../components/__tests__/AppRenderer.test.tsx | 89 +++++++++++++++++++ sdks/typescript/client/src/index.ts | 4 + 3 files changed, 120 insertions(+), 10 deletions(-) diff --git a/sdks/typescript/client/src/components/AppRenderer.tsx b/sdks/typescript/client/src/components/AppRenderer.tsx index 8bc5c34f..b67cda5d 100644 --- a/sdks/typescript/client/src/components/AppRenderer.tsx +++ b/sdks/typescript/client/src/components/AppRenderer.tsx @@ -4,6 +4,7 @@ import { type Client } from '@modelcontextprotocol/sdk/client/index.js'; import { type CallToolRequest, type CallToolResult, + type Implementation, type ListPromptsRequest, type ListPromptsResult, type ListResourcesRequest, @@ -27,6 +28,7 @@ import { type McpUiSizeChangedNotification, type McpUiToolInputPartialNotification, type McpUiHostContext, + type McpUiHostCapabilities, } from '@modelcontextprotocol/ext-apps/app-bridge'; import { AppFrame, type SandboxConfig } from './AppFrame'; @@ -86,6 +88,12 @@ export interface AppRendererProps { /** Host context (theme, viewport, locale, etc.) to pass to the guest UI */ hostContext?: McpUiHostContext; + /** Host application identification (name and version). Defaults to { name: 'MCP-UI Host', version: '1.0.0' } */ + hostInfo?: Implementation; + + /** Host capabilities to advertise to the MCP app. If not provided, capabilities are derived from serverCapabilities. */ + hostCapabilities?: McpUiHostCapabilities; + /** Handler for open-link requests from the guest UI */ onOpenLink?: ( params: McpUiOpenLinkRequest['params'], @@ -236,6 +244,8 @@ export const AppRenderer = forwardRef((prop toolInputPartial, toolCancelled, hostContext, + hostInfo, + hostCapabilities, onMessage, onOpenLink, onLoggingMessage, @@ -297,17 +307,24 @@ export const AppRenderer = forwardRef((prop const createBridge = () => { try { const serverCapabilities = client?.getServerCapabilities(); + + // Use provided hostInfo or defaults + const finalHostInfo: Implementation = hostInfo ?? { + name: 'MCP-UI Host', + version: '1.0.0', + }; + + // Use provided hostCapabilities or build from serverCapabilities + const finalHostCapabilities: McpUiHostCapabilities = hostCapabilities ?? { + openLinks: {}, + serverTools: serverCapabilities?.tools, + serverResources: serverCapabilities?.resources, + }; + const bridge = new AppBridge( client ?? null, - { - name: 'MCP-UI Host', - version: '1.0.0', - }, - { - openLinks: {}, - serverTools: serverCapabilities?.tools, - serverResources: serverCapabilities?.resources, - }, + finalHostInfo, + finalHostCapabilities, ); // Register message handler @@ -369,7 +386,7 @@ export const AppRenderer = forwardRef((prop return () => { mounted = false; }; - }, [client]); + }, [client, hostInfo, hostCapabilities]); // Effect 2: Fetch HTML if not provided useEffect(() => { diff --git a/sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx b/sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx index 4b6c58aa..d7ba8dd1 100644 --- a/sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx +++ b/sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx @@ -545,4 +545,93 @@ describe('', () => { }); }); }); + + describe('hostInfo prop', () => { + it('should use default hostInfo when not provided', async () => { + const AppBridgeMock = vi.mocked( + (await import('@modelcontextprotocol/ext-apps/app-bridge')).AppBridge, + ); + + render(); + + await waitFor(() => { + expect(screen.getByTestId('app-frame')).toBeInTheDocument(); + }); + + expect(AppBridgeMock).toHaveBeenCalledWith( + mockClient, + { name: 'MCP-UI Host', version: '1.0.0' }, + expect.any(Object), + ); + }); + + it('should use provided hostInfo', async () => { + const AppBridgeMock = vi.mocked( + (await import('@modelcontextprotocol/ext-apps/app-bridge')).AppBridge, + ); + + const customHostInfo = { name: 'goose', version: '2.3.4' }; + + render(); + + await waitFor(() => { + expect(screen.getByTestId('app-frame')).toBeInTheDocument(); + }); + + expect(AppBridgeMock).toHaveBeenCalledWith( + mockClient, + customHostInfo, + expect.any(Object), + ); + }); + }); + + describe('hostCapabilities prop', () => { + it('should derive hostCapabilities from serverCapabilities when not provided', async () => { + const AppBridgeMock = vi.mocked( + (await import('@modelcontextprotocol/ext-apps/app-bridge')).AppBridge, + ); + + render(); + + await waitFor(() => { + expect(screen.getByTestId('app-frame')).toBeInTheDocument(); + }); + + expect(AppBridgeMock).toHaveBeenCalledWith( + mockClient, + expect.any(Object), + { + openLinks: {}, + serverTools: {}, + serverResources: {}, + }, + ); + }); + + it('should use provided hostCapabilities', async () => { + const AppBridgeMock = vi.mocked( + (await import('@modelcontextprotocol/ext-apps/app-bridge')).AppBridge, + ); + + const customCapabilities = { + openLinks: {}, + serverTools: { listChanged: true }, + serverResources: { listChanged: true }, + logging: {}, + }; + + render(); + + await waitFor(() => { + expect(screen.getByTestId('app-frame')).toBeInTheDocument(); + }); + + expect(AppBridgeMock).toHaveBeenCalledWith( + mockClient, + expect.any(Object), + customCapabilities, + ); + }); + }); }); diff --git a/sdks/typescript/client/src/index.ts b/sdks/typescript/client/src/index.ts index a71e3a8f..63948b14 100644 --- a/sdks/typescript/client/src/index.ts +++ b/sdks/typescript/client/src/index.ts @@ -29,8 +29,12 @@ export { AppBridge, PostMessageTransport, type McpUiHostContext, + type McpUiHostCapabilities, } from '@modelcontextprotocol/ext-apps/app-bridge'; +// Re-export MCP SDK types commonly used with AppRenderer +export { type Implementation } from '@modelcontextprotocol/sdk/types.js'; + // The types needed to create a custom component library export type { ComponentLibrary, From bc97603751d17115c069d7a3d45f91f4b0b5b390 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:22:18 +0000 Subject: [PATCH 3/6] docs: document hostInfo and hostCapabilities props in AppRenderer Co-authored-by: idosal <18148989+idosal@users.noreply.github.com> --- docs/src/guide/mcp-apps.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/src/guide/mcp-apps.md b/docs/src/guide/mcp-apps.md index 8f55853a..4e2ce79c 100644 --- a/docs/src/guide/mcp-apps.md +++ b/docs/src/guide/mcp-apps.md @@ -509,6 +509,8 @@ function ToolUI({ client, toolName, toolInput, toolResult }) { - `toolResourceUri` - Optional pre-fetched resource URI - `toolInput` / `toolResult` - Tool arguments and results to pass to the UI - `hostContext` - Theme, locale, viewport info for the guest UI +- `hostInfo` - Host application identification (name and version). Defaults to `{ name: 'MCP-UI Host', version: '1.0.0' }` +- `hostCapabilities` - Host capabilities to advertise to the MCP app (e.g., `openLinks`, `serverTools`, `logging`) - `onOpenLink` / `onMessage` / `onLoggingMessage` - Handlers for guest UI requests **Ref Methods:** @@ -517,6 +519,42 @@ function ToolUI({ client, toolName, toolInput, toolResult }) { - `sendPromptListChanged()` - Notify guest when prompts change - `teardownResource()` - Clean up before unmounting +### Customizing Host Identity + +By default, `AppRenderer` identifies itself as "MCP-UI Host" to guest apps. You can customize the host identity and capabilities to properly identify your application: + +```tsx +import { AppRenderer } from '@mcp-ui/client'; +import type { Implementation, McpUiHostCapabilities } from '@mcp-ui/client'; + +function ToolUI({ client, toolName }) { + const hostInfo: Implementation = { + name: 'goose', + version: '2.3.4', + }; + + const hostCapabilities: McpUiHostCapabilities = { + openLinks: {}, + serverTools: { listChanged: true }, + serverResources: { listChanged: true }, + logging: {}, + }; + + return ( + window.open(url)} + /> + ); +} +``` + +This allows guest apps to know they're running in your specific host application and what capabilities are available. + ### Using Without an MCP Client You can use `AppRenderer` without a full MCP client by providing custom handlers: From 2029dc480b2f24c9072010787e40f8d9d42f2ac9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:23:10 +0000 Subject: [PATCH 4/6] fix: remove trailing whitespace in AppRenderer Co-authored-by: idosal <18148989+idosal@users.noreply.github.com> --- sdks/typescript/client/src/components/AppRenderer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdks/typescript/client/src/components/AppRenderer.tsx b/sdks/typescript/client/src/components/AppRenderer.tsx index b67cda5d..271b9ea1 100644 --- a/sdks/typescript/client/src/components/AppRenderer.tsx +++ b/sdks/typescript/client/src/components/AppRenderer.tsx @@ -307,20 +307,20 @@ export const AppRenderer = forwardRef((prop const createBridge = () => { try { const serverCapabilities = client?.getServerCapabilities(); - + // Use provided hostInfo or defaults const finalHostInfo: Implementation = hostInfo ?? { name: 'MCP-UI Host', version: '1.0.0', }; - + // Use provided hostCapabilities or build from serverCapabilities const finalHostCapabilities: McpUiHostCapabilities = hostCapabilities ?? { openLinks: {}, serverTools: serverCapabilities?.tools, serverResources: serverCapabilities?.resources, }; - + const bridge = new AppBridge( client ?? null, finalHostInfo, From 9e6d54eb1d3105b04cc7307c3da26b04811f1427 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 00:25:13 +0000 Subject: [PATCH 5/6] fix: add AppBridge cleanup to prevent memory leaks and fix docs example Co-authored-by: idosal <18148989+idosal@users.noreply.github.com> --- docs/src/guide/mcp-apps.md | 2 +- sdks/typescript/client/src/components/AppRenderer.tsx | 9 +++++++++ .../client/src/components/__tests__/AppRenderer.test.tsx | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/src/guide/mcp-apps.md b/docs/src/guide/mcp-apps.md index 4e2ce79c..52474c9a 100644 --- a/docs/src/guide/mcp-apps.md +++ b/docs/src/guide/mcp-apps.md @@ -544,7 +544,7 @@ function ToolUI({ client, toolName }) { window.open(url)} diff --git a/sdks/typescript/client/src/components/AppRenderer.tsx b/sdks/typescript/client/src/components/AppRenderer.tsx index 271b9ea1..03e66507 100644 --- a/sdks/typescript/client/src/components/AppRenderer.tsx +++ b/sdks/typescript/client/src/components/AppRenderer.tsx @@ -303,6 +303,7 @@ export const AppRenderer = forwardRef((prop // Effect 1: Create and configure AppBridge useEffect(() => { let mounted = true; + let currentBridge: AppBridge | null = null; const createBridge = () => { try { @@ -327,6 +328,8 @@ export const AppRenderer = forwardRef((prop finalHostCapabilities, ); + currentBridge = bridge; + // Register message handler bridge.onmessage = async (params, extra) => { if (onMessageRef.current) { @@ -385,6 +388,12 @@ export const AppRenderer = forwardRef((prop return () => { mounted = false; + // Clean up the bridge connection to prevent message listener accumulation + if (currentBridge) { + currentBridge.close().catch((err) => { + console.error('[AppRenderer] Error closing bridge:', err); + }); + } }; }, [client, hostInfo, hostCapabilities]); diff --git a/sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx b/sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx index d7ba8dd1..bff9f71e 100644 --- a/sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx +++ b/sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx @@ -58,6 +58,7 @@ vi.mock('@modelcontextprotocol/ext-apps/app-bridge', () => { sendResourceListChanged: vi.fn(), sendPromptListChanged: vi.fn(), teardownResource: vi.fn(), + close: vi.fn().mockResolvedValue(undefined), }; return mockBridgeInstance; }), From 69bce027b69273760c6c87ec1dd37690bbda2faa Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Thu, 23 Apr 2026 00:51:48 -0700 Subject: [PATCH 6/6] fix build --- sdks/typescript/client/src/index.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/sdks/typescript/client/src/index.ts b/sdks/typescript/client/src/index.ts index d502cee7..5e0f0cdd 100644 --- a/sdks/typescript/client/src/index.ts +++ b/sdks/typescript/client/src/index.ts @@ -34,12 +34,4 @@ export { // Re-export MCP SDK types commonly used with AppRenderer export type { Implementation, JSONRPCRequest } from '@modelcontextprotocol/sdk/types.js'; -// The types needed to create a custom component library -export type { - ComponentLibrary, - ComponentLibraryElement, - RemoteElementConfiguration, -} from './types'; - - export type { UIResourceMetadata } from './types';