diff --git a/docs/src/guide/client/overview.md b/docs/src/guide/client/overview.md index 1764a872..5b91f917 100644 --- a/docs/src/guide/client/overview.md +++ b/docs/src/guide/client/overview.md @@ -4,11 +4,16 @@ The `@mcp-ui/client` package helps you render UI resources sent from an MCP-enab ## What's Included? +### Components - **``**: The main component you'll use. It inspects the resource's `mimeType` and renders either `` or `` internally. - **``**: Internal component for HTML/URL resources - **``**: Internal component for remote DOM resources - **`isUIResource()`**: Utility function to check if content is a UI resource (replaces manual `content.type === 'resource' && content.resource.uri?.startsWith('ui://')` checks) +### Utility Functions +- **`getResourceMetadata(resource)`**: Extracts the resource's `_meta` content (standard MCP metadata) +- **`getUIResourceMetadata(resource)`**: Extracts only the MCP-UI specific metadata keys (prefixed with `mcpui.dev/ui-`) from the resource's `_meta` content + ## Purpose - **Standardized UI**: mcp-ui's client guarantees full compatibility with the latest MCP UI standards. - **Simplified Rendering**: Abstract away the complexities of handling different resource types. @@ -25,6 +30,94 @@ To build just this package from the monorepo root: pnpm build --filter @mcp-ui/client ``` +## Utility Functions Reference + +### `getResourceMetadata(resource)` + +Extracts the standard MCP metadata from a resource's `_meta` property. + +```typescript +import { getResourceMetadata } from '@mcp-ui/client'; + +const resource = { + uri: 'ui://example/demo', + mimeType: 'text/html', + text: '
Hello
', + _meta: { + title: 'Demo Component', + version: '1.0.0', + 'mcpui.dev/ui-preferred-frame-size': ['800px', '600px'], + 'mcpui.dev/ui-initial-render-data': { theme: 'dark' }, + author: 'Development Team' + } +}; + +const metadata = getResourceMetadata(resource); +console.log(metadata); +// Output: { +// title: 'Demo Component', +// version: '1.0.0', +// 'mcpui.dev/ui-preferred-frame-size': ['800px', '600px'], +// 'mcpui.dev/ui-initial-render-data': { theme: 'dark' }, +// author: 'Development Team' +// } +``` + +### `getUIResourceMetadata(resource)` + +Extracts only the MCP-UI specific metadata keys (those prefixed with `mcpui.dev/ui-`) from a resource's `_meta` property, with the prefixes removed for easier access. + +```typescript +import { getUIResourceMetadata } from '@mcp-ui/client'; + +const resource = { + uri: 'ui://example/demo', + mimeType: 'text/html', + text: '
Hello
', + _meta: { + title: 'Demo Component', + version: '1.0.0', + 'mcpui.dev/ui-preferred-frame-size': ['800px', '600px'], + 'mcpui.dev/ui-initial-render-data': { theme: 'dark' }, + author: 'Development Team' + } +}; + +const uiMetadata = getUIResourceMetadata(resource); +console.log(uiMetadata); +// Output: { +// 'preferred-frame-size': ['800px', '600px'], +// 'initial-render-data': { theme: 'dark' }, +// } +``` + +### Usage Examples + +These utility functions are particularly useful when you need to access metadata programmatically: + +```typescript +import { getUIResourceMetadata, UIResourceRenderer } from '@mcp-ui/client'; + +function SmartResourceRenderer({ resource }) { + const uiMetadata = getUIResourceMetadata(resource); + + // Use metadata to make rendering decisions + const initialRenderData = uiMetadata['initial-render-data']; + const containerClass = initialRenderData.preferredContext === 'hero' ? 'hero-container' : 'default-container'; + + return ( +
+ {preferredContext === 'hero' && ( +

Featured Component

+ )} + +
+ ); +} +``` + +## See More + See the following pages for more details: - [UIResourceRenderer Component](./resource-renderer.md) - **Main entry point** diff --git a/docs/src/guide/client/resource-renderer.md b/docs/src/guide/client/resource-renderer.md index 32324cf1..6d233705 100644 --- a/docs/src/guide/client/resource-renderer.md +++ b/docs/src/guide/client/resource-renderer.md @@ -28,6 +28,16 @@ For developers using frameworks other than React, a Web Component version is ava - **External URLs** (`text/uri-list`): External applications and websites - **Remote DOM Resources** (`application/vnd.mcp-ui.remote-dom`): Server-generated UI components using Shopify's remote-dom +## Metadata Integration + +The `UIResourceRenderer` automatically detects and uses metadata from resources created with the server SDK's `createUIResource()` function. It looks for MCP-UI specific metadata keys prefixed with `mcpui.dev/ui-` in the resource's `_meta` property: + +### Automatic Frame Sizing +- **`mcpui.dev/ui-preferred-frame-size`**: When present, this metadata is used as the initial size for iframe-based resources, overriding any default sizing behavior. + +### Automatic Data Passing +- **`mcpui.dev/ui-initial-render-data`**: When present, this data is automatically merged with the `iframeRenderData` prop (if provided) and passed to the iframe using the `ui-lifecycle-iframe-render-data` mechanism. + ## Props ```typescript @@ -118,6 +128,57 @@ function App({ mcpResource }) { } ``` +## Metadata Usage Examples + +When a resource is created on the server with metadata, the `UIResourceRenderer` automatically applies the configuration: + +```tsx +// Server-side resource creation (for reference) +// const serverResource = createUIResource({ +// uri: 'ui://chart/dashboard', +// content: { type: 'externalUrl', iframeUrl: 'https://charts.example.com' }, +// encoding: 'text', +// uiMetadata: { +// 'preferred-frame-size': [ '800px', '600px' ], +// 'initial-render-data': { theme: 'dark', userId: '123' } +// } +// }); + +// Client-side rendering - metadata is automatically applied +function Dashboard({ mcpResource }) { + const handleUIAction = async (result: UIActionResult) => { + // Handle UI actions + return { status: 'handled' }; + }; + + return ( + + ); +} + +// The UIResourceRenderer will: +// 1. Use ['800px', '600px'] as the initial iframe size +// 2. Merge server metadata with prop data: +// { theme: 'dark', userId: '123', sessionId: 'abc123', permissions: ['read', 'write'] } +// 3. Pass the combined data to the iframe via ui-lifecycle-iframe-render-data +``` + +### Metadata Precedence + +When both server metadata and component props provide similar data: +- **Frame sizing**: Server `preferred-frame-size` is used as the initial size, but can be overridden by component styling +- **Render data**: Server `initial-render-data` is merged with the `iframeRenderData` prop, with prop values taking precedence for duplicate keys + ## Utility Functions ### `isUIResource()` diff --git a/docs/src/guide/server/typescript/overview.md b/docs/src/guide/server/typescript/overview.md index d1568003..b07092ac 100644 --- a/docs/src/guide/server/typescript/overview.md +++ b/docs/src/guide/server/typescript/overview.md @@ -7,13 +7,30 @@ For a complete example, see the [`typescript-server-demo`](https://github.com/id ## 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 encoding 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), encoding method (text or blob), and metadata configuration. ## 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 `encoding: 'blob'` is specified. +- **Metadata Support**: Provides flexible metadata configuration for enhanced client-side rendering and resource management. + +## Metadata Features + +The `createUIResource()` function supports three types of metadata configuration to enhance resource functionality: + +### `metadata` +Standard MCP resource metadata that becomes the `_meta` property on the resource. This follows the MCP specification for resource metadata. + +### `uiMetadata` +MCP-UI specific configuration options. These keys are automatically prefixed with `mcpui.dev/ui-` in the resource metadata: + +- **`preferred-frame-size`**: Define the resource's preferred initial frame size (e.g., `{ width: 800, height: 600 }`) +- **`initial-render-data`**: Provide data that should be passed to the iframe when rendering + +### `resourceProps` +Additional properties that are spread directly onto the resource definition, allowing you to add any MCP specification-supported properties like `annotations`. ## Building diff --git a/docs/src/guide/server/typescript/usage-examples.md b/docs/src/guide/server/typescript/usage-examples.md index f7431749..c77cded2 100644 --- a/docs/src/guide/server/typescript/usage-examples.md +++ b/docs/src/guide/server/typescript/usage-examples.md @@ -139,12 +139,120 @@ console.log('Resource 5:', JSON.stringify(resource5, null, 2)); // of a toolResult in an MCP interaction. ``` +## Metadata Configuration Examples + +The `createUIResource` function supports three types of metadata configuration to enhance your UI resources: + +```typescript +import { createUIResource } from '@mcp-ui/server'; + +// Example 7: Using standard metadata +const resourceWithMetadata = createUIResource({ + uri: 'ui://analytics/dashboard', + content: { type: 'rawHtml', htmlString: '
Loading...
' }, + encoding: 'text', + metadata: { + title: 'Analytics Dashboard', + description: 'Real-time analytics and metrics', + created: '2024-01-15T10:00:00Z', + author: 'Analytics Team', + preferredRenderContext: 'sidebar' + } +}); +console.log('Resource with metadata:', JSON.stringify(resourceWithMetadata, null, 2)); +/* Output includes: +{ + "type": "resource", + "resource": { + "uri": "ui://analytics/dashboard", + "mimeType": "text/html", + "text": "
Loading...
", + "_meta": { + "title": "Analytics Dashboard", + "description": "Real-time analytics and metrics", + "created": "2024-01-15T10:00:00Z", + "author": "Analytics Team", + "preferredRenderContext": "sidebar" + } + } +} +*/ + +// Example 8: Using uiMetadata for client-side configuration +const resourceWithUIMetadata = createUIResource({ + uri: 'ui://chart/interactive', + content: { type: 'externalUrl', iframeUrl: 'https://charts.example.com/widget' }, + encoding: 'text', + uiMetadata: { + 'preferred-frame-size': ['800px', '600px'], + 'initial-render-data': { + theme: 'dark', + chartType: 'bar', + dataSet: 'quarterly-sales' + }, + } +}); +console.log('Resource with UI metadata:', JSON.stringify(resourceWithUIMetadata, null, 2)); +/* Output includes: +{ + "type": "resource", + "resource": { + "uri": "ui://chart/interactive", + "mimeType": "text/uri-list", + "text": "https://charts.example.com/widget", + "_meta": { + "mcpui.dev/ui-preferred-frame-size": ["800px", "600px"], + "mcpui.dev/ui-initial-render-data": { + "theme": "dark", + "chartType": "bar", + "dataSet": "quarterly-sales" + }, + } + } +} +*/ + +// Example 9: Using resourceProps for additional MCP properties +const resourceWithProps = createUIResource({ + uri: 'ui://form/user-profile', + content: { type: 'rawHtml', htmlString: '
...
' }, + encoding: 'text', + resourceProps: { + annotations: { + audience: ['developers', 'admins'], + priority: 'high' + } + } +}); +console.log('Resource with additional props:', JSON.stringify(resourceWithProps, null, 2)); +/* Output includes: +{ + "type": "resource", + "resource": { + "uri": "ui://form/user-profile", + "mimeType": "text/html", + "text": "
...
", + "annotations": { + "audience": ["developers", "admins"], + "priority": "high" + } + } +} +*/ +``` + +### Metadata Best Practices + +- **Use `metadata` for standard MCP resource information** like titles, descriptions, timestamps, and authorship +- **Use `uiMetadata` for client rendering hints** like preferred sizes, initial data, and context preferences +- **Use `resourceProps` for MCP specification properties** like annotations, descriptions at the resource level, and other standard fields + ## Advanced URI List Example You can provide multiple URLs in the `text/uri-list` format for fallback purposes. However, **MCP-UI requires a single URL** and will only use the first valid URL found: ```typescript -// Example 6: Multiple URLs with fallbacks (MCP-UI uses only the first) +// Example 10: Multiple URLs with fallbacks (MCP-UI uses only the first) const multiUrlContent = `# Primary dashboard https://dashboard.example.com/main @@ -154,7 +262,7 @@ https://backup.dashboard.example.com/main # Emergency fallback (will be logged but not used) https://emergency.dashboard.example.com/main`; -const resource6 = createUIResource({ +const resource = createUIResource({ uri: 'ui://dashboard-with-fallbacks/session-123', content: { type: 'externalUrl', iframeUrl: multiUrlContent }, encoding: 'text', diff --git a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx index 1db9a721..01653298 100644 --- a/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx +++ b/sdks/typescript/client/src/components/HTMLResourceRenderer.tsx @@ -1,7 +1,8 @@ import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import type { Resource } from '@modelcontextprotocol/sdk/types.js'; -import { UIActionResult } from '../types'; +import { UIActionResult, UIMetadataKey } from '../types'; import { processHTMLResource } from '../utils/processResource'; +import { getUIResourceMetadata } from '../utils/metadataUtils'; export type HTMLResourceRendererProps = { resource: Partial; @@ -16,7 +17,7 @@ export type HTMLResourceRendererProps = { }; }; -const InternalMessageType = { +export const InternalMessageType = { UI_MESSAGE_RECEIVED: 'ui-message-received', UI_MESSAGE_RESPONSE: 'ui-message-response', @@ -48,18 +49,32 @@ export const HTMLResourceRenderer = ({ [resource, proxy], ); + const uiMetadata = useMemo(() => getUIResourceMetadata(resource), [resource]); + const preferredFrameSize = uiMetadata[UIMetadataKey.PREFERRED_FRAME_SIZE] ?? ['100%', '100%']; + const metadataInitialRenderData = uiMetadata[UIMetadataKey.INITIAL_RENDER_DATA] ?? undefined; + + const initialRenderData = useMemo(() => { + if (!iframeRenderData && !metadataInitialRenderData) { + return undefined; + } + return { + ...metadataInitialRenderData, + ...iframeRenderData, + }; + }, [iframeRenderData, metadataInitialRenderData]); + const iframeSrcToRender = useMemo(() => { - if (iframeSrc && iframeRenderData) { + if (iframeSrc && initialRenderData) { const iframeUrl = new URL(iframeSrc); iframeUrl.searchParams.set(ReservedUrlParams.WAIT_FOR_RENDER_DATA, 'true'); return iframeUrl.toString(); } return iframeSrc; - }, [iframeSrc, iframeRenderData]); + }, [iframeSrc, initialRenderData]); const onIframeLoad = useCallback( (event: React.SyntheticEvent) => { - if (iframeRenderData) { + if (initialRenderData) { const iframeWindow = event.currentTarget.contentWindow; const iframeOrigin = iframeSrcToRender ? new URL(iframeSrcToRender).origin : '*'; postToFrame( @@ -68,13 +83,13 @@ export const HTMLResourceRenderer = ({ iframeOrigin, undefined, { - renderData: iframeRenderData, + renderData: initialRenderData, }, ); } iframeProps?.onLoad?.(event); }, - [iframeRenderData, iframeSrcToRender, iframeProps?.onLoad], + [initialRenderData, iframeSrcToRender, iframeProps?.onLoad], ); useEffect(() => { @@ -83,14 +98,14 @@ export const HTMLResourceRenderer = ({ // Only process the message if it came from this specific iframe if (iframeRef.current && source === iframeRef.current.contentWindow) { // if the iframe is ready, send the render data to the iframe - if (data?.type === InternalMessageType.UI_LIFECYCLE_IFRAME_READY && iframeRenderData) { + if (data?.type === InternalMessageType.UI_LIFECYCLE_IFRAME_READY && initialRenderData) { postToFrame( InternalMessageType.UI_LIFECYCLE_IFRAME_RENDER_DATA, source, origin, undefined, { - renderData: iframeRenderData, + renderData: initialRenderData, }, ); return; @@ -163,7 +178,7 @@ export const HTMLResourceRenderer = ({