Skip to content
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,13 @@ It accepts the following props:
- **`resource`**: The resource object from an MCP Tool response. It must include `uri`, `mimeType`, and content (`text`, `blob`)
- **`onUIAction`**: Optional callback for handling UI actions from the resource:
```typescript
{ type: 'tool', payload: { toolName: string, params: Record<string, unknown> } } |
{ type: 'intent', payload: { intent: string, params: Record<string, unknown> } } |
{ type: 'prompt', payload: { prompt: string } } |
{ type: 'notify', payload: { message: string } } |
{ type: 'link', payload: { url: string } }
{ type: 'tool', payload: { toolName: string, params: Record<string, unknown> }, messageId?: string } |
{ type: 'intent', payload: { intent: string, params: Record<string, unknown> }, messageId?: string } |
{ type: 'prompt', payload: { prompt: string }, messageId?: string } |
{ type: 'notify', payload: { message: string }, messageId?: string } |
{ type: 'link', payload: { url: string }, messageId?: string }
```
When actions include a `messageId`, the iframe automatically receives response messages for asynchronous handling.
- **`supportedContentTypes`**: Optional array to restrict which content types are allowed (`['rawHtml', 'externalUrl', 'remoteDom']`)
- **`htmlProps`**: Optional props for the internal `<HTMLResourceRenderer>`
- **`style`**: Optional custom styles for the iframe
Expand Down
17 changes: 12 additions & 5 deletions docs/src/guide/client/html-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,20 @@ The component accepts the following props:
- **`resource`**: The resource object from an `UIResource`. It should include `uri`, `mimeType`, and either `text` or `blob`.
- **`onUIAction`**: An optional callback that fires when the iframe content (for `ui://` resources) posts a message to your app. The message should look like:
```typescript
{ type: 'tool', payload: { toolName: string, params: Record<string, unknown> } } |
{ type: 'intent', payload: { intent: string, params: Record<string, unknown> } } |
{ type: 'prompt', payload: { prompt: string } } |
{ type: 'notify', payload: { message: string } } |
{ type: 'link', payload: { url: string } } |
{ type: 'tool', payload: { toolName: string, params: Record<string, unknown> }, messageId?: string } |
{ type: 'intent', payload: { intent: string, params: Record<string, unknown> }, messageId?: string } |
{ type: 'prompt', payload: { prompt: string }, messageId?: string } |
{ type: 'notify', payload: { message: string }, messageId?: string } |
{ type: 'link', payload: { url: string }, messageId?: string } |
```
If you don't provide a callback for a specific type, the default handler will be used.

**Asynchronous Response Handling**: When a message includes a `messageId` field, the iframe will automatically receive response messages:
- `ui-action-received`: Sent immediately when the message is received
- `ui-action-response`: Sent when your callback resolves successfully
- `ui-action-error`: Sent if your callback throws an error

See [Protocol Details](../protocol-details.md#asynchronous-communication-with-message-ids) for complete examples.
- **`style`**: (Optional) Custom styles for the iframe.
- **`proxy`**: (Optional) A URL to a proxy script. This is useful for hosts with a strict Content Security Policy (CSP). When provided, external URLs will be rendered in a nested iframe hosted at this URL. For example, if `proxy` is `https://my-proxy.com/`, the final URL will be `https://my-proxy.com/?url=<encoded_original_url>`. For your convenience, mcp-ui hosts a proxy script at `https://proxy.mcpui.dev`, which you can use as a the prop value without any setup (see `examples/external-url-demo`).
- **`iframeProps`**: (Optional) Custom props for the iframe.
Expand Down
12 changes: 7 additions & 5 deletions docs/src/guide/client/resource-renderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ interface UIResourceRendererProps {
- **`resource`**: The resource object from an MCP response. Should include `uri`, `mimeType`, and content (`text`, `blob`, or `content`)
- **`onUIAction`**: Optional callback for handling UI actions from the resource:
```typescript
{ type: 'tool', payload: { toolName: string, params: Record<string, unknown> } } |
{ type: 'intent', payload: { intent: string, params: Record<string, unknown> } } |
{ type: 'prompt', payload: { prompt: string } } |
{ type: 'notify', payload: { message: string } } |
{ type: 'link', payload: { url: string } }
{ type: 'tool', payload: { toolName: string, params: Record<string, unknown> }, messageId?: string } |
{ type: 'intent', payload: { intent: string, params: Record<string, unknown> }, messageId?: string } |
{ type: 'prompt', payload: { prompt: string }, messageId?: string } |
{ type: 'notify', payload: { message: string }, messageId?: string } |
{ type: 'link', payload: { url: string }, messageId?: string }
```

**Asynchronous Communication**: When actions include a `messageId`, the iframe automatically receives response messages (`ui-action-received`, `ui-action-response`, `ui-action-error`). See [Protocol Details](../protocol-details.md#asynchronous-communication-with-message-ids) for examples.
- **`supportedContentTypes`**: Optional array to restrict which content types are allowed (`['rawHtml', 'externalUrl', 'remoteDom']`)
- **`htmlProps`**: Optional props for the `<HTMLResourceRenderer>`
- **`style`**: Optional custom styles for iframe-based resources
Expand Down
177 changes: 177 additions & 0 deletions docs/src/guide/client/usage-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,181 @@ export default App;

---

## Handling Asynchronous Actions with Message IDs

When your iframe content needs to track the status of long-running operations, you can use the `messageId` field to receive acknowledgment and response messages. Here's a complete example:

### HTML Resource with Async Communication

```typescript
import React, { useState } from 'react';
import { UIResourceRenderer } from '@mcp-ui/client';

const AsyncExampleApp: React.FC = () => {
const [actionStatus, setActionStatus] = useState<string>('Ready');
const [actionResult, setActionResult] = useState<any>(null);

const handleAsyncUIAction = async (result: UIActionResult): Promise<any> => {
console.log(`Received action with messageId: ${result.messageId}`);
setActionStatus('Processing...');

// Simulate an async operation (e.g., API call, database query)
await new Promise(resolve => setTimeout(resolve, 2000));

if (result.type === 'tool' && result.payload.toolName === 'processData') {
// Simulate success or failure based on params
if (result.payload.params.shouldFail) {
throw new Error('Simulated processing error');
}

return {
status: 'success',
processedData: `Processed: ${result.payload.params.data}`,
timestamp: new Date().toISOString()
};
}

return { status: 'unknown action' };
};

const asyncHtmlResource = {
uri: 'ui://async-example/demo',
mimeType: 'text/html' as const,
text: `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
button { margin: 10px; padding: 10px 20px; }
.status { padding: 10px; margin: 10px 0; border-radius: 4px; }
.pending { background: #fff3cd; border: 1px solid #ffeaa7; }
.success { background: #d4edda; border: 1px solid #c3e6cb; }
.error { background: #f8d7da; border: 1px solid #f5c6cb; }
</style>
</head>
<body>
<h3>Async Action Demo</h3>
<button onclick="processData('success')">Process Data (Success)</button>
<button onclick="processData('error')">Process Data (Error)</button>
<div id="status">Ready</div>
<div id="result"></div>

<script>
let messageCounter = 0;
const pendingRequests = new Map();

function generateMessageId() {
return \`async-msg-\${Date.now()}-\${++messageCounter}\`;
}

function updateStatus(message, className = '') {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = 'status ' + className;
}

function updateResult(content) {
document.getElementById('result').innerHTML = content;
}

function processData(mode) {
const messageId = generateMessageId();

updateStatus('Sending request...', 'pending');
updateResult('');

pendingRequests.set(messageId, { startTime: Date.now(), mode });

window.parent.postMessage({
type: 'tool',
messageId: messageId,
payload: {
toolName: 'processData',
params: {
data: \`Sample data (\${mode})\`,
shouldFail: mode === 'error',
timestamp: Date.now()
}
}
}, '*');
}

// Listen for response messages
window.addEventListener('message', (event) => {
const message = event.data;

if (!message.messageId || !pendingRequests.has(message.messageId)) {
return;
}

const request = pendingRequests.get(message.messageId);

switch (message.type) {
case 'ui-action-received':
updateStatus('Request acknowledged, processing...', 'pending');
break;

case 'ui-action-response':
updateStatus('Completed successfully!', 'success');
updateResult(\`
<h4>Response:</h4>
<pre>\${JSON.stringify(message.payload.response, null, 2)}</pre>
\`);
pendingRequests.delete(message.messageId);
break;

case 'ui-action-error':
updateStatus('Error occurred!', 'error');
updateResult(\`
<h4>Error:</h4>
<div style="color: red;">\${JSON.stringify(message.payload.error, null, 2)}</div>
\`);
pendingRequests.delete(message.messageId);
break;
}
});
</script>
</body>
</html>
`
};

return (
<div>
<h2>Async Communication Example</h2>
<p>Host Status: {actionStatus}</p>
{actionResult && (
<div>
<h4>Last Host Result:</h4>
<pre>{JSON.stringify(actionResult, null, 2)}</pre>
</div>
)}

<UIResourceRenderer
resource={asyncHtmlResource}
onUIAction={handleAsyncUIAction}
/>
</div>
);
};
```

### Key Features Demonstrated

1. **Message ID Generation**: The iframe creates unique message IDs for each request
2. **Request Tracking**: Pending requests are stored to match responses
3. **Status Updates**: The UI shows different states (pending, success, error)
4. **Response Handling**: Different message types trigger appropriate UI updates
5. **Cleanup**: Completed requests are removed from pending tracking

This pattern is especially useful for:
- Long-running server operations
- File uploads or downloads
- Database queries
- External API calls
- Multi-step workflows

---

That's it! Just use `<UIResourceRenderer />` with the right props and you're ready to render interactive HTML from MCP resources in your React app. The `UIResourceRenderer` automatically detects the resource type and renders the appropriate component internally. If you need more details, check out the [UIResourceRenderer Component](./resource-renderer.md) page.
139 changes: 139 additions & 0 deletions docs/src/guide/protocol-details.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ if (

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:**

```html
Expand Down Expand Up @@ -120,3 +122,140 @@ window.addEventListener('message', (event) => {
}
});
```

### 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:**

1. **Iframe sends message with `messageId`:**
```javascript
window.parent.postMessage({
type: 'tool',
messageId: 'unique-request-id-123',
payload: { toolName: 'myAsyncTool', params: { data: 'some data' } }
}, '*');
```

2. **Host responds with acknowledgment:**
```javascript
// The iframe receives this message back
{
type: 'ui-action-received',
messageId: 'unique-request-id-123',
}
```

3. **When `onUIAction` completes successfully:**
```javascript
// The iframe receives the actual response
{
type: 'ui-action-response',
messageId: 'unique-request-id-123',
payload: {
response: { /* the result from onUIAction */ }
}
}
```

4. **If `onUIAction` encounters an error:**
```javascript
// The iframe receives the error
{
type: 'ui-action-error',
messageId: 'unique-request-id-123',
payload: {
error: { /* the error object */ }
}
}
```

**Complete Iframe Example with Async Handling:**

```html
<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-action-received':
statusEl.textContent = 'Request acknowledged, processing...';
break;

case 'ui-action-response':
statusEl.textContent = 'Completed successfully!';
resultEl.innerHTML = `<pre>${JSON.stringify(message.payload.response, null, 2)}</pre>`;
pendingRequests.delete(message.messageId);
break;

case 'ui-action-error':
statusEl.textContent = 'Error occurred!';
resultEl.innerHTML = `<div style="color: red;">Error: ${JSON.stringify(message.payload.error)}</div>`;
pendingRequests.delete(message.messageId);
break;
}
});
</script>
```

### Message Types

The following internal message types are available as constants:

- `InternalMessageType.UI_ACTION_RECEIVED` (`'ui-action-received'`)
- `InternalMessageType.UI_ACTION_RESPONSE` (`'ui-action-response'`)
- `InternalMessageType.UI_ACTION_ERROR` (`'ui-action-error'`)

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 an `onUIAction` callback.
- **Unique IDs**: Ensure `messageId` values are unique to avoid conflicts between multiple pending requests.
- **Cleanup**: Always clean up pending request tracking when you receive responses to avoid memory leaks.
Loading