Protocol Details
This section dives deeper into the UIResource and its intended usage.
UIResource Recap
export interface UIResource {
type: 'resource';
resource: {
uri: string;
mimeType: 'text/html' | 'text/uri-list' | 'application/vnd.mcp-ui.remote-dom';
text?: string;
blob?: string;
};
}2
3
4
5
6
7
8
9
URI Schemes
ui://<component-name>/<instance-id>- Purpose: For all UI resources. The rendering method is determined by
mimeType. - Content:
textorblobcontains either HTML string or URL string. - Client Action:
- If
mimeType: 'text/html'→ Render in a sandboxed iframe usingsrcdoc - If
mimeType: 'text/uri-list'→ Render in a sandboxed iframe usingsrc - If
mimeType: 'application/vnd.mcp-ui.remote-dom'→ Execute in sandboxed iframe and render in the tree
- If
- Examples:
- HTML content: A custom button, a small form, a data visualization snippet
- URL content: Embedding a Grafana dashboard, a third-party widget, a mini-application
- RemoteDOM content: A component to be rendered with the host's look-and-feel (component library)
- Purpose: For all UI resources. The rendering method is determined by
Content encoding: text vs. blob
text: Simple, direct string. Good for smaller, less complex content.blob: Base64 encoded string.- Pros: Handles special characters robustly, can be better for larger payloads, ensures integrity during JSON transport.
- Cons: Requires Base64 decoding on the client, slightly increases payload size.
URI List Format Support
When using mimeType: 'text/uri-list', the content follows the standard URI list format (RFC 2483). However, MCP-UI requires a single URL for rendering. For security reasons, the protocol must be http/s.
- Single URL Requirement: MCP-UI will use only the first valid URL found
- Multiple URLs: If multiple URLs are provided, the client will use the first valid URL and log a warning about the ignored alternatives
- Comments: Lines starting with
#are treated as comments and ignored - Empty lines: Blank lines are ignored
Example URI List Content:
# Primary dashboard URL
https://dashboard.example.com/main
# Backup dashboard URL (will be ignored but logged)
https://backup.dashboard.example.com/main2
3
4
5
Client Behavior:
- Uses
https://dashboard.example.com/mainfor rendering - Logs:
"Multiple URLs found in uri-list content. Using the first URL: "https://dashboard.example.com/main". Other URLs ignored: ["https://backup.dashboard.example.com/main"]"
This design allows for fallback URLs to be specified in the standard format while maintaining simple client implementation that focuses on a single primary URL.
Recommended Client-Side Pattern
Client-side hosts should check for the ui:// URI scheme first to identify MCP-UI resources, rather than checking mimeType:
// ✅ Recommended: Check URI scheme first
if (
mcpResource.type === 'resource' &&
mcpResource.resource.uri?.startsWith('ui://')
) {
return <UIResourceRenderer resource={mcpResource.resource} onUIAction={handleAction} />;
}
// ❌ Not recommended: Check mimeType first
if (
mcpResource.type === 'resource' &&
(mcpResource.resource.mimeType === 'text/html' || mcpResource.resource.mimeType === 'text/uri-list')
) {
return <UIResourceRenderer resource={mcpResource.resource} onUIAction={handleAction} />;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
Benefits of URI-first checking:
- Future-proof: Works with new content types like
application/javascript - Semantic clarity:
ui://clearly indicates this is a UI resource - Simpler logic: Let the
UIResourceRenderercomponent handle mimeType-based rendering internally
Communication (Client <-> Iframe)
For ui:// resources, you can use window.parent.postMessage to send data or actions from the iframe back to the host client application. The client application should set up an event listener for message events.
Basic Communication
Iframe Script Example:
<button onclick="handleAction()">Submit Data</button>
<script>
function handleAction() {
const data = { action: 'formData', value: 'someValue' };
// IMPORTANT: Always specify the targetOrigin for security!
// Use '*' only if the parent origin is unknown or variable and security implications are understood.
window.parent.postMessage(
{ type: 'tool', payload: { toolName: 'myCustomTool', params: data } },
'*',
);
}
</script>2
3
4
5
6
7
8
9
10
11
12
Client-Side Handler:
window.addEventListener('message', (event) => {
// Add origin check for security: if (event.origin !== "expectedOrigin") return;
if (event.data && event.data.tool) {
// Call the onUIAction prop of UIResourceRenderer
}
});2
3
4
5
6
Asynchronous Communication with Message IDs
For iframe content that needs to handle asynchronous responses, you can include a messageId field in your UI action messages. When the host provides an onUIAction callback, the iframe will receive acknowledgment and response messages.
Message Flow:
Iframe sends message with
messageId:javascriptwindow.parent.postMessage({ type: 'tool', messageId: 'unique-request-id-123', payload: { toolName: 'myAsyncTool', params: { data: 'some data' } } }, '*');1
2
3
4
5Host responds with acknowledgment:
javascript// The iframe receives this message back { type: 'ui-message-received', messageId: 'unique-request-id-123', }1
2
3
4
5When
onUIActioncompletes successfully:javascript// The iframe receives the actual response { type: 'ui-message-response', messageId: 'unique-request-id-123', payload: { response: { /* the result from onUIAction */ } } }1
2
3
4
5
6
7
8If
onUIActionencounters an error:javascript// The iframe receives the error { type: 'ui-message-response', messageId: 'unique-request-id-123', payload: { error: { /* the error object */ } } }1
2
3
4
5
6
7
8
Complete Iframe Example with Async Handling:
<button onclick="handleAsyncAction()">Async Action</button>
<div id="status">Ready</div>
<div id="result"></div>
<script>
let messageCounter = 0;
const pendingRequests = new Map();
function generateMessageId() {
return `msg-${Date.now()}-${++messageCounter}`;
}
function handleAsyncAction() {
const messageId = generateMessageId();
const statusEl = document.getElementById('status');
const resultEl = document.getElementById('result');
statusEl.textContent = 'Sending request...';
// Store the request context
pendingRequests.set(messageId, {
startTime: Date.now(),
action: 'async-tool-call'
});
// Send the message with messageId
window.parent.postMessage({
type: 'tool',
messageId: messageId,
payload: {
toolName: 'processData',
params: { data: 'example data', timestamp: Date.now() }
}
}, '*');
}
// Listen for responses from the host
window.addEventListener('message', (event) => {
const message = event.data;
if (!message.messageId || !pendingRequests.has(message.messageId)) {
return; // Not for us or unknown request
}
const statusEl = document.getElementById('status');
const resultEl = document.getElementById('result');
const request = pendingRequests.get(message.messageId);
switch (message.type) {
case 'ui-message-received':
statusEl.textContent = 'Request acknowledged, processing...';
break;
case 'ui-message-response':
if (message.payload.error) {
statusEl.textContent = 'Error occurred!';
resultEl.innerHTML = `<div style="color: red;">Error: ${JSON.stringify(message.payload.error)}</div>`;
pendingRequests.delete(message.messageId);
break;
}
statusEl.textContent = 'Completed successfully!';
resultEl.innerHTML = `<pre>${JSON.stringify(message.payload.response, null, 2)}</pre>`;
pendingRequests.delete(message.messageId);
break;
}
});
</script>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
Message Types
The following internal message types are available as constants:
InternalMessageType.UI_MESSAGE_RECEIVED('ui-message-received')InternalMessageType.UI_MESSAGE_RESPONSE('ui-message-response')
These types are exported from both @mcp-ui/client and @mcp-ui/server packages.
Important Notes:
- Message ID is optional: If you don't provide a
messageId, the iframe will not receive response messages. - Only with
onUIAction: Response messages are only sent when the host provides anonUIActioncallback. - Unique IDs: Ensure
messageIdvalues are unique to avoid conflicts between multiple pending requests. - Cleanup: Always clean up pending request tracking when you receive responses to avoid memory leaks.

