Getting Started
This guide will help you get started with building MCP Apps using the @mcp-ui/* packages.
Prerequisites
- Node.js (v22.x recommended for the TypeScript SDK)
- pnpm (v9 or later recommended for the TypeScript SDK)
- Ruby (v3.x recommended for the Ruby SDK)
- Python (v3.10+ recommended for the Python SDK)
Installation
For TypeScript
# Server SDK
npm install @mcp-ui/server @modelcontextprotocol/ext-apps
# Client SDK
npm install @mcp-ui/client2
3
4
5
For Ruby
gem install mcp_ui_serverFor Python
pip install mcp-ui-serverQuick Start: MCP Apps Pattern
Server Side
Create a tool with an interactive UI using registerAppTool and _meta.ui.resourceUri:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerAppTool, registerAppResource } from '@modelcontextprotocol/ext-apps/server';
import { createUIResource } from '@mcp-ui/server';
import { z } from 'zod';
// 1. Create your MCP server
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
// 2. Create the UI resource with interactive HTML
const widgetUI = await createUIResource({
uri: 'ui://my-server/widget',
content: {
type: 'rawHtml',
htmlString: `
<html>
<body>
<h1>Interactive Widget</h1>
<button onclick="sendMessage()">Send Message</button>
<div id="status">Ready</div>
<script type="module">
import { App } from 'https://esm.sh/@modelcontextprotocol/ext-apps@0.4.1';
// Initialize the MCP Apps client
const app = new App({ name: 'widget', version: '1.0.0' });
// Listen for tool input
app.ontoolinput = (params) => {
document.getElementById('status').textContent =
'Received: ' + JSON.stringify(params.input);
};
// Send a message to the conversation
window.sendMessage = async () => {
await app.sendMessage({
role: 'user',
content: [{ type: 'text', text: 'Tell me more about this widget' }]
});
};
// Connect to the host
await app.connect();
</script>
</body>
</html>
`,
},
encoding: 'text',
});
// 3. Register the resource handler
registerAppResource(
server,
'widget_ui',
widgetUI.resource.uri,
{},
async () => ({
contents: [widgetUI.resource]
})
);
// 4. Register the tool with _meta.ui.resourceUri
registerAppTool(
server,
'show_widget',
{
description: 'Show an interactive widget',
inputSchema: {
query: z.string().describe('User query'),
},
_meta: {
ui: {
resourceUri: widgetUI.resource.uri // Links tool to UI
}
}
},
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
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
MCP Apps Protocol
The example above uses the @modelcontextprotocol/ext-apps App class for communication. See Protocol Details for the full JSON-RPC API.
Client Side
Render tool UIs with AppRenderer:
import { AppRenderer } from '@mcp-ui/client';
function ToolUI({ client, toolName, toolInput, toolResult }) {
return (
<AppRenderer
client={client}
toolName={toolName}
sandbox={{ url: new URL('/sandbox_proxy.html', window.location.origin) }}
toolInput={toolInput}
toolResult={toolResult}
onOpenLink={async ({ url }) => {
// Validate URL scheme before opening
if (url.startsWith('https://') || url.startsWith('http://')) {
window.open(url);
}
return { isError: false };
}}
onMessage={async (params) => {
console.log('Message from UI:', params);
// Handle the message (e.g., send to AI conversation)
return { isError: false };
}}
onError={(error) => console.error('UI error:', error)}
/>
);
}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
Using Without an MCP Client
You can use AppRenderer without a full MCP client by providing callbacks:
<AppRenderer
toolName="show_widget"
toolResourceUri="ui://my-server/widget"
sandbox={{ url: sandboxUrl }}
onReadResource={async ({ uri }) => {
// Fetch resource from your backend
return myBackend.readResource({ uri });
}}
onCallTool={async (params) => {
return myBackend.callTool(params);
}}
toolInput={{ query: 'hello' }}
/>2
3
4
5
6
7
8
9
10
11
12
13
Or provide pre-fetched HTML directly:
<AppRenderer
toolName="show_widget"
sandbox={{ url: sandboxUrl }}
html={preloadedHtml} // Skip resource fetching
toolInput={{ query: 'hello' }}
/>2
3
4
5
6
Resource Types
MCP Apps supports several UI content types:
1. HTML Resources
Direct HTML content rendered in a sandboxed iframe:
const htmlResource = await createUIResource({
uri: 'ui://my-tool/widget',
content: { type: 'rawHtml', htmlString: '<h1>Hello World</h1>' },
encoding: 'text',
});2
3
4
5
2. External URLs
Fetch an external page's HTML and serve it as a UI resource. The SDK fetches the URL's contents server-side and injects a <base> tag so relative paths (CSS, JS, images) resolve correctly:
const urlResource = await createUIResource({
uri: 'ui://my-tool/external',
content: { type: 'externalUrl', iframeUrl: 'https://example.com' },
encoding: 'text',
});
// The resource now contains the fetched HTML with a <base href="https://example.com"> tag2
3
4
5
6
Declaring UI Extension Support
When creating your MCP client, declare UI extension support:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
type ClientCapabilitiesWithExtensions,
UI_EXTENSION_CAPABILITIES,
} from '@mcp-ui/client';
const capabilities: ClientCapabilitiesWithExtensions = {
roots: { listChanged: true },
extensions: UI_EXTENSION_CAPABILITIES,
};
const client = new Client(
{ name: 'my-app', version: '1.0.0' },
{ capabilities }
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
Building from Source
Clone and Install
git clone https://github.com/idosal/mcp-ui.git
cd mcp-ui
pnpm install2
3
Build All Packages
pnpm --filter=!@mcp-ui/docs buildRun Tests
pnpm testNext Steps
- Server SDKs: Learn how to create resources with our server-side packages.
- Client SDK: Learn how to render resources.
- Protocol & Components:

