From 7e5dd59717cb35827d05adf94fed27e46295c64d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 22 Oct 2025 21:13:10 +0000
Subject: [PATCH 1/6] Initial plan
From 5d438a48fbec1d8490a9c73d22ec3141072953b4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 22 Oct 2025 21:27:00 +0000
Subject: [PATCH 2/6] Add metadata support to Python SDK
Co-authored-by: idosal <18148989+idosal@users.noreply.github.com>
---
sdks/python/server/src/mcp_ui_server/core.py | 39 +++
sdks/python/server/src/mcp_ui_server/types.py | 12 +
sdks/python/server/tests/test_metadata.py | 225 ++++++++++++++++++
3 files changed, 276 insertions(+)
create mode 100644 sdks/python/server/tests/test_metadata.py
diff --git a/sdks/python/server/src/mcp_ui_server/core.py b/sdks/python/server/src/mcp_ui_server/core.py
index 0bdeecf9..b4d387c8 100644
--- a/sdks/python/server/src/mcp_ui_server/core.py
+++ b/sdks/python/server/src/mcp_ui_server/core.py
@@ -8,6 +8,7 @@
from .exceptions import InvalidContentError, InvalidURIError
from .types import (
+ UI_METADATA_PREFIX,
CreateUIResourceOptions,
MimeType,
UIActionResultIntent,
@@ -30,6 +31,39 @@ def __init__(self, resource: TextResourceContents | BlobResourceContents, **kwar
)
+def _get_additional_resource_props(options: CreateUIResourceOptions) -> dict[str, Any]:
+ """Get additional resource properties including metadata.
+
+ Prefixes UI-specific metadata with the UI metadata prefix to be recognized by the client.
+
+ Args:
+ options: The UI resource options
+
+ Returns:
+ Dictionary of additional properties to merge into the resource
+ """
+ additional_props: dict[str, Any] = {}
+
+ # Prefix ui specific metadata with the prefix to be recognized by the client
+ if options.uiMetadata or options.metadata:
+ ui_prefixed_metadata: dict[str, Any] = {}
+
+ if options.uiMetadata:
+ for key, value in options.uiMetadata.items():
+ ui_prefixed_metadata[f"{UI_METADATA_PREFIX}{key}"] = value
+
+ # Allow user defined metadata to override ui metadata
+ _meta: dict[str, Any] = {
+ **ui_prefixed_metadata,
+ **(options.metadata or {}),
+ }
+
+ if _meta:
+ additional_props["_meta"] = _meta
+
+ return additional_props
+
+
def create_ui_resource(options_dict: dict[str, Any]) -> UIResource:
"""Create a UIResource.
@@ -108,17 +142,22 @@ def create_ui_resource(options_dict: dict[str, Any]) -> UIResource:
# Create resource based on encoding type
encoding = options.encoding
+ # Get additional properties including metadata
+ additional_props = _get_additional_resource_props(options)
+
if encoding == "text":
resource: TextResourceContents | BlobResourceContents = TextResourceContents(
uri=AnyUrl(options.uri),
mimeType=mime_type,
text=actual_content_string,
+ **additional_props,
)
elif encoding == "blob":
resource = BlobResourceContents(
uri=AnyUrl(options.uri),
mimeType=mime_type,
blob=base64.b64encode(actual_content_string.encode('utf-8')).decode('ascii'),
+ **additional_props,
)
else:
raise InvalidContentError(f"Invalid encoding type: {encoding}")
diff --git a/sdks/python/server/src/mcp_ui_server/types.py b/sdks/python/server/src/mcp_ui_server/types.py
index 38302d7c..59ad6a8b 100644
--- a/sdks/python/server/src/mcp_ui_server/types.py
+++ b/sdks/python/server/src/mcp_ui_server/types.py
@@ -40,11 +40,23 @@ class RemoteDomPayload(BaseModel):
ResourceContentPayload = RawHtmlPayload | ExternalUrlPayload | RemoteDomPayload
+# UI Metadata constants
+UI_METADATA_PREFIX = "mcpui.dev/ui-"
+
+
+class UIMetadataKey:
+ """Keys for UI metadata."""
+ PREFERRED_FRAME_SIZE = "preferred-frame-size"
+ INITIAL_RENDER_DATA = "initial-render-data"
+
+
class CreateUIResourceOptions(BaseModel):
"""Options for creating a UI resource."""
uri: URI
content: ResourceContentPayload
encoding: Literal["text", "blob"]
+ uiMetadata: dict[str, Any] | None = None
+ metadata: dict[str, Any] | None = None
class GenericActionMessage(BaseModel):
diff --git a/sdks/python/server/tests/test_metadata.py b/sdks/python/server/tests/test_metadata.py
new file mode 100644
index 00000000..d0b94ce2
--- /dev/null
+++ b/sdks/python/server/tests/test_metadata.py
@@ -0,0 +1,225 @@
+"""Tests for UI metadata functionality."""
+
+import pytest
+
+from mcp_ui_server import create_ui_resource
+from mcp_ui_server.types import UI_METADATA_PREFIX
+
+
+@pytest.fixture
+def basic_raw_html_options():
+ """Fixture for basic raw HTML options."""
+ return {
+ "uri": "ui://test-html",
+ "content": {"type": "rawHtml", "htmlString": "
Test
"},
+ "encoding": "text",
+ }
+
+
+class TestUIMetadata:
+ """Test suite for UI metadata functionality."""
+
+ def test_create_resource_with_ui_metadata(self, basic_raw_html_options):
+ """Test creating a resource with uiMetadata."""
+ options = {
+ **basic_raw_html_options,
+ "uiMetadata": {
+ "preferred-frame-size": [800, 600],
+ }
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ # Check that metadata is properly prefixed and included (meta field, serializes to _meta with by_alias=True)
+ assert result["resource"]["meta"] is not None
+ assert f"{UI_METADATA_PREFIX}preferred-frame-size" in result["resource"]["meta"]
+ assert result["resource"]["meta"][f"{UI_METADATA_PREFIX}preferred-frame-size"] == [800, 600]
+
+ # Also verify by_alias=True serialization produces _meta
+ result_with_alias = resource.model_dump(by_alias=True)
+ assert "_meta" in result_with_alias["resource"]
+ assert result_with_alias["resource"]["_meta"][f"{UI_METADATA_PREFIX}preferred-frame-size"] == [800, 600]
+
+ def test_create_resource_with_multiple_ui_metadata_fields(self, basic_raw_html_options):
+ """Test creating a resource with multiple uiMetadata fields."""
+ options = {
+ **basic_raw_html_options,
+ "uiMetadata": {
+ "preferred-frame-size": ["800px", "600px"],
+ "initial-render-data": {
+ "theme": "dark",
+ "chartType": "bar",
+ }
+ }
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ # Check that all metadata fields are properly prefixed and included
+ assert result["resource"]["meta"] is not None
+ assert f"{UI_METADATA_PREFIX}preferred-frame-size" in result["resource"]["meta"]
+ assert result["resource"]["meta"][f"{UI_METADATA_PREFIX}preferred-frame-size"] == ["800px", "600px"]
+ assert f"{UI_METADATA_PREFIX}initial-render-data" in result["resource"]["meta"]
+ assert result["resource"]["meta"][f"{UI_METADATA_PREFIX}initial-render-data"] == {
+ "theme": "dark",
+ "chartType": "bar",
+ }
+
+ def test_create_resource_with_custom_metadata(self, basic_raw_html_options):
+ """Test creating a resource with custom metadata (non-UI)."""
+ options = {
+ **basic_raw_html_options,
+ "metadata": {
+ "customKey": "customValue",
+ "anotherKey": 123,
+ }
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ # Check that custom metadata is included without prefix
+ assert result["resource"]["meta"] is not None
+ assert result["resource"]["meta"]["customKey"] == "customValue"
+ assert result["resource"]["meta"]["anotherKey"] == 123
+
+ def test_create_resource_with_both_ui_and_custom_metadata(self, basic_raw_html_options):
+ """Test creating a resource with both uiMetadata and custom metadata."""
+ options = {
+ **basic_raw_html_options,
+ "uiMetadata": {
+ "preferred-frame-size": [800, 600],
+ },
+ "metadata": {
+ "customKey": "customValue",
+ }
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ # Check that both types of metadata are included
+ assert result["resource"]["meta"] is not None
+ assert f"{UI_METADATA_PREFIX}preferred-frame-size" in result["resource"]["meta"]
+ assert result["resource"]["meta"][f"{UI_METADATA_PREFIX}preferred-frame-size"] == [800, 600]
+ assert result["resource"]["meta"]["customKey"] == "customValue"
+
+ def test_metadata_override_behavior(self, basic_raw_html_options):
+ """Test that custom metadata can override ui metadata if keys conflict."""
+ options = {
+ **basic_raw_html_options,
+ "uiMetadata": {
+ "preferred-frame-size": [800, 600],
+ },
+ "metadata": {
+ f"{UI_METADATA_PREFIX}preferred-frame-size": [1024, 768],
+ }
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ # Custom metadata should override UI metadata
+ assert result["resource"]["meta"] is not None
+ assert result["resource"]["meta"][f"{UI_METADATA_PREFIX}preferred-frame-size"] == [1024, 768]
+
+ def test_create_resource_without_metadata(self, basic_raw_html_options):
+ """Test creating a resource without any metadata."""
+ resource = create_ui_resource(basic_raw_html_options)
+
+ result = resource.model_dump()
+
+ # No metadata should be present
+ assert result["resource"]["meta"] is None
+
+ def test_metadata_with_external_url_content(self):
+ """Test metadata with external URL content type."""
+ options = {
+ "uri": "ui://test-url",
+ "content": {
+ "type": "externalUrl",
+ "iframeUrl": "https://example.com",
+ },
+ "encoding": "text",
+ "uiMetadata": {
+ "preferred-frame-size": ["100%", "500px"],
+ }
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ assert result["resource"]["meta"] is not None
+ assert f"{UI_METADATA_PREFIX}preferred-frame-size" in result["resource"]["meta"]
+ assert result["resource"]["meta"][f"{UI_METADATA_PREFIX}preferred-frame-size"] == ["100%", "500px"]
+
+ def test_metadata_with_remote_dom_content(self):
+ """Test metadata with remote DOM content type."""
+ options = {
+ "uri": "ui://test-remote-dom",
+ "content": {
+ "type": "remoteDom",
+ "script": "const p = document.createElement('p');",
+ "framework": "react",
+ },
+ "encoding": "text",
+ "uiMetadata": {
+ "initial-render-data": {"userId": "123"},
+ }
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ assert result["resource"]["meta"] is not None
+ assert f"{UI_METADATA_PREFIX}initial-render-data" in result["resource"]["meta"]
+ assert result["resource"]["meta"][f"{UI_METADATA_PREFIX}initial-render-data"] == {"userId": "123"}
+
+ def test_metadata_with_blob_encoding(self):
+ """Test metadata with blob encoding."""
+ options = {
+ "uri": "ui://test-blob",
+ "content": {"type": "rawHtml", "htmlString": "Blob
"},
+ "encoding": "blob",
+ "uiMetadata": {
+ "preferred-frame-size": [640, 480],
+ }
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ # Verify metadata is present with blob encoding
+ assert result["resource"]["meta"] is not None
+ assert f"{UI_METADATA_PREFIX}preferred-frame-size" in result["resource"]["meta"]
+ assert result["resource"]["meta"][f"{UI_METADATA_PREFIX}preferred-frame-size"] == [640, 480]
+ # Verify blob is also present
+ assert "blob" in result["resource"]
+
+ def test_empty_ui_metadata_dict(self, basic_raw_html_options):
+ """Test creating a resource with empty uiMetadata dict."""
+ options = {
+ **basic_raw_html_options,
+ "uiMetadata": {}
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ # Empty metadata dict should not create meta field
+ assert result["resource"]["meta"] is None
+
+ def test_empty_custom_metadata_dict(self, basic_raw_html_options):
+ """Test creating a resource with empty custom metadata dict."""
+ options = {
+ **basic_raw_html_options,
+ "metadata": {}
+ }
+ resource = create_ui_resource(options)
+
+ result = resource.model_dump()
+
+ # Empty metadata dict should not create meta field
+ assert result["resource"]["meta"] is None
From 9b19f945e971b9ba3150beb30b0475d83251e15e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 22 Oct 2025 21:28:42 +0000
Subject: [PATCH 3/6] Update Python SDK documentation for metadata support
Co-authored-by: idosal <18148989+idosal@users.noreply.github.com>
---
sdks/python/server/README.md | 108 ++++++++++++++++++++++++++++++++++-
1 file changed, 107 insertions(+), 1 deletion(-)
diff --git a/sdks/python/server/README.md b/sdks/python/server/README.md
index 64621816..54f08536 100644
--- a/sdks/python/server/README.md
+++ b/sdks/python/server/README.md
@@ -133,6 +133,110 @@ blob_resource = create_ui_resource({
})
```
+### UI Metadata
+
+Enhance resources with metadata for client-side handling. The SDK automatically prefixes UI-specific metadata with `mcpui.dev/ui-` to distinguish it from custom metadata.
+
+#### Preferred Frame Size
+
+Specify preferred dimensions for UI rendering:
+
+```python
+resource = create_ui_resource({
+ "uri": "ui://chart",
+ "content": {
+ "type": "externalUrl",
+ "iframeUrl": "https://charts.example.com/widget"
+ },
+ "encoding": "text",
+ "uiMetadata": {
+ "preferred-frame-size": [800, 600] # width, height in pixels or css units
+ }
+})
+```
+
+#### Initial Render Data
+
+Provide data to components at initialization:
+
+```python
+resource = create_ui_resource({
+ "uri": "ui://dashboard",
+ "content": {
+ "type": "remoteDom",
+ "script": """
+ function Dashboard({ theme, userId }) {
+ // Component receives initial data
+ return Dashboard for user {userId}
;
+ }
+ """,
+ "framework": "react"
+ },
+ "encoding": "text",
+ "uiMetadata": {
+ "initial-render-data": {
+ "theme": "dark",
+ "userId": "123"
+ }
+ }
+})
+```
+
+#### Multiple Metadata Fields
+
+Combine multiple metadata fields:
+
+```python
+resource = create_ui_resource({
+ "uri": "ui://data-viz",
+ "content": {
+ "type": "rawHtml",
+ "htmlString": ""
+ },
+ "encoding": "text",
+ "uiMetadata": {
+ "preferred-frame-size": ["800px", "600px"],
+ "initial-render-data": {
+ "chartType": "bar",
+ "dataSet": "quarterly-sales"
+ }
+ }
+})
+```
+
+#### Custom Metadata
+
+Add custom metadata alongside UI metadata:
+
+```python
+resource = create_ui_resource({
+ "uri": "ui://custom-widget",
+ "content": {
+ "type": "rawHtml",
+ "htmlString": "Widget
"
+ },
+ "encoding": "text",
+ "uiMetadata": {
+ "preferred-frame-size": [640, 480]
+ },
+ "metadata": {
+ "customKey": "customValue",
+ "version": "1.0.0"
+ }
+})
+
+# Result includes both prefixed UI metadata and custom metadata:
+# {
+# "resource": {
+# "meta": {
+# "mcpui.dev/ui-preferred-frame-size": [640, 480],
+# "customKey": "customValue",
+# "version": "1.0.0"
+# }
+# }
+# }
+```
+
### UI Actions
Create action results for user interactions:
@@ -266,7 +370,9 @@ Creates a UI resource from the given options.
{
"uri": str, # Must start with "ui://"
"content": Union[RawHtmlPayload, ExternalUrlPayload, RemoteDomPayload],
- "encoding": Literal["text", "blob"]
+ "encoding": Literal["text", "blob"],
+ "uiMetadata": Optional[dict[str, Any]], # UI-specific metadata (auto-prefixed)
+ "metadata": Optional[dict[str, Any]] # Custom metadata
}
```
From abf9ad4473aeecad5d62bc996f59053cd5d6cdea Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 22 Oct 2025 21:30:47 +0000
Subject: [PATCH 4/6] Add metadata example to Python server demo
Co-authored-by: idosal <18148989+idosal@users.noreply.github.com>
---
examples/python-server-demo/python_server_demo.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/examples/python-server-demo/python_server_demo.py b/examples/python-server-demo/python_server_demo.py
index d29ab6f0..be40f4d7 100644
--- a/examples/python-server-demo/python_server_demo.py
+++ b/examples/python-server-demo/python_server_demo.py
@@ -20,14 +20,17 @@
@mcp.tool()
def show_external_url() -> list[UIResource]:
- """Creates a UI resource displaying an external URL (example.com)."""
+ """Creates a UI resource displaying an external URL (example.com) with preferred frame size."""
ui_resource = create_ui_resource({
"uri": "ui://greeting",
"content": {
"type": "externalUrl",
"iframeUrl": "https://example.com"
},
- "encoding": "text"
+ "encoding": "text",
+ "uiMetadata": {
+ "preferred-frame-size": [800, 600]
+ }
})
return [ui_resource]
From 92c96551833097c7b5253fddef6c325f1d1c4bb5 Mon Sep 17 00:00:00 2001
From: Ido Salomon
Date: Sat, 25 Oct 2025 22:54:29 +0300
Subject: [PATCH 5/6] fixes
---
examples/python-server-demo/README.md | 42 +++++++++++++++++--
.../python-server-demo/python_server_demo.py | 4 +-
.../server/src/mcp_ui_server/__init__.py | 2 +
3 files changed, 42 insertions(+), 6 deletions(-)
diff --git a/examples/python-server-demo/README.md b/examples/python-server-demo/README.md
index a1cb633e..5edb2f1d 100644
--- a/examples/python-server-demo/README.md
+++ b/examples/python-server-demo/README.md
@@ -4,11 +4,13 @@ A Python MCP server implementation inspired by the TypeScript server demo. This
## Features
-This server provides three tools that create different types of UI resources:
+This server provides multiple tools that demonstrate different types of UI resources and metadata capabilities:
-- **showExternalUrl** - Creates a UI resource displaying an external URL (example.com)
-- **showRawHtml** - Creates a UI resource with raw HTML content
-- **showRemoteDom** - Creates a UI resource with remote DOM script using React framework
+### Basic UI Resources
+- **show_external_url** - Creates a UI resource displaying an external URL (example.com) with preferred frame size metadata
+- **show_raw_html** - Creates a UI resource with raw HTML content
+- **show_remote_dom** - Creates a UI resource with remote DOM script using React framework
+- **show_action_html** - Creates a UI resource with interactive buttons demonstrating intent actions
## Installation
@@ -50,6 +52,38 @@ Each tool returns an MCP resource that can be rendered by MCP UI clients:
- **Raw HTML**: Returns a resource with HTML content `Hello from Raw HTML
`
- **Remote DOM**: Returns a resource with JavaScript that creates UI elements dynamically
+## UI Metadata
+
+The SDK supports UI metadata through the `uiMetadata` parameter in `create_ui_resource()`:
+1. Prefixes all `uiMetadata` keys with `mcpui.dev/ui-`
+2. Merges prefixed metadata with any custom metadata
+3. Adds the combined metadata to the resource's `_meta` field
+4. Custom metadata keys are preserved as-is (not prefixed)
+
+### Example Usage
+
+```python
+from mcp_ui_server import create_ui_resource
+
+ui_resource = create_ui_resource({
+ "uri": "ui://my-component",
+ "content": {
+ "type": "rawHtml",
+ "htmlString": "Hello
"
+ },
+ "encoding": "text",
+ "uiMetadata": {
+ "preferred-frame-size": ["1200", "800"],
+ },
+ # Optional: custom metadata (not prefixed)
+ "metadata": {
+ "custom.author": "My Server",
+ "custom.version": "1.0.0"
+ }
+})
+```
+
+
## Development
```bash
diff --git a/examples/python-server-demo/python_server_demo.py b/examples/python-server-demo/python_server_demo.py
index be40f4d7..47cf2cef 100644
--- a/examples/python-server-demo/python_server_demo.py
+++ b/examples/python-server-demo/python_server_demo.py
@@ -12,7 +12,7 @@
import argparse
from mcp.server.fastmcp import FastMCP
-from mcp_ui_server import create_ui_resource
+from mcp_ui_server import create_ui_resource, UIMetadataKey
from mcp_ui_server.core import UIResource
# Create FastMCP instance
@@ -29,7 +29,7 @@ def show_external_url() -> list[UIResource]:
},
"encoding": "text",
"uiMetadata": {
- "preferred-frame-size": [800, 600]
+ UIMetadataKey.PREFERRED_FRAME_SIZE: ["800px", "600px"] # CSS dimension strings (can be px, %, vh, etc.)
}
})
return [ui_resource]
diff --git a/sdks/python/server/src/mcp_ui_server/__init__.py b/sdks/python/server/src/mcp_ui_server/__init__.py
index ae80511d..51814ef0 100644
--- a/sdks/python/server/src/mcp_ui_server/__init__.py
+++ b/sdks/python/server/src/mcp_ui_server/__init__.py
@@ -24,6 +24,7 @@
UIActionResultPrompt,
UIActionResultToolCall,
UIActionType,
+ UIMetadataKey,
)
__version__ = "5.2.0"
@@ -40,6 +41,7 @@
"UIActionResultLink",
"UIActionResultIntent",
"UIActionResultNotification",
+ "UIMetadataKey",
"UIResource",
"create_ui_resource",
"ui_action_result_tool_call",
From b3d50a1bfe78d93fd371cabe32e0e88da9e0352a Mon Sep 17 00:00:00 2001
From: Ido Salomon
Date: Sat, 25 Oct 2025 23:00:45 +0300
Subject: [PATCH 6/6] fixes
---
examples/python-server-demo/README.md | 28 +++++++---
sdks/python/server/src/mcp_ui_server/core.py | 22 +++++++-
sdks/python/server/src/mcp_ui_server/types.py | 54 ++++++++++++++++++-
3 files changed, 94 insertions(+), 10 deletions(-)
diff --git a/examples/python-server-demo/README.md b/examples/python-server-demo/README.md
index 5edb2f1d..e725d741 100644
--- a/examples/python-server-demo/README.md
+++ b/examples/python-server-demo/README.md
@@ -54,16 +54,31 @@ Each tool returns an MCP resource that can be rendered by MCP UI clients:
## UI Metadata
-The SDK supports UI metadata through the `uiMetadata` parameter in `create_ui_resource()`:
-1. Prefixes all `uiMetadata` keys with `mcpui.dev/ui-`
-2. Merges prefixed metadata with any custom metadata
-3. Adds the combined metadata to the resource's `_meta` field
+The SDK supports UI metadata through the `uiMetadata` parameter in `create_ui_resource()`.
+
+### Metadata Keys
+
+Use the `UIMetadataKey` constants for type-safe metadata keys:
+
+- **`UIMetadataKey.PREFERRED_FRAME_SIZE`**: CSS dimensions for the iframe
+ - Type: `list[str, str]` - [width, height] as CSS dimension strings
+ - Examples: `["800px", "600px"]`, `["100%", "50vh"]`, `["50rem", "80%"]`
+ - Must include CSS units (px, %, vh, vw, rem, em, etc.)
+
+- **`UIMetadataKey.INITIAL_RENDER_DATA`**: Initial data for the UI component
+ - Type: `dict[str, Any]` - Any JSON-serializable dictionary
+
+### How It Works
+
+1. All `uiMetadata` keys are automatically prefixed with `mcpui.dev/ui-`
+2. Prefixed metadata is merged with any custom `metadata`
+3. The combined metadata is added to the resource's `_meta` field
4. Custom metadata keys are preserved as-is (not prefixed)
### Example Usage
```python
-from mcp_ui_server import create_ui_resource
+from mcp_ui_server import create_ui_resource, UIMetadataKey
ui_resource = create_ui_resource({
"uri": "ui://my-component",
@@ -73,7 +88,8 @@ ui_resource = create_ui_resource({
},
"encoding": "text",
"uiMetadata": {
- "preferred-frame-size": ["1200", "800"],
+ UIMetadataKey.PREFERRED_FRAME_SIZE: ["1200px", "800px"],
+ # Or use string literal: "preferred-frame-size": ["1200px", "800px"]
},
# Optional: custom metadata (not prefixed)
"metadata": {
diff --git a/sdks/python/server/src/mcp_ui_server/core.py b/sdks/python/server/src/mcp_ui_server/core.py
index b4d387c8..c15332b8 100644
--- a/sdks/python/server/src/mcp_ui_server/core.py
+++ b/sdks/python/server/src/mcp_ui_server/core.py
@@ -70,15 +70,33 @@ def create_ui_resource(options_dict: dict[str, Any]) -> UIResource:
This is the object that should be included in the 'content' array of a toolResult.
Args:
- options: Configuration for the interactive resource
+ options_dict: Configuration dictionary for the interactive resource. Keys:
+ - uri (str): Resource identifier starting with 'ui://'
+ - content (dict): Content payload (type: rawHtml, externalUrl, or remoteDom)
+ - encoding (str): 'text' or 'blob'
+ - uiMetadata (dict, optional): UI metadata. Use UIMetadataKey constants:
+ * UIMetadataKey.PREFERRED_FRAME_SIZE: list[str, str] - CSS dimensions like ["800px", "600px"]
+ * UIMetadataKey.INITIAL_RENDER_DATA: dict - Initial data for the UI
+ - metadata (dict, optional): Custom metadata (not prefixed)
Returns:
- A UIResource instance
+ A UIResource instance ready to be included in tool results
Raises:
InvalidURIError: If the URI doesn't start with 'ui://'
InvalidContentError: If content validation fails
MCPUIServerError: For other errors
+
+ Example:
+ >>> from mcp_ui_server import create_ui_resource, UIMetadataKey
+ >>> resource = create_ui_resource({
+ ... "uri": "ui://my-widget",
+ ... "content": {"type": "rawHtml", "htmlString": "Hello
"},
+ ... "encoding": "text",
+ ... "uiMetadata": {
+ ... UIMetadataKey.PREFERRED_FRAME_SIZE: ["800px", "600px"]
+ ... }
+ ... })
"""
options = CreateUIResourceOptions.model_validate(options_dict)
# Validate URI
diff --git a/sdks/python/server/src/mcp_ui_server/types.py b/sdks/python/server/src/mcp_ui_server/types.py
index 59ad6a8b..1c8ac4c0 100644
--- a/sdks/python/server/src/mcp_ui_server/types.py
+++ b/sdks/python/server/src/mcp_ui_server/types.py
@@ -45,13 +45,63 @@ class RemoteDomPayload(BaseModel):
class UIMetadataKey:
- """Keys for UI metadata."""
+ """Keys for UI metadata with their expected value types.
+
+ These constants should be used as keys in the uiMetadata dictionary to avoid typos
+ and improve code maintainability.
+
+ Attributes:
+ PREFERRED_FRAME_SIZE: Key for specifying preferred iframe dimensions.
+ - Expected value type: list[str, str] or tuple[str, str]
+ - Format: [width, height] as CSS dimension strings
+ - Examples:
+ * ["800px", "600px"] - Fixed pixel dimensions
+ * ["100%", "50vh"] - Responsive with percentage and viewport height
+ * ["50rem", "80%"] - Relative and percentage units
+ - Important: Must be strings with CSS units (px, %, vh, vw, rem, em, etc.)
+ - Applied directly to iframe's CSS width and height properties
+
+ INITIAL_RENDER_DATA: Key for passing initial data to the UI component.
+ - Expected value type: dict[str, Any]
+ - Format: Any JSON-serializable dictionary
+ - Examples:
+ * {"user": {"id": "123", "name": "John"}}
+ * {"config": {"theme": "dark", "language": "en"}}
+ - Data is passed to the iframe on initial render
+
+ Example usage:
+ ```python
+ from mcp_ui_server import create_ui_resource, UIMetadataKey
+
+ ui_resource = create_ui_resource({
+ "uri": "ui://my-component",
+ "content": {"type": "rawHtml", "htmlString": "Hello
"},
+ "encoding": "text",
+ "uiMetadata": {
+ UIMetadataKey.PREFERRED_FRAME_SIZE: ["800px", "600px"],
+ UIMetadataKey.INITIAL_RENDER_DATA: {"user": {"id": "123"}}
+ }
+ })
+ ```
+ """
PREFERRED_FRAME_SIZE = "preferred-frame-size"
INITIAL_RENDER_DATA = "initial-render-data"
class CreateUIResourceOptions(BaseModel):
- """Options for creating a UI resource."""
+ """Options for creating a UI resource.
+
+ Attributes:
+ uri: The resource identifier. Must start with 'ui://'
+ content: The resource content payload (rawHtml, externalUrl, or remoteDom)
+ encoding: Whether to encode as 'text' or 'blob' (base64)
+ uiMetadata: UI-specific metadata that will be prefixed with 'mcpui.dev/ui-'
+ Use UIMetadataKey constants for type-safe keys:
+ - UIMetadataKey.PREFERRED_FRAME_SIZE: list[str, str] - CSS dimensions
+ - UIMetadataKey.INITIAL_RENDER_DATA: dict[str, Any] - Initial data
+ metadata: Custom metadata (not prefixed). Merged with prefixed uiMetadata.
+ Example: {"custom.author": "Server Name", "custom.version": "1.0.0"}
+ """
uri: URI
content: ResourceContentPayload
encoding: Literal["text", "blob"]