From df869cfe50858bf5db5b0950422c865f61604380 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Thu, 17 Jul 2025 20:29:04 +0300 Subject: [PATCH 1/6] rename flavor to framework --- README.md | 4 ++-- docs/src/guide/client/remote-dom-resource.md | 2 +- docs/src/guide/client/usage-examples.md | 4 ++-- docs/src/guide/server/usage-examples.md | 4 ++-- examples/remote-dom-demo/src/App.tsx | 4 ++-- examples/server/src/index.ts | 4 ++-- packages/client/README.md | 4 ++-- .../src/components/RemoteDOMResourceRenderer.tsx | 12 ++++++------ .../__tests__/RemoteDOMResourceRenderer.test.tsx | 14 +++++++------- .../__tests__/UIResourceRenderer.test.tsx | 2 +- packages/server/README.md | 4 ++-- packages/server/src/__tests__/index.test.ts | 14 +++++++------- packages/server/src/index.ts | 2 +- packages/server/src/types.ts | 6 +++--- 14 files changed, 40 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index ae69d346..3021f237 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Rendered using the `` component, which displays content Rendered using the internal `` component, which utilizes Shopify's [`remote-dom`](https://github.com/Shopify/remote-dom). The server responds with a script that describes the UI and events. On the host, the script is securely rendered in a sandboxed iframe, and the UI changes are communicated to the host in JSON, where they're rendered using the host's component library. This is more flexible than iframes and allows for UIs that match the host's look-and-feel. -* **`mimeType`**: `application/vnd.mcp-ui.remote-dom; flavor={react | webcomponents}` +* **`mimeType`**: `application/vnd.mcp-ui.remote-dom; framework={react | webcomponents}` ### UI Action @@ -162,7 +162,7 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to }); root.appendChild(button); `, - flavor: 'react', // or 'webcomponents' + framework: 'react', // or 'webcomponents' }, delivery: 'text', }); diff --git a/docs/src/guide/client/remote-dom-resource.md b/docs/src/guide/client/remote-dom-resource.md index 7dff045c..9e4a7ccc 100644 --- a/docs/src/guide/client/remote-dom-resource.md +++ b/docs/src/guide/client/remote-dom-resource.md @@ -15,7 +15,7 @@ This ensures that no arbitrary code from the server runs in the main application ## Props -- **`resource`**: The `resource` object from an MCP message. The `mimeType` must be `application/vnd.mcp-ui.remote-dom+javascript; flavor={react | webcomponents}`. +- **`resource`**: The `resource` object from an MCP message. The `mimeType` must be `application/vnd.mcp-ui.remote-dom+javascript; framework={react | webcomponents}`. - **`library`**: A component library that maps remote element tag names (e.g., "button") to your host's React components. - **`onUIAction`**: A callback function to handle events (e.g., button clicks) initiated from the remote UI. diff --git a/docs/src/guide/client/usage-examples.md b/docs/src/guide/client/usage-examples.md index 64ec345c..e2b6b6f3 100644 --- a/docs/src/guide/client/usage-examples.md +++ b/docs/src/guide/client/usage-examples.md @@ -38,7 +38,7 @@ const remoteDomResource = { type: 'resource', resource: { uri: 'ui://remote-component/action-button', - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=react', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', text: remoteDomScript, }, }; @@ -130,7 +130,7 @@ const fetchMcpResource = async (id: string): Promise => { type: 'resource', resource: { uri: 'ui://remote-component/action-button', - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=react', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', text: remoteDomScript, }, }; diff --git a/docs/src/guide/server/usage-examples.md b/docs/src/guide/server/usage-examples.md index 12d00f64..38cc99e6 100644 --- a/docs/src/guide/server/usage-examples.md +++ b/docs/src/guide/server/usage-examples.md @@ -117,7 +117,7 @@ const resource5 = createUIResource({ content: { type: 'remoteDom', script: remoteDomScript, - flavor: 'react', // or 'webcomponents' + framework: 'react', // or 'webcomponents' }, delivery: 'text', }); @@ -127,7 +127,7 @@ console.log('Resource 5:', JSON.stringify(resource5, null, 2)); "type": "resource", "resource": { "uri": "ui://remote-component/action-button", - "mimeType": "application/vnd.mcp-ui.remote-dom+javascript; flavor=react", + "mimeType": "application/vnd.mcp-ui.remote-dom+javascript; framework=react", "text": "\\n const button = document.createElement('ui-button');\\n button.setAttribute('label', 'Click me for a tool call!');\\n button.addEventListener('press', () => {\\n window.parent.postMessage({ type: 'tool', payload: { toolName: 'uiInteraction', params: { action: 'button-click', from: 'remote-dom' } } }, '*');\\n });\\n root.appendChild(button);\\n" } } diff --git a/examples/remote-dom-demo/src/App.tsx b/examples/remote-dom-demo/src/App.tsx index 06e5f731..31a874a6 100644 --- a/examples/remote-dom-demo/src/App.tsx +++ b/examples/remote-dom-demo/src/App.tsx @@ -98,7 +98,7 @@ function App() { const mockResourceReact = useMemo( () => ({ - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=react', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', text: scriptContent, }), [scriptContent], @@ -106,7 +106,7 @@ function App() { const mockResourceWebComponents = useMemo( () => ({ - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=webcomponents', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=webcomponents', text: scriptContent, }), [scriptContent], diff --git a/examples/server/src/index.ts b/examples/server/src/index.ts index c112dedd..ddd89278 100644 --- a/examples/server/src/index.ts +++ b/examples/server/src/index.ts @@ -192,7 +192,7 @@ export class MyMCP extends McpAgent { delivery: 'text', content: { type: 'remoteDom', - flavor: 'react', + framework: 'react', script: ` // Create a state variable to track the current logo let isDarkMode = false; @@ -265,7 +265,7 @@ export class MyMCP extends McpAgent { delivery: 'text', content: { type: 'remoteDom', - flavor: 'webcomponents', + framework: 'webcomponents', script: ` // Create a state variable to track the current logo let isDarkMode = false; diff --git a/packages/client/README.md b/packages/client/README.md index 5ae218db..57f1215a 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -102,7 +102,7 @@ Rendered using the `` component, which displays content Rendered using the `` component, which uses Shopify's [`remote-dom`](https://github.com/Shopify/remote-dom). The server responds with a script that describes the UI and events. On the host, the script is securely rendered in a sandboxed iframe, and the UI changes are communicated to the host in JSON, where they're rendered using the host's component library. This is more flexible than iframes and allows for UIs that match the host's look-and-feel. -* **`mimeType`**: `application/vnd.mcp-ui.remote-dom; flavor={react | webcomponents}` +* **`mimeType`**: `application/vnd.mcp-ui.remote-dom; framework={react | webcomponents}` ### UI Action @@ -162,7 +162,7 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to }); root.appendChild(button); `, - flavor: 'react', // or 'webcomponents' + framework: 'react', // or 'webcomponents' }, delivery: 'text', }); diff --git a/packages/client/src/components/RemoteDOMResourceRenderer.tsx b/packages/client/src/components/RemoteDOMResourceRenderer.tsx index c5138cc7..41e5a88a 100644 --- a/packages/client/src/components/RemoteDOMResourceRenderer.tsx +++ b/packages/client/src/components/RemoteDOMResourceRenderer.tsx @@ -36,19 +36,19 @@ export const RemoteDOMResourceRenderer: React.FC = ({ const threadRef = useRef | null>(null); const [error, setError] = useState(null); - const flavor = useMemo(() => { + const framework = useMemo(() => { const mimeType = resource.mimeType || ''; - if (mimeType.includes('flavor=react')) { + if (mimeType.includes('framework=react')) { return 'react'; } // Default to webcomponents for legacy or unspecified return 'webcomponents'; }, [resource.mimeType]); - const componentKey = `${library?.name}-${flavor}`; + const componentKey = `${library?.name}-${framework}`; const { receiver, components } = useMemo(() => { - switch (flavor) { + switch (framework) { case 'react': { const reactReceiver = new RemoteReceiver(); const componentLibrary = library || basicComponentLibrary; @@ -131,7 +131,7 @@ export const RemoteDOMResourceRenderer: React.FC = ({ const options = { code, remoteElements, - useReactRenderer: flavor === 'react', + useReactRenderer: framework === 'react', componentLibrary: library?.name, }; thread.imports @@ -156,7 +156,7 @@ export const RemoteDOMResourceRenderer: React.FC = ({ onLoad={handleIframeLoad} /> - {flavor === 'react' && components ? ( + {framework === 'react' && components ? ( ) : ( diff --git a/packages/client/src/components/__tests__/RemoteDOMResourceRenderer.test.tsx b/packages/client/src/components/__tests__/RemoteDOMResourceRenderer.test.tsx index 160dc497..a3ae7527 100644 --- a/packages/client/src/components/__tests__/RemoteDOMResourceRenderer.test.tsx +++ b/packages/client/src/components/__tests__/RemoteDOMResourceRenderer.test.tsx @@ -28,10 +28,10 @@ describe('', () => { vi.clearAllMocks(); }); - it('should use React renderer when mimeType includes "flavor=react"', () => { + it('should use React renderer when mimeType includes "framework=react"', () => { const resource = { ...baseResource, - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=react', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', }; render(); @@ -40,10 +40,10 @@ describe('', () => { expect(screen.queryByTestId('standard-dom-renderer-container')).not.toBeInTheDocument(); }); - it('should use standard DOM renderer when mimeType includes "flavor=webcomponents"', () => { + it('should use standard DOM renderer when mimeType includes "framework=webcomponents"', () => { const resource = { ...baseResource, - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=webcomponents', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=webcomponents', }; render(); @@ -61,10 +61,10 @@ describe('', () => { expect(screen.queryByTestId('remote-root-renderer')).not.toBeInTheDocument(); }); - it('should default to standard DOM renderer for an unknown flavor', () => { + it('should default to standard DOM renderer for an unknown framework', () => { const resource = { ...baseResource, - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=unknown', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=unknown', }; render(); @@ -76,7 +76,7 @@ describe('', () => { it('should use the provided component library', () => { const resource = { ...baseResource, - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=react', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', }; render(); diff --git a/packages/client/src/components/__tests__/UIResourceRenderer.test.tsx b/packages/client/src/components/__tests__/UIResourceRenderer.test.tsx index a671a0a0..05e34afe 100644 --- a/packages/client/src/components/__tests__/UIResourceRenderer.test.tsx +++ b/packages/client/src/components/__tests__/UIResourceRenderer.test.tsx @@ -43,7 +43,7 @@ describe('', () => { it('should render RemoteDOMResourceRenderer for "remote-dom" mimeType', () => { const resource = { ...baseResource, - mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; flavor=react', + mimeType: 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', }; render(); expect(screen.getByTestId('remote-dom-resource')).toBeInTheDocument(); diff --git a/packages/server/README.md b/packages/server/README.md index 5ae218db..57f1215a 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -102,7 +102,7 @@ Rendered using the `` component, which displays content Rendered using the `` component, which uses Shopify's [`remote-dom`](https://github.com/Shopify/remote-dom). The server responds with a script that describes the UI and events. On the host, the script is securely rendered in a sandboxed iframe, and the UI changes are communicated to the host in JSON, where they're rendered using the host's component library. This is more flexible than iframes and allows for UIs that match the host's look-and-feel. -* **`mimeType`**: `application/vnd.mcp-ui.remote-dom; flavor={react | webcomponents}` +* **`mimeType`**: `application/vnd.mcp-ui.remote-dom; framework={react | webcomponents}` ### UI Action @@ -162,7 +162,7 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to }); root.appendChild(button); `, - flavor: 'react', // or 'webcomponents' + framework: 'react', // or 'webcomponents' }, delivery: 'text', }); diff --git a/packages/server/src/__tests__/index.test.ts b/packages/server/src/__tests__/index.test.ts index 95a88cad..4d7aa794 100644 --- a/packages/server/src/__tests__/index.test.ts +++ b/packages/server/src/__tests__/index.test.ts @@ -99,13 +99,13 @@ describe('@mcp-ui/server', () => { ); }); - it('should create a text-based remote DOM resource with React flavor', () => { + it('should create a text-based remote DOM resource with React framework', () => { const options = { uri: 'ui://test-remote-dom-react' as const, content: { type: 'remoteDom' as const, script: '

React Component

', - flavor: 'react' as const, + framework: 'react' as const, }, delivery: 'text' as const, }; @@ -113,25 +113,25 @@ describe('@mcp-ui/server', () => { expect(resource.type).toBe('resource'); expect(resource.resource.uri).toBe('ui://test-remote-dom-react'); expect(resource.resource.mimeType).toBe( - 'application/vnd.mcp-ui.remote-dom+javascript; flavor=react', + 'application/vnd.mcp-ui.remote-dom+javascript; framework=react', ); expect(resource.resource.text).toBe('

React Component

'); expect(resource.resource.blob).toBeUndefined(); }); - it('should create a blob-based remote DOM resource with Web Components flavor', () => { + it('should create a blob-based remote DOM resource with Web Components framework', () => { const options = { uri: 'ui://test-remote-dom-wc' as const, content: { type: 'remoteDom' as const, script: '

Web Component

', - flavor: 'webcomponents' as const, + framework: 'webcomponents' as const, }, delivery: 'blob' as const, }; const resource = createUIResource(options); expect(resource.resource.mimeType).toBe( - 'application/vnd.mcp-ui.remote-dom+javascript; flavor=webcomponents', + 'application/vnd.mcp-ui.remote-dom+javascript; framework=webcomponents', ); expect(resource.resource.blob).toBe(Buffer.from('

Web Component

').toString('base64')); expect(resource.resource.text).toBeUndefined(); @@ -143,7 +143,7 @@ describe('@mcp-ui/server', () => { content: { type: 'remoteDom' as const, script: '

Invalid

', - flavor: 'react' as const, + framework: 'react' as const, }, delivery: 'text' as const, }; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index cf1ecd9c..b87803e6 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -89,7 +89,7 @@ export function createUIResource(options: CreateUIResourceOptions): UIResource { "MCP SDK: content.script must be provided as a string when content.type is 'remoteDom'.", ); } - mimeType = `application/vnd.mcp-ui.remote-dom+javascript; flavor=${options.content.flavor}`; + mimeType = `application/vnd.mcp-ui.remote-dom+javascript; framework=${options.content.framework}`; } else { // This case should ideally be prevented by TypeScript's discriminated union checks const exhaustiveCheckContent: never = options.content; diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index e7835e24..0e31f119 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -5,8 +5,8 @@ export type URI = `ui://${string}`; export type MimeType = | 'text/html' | 'text/uri-list' - | 'application/vnd.mcp-ui.remote-dom+javascript; flavor=react' - | 'application/vnd.mcp-ui.remote-dom+javascript; flavor=webcomponents'; + | 'application/vnd.mcp-ui.remote-dom+javascript; framework=react' + | 'application/vnd.mcp-ui.remote-dom+javascript; framework=webcomponents'; export type HTMLTextContent = { uri: URI; @@ -28,7 +28,7 @@ export type ResourceContentPayload = | { type: 'remoteDom'; script: string; - flavor: 'react' | 'webcomponents'; + framework: 'react' | 'webcomponents'; }; export interface CreateUIResourceOptions { From a7aa088535e9264090858e0651ba00bac6a01e72 Mon Sep 17 00:00:00 2001 From: Ido Salomon Date: Thu, 17 Jul 2025 20:30:12 +0300 Subject: [PATCH 2/6] rename delivery to encoding --- README.md | 6 +- docs/src/guide/getting-started.md | 2 +- docs/src/guide/introduction.md | 2 +- docs/src/guide/protocol-details.md | 2 +- docs/src/guide/server/overview.md | 4 +- docs/src/guide/server/usage-examples.md | 20 +- docs/src/index.md | 2 +- examples/server/src/index.ts | 8 +- packages/client/README.md | 6 +- packages/server/README.md | 6 +- packages/server/src/__tests__/index.test.ts | 20 +- packages/server/src/index.ts | 6 +- packages/server/src/types.ts | 2 +- sdks/ruby/lib/mcp_ui_server.rb | 131 +++++++++ sdks/ruby/spec/mcp_ui_server_spec.rb | 169 ++++++++++++ sdks/typescript/server/README.md | 290 ++++++++++++++++++++ 16 files changed, 633 insertions(+), 43 deletions(-) create mode 100644 sdks/ruby/lib/mcp_ui_server.rb create mode 100644 sdks/ruby/spec/mcp_ui_server_spec.rb create mode 100644 sdks/typescript/server/README.md diff --git a/README.md b/README.md index 3021f237..405b3227 100644 --- a/README.md +++ b/README.md @@ -139,14 +139,14 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to const htmlResource = createUIResource({ uri: 'ui://greeting/1', content: { type: 'rawHtml', htmlString: '

Hello, MCP UI!

' }, - delivery: 'text', + encoding: 'text', }); // External URL const externalUrlResource = createUIResource({ uri: 'ui://greeting/1', content: { type: 'externalUrl', iframeUrl: 'https://example.com' }, - delivery: 'text', + encoding: 'text', }); // remote-dom @@ -164,7 +164,7 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to `, framework: 'react', // or 'webcomponents' }, - delivery: 'text', + encoding: 'text', }); ``` diff --git a/docs/src/guide/getting-started.md b/docs/src/guide/getting-started.md index 13a07fa3..6d61a76e 100644 --- a/docs/src/guide/getting-started.md +++ b/docs/src/guide/getting-started.md @@ -62,7 +62,7 @@ const myHtmlPayload = `

Hello from Server!

Timestamp: ${new Date().toI const resourceBlock = createUIResource({ uri: 'ui://server-generated/item1', content: { type: 'rawHtml', htmlString: myHtmlPayload }, - delivery: 'text', + encoding: 'text', }); diff --git a/docs/src/guide/introduction.md b/docs/src/guide/introduction.md index 9f3b36c3..ec911bb6 100644 --- a/docs/src/guide/introduction.md +++ b/docs/src/guide/introduction.md @@ -56,7 +56,7 @@ import { createUIResource } from '@mcp-ui/server'; const resource = createUIResource({ uri: 'ui://my-tool/dashboard', content: { type: 'rawHtml', htmlString: '

Dashboard

' }, - delivery: 'text' + encoding: 'text' }); // Return in MCP response diff --git a/docs/src/guide/protocol-details.md b/docs/src/guide/protocol-details.md index 2ae212ed..54fbb262 100644 --- a/docs/src/guide/protocol-details.md +++ b/docs/src/guide/protocol-details.md @@ -31,7 +31,7 @@ export interface UIResource { - 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) -## Content Delivery: `text` vs. `blob` +## Content encoding: `text` vs. `blob` - **`text`**: Simple, direct string. Good for smaller, less complex content. - **`blob`**: Base64 encoded string. diff --git a/docs/src/guide/server/overview.md b/docs/src/guide/server/overview.md index 8bf77e24..543af1ff 100644 --- a/docs/src/guide/server/overview.md +++ b/docs/src/guide/server/overview.md @@ -5,13 +5,13 @@ The `@mcp-ui/server` package provides server-side utilities to help construct `U ## Key Exports - **`createUIResource(options: CreateUIResourceOptions): UIResource`**: - The primary function for creating UI snippets. It takes an options object to define the URI, content (direct HTML or external URL), and delivery method (text or blob). + The primary function for creating UI snippets. It takes an options object to define the URI, content (direct HTML or external URL), and encoding method (text or blob). ## Purpose - **Ease of Use**: Simplifies the creation of valid `UIResource` objects. - **Validation**: Includes basic validation (e.g., URI prefixes matching content type). -- **Encoding**: Handles Base64 encoding when `delivery: 'blob'` is specified. +- **Encoding**: Handles Base64 encoding when `encoding: 'blob'` is specified. ## Building diff --git a/docs/src/guide/server/usage-examples.md b/docs/src/guide/server/usage-examples.md index 38cc99e6..8a4b6ccd 100644 --- a/docs/src/guide/server/usage-examples.md +++ b/docs/src/guide/server/usage-examples.md @@ -26,7 +26,7 @@ console.log('Shared Enum from server usage:', PlaceholderEnum.FOO); const resource1 = createUIResource({ uri: 'ui://my-component/instance-1', content: { type: 'rawHtml', htmlString: '

Hello World

' }, - delivery: 'text', + encoding: 'text', }); console.log('Resource 1:', JSON.stringify(resource1, null, 2)); /* Output for Resource 1: @@ -44,7 +44,7 @@ console.log('Resource 1:', JSON.stringify(resource1, null, 2)); const resource2 = createUIResource({ uri: 'ui://my-component/instance-2', content: { type: 'rawHtml', htmlString: '

Complex HTML

' }, - delivery: 'blob', + encoding: 'blob', }); console.log( 'Resource 2 (blob will be Base64):', @@ -61,12 +61,12 @@ console.log( } */ -// Example 3: External URL, text delivery +// Example 3: External URL, text encoding const dashboardUrl = 'https://my.analytics.com/dashboard/123'; const resource3 = createUIResource({ uri: 'ui://analytics-dashboard/main', content: { type: 'externalUrl', iframeUrl: dashboardUrl }, - delivery: 'text', + encoding: 'text', }); console.log('Resource 3:', JSON.stringify(resource3, null, 2)); /* Output for Resource 3: @@ -80,12 +80,12 @@ console.log('Resource 3:', JSON.stringify(resource3, null, 2)); } */ -// Example 4: External URL, blob delivery (URL is Base64 encoded) +// Example 4: External URL, blob encoding (URL is Base64 encoded) const chartApiUrl = 'https://charts.example.com/api?type=pie&data=1,2,3'; const resource4 = createUIResource({ uri: 'ui://live-chart/session-xyz', content: { type: 'externalUrl', iframeUrl: chartApiUrl }, - delivery: 'blob', + encoding: 'blob', }); console.log( 'Resource 4 (blob will be Base64 of URL):', @@ -102,7 +102,7 @@ console.log( } */ -// Example 5: Remote DOM script, text delivery +// Example 5: Remote DOM script, text encoding const remoteDomScript = ` const button = document.createElement('ui-button'); button.setAttribute('label', 'Click me for a tool call!'); @@ -119,7 +119,7 @@ const resource5 = createUIResource({ script: remoteDomScript, framework: 'react', // or 'webcomponents' }, - delivery: 'text', + encoding: 'text', }); console.log('Resource 5:', JSON.stringify(resource5, null, 2)); /* Output for Resource 5: @@ -154,7 +154,7 @@ https://emergency.dashboard.example.com/main`; const resource6 = createUIResource({ uri: 'ui://dashboard-with-fallbacks/session-123', content: { type: 'externalUrl', iframeUrl: multiUrlContent }, - delivery: 'text', + encoding: 'text', }); /* The client will: @@ -176,7 +176,7 @@ try { createUIResource({ uri: 'invalid://should-be-ui', content: { type: 'externalUrl', iframeUrl: 'https://example.com' }, - delivery: 'text', + encoding: 'text', }); } catch (e: any) { console.error('Caught expected error:', e.message); diff --git a/docs/src/index.md b/docs/src/index.md index 1dd04cb2..fd23a651 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -56,7 +56,7 @@ const interactiveForm = createUIResource({ type: 'externalUrl', iframeUrl: 'https://yourapp.com' }, - delivery: 'text', + encoding: 'text', }); ``` diff --git a/examples/server/src/index.ts b/examples/server/src/index.ts index ddd89278..b81b3287 100644 --- a/examples/server/src/index.ts +++ b/examples/server/src/index.ts @@ -153,7 +153,7 @@ export class MyMCP extends McpAgent { const resourceBlock = createUIResource({ uri: uniqueUIAppUri, content: { type: 'externalUrl', iframeUrl: pickerPageUrl }, - delivery: 'text', // The URL itself is delivered as text + encoding: 'text', // The URL itself is delivered as text }); return { @@ -177,7 +177,7 @@ export class MyMCP extends McpAgent { const resourceBlock = createUIResource({ uri: uniqueUIAppUri, content: { type: 'externalUrl', iframeUrl: pickerPageUrl }, - delivery: 'text', // The URL itself is delivered as text + encoding: 'text', // The URL itself is delivered as text }); return { @@ -189,7 +189,7 @@ export class MyMCP extends McpAgent { this.server.tool('show_remote_dom_react', 'Shows a react remote-dom component', async () => { const resourceBlock = createUIResource({ uri: `ui://remote-dom-react/${Date.now()}` as `ui://${string}`, - delivery: 'text', + encoding: 'text', content: { type: 'remoteDom', framework: 'react', @@ -262,7 +262,7 @@ export class MyMCP extends McpAgent { async () => { const resourceBlock = createUIResource({ uri: `ui://remote-dom-wc/${Date.now()}` as `ui://${string}`, - delivery: 'text', + encoding: 'text', content: { type: 'remoteDom', framework: 'webcomponents', diff --git a/packages/client/README.md b/packages/client/README.md index 57f1215a..72847256 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -139,14 +139,14 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to const htmlResource = createUIResource({ uri: 'ui://greeting/1', content: { type: 'rawHtml', htmlString: '

Hello, MCP UI!

' }, - delivery: 'text', + encoding: 'text', }); // External URL const externalUrlResource = createUIResource({ uri: 'ui://greeting/1', content: { type: 'externalUrl', iframeUrl: 'https://example.com' }, - delivery: 'text', + encoding: 'text', }); // remote-dom @@ -164,7 +164,7 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to `, framework: 'react', // or 'webcomponents' }, - delivery: 'text', + encoding: 'text', }); ``` diff --git a/packages/server/README.md b/packages/server/README.md index 57f1215a..72847256 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -139,14 +139,14 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to const htmlResource = createUIResource({ uri: 'ui://greeting/1', content: { type: 'rawHtml', htmlString: '

Hello, MCP UI!

' }, - delivery: 'text', + encoding: 'text', }); // External URL const externalUrlResource = createUIResource({ uri: 'ui://greeting/1', content: { type: 'externalUrl', iframeUrl: 'https://example.com' }, - delivery: 'text', + encoding: 'text', }); // remote-dom @@ -164,7 +164,7 @@ You can use [GitMCP](https://gitmcp.io/idosal/mcp-ui) to give your IDE access to `, framework: 'react', // or 'webcomponents' }, - delivery: 'text', + encoding: 'text', }); ``` diff --git a/packages/server/src/__tests__/index.test.ts b/packages/server/src/__tests__/index.test.ts index 4d7aa794..2fab3870 100644 --- a/packages/server/src/__tests__/index.test.ts +++ b/packages/server/src/__tests__/index.test.ts @@ -6,7 +6,7 @@ describe('@mcp-ui/server', () => { const options = { uri: 'ui://test-html' as const, content: { type: 'rawHtml' as const, htmlString: '

Test

' }, - delivery: 'text' as const, + encoding: 'text' as const, }; const resource = createUIResource(options); expect(resource.type).toBe('resource'); @@ -20,7 +20,7 @@ describe('@mcp-ui/server', () => { const options = { uri: 'ui://test-html-blob' as const, content: { type: 'rawHtml' as const, htmlString: '

Blob

' }, - delivery: 'blob' as const, + encoding: 'blob' as const, }; const resource = createUIResource(options); expect(resource.resource.blob).toBe(Buffer.from('

Blob

').toString('base64')); @@ -34,7 +34,7 @@ describe('@mcp-ui/server', () => { type: 'externalUrl' as const, iframeUrl: 'https://example.com', }, - delivery: 'text' as const, + encoding: 'text' as const, }; const resource = createUIResource(options); expect(resource.resource.uri).toBe('ui://test-url'); @@ -50,7 +50,7 @@ describe('@mcp-ui/server', () => { type: 'externalUrl' as const, iframeUrl: 'https://example.com/blob', }, - delivery: 'blob' as const, + encoding: 'blob' as const, }; const resource = createUIResource(options); expect(resource.resource.mimeType).toBe('text/uri-list'); @@ -64,7 +64,7 @@ describe('@mcp-ui/server', () => { const options = { uri: 'ui://test-html-blob' as const, content: { type: 'rawHtml' as const, htmlString: '

Blob

' }, - delivery: 'blob' as const, + encoding: 'blob' as const, }; const resource = createUIResource(options); expect(resource.resource.mimeType).toBe('text/html'); @@ -76,7 +76,7 @@ describe('@mcp-ui/server', () => { const options = { uri: 'invalid://test-html' as const, content: { type: 'rawHtml' as const, htmlString: '

Test

' }, - delivery: 'text' as const, + encoding: 'text' as const, }; // @ts-expect-error We are intentionally passing an invalid URI to test the error. expect(() => createUIResource(options)).toThrow( @@ -91,7 +91,7 @@ describe('@mcp-ui/server', () => { type: 'externalUrl' as const, iframeUrl: 'https://example.com', }, - delivery: 'text' as const, + encoding: 'text' as const, }; // @ts-expect-error We are intentionally passing an invalid URI to test the error. expect(() => createUIResource(options)).toThrow( @@ -107,7 +107,7 @@ describe('@mcp-ui/server', () => { script: '

React Component

', framework: 'react' as const, }, - delivery: 'text' as const, + encoding: 'text' as const, }; const resource = createUIResource(options); expect(resource.type).toBe('resource'); @@ -127,7 +127,7 @@ describe('@mcp-ui/server', () => { script: '

Web Component

', framework: 'webcomponents' as const, }, - delivery: 'blob' as const, + encoding: 'blob' as const, }; const resource = createUIResource(options); expect(resource.resource.mimeType).toBe( @@ -145,7 +145,7 @@ describe('@mcp-ui/server', () => { script: '

Invalid

', framework: 'react' as const, }, - delivery: 'text' as const, + encoding: 'text' as const, }; // @ts-expect-error We are intentionally passing an invalid URI to test the error. expect(() => createUIResource(options)).toThrow( diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b87803e6..329a3cc1 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -98,7 +98,7 @@ export function createUIResource(options: CreateUIResourceOptions): UIResource { let resource: UIResource['resource']; - switch (options.delivery) { + switch (options.encoding) { case 'text': resource = { uri: options.uri, @@ -114,8 +114,8 @@ export function createUIResource(options: CreateUIResourceOptions): UIResource { }; break; default: { - const exhaustiveCheck: never = options.delivery; - throw new Error(`Invalid delivery type: ${exhaustiveCheck}`); + const exhaustiveCheck: never = options.encoding; + throw new Error(`Invalid encoding type: ${exhaustiveCheck}`); } } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 0e31f119..e5ca0228 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -34,7 +34,7 @@ export type ResourceContentPayload = export interface CreateUIResourceOptions { uri: URI; content: ResourceContentPayload; - delivery: 'text' | 'blob'; + encoding: 'text' | 'blob'; } export type UIActionType = 'tool' | 'prompt' | 'link' | 'intent' | 'notify'; diff --git a/sdks/ruby/lib/mcp_ui_server.rb b/sdks/ruby/lib/mcp_ui_server.rb new file mode 100644 index 00000000..dc16aef1 --- /dev/null +++ b/sdks/ruby/lib/mcp_ui_server.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require_relative 'mcp_ui_server/version' +require 'base64' + +# The McpUiServer module provides helper methods for creating UI resources +# compatible with the Model Context Protocol UI (mcp-ui) client. +module McpUiServer + class Error < StandardError; end + + # MIME type constants + MIME_TYPE_HTML = 'text/html' + MIME_TYPE_URI_LIST = 'text/uri-list' + MIME_TYPE_REMOTE_DOM = 'application/vnd.mcp-ui.remote-dom+javascript; flavor=%s' + + # Content type constants (Ruby snake_case) + CONTENT_TYPE_RAW_HTML = :raw_html + CONTENT_TYPE_EXTERNAL_URL = :external_url + CONTENT_TYPE_REMOTE_DOM = :remote_dom + + # Protocol mapping (snake_case to camelCase for protocol consistency) + PROTOCOL_CONTENT_TYPES = { + raw_html: 'rawHtml', + external_url: 'externalUrl', + remote_dom: 'remoteDom' + }.freeze + + # Required content keys for each content type + REQUIRED_CONTENT_KEYS = { + CONTENT_TYPE_RAW_HTML => :htmlString, + CONTENT_TYPE_EXTERNAL_URL => :iframeUrl, + CONTENT_TYPE_REMOTE_DOM => :script + }.freeze + + # URI scheme constant + UI_URI_SCHEME = 'ui://' + + # Creates a UIResource hash structure for an MCP response. + # This structure can then be serialized to JSON by your web framework. + # + # @param uri [String] The unique identifier for the resource (e.g., 'ui://greeting/1'). + # @param content [Hash] A hash describing the UI content. + # - :type [Symbol] The type of content. One of :raw_html, :external_url, or :remote_dom. + # - :htmlString [String] The raw HTML content (required if type is :raw_html). + # - :iframeUrl [String] The URL for an external page (required if type is :external_url). + # - :script [String] The remote-dom script (required if type is :remote_dom). + # - :flavor [Symbol] The remote-dom flavor, e.g., :react or :webcomponents (required, for :remote_dom). + # @param encoding [Symbol] The encoding method. :text for plain string, :blob for base64 encoded. + # + # @return [Hash] A UIResource hash ready to be included in an MCP response. + # + # @raise [McpUiServer::Error] if URI scheme is invalid, content type is unknown, + # encoding type is unknown, or required content keys are missing. + def self.create_ui_resource(uri:, content:, encoding: :text) + validate_uri_scheme(uri) + + resource = { uri: uri } + + content_value = process_content(content, resource) + process_encoding(encoding, resource, content_value) + + { + type: 'resource', + resource: resource + } + end + + # private + + def self.validate_uri_scheme(uri) + raise Error, "URI must start with '#{UI_URI_SCHEME}' but got: #{uri}" unless uri.start_with?(UI_URI_SCHEME) + end + private_class_method :validate_uri_scheme + + def self.validate_content_type(content_type) + return if PROTOCOL_CONTENT_TYPES.key?(content_type) + + supported_types = PROTOCOL_CONTENT_TYPES.keys.join(', ') + raise Error, "Unknown content type: #{content_type}. Supported types: #{supported_types}" + end + private_class_method :validate_content_type + + def self.process_content(content, resource) + content_type = content.fetch(:type) + validate_content_type(content_type) + + case content_type + when CONTENT_TYPE_RAW_HTML + process_raw_html_content(content, resource) + when CONTENT_TYPE_EXTERNAL_URL + process_external_url_content(content, resource) + when CONTENT_TYPE_REMOTE_DOM + process_remote_dom_content(content, resource) + end + end + private_class_method :process_content + + def self.process_raw_html_content(content, resource) + resource[:mimeType] = MIME_TYPE_HTML + required_key = REQUIRED_CONTENT_KEYS[CONTENT_TYPE_RAW_HTML] + content.fetch(required_key) { raise Error, "Missing required key :#{required_key} for raw_html content" } + end + private_class_method :process_raw_html_content + + def self.process_external_url_content(content, resource) + resource[:mimeType] = MIME_TYPE_URI_LIST + required_key = REQUIRED_CONTENT_KEYS[CONTENT_TYPE_EXTERNAL_URL] + content.fetch(required_key) { raise Error, "Missing required key :#{required_key} for external_url content" } + end + private_class_method :process_external_url_content + + def self.process_remote_dom_content(content, resource) + flavor = content.fetch(:flavor) { raise Error, 'Missing required key :flavor for remote_dom content' } + resource[:mimeType] = MIME_TYPE_REMOTE_DOM % flavor + required_key = REQUIRED_CONTENT_KEYS[CONTENT_TYPE_REMOTE_DOM] + content.fetch(required_key) { raise Error, "Missing required key :#{required_key} for remote_dom content" } + end + private_class_method :process_remote_dom_content + + def self.process_encoding(encoding, resource, content_value) + case encoding + when :text + resource[:text] = content_value + when :blob + resource[:blob] = Base64.strict_encode64(content_value) + else + raise Error, "Unknown encoding type: #{encoding}. Supported types: :text, :blob" + end + end + private_class_method :process_encoding +end diff --git a/sdks/ruby/spec/mcp_ui_server_spec.rb b/sdks/ruby/spec/mcp_ui_server_spec.rb new file mode 100644 index 00000000..75a2554e --- /dev/null +++ b/sdks/ruby/spec/mcp_ui_server_spec.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require 'mcp_ui_server' +require 'base64' + +RSpec.describe McpUiServer do + it 'has a version number' do + expect(McpUiServer::VERSION).not_to be_nil + end + + describe '.create_ui_resource' do + let(:uri) { 'ui://test/1' } + + context 'with raw_html content' do + let(:content) { { type: :raw_html, htmlString: '

Hello

' } } + + it 'creates a resource with text/html mimetype and text encoding' do + resource = described_class.create_ui_resource(uri: uri, content: content) + expect(resource[:type]).to eq('resource') + expect(resource[:resource][:uri]).to eq(uri) + expect(resource[:resource][:mimeType]).to eq('text/html') + expect(resource[:resource][:text]).to eq('

Hello

') + end + + it 'creates a resource with blob encoding' do + resource = described_class.create_ui_resource(uri: uri, content: content, encoding: :blob) + expect(resource[:resource][:blob]).to eq(Base64.strict_encode64('

Hello

')) + end + end + + context 'with external_url content' do + let(:content) { { type: :external_url, iframeUrl: 'https://example.com' } } + + it 'creates a resource with text/uri-list mimetype' do + resource = described_class.create_ui_resource(uri: uri, content: content) + expect(resource[:resource][:mimeType]).to eq('text/uri-list') + expect(resource[:resource][:text]).to eq('https://example.com') + end + + it 'creates a resource with blob encoding' do + resource = described_class.create_ui_resource(uri: uri, content: content, encoding: :blob) + expect(resource[:resource][:blob]).to eq(Base64.strict_encode64('https://example.com')) + end + end + + context 'with remote_dom content' do + let(:script) { 'console.log("hello")' } + + it 'creates a resource with react flavor' do + content = { type: :remote_dom, script: script, flavor: :react } + resource = described_class.create_ui_resource(uri: uri, content: content) + expect(resource[:resource][:mimeType]).to eq('application/vnd.mcp-ui.remote-dom+javascript; flavor=react') + expect(resource[:resource][:text]).to eq(script) + end + + it 'creates a resource with a specified flavor as a symbol' do + content = { type: :remote_dom, script: script, flavor: :webcomponents } + resource = described_class.create_ui_resource(uri: uri, content: content) + expected_mime_type = 'application/vnd.mcp-ui.remote-dom+javascript; flavor=webcomponents' + expect(resource[:resource][:mimeType]).to eq(expected_mime_type) + end + + it 'creates a resource with a specified flavor as a string' do + content = { type: :remote_dom, script: script, flavor: 'react' } + resource = described_class.create_ui_resource(uri: uri, content: content) + expect(resource[:resource][:mimeType]).to eq('application/vnd.mcp-ui.remote-dom+javascript; flavor=react') + end + + it 'creates a resource with blob encoding' do + content = { type: :remote_dom, script: script, flavor: :react } + resource = described_class.create_ui_resource(uri: uri, content: content, encoding: :blob) + expect(resource[:resource][:blob]).to eq(Base64.strict_encode64(script)) + end + + it 'creates a resource with blob encoding and a specified flavor' do + content = { type: :remote_dom, script: script, flavor: :webcomponents } + resource = described_class.create_ui_resource(uri: uri, content: content, encoding: :blob) + expected_mime_type = 'application/vnd.mcp-ui.remote-dom+javascript; flavor=webcomponents' + expect(resource[:resource][:mimeType]).to eq(expected_mime_type) + expect(resource[:resource][:blob]).to eq(Base64.strict_encode64(script)) + end + end + + context 'with invalid input' do + it 'raises McpUiServer::Error for invalid URI scheme' do + invalid_uri = 'http://test/1' + content = { type: :raw_html, htmlString: '

Hello

' } + expect do + described_class.create_ui_resource(uri: invalid_uri, content: content) + end.to raise_error(McpUiServer::Error, "URI must start with 'ui://' but got: #{invalid_uri}") + end + + it 'raises McpUiServer::Error for unknown content type' do + content = { type: :invalid, data: 'foo' } + expect do + described_class.create_ui_resource(uri: uri, content: content) + end.to raise_error(McpUiServer::Error, /Unknown content type: invalid/) + end + + it 'raises McpUiServer::Error for camelCase string content type' do + content = { type: 'rawHtml', htmlString: '

Hello

' } + expect do + described_class.create_ui_resource(uri: uri, content: content) + end.to raise_error(McpUiServer::Error, /Unknown content type: rawHtml/) + end + + it 'raises McpUiServer::Error for unknown encoding type' do + content = { type: :raw_html, htmlString: '

Hello

' } + expect do + described_class.create_ui_resource(uri: uri, content: content, encoding: :invalid) + end.to raise_error(McpUiServer::Error, /Unknown encoding type: invalid/) + end + + it 'raises McpUiServer::Error if htmlString is missing' do + content = { type: :raw_html } + expect do + described_class.create_ui_resource(uri: uri, content: content) + end.to raise_error(McpUiServer::Error, /Missing required key :htmlString for raw_html content/) + end + + it 'raises McpUiServer::Error if iframeUrl is missing' do + content = { type: :external_url } + expect do + described_class.create_ui_resource(uri: uri, content: content) + end.to raise_error(McpUiServer::Error, /Missing required key :iframeUrl for external_url content/) + end + + it 'raises McpUiServer::Error if flavor is missing' do + content = { type: :remote_dom, script: 'console.log("foo")' } + expect do + described_class.create_ui_resource(uri: uri, content: content) + end.to raise_error(McpUiServer::Error, /Missing required key :flavor for remote_dom content/) + end + + it 'raises McpUiServer::Error if script is missing' do + content = { type: :remote_dom, flavor: :react } + expect do + described_class.create_ui_resource(uri: uri, content: content) + end.to raise_error(McpUiServer::Error, /Missing required key :script for remote_dom content/) + end + end + + context 'with constants' do + it 'defines expected MIME type constants' do + expect(McpUiServer::MIME_TYPE_HTML).to eq('text/html') + expect(McpUiServer::MIME_TYPE_URI_LIST).to eq('text/uri-list') + expect(McpUiServer::MIME_TYPE_REMOTE_DOM).to eq('application/vnd.mcp-ui.remote-dom+javascript; flavor=%s') + end + + it 'defines expected content type constants' do + expect(McpUiServer::CONTENT_TYPE_RAW_HTML).to eq(:raw_html) + expect(McpUiServer::CONTENT_TYPE_EXTERNAL_URL).to eq(:external_url) + expect(McpUiServer::CONTENT_TYPE_REMOTE_DOM).to eq(:remote_dom) + end + + it 'defines protocol content type mapping' do + expect(McpUiServer::PROTOCOL_CONTENT_TYPES).to include( + raw_html: 'rawHtml', + external_url: 'externalUrl', + remote_dom: 'remoteDom' + ) + end + + it 'defines URI scheme constant' do + expect(McpUiServer::UI_URI_SCHEME).to eq('ui://') + end + end + end +end diff --git a/sdks/typescript/server/README.md b/sdks/typescript/server/README.md new file mode 100644 index 00000000..c6900eb0 --- /dev/null +++ b/sdks/typescript/server/README.md @@ -0,0 +1,290 @@ +## 📦 Model Context Protocol UI SDK + +

+ image +

+ +

+ Server Version + Client Version + Ruby Server SDK Version + MCP Documentation +

+ +

+ What's mcp-ui? • + Core Concepts • + Installation • + Quickstart • + Examples • + Security • + Roadmap • + Contributing • + License +

+ +---- + +**`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 community playground for MCP UI ideas. Expect rapid iteration and enhancements!* + +

+ +

+ +## 💡 What's `mcp-ui`? + +`mcp-ui` is a collection of SDKs comprising: + +* **`@mcp-ui/server` (TypeScript)**: Utilities to generate UI resources (`UIResource`) on your MCP server. +* **`@mcp-ui/client` (TypeScript)**: UI components (e.g., ``) to render the UI resources and handle their events. +* **`mcp_ui_server` (Ruby)**: Utilities to generate UI resources on your MCP server in a Ruby environment. + +Together, they let you define reusable UI snippets on the server side, seamlessly and securely render them in the client, and react to their actions in the MCP host environment. + +## ✨ Core Concepts + +In essence, by using `mcp-ui` SDKs, servers and hosts can agree on contracts that enable them to create and render interactive UI snippets (as a path to a standardized UI approach in MCP). + +### UI Resource +The primary payload returned from the server to the client is the `UIResource`: + +```ts +interface UIResource { + type: 'resource'; + resource: { + uri: string; // e.g., ui://component/id + mimeType: 'text/html' | 'text/uri-list' | 'application/vnd.mcp-ui.remote-dom'; // text/html for HTML content, text/uri-list for URL content, application/vnd.mcp-ui.remote-dom for remote-dom content (Javascript) + text?: string; // Inline HTML, external URL, or remote-dom script + blob?: string; // Base64-encoded HTML, URL, or remote-dom script + }; +} +``` + +* **`uri`**: Unique identifier for caching and routing + * `ui://…` — UI resources (rendering method determined by mimeType) +* **`mimeType`**: `text/html` for HTML content (iframe srcDoc), `text/uri-list` for URL content (iframe src), `application/vnd.mcp-ui.remote-dom` for remote-dom content (Javascript) + * **MCP-UI requires a single URL**: While `text/uri-list` format supports multiple URLs, MCP-UI uses only the first valid `http/s` URL and warns if additional URLs are found +* **`text` vs. `blob`**: Choose `text` for simple strings; use `blob` for larger or encoded content. + +### Resource Renderer + +The UI Resource is rendered in the `` component. It automatically detects the resource type and renders the appropriate component. + +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 } } | + { type: 'intent', payload: { intent: string, params: Record } } | + { type: 'prompt', payload: { prompt: string } } | + { type: 'notification', payload: { message: string } } | + { type: 'link', payload: { url: string } } + ``` +- **`supportedContentTypes`**: Optional array to restrict which content types are allowed (`['rawHtml', 'externalUrl', 'remoteDom']`) +- **`htmlProps`**: Optional props for the internal `` + - **`style`**: Optional custom styles for the iframe + - **`iframeProps`**: Optional props passed to the iframe element +- **`remoteDomProps`**: Optional props for the internal `` + - **`library`**: Optional component library for Remote DOM resources (defaults to `basicComponentLibrary`) + - **`remoteElements`**: remote element definitions for Remote DOM resources. + +### Supported Resource Types + +#### HTML (`text/html` and `text/uri-list`) + +Rendered using the internal `` component, which displays content inside an `