MCP Apps Integration
The MCP Apps adapter in @mcp-ui/server enables your MCP-UI HTML widget to run inside MCP Apps-compliant hosts that implement the MCP Apps SEP protocol. This guide walks you through the integration process.
Overview
MCP-UI has been standardized into MCP Apps. There were changes in the messaging protocol that will break existing MCP-UI apps (e.g., event names, JSON-RPC format, etc.) The adapter automatically translates the MCP-UI protocol to MCP Apps SEP, allowing you to maintain compatibility with both MCP-UI and MCP Apps hosts.
With this adapter, your MCP-UI apps will work in both legacy MCP-UI hosts and new MCP Apps hosts out-of-the-box, allowing for a smooth migration. MCP-UI's server SDK will soon support MCP Apps directly.
How It Works
┌─────────────────────────────────────────────────────────────────┐
│ MCP Apps Host │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Sandbox Iframe │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Tool UI Iframe │ │ │
│ │ │ ┌───────────────┐ ┌──────────────────────────┐ │ │ │
│ │ │ │ MCP-UI │───▶│ MCP Apps Adapter │ │ │ │
│ │ │ │ Widget │◀───│ (injected script) │ │ │ │
│ │ │ └───────────────┘ └──────────────────────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ MCP-UI Protocol │ JSON-RPC │ │ │
│ │ │ ▼ ▼ │ │ │
│ │ │ postMessage postMessage │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ MCP Apps SEP Protocol │
└─────────────────────────────────────────────────────────────────┘2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
The adapter:
- Intercepts MCP-UI messages from your widget
- Translates them to MCP Apps SEP JSON-RPC format
- Sends them to the host via postMessage
- Receives host responses and translates them back to MCP-UI format
Quick Start
1. Create a UI Resource with the MCP Apps Adapter
import { createUIResource } from '@mcp-ui/server';
const widgetUI = createUIResource({
uri: 'ui://my-server/widget',
encoding: 'text',
content: {
type: 'rawHtml',
htmlString: `
<html>
<body>
<div id="app">Loading...</div>
<script>
// Listen for render data from the adapter
window.addEventListener('message', (event) => {
if (event.data.type === 'ui-lifecycle-iframe-render-data') {
const { toolInput, toolOutput } = event.data.payload.renderData;
document.getElementById('app').textContent =
JSON.stringify({ toolInput, toolOutput }, null, 2);
}
});
// Signal that the widget is ready
window.parent.postMessage({ type: 'ui-lifecycle-iframe-ready' }, '*');
</script>
</body>
</html>
`,
},
adapters: {
mcpApps: {
enabled: true,
},
},
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2. Register the Resource and Tool
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createUIResource, RESOURCE_URI_META_KEY } from '@mcp-ui/server';
import { z } from 'zod';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
// Create the UI resource (from step 1)
const widgetUI = createUIResource({
uri: 'ui://my-server/widget',
// ... (same as above)
});
// Register the resource so the host can fetch it
server.registerResource(
'widget_ui', // Resource name
widgetUI.resource.uri, // Resource URI
{}, // Resource metadata
async () => ({
contents: [widgetUI.resource]
})
);
// Register the tool with _meta linking to the UI resource
server.registerTool(
'my_widget',
{
description: 'An interactive widget',
inputSchema: {
query: z.string().describe('User query'),
},
// This tells MCP Apps hosts where to find the UI
_meta: {
[RESOURCE_URI_META_KEY]: widgetUI.resource.uri
}
},
async ({ query }) => {
return {
content: [{ type: 'text', text: `Processing: ${query}` }],
};
}
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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.
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:
server.registerTool(
'my_widget',
{
description: 'An interactive widget',
inputSchema: {
query: z.string().describe('User query'),
},
// For MCP Apps hosts - points to the registered resource
_meta: {
[RESOURCE_URI_META_KEY]: widgetUI.resource.uri
}
},
async ({ query }) => {
// Create an embedded UI resource for MCP-UI hosts (no adapter)
const embeddedResource = createUIResource({
uri: `ui://my-server/widget/${query}`,
encoding: 'text',
content: {
type: 'rawHtml',
htmlString: renderWidget(query), // Your widget HTML
},
// No adapters - this is for MCP-UI hosts
});
return {
content: [
{ type: 'text', text: `Processing: ${query}` },
embeddedResource // Include for MCP-UI hosts
],
};
}
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Important: The embedded MCP-UI resource should not enable the MCP Apps adapter. It is for hosts that expect embedded resources in tool responses. MCP Apps hosts will ignore the embedded resource and instead fetch the UI from the registered resource URI in
_meta.
Protocol Translation Reference
Widget → Host (Outgoing)
| MCP-UI Action | MCP Apps Method | Description |
|---|---|---|
tool | tools/call | Call another tool |
prompt | ui/message | Send a follow-up message to the conversation |
link | ui/open-link | Open a URL in a new tab |
notify | notifications/message | Log a message to the host |
intent | ui/message | Send an intent (translated to message) |
ui-size-change | ui/notifications/size-change | Request widget resize |
Host → Widget (Incoming)
| MCP Apps Notification | MCP-UI Message | Description |
|---|---|---|
ui/notifications/tool-input | ui-lifecycle-iframe-render-data | Complete tool arguments |
ui/notifications/tool-input-partial | ui-lifecycle-iframe-render-data | Streaming partial arguments |
ui/notifications/tool-result | ui-lifecycle-iframe-render-data | Tool execution result |
ui/notifications/host-context-changed | ui-lifecycle-iframe-render-data | Theme, locale, viewport changes |
Configuration Options
createUIResource({
// ...
adapters: {
mcpApps: {
enabled: true,
config: {
// Timeout for async operations (default: 30000ms)
timeout: 60000,
},
},
},
});2
3
4
5
6
7
8
9
10
11
12
MIME Type
When the MCP Apps adapter is enabled, the resource MIME type is automatically set to text/html+mcp. This is the expected MIME type for MCP Apps-compliant hosts.
Receiving Data in Your Widget
The adapter sends data to your widget via the standard MCP-UI ui-lifecycle-iframe-render-data message:
window.addEventListener('message', (event) => {
if (event.data.type === 'ui-lifecycle-iframe-render-data') {
const { renderData } = event.data.payload;
// Tool input arguments
const toolInput = renderData.toolInput;
// Tool execution result (if available)
const toolOutput = renderData.toolOutput;
// Widget state (if supported by host)
const widgetState = renderData.widgetState;
// Host context
const theme = renderData.theme; // 'light' | 'dark' | 'system'
const locale = renderData.locale; // e.g., 'en-US'
const displayMode = renderData.displayMode; // 'inline' | 'fullscreen' | 'pip'
const maxHeight = renderData.maxHeight;
// Update your UI with the data
updateWidget(renderData);
}
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Sending Actions from Your Widget
Use standard MCP-UI postMessage calls - the adapter translates them automatically:
// Send a prompt to the conversation
window.parent.postMessage({
type: 'prompt',
payload: { prompt: 'What is the weather like today?' }
}, '*');
// Open a link
window.parent.postMessage({
type: 'link',
payload: { url: 'https://example.com' }
}, '*');
// Call another tool
window.parent.postMessage({
type: 'tool',
payload: {
toolName: 'get_weather',
params: { city: 'San Francisco' }
}
}, '*');
// Send a notification
window.parent.postMessage({
type: 'notify',
payload: { message: 'Widget loaded successfully' }
}, '*');
// Request resize
window.parent.postMessage({
type: 'ui-size-change',
payload: { width: 500, height: 400 }
}, '*');2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Mutual Exclusivity with Apps SDK Adapter
Only one adapter can be enabled at a time. The TypeScript types enforce this:
// ✅ Valid: MCP Apps adapter only
adapters: { mcpApps: { enabled: true } }
// ✅ Valid: Apps SDK adapter only (for ChatGPT)
adapters: { appsSdk: { enabled: true } }
// ❌ TypeScript error: Cannot enable both
adapters: { mcpApps: { enabled: true }, appsSdk: { enabled: true } }2
3
4
5
6
7
8
If you need to support both MCP Apps hosts and ChatGPT, create separate resources:
// For MCP Apps hosts
const mcpAppsResource = createUIResource({
uri: 'ui://my-server/widget-mcp-apps',
content: { type: 'rawHtml', htmlString: widgetHtml },
adapters: { mcpApps: { enabled: true } },
});
// For ChatGPT/Apps SDK hosts
const appsSdkResource = createUIResource({
uri: 'ui://my-server/widget-apps-sdk',
content: { type: 'rawHtml', htmlString: widgetHtml },
adapters: { appsSdk: { enabled: true } },
});2
3
4
5
6
7
8
9
10
11
12
13
Complete Example
See the mcp-apps-demo example for a complete working implementation.
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 { z } from 'zod';
const app = express();
app.use(cors({ origin: '*', exposedHeaders: ['Mcp-Session-Id'] }));
app.use(express.json());
// ... (transport setup)
const server = new McpServer({ name: 'demo', version: '1.0.0' });
const graphUI = createUIResource({
uri: 'ui://demo/graph',
encoding: 'text',
content: {
type: 'rawHtml',
htmlString: `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui; padding: 20px; }
.data { background: #f5f5f5; padding: 10px; border-radius: 8px; }
</style>
</head>
<body>
<h1>graph</h1>
<div class="data" id="data">Waiting for data...</div>
<button onclick="sendPrompt()">Ask Follow-up</button>
<script>
window.addEventListener('message', (e) => {
if (e.data.type === 'ui-lifecycle-iframe-render-data') {
document.getElementById('data').textContent =
JSON.stringify(e.data.payload.renderData, null, 2);
}
});
function sendPrompt() {
window.parent.postMessage({
type: 'prompt',
payload: { prompt: 'Tell me more about this data' }
}, '*');
}
window.parent.postMessage({ type: 'ui-lifecycle-iframe-ready' }, '*');
</script>
</body>
</html>
`,
},
adapters: {
mcpApps: { enabled: true },
},
});
// Register the UI resource
server.registerResource(
'graph_ui',
graphUI.resource.uri,
{},
async () => ({
contents: [graphUI.resource]
})
);
// Register the tool with _meta linking to the UI resource
server.registerTool(
'show_graph',
{
description: 'Display an interactive graph',
inputSchema: {
title: z.string().describe('Graph title'),
},
// For MCP Apps hosts - points to the registered resource
_meta: {
[RESOURCE_URI_META_KEY]: graphUI.resource.uri
}
},
async ({ title }) => {
// Create embedded resource for MCP-UI hosts (no adapter)
const embeddedResource = createUIResource({
uri: `ui://demo/graph/${encodeURIComponent(title)}`,
encoding: 'text',
content: {
type: 'rawHtml',
htmlString: `<html><body><h1>Graph: ${title}</h1></body></html>`,
},
// No adapters - for MCP-UI hosts only
});
return {
content: [
{ type: 'text', text: `Graph: ${title}` },
embeddedResource // Included for MCP-UI hosts
],
};
}
);
// ... (server setup)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
Debugging
The adapter logs debug information to the browser console. Look for messages prefixed with [MCP Apps Adapter]:
[MCP Apps Adapter] Initializing adapter...
[MCP Apps Adapter] Sending ui/initialize request
[MCP Apps Adapter] Received JSON-RPC message: {...}
[MCP Apps Adapter] Intercepted MCP-UI message: prompt2
3
4
Related Resources
- MCP Apps SEP Specification
- @modelcontextprotocol/ext-apps
- Apps SDK Integration - For ChatGPT integration
- Protocol Details - MCP-UI protocol reference

