Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

**`mcp-ui`** brings interactive web components to the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP). Deliver rich, dynamic UI resources directly from your MCP server to be rendered by the client. Take AI interaction to the next level!

> *This project is an experimental playground for MCP UI ideas. Expect rapid iteration and community-driven enhancements!*
> *This project is an experimental community playground for MCP UI ideas. Expect rapid iteration and enhancements!*

<video src="https://github.com/user-attachments/assets/51f7c712-8133-4d7c-86d3-fdca550b9767"></video>

Expand All @@ -33,6 +33,11 @@

Together, they let you define reusable UI resource blocks on the server side, seamlessly display them in the client, and react to their actions in the MCP host environment.

**North star** -
* Enable servers to deliver rich, interactive UIs with ergonomic APIs
* Allow any host to support UI with its own look-and-feel
* Eliminate security concerns (limit/remove local code execution)


## ✨ Core Concepts

Expand All @@ -44,7 +49,7 @@ The primary payload exchanged between the server and the client:
interface HtmlResourceBlock {
type: 'resource';
resource: {
uri: string; // ui://component/id"
uri: string; // ui://component/id
mimeType: 'text/html' | 'text/uri-list'; // text/html for HTML content, text/uri-list for URL content
text?: string; // Inline HTML or external URL
blob?: string; // Base64-encoded HTML or URL
Expand All @@ -59,8 +64,15 @@ interface HtmlResourceBlock {
* **`text` vs. `blob`**: Choose `text` for simple strings; use `blob` for larger or encoded content.

It's rendered in the client with the `<HtmlResource>` React component.
The component accepts the following props:

* **`resource`**: The `resource` object from an MCP message.
* **`onUiAction`**: A callback function to handle events from the resource.
* **`supportedContentTypes`**: (Optional) An array of content types to allow. Can include `'rawHtml'` and/or `'externalUrl'`. If omitted, all supported types are rendered. This is useful for restricting content types due to capability or security considerations.
* **`style`**: (Optional) Custom styles for the iframe.
* **`iframeProps`**: (Optional) Custom iframe props.

The HTML method is limited, and the external app method isn't secure enough for untrusted 3rd party sites. We need a better method. Some ideas we should explore: RSC, remotedom, etc.
The HTML method is limited, and the external app method isn't secure enough for untrusted sites. We need a better method. We're exploring web components and remote-dom as alternatives that can allow the servers to render their components with the host's look-and-feel without local code execution.

### UI Action

Expand Down Expand Up @@ -115,6 +127,7 @@ yarn add @mcp-ui/server @mcp-ui/client
return (
<HtmlResource
resource={mcpResource.resource}
supportedContentTypes={['rawHtml']}
onUiAction={(result) => {
console.log('Action:', result);
return { status: 'ok' };
Expand Down Expand Up @@ -147,11 +160,12 @@ Drop those URLs into any MCP-compatible host to see `mcp-ui` in action.
## 🛣️ Roadmap

- [X] Add online playground
- [ ] Support React Server Components
- [ ] Support Remote-DOM
- [ ] Support additional client-side libraries (e.g., Vue)
- [ ] Expand UI Action API (beyond tool calls)
- [ ] Do more with Resources and Sampling
- [X] Expand UI Action API (beyond tool calls)
- [ ] Add
- [ ] Support Web Components (in progress)
- [ ] Support Remote-DOM (in progress)
- [ ] Add component libraries (in progress)
- [ ] Support additional client-side libraries and render engines (e.g., Vue, TUI, etc.)

## 🤝 Contributing

Expand Down
8 changes: 6 additions & 2 deletions docs/src/guide/client/html-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface HtmlResourceProps {
}
```

The component accepts the following props:

- **`resource`**: The resource object from an `HtmlResourceBlock`. 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
Expand All @@ -24,7 +26,9 @@ export interface HtmlResourceProps {
{ type: 'link', payload: { url: string } } |
```
If you don't provide a callback for a specific type, the default handler will be used.
- **`style`** (optional): Custom styles for the iframe.
- **`supportedContentTypes`**: (Optional) An array of content types to allow. Can include `'rawHtml'` and/or `'externalUrl'`. If omitted, all supported types are rendered. This is useful for restricting content types due to capability or security considerations.
- **`style`**: (Optional) Custom styles for the iframe.
- ** `iframeProps` **: (Optional) Custom props for the iframe.

## How It Works

Expand Down Expand Up @@ -101,6 +105,6 @@ The `HtmlResource` component maintains backwards compatibility with the legacy `

## Security Notes

- **`sandbox` attribute**: Restricts what the iframe can do. `allow-scripts` is needed for interactivity. `allow-same-origin` is external apps. Caution - the external app method isn's not a secure way to render untrusted code. We're working on new methods to alleviate security concerns.
- **`sandbox` attribute**: Restricts what the iframe can do. `allow-scripts` is needed for interactivity. `allow-same-origin` is external apps. Caution - the external app method isn't a secure way to render untrusted code. We're working on new methods to alleviate security concerns.
- **`postMessage` origin**: When sending messages from the iframe, always specify the target origin for safety. The component listens globally, so your iframe content should be explicit.
- **Content Sanitization**: HTML is rendered as-is. If you don't fully trust the source, sanitize the HTML before passing it in, or rely on the iframe's sandboxing.
19 changes: 16 additions & 3 deletions examples/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

**`mcp-ui`** brings interactive web components to the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP). Deliver rich, dynamic UI resources directly from your MCP server to be rendered by the client. Take AI interaction to the next level!

> *This project is an experimental playground for MCP UI ideas. Expect rapid iteration and community-driven enhancements!*
> *This project is an experimental community playground for MCP UI ideas. Expect rapid iteration and enhancements!*

<video src="https://github.com/user-attachments/assets/51f7c712-8133-4d7c-86d3-fdca550b9767"></video>

Expand All @@ -33,6 +33,11 @@

Together, they let you define reusable UI resource blocks on the server side, seamlessly display them in the client, and react to their actions in the MCP host environment.

**North star** -
* Enable servers to deliver rich, interactive UIs with ergonomic APIs
* Allow any host to support UI with its own look-and-feel
* Eliminate security concerns (limit/remove local code execution)


## ✨ Core Concepts

Expand All @@ -44,7 +49,7 @@ The primary payload exchanged between the server and the client:
interface HtmlResourceBlock {
type: 'resource';
resource: {
uri: string; // e.g. "ui://component/id"
uri: string; // ui://component/id
mimeType: 'text/html' | 'text/uri-list'; // text/html for HTML content, text/uri-list for URL content
text?: string; // Inline HTML or external URL
blob?: string; // Base64-encoded HTML or URL
Expand All @@ -59,8 +64,15 @@ interface HtmlResourceBlock {
* **`text` vs. `blob`**: Choose `text` for simple strings; use `blob` for larger or encoded content.

It's rendered in the client with the `<HtmlResource>` React component.
The component accepts the following props:

* **`resource`**: The `resource` object from an MCP message.
* **`onUiAction`**: A callback function to handle events from the resource.
* **`supportedContentTypes`**: (Optional) An array of content types to allow. Can include `'rawHtml'` and/or `'externalUrl'`. If omitted, all supported types are rendered. This is useful for restricting content types due to capability or security considerations.
* **`style`**: (Optional) Custom styles for the iframe.
* **`iframeProps`**: (Optional) Custom iframe props.

The HTML method is limited, and the external app method isn't secure enough for untrusted 3rd party sites. We need a better method. Some ideas we should explore: RSC, remotedom, etc.
The HTML method is limited, and the external app method isn't secure enough for untrusted sites. We need a better method. We're exploring web components and remote-dom as alternatives that can allow the servers to render their components with the host's look-and-feel without local code execution.

### UI Action

Expand Down Expand Up @@ -115,6 +127,7 @@ yarn add @mcp-ui/server @mcp-ui/client
return (
<HtmlResource
resource={mcpResource.resource}
supportedContentTypes={['rawHtml']}
onUiAction={(result) => {
console.log('Action:', result);
return { status: 'ok' };
Expand Down
32 changes: 23 additions & 9 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

**`mcp-ui`** brings interactive web components to the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP). Deliver rich, dynamic UI resources directly from your MCP server to be rendered by the client. Take AI interaction to the next level!

> *This project is an experimental playground for MCP UI ideas. Expect rapid iteration and community-driven enhancements!*
> *This project is an experimental community playground for MCP UI ideas. Expect rapid iteration and enhancements!*

<video src="https://github.com/user-attachments/assets/51f7c712-8133-4d7c-86d3-fdca550b9767"></video>

Expand All @@ -33,6 +33,11 @@

Together, they let you define reusable UI resource blocks on the server side, seamlessly display them in the client, and react to their actions in the MCP host environment.

**North star** -
* Enable servers to deliver rich, interactive UIs with ergonomic APIs
* Allow any host to support UI with its own look-and-feel
* Eliminate security concerns (limit/remove local code execution)


## ✨ Core Concepts

Expand All @@ -44,10 +49,10 @@ The primary payload exchanged between the server and the client:
interface HtmlResourceBlock {
type: 'resource';
resource: {
uri: string; // e.g. "ui://component/id"
uri: string; // ui://component/id
mimeType: 'text/html' | 'text/uri-list'; // text/html for HTML content, text/uri-list for URL content
text?: string; // Inline HTML or external URL
blob?: string; // Base64-encoded HTML or URL (for large payloads)
blob?: string; // Base64-encoded HTML or URL
};
}
```
Expand All @@ -59,8 +64,15 @@ interface HtmlResourceBlock {
* **`text` vs. `blob`**: Choose `text` for simple strings; use `blob` for larger or encoded content.

It's rendered in the client with the `<HtmlResource>` React component.
The component accepts the following props:

* **`resource`**: The `resource` object from an MCP message.
* **`onUiAction`**: A callback function to handle events from the resource.
* **`supportedContentTypes`**: (Optional) An array of content types to allow. Can include `'rawHtml'` and/or `'externalUrl'`. If omitted, all supported types are rendered. This is useful for restricting content types due to capability or security considerations.
* **`style`**: (Optional) Custom styles for the iframe.
* **`iframeProps`**: (Optional) Custom iframe props.

The HTML method is limited, and the external app method isn't secure enough for untrusted 3rd party sites. We need a better method. Some ideas we should explore: RSC, remotedom, etc.
The HTML method is limited, and the external app method isn't secure enough for untrusted sites. We need a better method. We're exploring web components and remote-dom as alternatives that can allow the servers to render their components with the host's look-and-feel without local code execution.

### UI Action

Expand Down Expand Up @@ -115,6 +127,7 @@ yarn add @mcp-ui/server @mcp-ui/client
return (
<HtmlResource
resource={mcpResource.resource}
supportedContentTypes={['rawHtml']}
onUiAction={(result) => {
console.log('Action:', result);
return { status: 'ok' };
Expand Down Expand Up @@ -147,11 +160,12 @@ Drop those URLs into any MCP-compatible host to see `mcp-ui` in action.
## 🛣️ Roadmap

- [X] Add online playground
- [ ] Support React Server Components
- [ ] Support Remote-DOM
- [ ] Support additional client-side libraries (e.g., Vue)
- [ ] Expand UI Action API (beyond tool calls)
- [ ] Do more with Resources and Sampling
- [X] Expand UI Action API (beyond tool calls)
- [ ] Add
- [ ] Support Web Components (in progress)
- [ ] Support Remote-DOM (in progress)
- [ ] Add component libraries (in progress)
- [ ] Support additional client-side libraries and render engines (e.g., Vue, TUI, etc.)

## 🤝 Contributing

Expand Down
138 changes: 74 additions & 64 deletions packages/client/src/components/HtmlResource.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import type { Resource } from '@modelcontextprotocol/sdk/types.js';
import { UiActionResult } from '../types';
import { UiActionResult, ResourceContentType } from '../types';
import { processResource } from '../utils/processResource';

export type RenderHtmlResourceProps = {
Expand All @@ -11,84 +11,94 @@ export type RenderHtmlResourceProps = {
React.HTMLAttributes<HTMLIFrameElement>,
'src' | 'srcDoc' | 'ref' | 'style'
>;
supportedContentTypes?: ResourceContentType[];
};

export const HtmlResource = React.forwardRef<
HTMLIFrameElement | null,
RenderHtmlResourceProps
>(({ resource, onUiAction, style, iframeProps }, ref) => {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
useImperativeHandle(ref, () => iframeRef.current as HTMLIFrameElement);
>(
(
{ resource, onUiAction, style, iframeProps, supportedContentTypes },
ref,
) => {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
useImperativeHandle(ref, () => iframeRef.current as HTMLIFrameElement);

const { error, iframeSrc, iframeRenderMode, htmlString } = useMemo(
() => processResource(resource),
[resource],
);
const { error, iframeSrc, iframeRenderMode, htmlString } = useMemo(
() => processResource(resource, supportedContentTypes),
[resource, supportedContentTypes],
);

useEffect(() => {
function handleMessage(event: MessageEvent) {
// Only process the message if it came from this specific iframe
if (
iframeRef.current &&
event.source === iframeRef.current.contentWindow
) {
const uiActionResult = event.data as UiActionResult;
if (!uiActionResult) {
return;
useEffect(() => {
function handleMessage(event: MessageEvent) {
// Only process the message if it came from this specific iframe
if (
iframeRef.current &&
event.source === iframeRef.current.contentWindow
) {
const uiActionResult = event.data as UiActionResult;
if (!uiActionResult) {
return;
}
onUiAction?.(uiActionResult)?.catch((err) => {
console.error(
'Error handling UI action result in RenderHtmlResource:',
err,
);
});
}
onUiAction?.(uiActionResult)?.catch((err) => {
console.error(
'Error handling UI action result in RenderHtmlResource:',
err,
);
});
}
}
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [onUiAction]);
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [onUiAction]);

if (error) return <p className="text-red-500">{error}</p>;
if (error) return <p className="text-red-500">{error}</p>;

if (iframeRenderMode === 'srcDoc') {
if (htmlString === null || htmlString === undefined) {
if (!error) {
return <p className="text-orange-500">No HTML content to display.</p>;
if (iframeRenderMode === 'srcDoc') {
if (htmlString === null || htmlString === undefined) {
if (!error) {
return <p className="text-orange-500">No HTML content to display.</p>;
}
return null;
}
return null;
}
return (
<iframe
srcDoc={htmlString}
sandbox="allow-scripts"
style={{ width: '100%', minHeight: 200, ...style }}
title="MCP HTML Resource (Embedded Content)"
{...iframeProps}
ref={iframeRef}
/>
);
} else if (iframeRenderMode === 'src') {
if (iframeSrc === null || iframeSrc === undefined) {
if (!error) {
return (
<p className="text-orange-500">No URL provided for HTML resource.</p>
);
return (
<iframe
srcDoc={htmlString}
sandbox="allow-scripts"
style={{ width: '100%', minHeight: 200, ...style }}
title="MCP HTML Resource (Embedded Content)"
{...iframeProps}
ref={iframeRef}
/>
);
} else if (iframeRenderMode === 'src') {
if (iframeSrc === null || iframeSrc === undefined) {
if (!error) {
return (
<p className="text-orange-500">
No URL provided for HTML resource.
</p>
);
}
return null;
}
return null;
return (
<iframe
src={iframeSrc}
sandbox="allow-scripts allow-same-origin" // unsafe
style={{ width: '100%', minHeight: 200, ...style }}
title="MCP HTML Resource (URL)"
{...iframeProps}
ref={iframeRef}
/>
);
}

return (
<iframe
src={iframeSrc}
sandbox="allow-scripts allow-same-origin" // unsafe
style={{ width: '100%', minHeight: 200, ...style }}
title="MCP HTML Resource (URL)"
{...iframeProps}
ref={iframeRef}
/>
<p className="text-gray-500">Initializing HTML resource display...</p>
);
}

return <p className="text-gray-500">Initializing HTML resource display...</p>;
});
},
);

HtmlResource.displayName = 'HtmlResource';
Loading