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 = ({