Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions examples/python-server-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -50,6 +52,54 @@ Each tool returns an MCP resource that can be rendered by MCP UI clients:
- **Raw HTML**: Returns a resource with HTML content `<h1>Hello from Raw HTML</h1>`
- **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()`.

### 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, UIMetadataKey

ui_resource = create_ui_resource({
"uri": "ui://my-component",
"content": {
"type": "rawHtml",
"htmlString": "<h1>Hello</h1>"
},
"encoding": "text",
"uiMetadata": {
UIMetadataKey.PREFERRED_FRAME_SIZE: ["1200px", "800px"],
# Or use string literal: "preferred-frame-size": ["1200px", "800px"]
},
# Optional: custom metadata (not prefixed)
"metadata": {
"custom.author": "My Server",
"custom.version": "1.0.0"
}
})
```


## Development

```bash
Expand Down
9 changes: 6 additions & 3 deletions examples/python-server-demo/python_server_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,25 @@
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
mcp = FastMCP("python-server-demo")

@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": {
UIMetadataKey.PREFERRED_FRAME_SIZE: ["800px", "600px"] # CSS dimension strings (can be px, %, vh, etc.)
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using UIMetadataKey.PREFERRED_FRAME_SIZE as a dictionary key follows Python best practices, but the comment suggests this can accept CSS unit strings while the constant is defined as a plain string 'preferred-frame-size'. Consider documenting the expected value types (e.g., list of strings or list of numbers) in the UIMetadataKey class docstring to clarify the API contract.

Copilot uses AI. Check for mistakes.
}
})
return [ui_resource]

Expand Down
108 changes: 107 additions & 1 deletion sdks/python/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment indicates 'pixels or css units' but the example shows numeric values without units. This creates ambiguity about whether clients should pass [800, 600] (integers) or ['800px', '600px'] (strings). Consider providing examples for both cases or clarifying which format is preferred and how they differ in behavior.

Copilot uses AI. Check for mistakes.
}
})
```

#### 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 <div>Dashboard for user {userId}</div>;
}
""",
"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": "<canvas id='chart'></canvas>"
},
"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": "<div>Widget</div>"
},
"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:
Expand Down Expand Up @@ -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
}
```

Expand Down
2 changes: 2 additions & 0 deletions sdks/python/server/src/mcp_ui_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
UIActionResultPrompt,
UIActionResultToolCall,
UIActionType,
UIMetadataKey,
)

__version__ = "5.2.0"
Expand All @@ -40,6 +41,7 @@
"UIActionResultLink",
"UIActionResultIntent",
"UIActionResultNotification",
"UIMetadataKey",
"UIResource",
"create_ui_resource",
"ui_action_result_tool_call",
Expand Down
61 changes: 59 additions & 2 deletions sdks/python/server/src/mcp_ui_server/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from .exceptions import InvalidContentError, InvalidURIError
from .types import (
UI_METADATA_PREFIX,
CreateUIResourceOptions,
MimeType,
UIActionResultIntent,
Expand All @@ -30,21 +31,72 @@ 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.

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": "<h1>Hello</h1>"},
... "encoding": "text",
... "uiMetadata": {
... UIMetadataKey.PREFERRED_FRAME_SIZE: ["800px", "600px"]
... }
... })
"""
options = CreateUIResourceOptions.model_validate(options_dict)
# Validate URI
Expand Down Expand Up @@ -108,17 +160,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}")
Expand Down
Loading
Loading