Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9184830
UITemplatedToolCallRendererProps for MCP Apps
ochafik Nov 21, 2025
73c2c0d
Upgrade MCP SDK to 1.22.0 (many (Embedded)Resource type fixes)
ochafik Nov 22, 2025
3b23ede
Update adapter.ts
ochafik Nov 22, 2025
5342f81
Update UIResourceRendererWC.test.tsx
ochafik Nov 22, 2025
6d07a4b
add missing client dep (vite-tsconfig-paths)
ochafik Nov 22, 2025
098d10e
Merge branch 'ochafik/upgrade' into mcp-apps
ochafik Nov 22, 2025
c55ef76
update to latest ext-apps example renderer
ochafik Nov 24, 2025
e0c4421
Sync with latest ext-apps, fix PR review comments
ochafik Dec 8, 2025
d12ddcc
Add AppFrame component, refactor AppRenderer, use ext-apps v0.1.0
ochafik Dec 12, 2025
8aa7205
Merge branch 'main' into mcp-apps
ochafik Dec 12, 2025
a312e52
feat: use ext-apps branch with setter-based MCP forwarding handlers
ochafik Dec 12, 2025
cae7999
Merge upstream/main into mcp-apps
ochafik Dec 12, 2025
3cf3c37
feat(client): expose MCP request handlers and AppBridge ref
ochafik Dec 12, 2025
e6dbe15
refactor(client): require AppBridge in AppFrame, cleaner AppRenderer API
ochafik Dec 12, 2025
fc16af4
refactor(client): require AppBridge in AppFrame, cleaner AppRenderer API
ochafik Dec 12, 2025
196401e
Merge origin/mcp-apps and use hostContext prop instead of setHostCont…
ochafik Dec 12, 2025
681bb43
chore: prettier formatting + re-export McpUiHostContext type
ochafik Dec 12, 2025
a84faee
refactor(client): cleaner API with props instead of ref methods
ochafik Dec 12, 2025
fc32fc6
refactor(client): use camelCase for all callback props
ochafik Dec 12, 2025
f750c98
test(client): add comprehensive tests for AppRenderer
ochafik Dec 12, 2025
d2d350c
chore: update ext-apps to latest, fix test types
ochafik Dec 13, 2025
38a7fd0
chore: add ESLint flat config for v9 compatibility
ochafik Dec 13, 2025
93f8759
chore: switch ext-apps dependency to main branch
ochafik Dec 15, 2025
b2673b9
chore(client): regenerate iframe-bundle with updated dependencies
ochafik Dec 16, 2025
fece80e
feat(client): make Client optional with onReadResource alternative
ochafik Dec 16, 2025
23f28e9
chore: update ext-apps to ^0.2.0 and MCP SDK to ^1.24.0
ochafik Dec 16, 2025
28f61b7
docs: add AppRenderer/AppFrame docs, fix sandbox promise rejection
ochafik Dec 16, 2025
8ff3186
Merge origin/main into mcp-apps to update PR #147
idosal Dec 18, 2025
0d64f86
fix build
idosal Dec 18, 2025
d49b412
remove mcp-ui/shared dependency
idosal Dec 20, 2025
9bfcea7
remove deprecated AppRenderer API
idosal Dec 20, 2025
73b7696
fix lifecycle
idosal Dec 20, 2025
9be1bf9
Merge origin/main into mcp-apps - resolve UIResourceRendererWC.tsx co…
idosal Dec 20, 2025
9244180
use experimental ext-apps for types
idosal Dec 22, 2025
c042d9c
pnpm lock
idosal Dec 22, 2025
f960673
Update sdks/typescript/client/src/components/AppFrame.tsx
ochafik Jan 7, 2026
4b67191
fix: remove onLoggingMessage from AppFrame
ochafik Jan 7, 2026
c3d0806
Update to latest sdk changes: registerApp*, _meta.ui, getToolUiResour…
ochafik Jan 7, 2026
387879b
chore: upgrade @modelcontextprotocol/ext-apps to 0.3.1
ochafik Jan 9, 2026
3fc3435
refactor: use SANDBOX_PROXY_READY_METHOD from ext-apps 0.3.1
ochafik Jan 9, 2026
d4c623a
feat(client): add UI extension capabilities for client capability neg…
ochafik Jan 10, 2026
558d915
fix(server): wrap case block with braces to fix no-case-declarations …
ochafik Jan 10, 2026
d256c63
feat(client): add CSP query parameter support for HTTP header-based CSP
ochafik Jan 11, 2026
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
55 changes: 54 additions & 1 deletion docs/src/guide/client/using-a-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ You can find a complete example for a site with restrictive CSP that uses the ho
```mermaid
sequenceDiagram
participant Host as Host Page
participant Server as Proxy Server
participant Proxy as Proxy iframe
participant Inner as Inner iframe (UI widget)
Host->>Proxy: Load proxy (with "?url" or "?contentType=rawhtml")
Host->>Server: Request proxy (with "?csp=<json>&contentType=rawhtml")
Server->>Server: Parse CSP from query param
Server-->>Proxy: Serve HTML with CSP HTTP headers
alt External URL
Proxy->>Inner: Create with src = decoded url
else rawHtml
Expand Down Expand Up @@ -79,6 +82,56 @@ A valid proxy script must:
3. **Sandbox the Iframe**: For external URLs, the nested iframe should be sandboxed with `allow-scripts allow-same-origin`. For raw HTML mode, the inner iframe does **not** use a sandbox attribute—this is intentional because `document.write()` requires same-origin access to the iframe's document. Security for raw HTML is enforced by the outer iframe's sandbox (controlled by the host) and the double-iframe isolation architecture.
4. **Forward `postMessage` Events**: To allow communication between the host application and the embedded external URL, the proxy needs to forward `message` events between `window.parent` and the iframe's `contentWindow`. For security, it's critical to use a specific `targetOrigin` instead of `*` in `postMessage` calls whenever possible. The `targetOrigin` for messages to the iframe should be the external URL's origin; Messages to the parent will default to `*`.
5. **Permissive Proxy CSP**: Serve the proxy page with a permissive CSP that does not block nested iframe content (e.g., allowing scripts, styles, images) since the host CSP is intentionally not applied on the proxy origin.
6. **(Recommended) CSP via HTTP Headers**: For enhanced security, the proxy server can read a `csp` query parameter and set Content-Security-Policy HTTP headers. See [CSP Query Parameter](#csp-query-parameter) below.

### CSP Query Parameter

When CSP metadata is provided, `mcp-ui` appends it to the proxy URL as a `?csp=<json>` query parameter. This allows proxy servers to set CSP via HTTP headers, which is more secure than meta tags or postMessage-based CSP injection (which can be bypassed by malicious content).

**Example URL:**
```
https://my-proxy.com/?contentType=rawhtml&csp={"connectDomains":["https://api.example.com"],"resourceDomains":["https://cdn.example.com"]}
```

**Server-side implementation (Express example):**
```typescript
import type { McpUiResourceCsp } from '@modelcontextprotocol/ext-apps/app-bridge';

app.get('/proxy', (req, res) => {
let cspConfig: McpUiResourceCsp | undefined;
if (typeof req.query.csp === 'string') {
try {
cspConfig = JSON.parse(req.query.csp);
} catch (e) { /* ignore invalid JSON */ }
}

const cspHeader = buildCspHeader(cspConfig);
res.setHeader('Content-Security-Policy', cspHeader);
res.sendFile('proxy.html');
});

function buildCspHeader(csp?: McpUiResourceCsp): string {
const resourceDomains = csp?.resourceDomains?.join(' ') ?? '';
const connectDomains = csp?.connectDomains?.join(' ') ?? '';
const frameDomains = csp?.frameDomains?.join(' ');

return [
"default-src 'self' 'unsafe-inline'",
`script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: ${resourceDomains}`.trim(),
`style-src 'self' 'unsafe-inline' blob: data: ${resourceDomains}`.trim(),
`img-src 'self' data: blob: ${resourceDomains}`.trim(),
`font-src 'self' data: blob: ${resourceDomains}`.trim(),
`connect-src 'self' ${connectDomains}`.trim(),
`worker-src 'self' blob: ${resourceDomains}`.trim(),
frameDomains ? `frame-src ${frameDomains}` : "frame-src 'none'",
"object-src 'none'",
].join('; ');
}
```

::: tip
The CSP is also sent via `postMessage` after the sandbox loads as a fallback for proxies that don't support the query parameter approach. However, HTTP header-based CSP is strongly recommended as it's tamper-proof.
:::

### Example Self-Hosted Proxy

Expand Down
172 changes: 160 additions & 12 deletions docs/src/guide/mcp-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ const widgetUI = createUIResource({

```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createUIResource, RESOURCE_URI_META_KEY } from '@mcp-ui/server';
import { createUIResource } from '@mcp-ui/server';
import { registerAppTool, registerAppResource } from '@modelcontextprotocol/ext-apps/server';
import { z } from 'zod';

const server = new McpServer({ name: 'my-server', version: '1.0.0' });
Expand All @@ -96,7 +97,8 @@ const widgetUI = createUIResource({
});

// Register the resource so the host can fetch it
server.registerResource(
registerAppResource(
server,
'widget_ui', // Resource name
widgetUI.resource.uri, // Resource URI
{}, // Resource metadata
Expand All @@ -106,7 +108,8 @@ server.registerResource(
);

// Register the tool with _meta linking to the UI resource
server.registerTool(
registerAppTool(
server,
'my_widget',
{
description: 'An interactive widget',
Expand All @@ -115,7 +118,9 @@ server.registerTool(
},
// This tells MCP Apps hosts where to find the UI
_meta: {
[RESOURCE_URI_META_KEY]: widgetUI.resource.uri
ui: {
resourceUri: widgetUI.resource.uri
}
}
},
async ({ query }) => {
Expand All @@ -126,14 +131,15 @@ server.registerTool(
);
```

The key requirement for MCP Apps hosts is that the tool's `_meta` contains the `ui/resourceUri` key pointing to the UI resource URI. This tells the host where to fetch the widget HTML.
The key requirement for MCP Apps hosts is that the tool's `_meta.ui.resourceUri` points to the UI resource URI. This tells the host where to fetch the widget HTML.

### 3. Add the MCP-UI Embedded Resource to Tool Responses

To support **MCP-UI hosts** (which expect embedded resources in tool responses), also return a `createUIResource` result **without** the MCP Apps adapter:
To support **MCP-UI hosts** (which expect embedded resources in tool responses), also return a `createUIResource` result:

```typescript
server.registerTool(
registerAppTool(
server,
'my_widget',
{
description: 'An interactive widget',
Expand All @@ -142,7 +148,9 @@ server.registerTool(
},
// For MCP Apps hosts - points to the registered resource
_meta: {
[RESOURCE_URI_META_KEY]: widgetUI.resource.uri
ui: {
resourceUri: widgetUI.resource.uri
}
}
},
async ({ query }) => {
Expand Down Expand Up @@ -326,7 +334,8 @@ import express from 'express';
import cors from 'cors';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createUIResource, RESOURCE_URI_META_KEY } from '@mcp-ui/server';
import { createUIResource } from '@mcp-ui/server';
import { registerAppTool, registerAppResource } from '@modelcontextprotocol/ext-apps/server';
import { z } from 'zod';

const app = express();
Expand Down Expand Up @@ -383,7 +392,8 @@ const graphUI = createUIResource({
});

// Register the UI resource
server.registerResource(
registerAppResource(
server,
'graph_ui',
graphUI.resource.uri,
{},
Expand All @@ -393,7 +403,8 @@ server.registerResource(
);

// Register the tool with _meta linking to the UI resource
server.registerTool(
registerAppTool(
server,
'show_graph',
{
description: 'Display an interactive graph',
Expand All @@ -402,7 +413,9 @@ server.registerTool(
},
// For MCP Apps hosts - points to the registered resource
_meta: {
[RESOURCE_URI_META_KEY]: graphUI.resource.uri
ui: {
resourceUri: graphUI.resource.uri
}
}
},
async ({ title }) => {
Expand Down Expand Up @@ -440,6 +453,141 @@ The adapter logs debug information to the browser console. Look for messages pre
[MCP Apps Adapter] Intercepted MCP-UI message: prompt
```

## Host-Side Rendering (Client SDK)

The `@mcp-ui/client` package provides React components for rendering MCP Apps tool UIs in your host application.

### AppRenderer Component

`AppRenderer` is the high-level component that handles the complete lifecycle of rendering an MCP tool's UI:

```tsx
import { AppRenderer, type AppRendererHandle } from '@mcp-ui/client';

function ToolUI({ client, toolName, toolInput, toolResult }) {
const appRef = useRef<AppRendererHandle>(null);

return (
<AppRenderer
ref={appRef}
client={client}
toolName={toolName}
sandbox={{ url: new URL('http://localhost:8765/sandbox_proxy.html') }}
toolInput={toolInput}
toolResult={toolResult}
hostContext={{ theme: 'dark' }}
onOpenLink={async ({ url }) => window.open(url)}
onMessage={async (params) => {
console.log('Message from tool UI:', params);
return { isError: false };
}}
onError={(error) => console.error('Tool UI error:', error)}
/>
);
}
```

**Key Props:**
- `client` - Optional MCP client for automatic resource fetching and MCP request forwarding
- `toolName` - Name of the tool to render UI for
- `sandbox` - Sandbox configuration with the sandbox proxy URL
- `html` - Optional pre-fetched HTML (skips resource fetching)
- `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
- `onOpenLink` / `onMessage` / `onLoggingMessage` - Handlers for guest UI requests

**Ref Methods:**
- `sendToolListChanged()` - Notify guest when tools change
- `sendResourceListChanged()` - Notify guest when resources change
- `sendPromptListChanged()` - Notify guest when prompts change
- `teardownResource()` - Clean up before unmounting

### Using Without an MCP Client

You can use `AppRenderer` without a full MCP client by providing custom handlers:

```tsx
<AppRenderer
// No client - use callbacks instead
toolName="my-tool"
toolResourceUri="ui://my-server/my-tool"
sandbox={{ url: sandboxUrl }}
onReadResource={async ({ uri }) => {
// Proxy to your MCP client in a different context
return myMcpProxy.readResource({ uri });
}}
onCallTool={async (params) => {
return myMcpProxy.callTool(params);
}}
/>
```

Or provide pre-fetched HTML directly:

```tsx
<AppRenderer
toolName="my-tool"
sandbox={{ url: sandboxUrl }}
html={preloadedHtml} // Skip all resource fetching
toolInput={args}
/>
```

### AppFrame Component

`AppFrame` is the lower-level component for when you already have the HTML content and an `AppBridge` instance:

```tsx
import { AppFrame, AppBridge } from '@mcp-ui/client';

function LowLevelToolUI({ html, client }) {
const bridge = useMemo(() => new AppBridge(client, hostInfo, capabilities), [client]);

return (
<AppFrame
html={html}
sandbox={{ url: sandboxUrl }}
appBridge={bridge}
toolInput={{ query: 'test' }}
onSizeChanged={(size) => console.log('Size changed:', size)}
/>
);
}
```

### Sandbox Proxy

Both components require a sandbox proxy HTML file to be served. This provides security isolation for the guest UI. The sandbox proxy URL should point to a page that loads the MCP Apps sandbox proxy script.

## Declaring UI Extension Support

When creating your MCP client, declare UI extension support using the provided type and capabilities:

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
type ClientCapabilitiesWithExtensions,
UI_EXTENSION_CAPABILITIES,
} from '@mcp-ui/client';

const capabilities: ClientCapabilitiesWithExtensions = {
// Standard capabilities
roots: { listChanged: true },
// UI extension support (SEP-1724 pattern)
extensions: UI_EXTENSION_CAPABILITIES,
};

const client = new Client(
{ name: 'my-app', version: '1.0.0' },
{ capabilities }
);
```

This tells MCP servers that your client can render UI resources with MIME type `text/html;profile=mcp-app`.

> **Note:** This uses the `extensions` field pattern from [SEP-1724](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1724), which is not yet part of the official MCP protocol.

## Related Resources

- [MCP Apps SEP Specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)
Expand Down
3 changes: 2 additions & 1 deletion examples/external-url-demo/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
2 changes: 1 addition & 1 deletion examples/mcp-apps-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"dependencies": {
"@mcp-ui/server": "workspace:*",
"@modelcontextprotocol/ext-apps": "^0.2.2",
"@modelcontextprotocol/sdk": "^1.22.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"zod": "^3.22.4"
Expand Down
14 changes: 10 additions & 4 deletions examples/mcp-apps-demo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import cors from 'cors';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { createUIResource, RESOURCE_URI_META_KEY } from '@mcp-ui/server';
import { createUIResource } from '@mcp-ui/server';
import { registerAppTool, registerAppResource } from '@modelcontextprotocol/ext-apps/server';

import { randomUUID } from 'crypto';
import { z } from 'zod';

Expand Down Expand Up @@ -365,7 +367,8 @@ app.post('/mcp', async (req, res) => {
});

// Register the UI resource so the host can fetch it
server.registerResource(
registerAppResource(
server,
'weather_dashboard_ui',
weatherDashboardUI.resource.uri,
{},
Expand All @@ -375,7 +378,8 @@ app.post('/mcp', async (req, res) => {
);

// Register the tool with _meta linking to the UI resource
server.registerTool(
registerAppTool(
server,
'weather_dashboard',
{
description: 'Interactive weather dashboard widget',
Expand All @@ -384,7 +388,9 @@ app.post('/mcp', async (req, res) => {
},
// This tells MCP Apps hosts where to find the UI
_meta: {
[RESOURCE_URI_META_KEY]: weatherDashboardUI.resource.uri,
ui: {
resourceUri: weatherDashboardUI.resource.uri
}
},
},
async ({ location }) => {
Expand Down
Loading
Loading