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"]