Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/src/guide/mcp-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,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
- `onFallbackRequest` - Catch-all for JSON-RPC requests not handled by the built-in handlers (see [Handling Custom Requests](#handling-custom-requests-onfallbackrequest))

Expand All @@ -506,6 +508,43 @@ function ToolUI({ client, toolName, toolInput, toolResult }) {
- `sendPromptListChanged()` - Notify guest when prompts change
- `teardownResource()` - Clean up before unmounting

<<<<<<< copilot/add-hostinfo-hostcapabilities-props
### 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 (
<AppRenderer
client={client}
toolName={toolName}
sandbox={{ url: new URL('http://localhost:8765/sandbox_proxy.html') }}
hostInfo={hostInfo}
hostCapabilities={hostCapabilities}
onOpenLink={async ({ url }) => window.open(url)}
/>
Comment on lines +534 to +541
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example uses sandbox={{ url: sandboxUrl }} but sandboxUrl is not defined in the snippet, which makes the docs example non-copy-pastable. Consider defining sandboxUrl (e.g., new URL(...)) in the example or inlining it, consistent with the earlier AppRenderer example in this guide.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 9e6d54e. Changed the example to inline define the sandbox URL: sandbox={{ url: new URL('http://localhost:8765/sandbox_proxy.html') }} to match the earlier example in the guide.

);
}
```

This allows guest apps to know they're running in your specific host application and what capabilities are available.
=======
### Handling Custom Requests (`onFallbackRequest`)

AppRenderer includes built-in handlers for standard MCP Apps methods (`tools/call`, `ui/message`, `ui/open-link`, etc.). The `onFallbackRequest` prop lets you handle **any JSON-RPC request that doesn't match a built-in handler**. This is useful for:
Expand Down Expand Up @@ -554,6 +593,7 @@ The `sendExperimentalRequest` helper sends a properly formatted JSON-RPC request
::: tip Method Naming Convention
Use the `x/<namespace>/<action>` prefix for experimental methods (e.g., `x/clipboard/write`). Standard MCP methods not yet in the Apps spec (e.g., `sampling/createMessage`) should use their canonical method names. When an experimental method proves useful, it can be promoted to a standard method in the [ext-apps spec](https://github.com/modelcontextprotocol/ext-apps).
:::
>>>>>>> main

### Using Without an MCP Client

Expand Down
46 changes: 36 additions & 10 deletions sdks/typescript/client/src/components/AppRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
type CallToolRequest,
type CallToolResult,
type Implementation,
type JSONRPCRequest,
type ListPromptsRequest,
type ListPromptsResult,
Expand All @@ -28,6 +29,7 @@ import {
type McpUiSizeChangedNotification,
type McpUiToolInputPartialNotification,
type McpUiHostContext,
type McpUiHostCapabilities,
} from '@modelcontextprotocol/ext-apps/app-bridge';

import { AppFrame, type SandboxConfig } from './AppFrame';
Expand Down Expand Up @@ -87,6 +89,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'],
Expand Down Expand Up @@ -268,6 +276,8 @@ export const AppRenderer = forwardRef<AppRendererHandle, AppRendererProps>((prop
toolInputPartial,
toolCancelled,
hostContext,
hostInfo,
hostCapabilities,
onMessage,
onOpenLink,
onLoggingMessage,
Expand Down Expand Up @@ -328,23 +338,33 @@ export const AppRenderer = forwardRef<AppRendererHandle, AppRendererProps>((prop
// Effect 1: Create and configure AppBridge
useEffect(() => {
let mounted = true;
let currentBridge: AppBridge | null = null;

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,
);

currentBridge = bridge;

// Register message handler
bridge.onmessage = async (params, extra) => {
if (onMessageRef.current) {
Expand Down Expand Up @@ -416,8 +436,14 @@ export const AppRenderer = forwardRef<AppRendererHandle, AppRendererProps>((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]);
}, [client, hostInfo, hostCapabilities]);

// Effect 2: Fetch HTML if not provided
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ vi.mock('@modelcontextprotocol/ext-apps/app-bridge', () => {
sendResourceListChanged: vi.fn(),
sendPromptListChanged: vi.fn(),
teardownResource: vi.fn(),
close: vi.fn().mockResolvedValue(undefined),
};
return mockBridgeInstance;
}),
Expand Down Expand Up @@ -667,4 +668,93 @@ describe('<AppRenderer />', () => {
});
});
});

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(<AppRenderer {...defaultProps} />);

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(<AppRenderer {...defaultProps} hostInfo={customHostInfo} />);

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(<AppRenderer {...defaultProps} />);

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(<AppRenderer {...defaultProps} hostCapabilities={customCapabilities} />);

await waitFor(() => {
expect(screen.getByTestId('app-frame')).toBeInTheDocument();
});

expect(AppBridgeMock).toHaveBeenCalledWith(
mockClient,
expect.any(Object),
customCapabilities,
);
});
});
});
5 changes: 3 additions & 2 deletions sdks/typescript/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ export {
AppBridge,
PostMessageTransport,
type McpUiHostContext,
type McpUiHostCapabilities,
} from '@modelcontextprotocol/ext-apps/app-bridge';

// Re-export JSONRPCRequest for typing onFallbackRequest handlers
export type { JSONRPCRequest } from '@modelcontextprotocol/sdk/types.js';
// Re-export MCP SDK types commonly used with AppRenderer
export type { Implementation, JSONRPCRequest } from '@modelcontextprotocol/sdk/types.js';

export type { UIResourceMetadata } from './types';
Loading