Merge branch 'feature/mcp-ui' into bot_manager

This commit is contained in:
朱潮 2026-05-19 17:46:26 +08:00
commit 35ceca1af3
6 changed files with 333 additions and 298 deletions

View File

@ -397,55 +397,55 @@ def _create_ui_resource(uri: str, html: str, width: str = "100%", height: str =
return {"content": [{"type": "text", "text": resource_json}]} return {"content": [{"type": "text", "text": resource_json}]}
def _handle_data_viz(uri: str, arguments: Dict[str, Any]) -> Dict[str, Any]: def _handle_data_viz(uri: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""Unified handler for all data visualization URIs. """Unified handler for all data visualization URIs.
Parses URI path to route to the appropriate HTML builder. Parses URI path to route to the appropriate HTML builder.
All content is self-contained in resource.text. All content is self-contained in resource.text.
""" """
title = arguments.get("title", "Dashboard") title = data.get("title", "Dashboard")
theme = arguments.get("theme", "light") theme = data.get("theme", "light")
path = uri.replace("ui://data-dashboard/", "") path = uri.replace("ui://data-dashboard/", "")
if path == "metrics": if path == "metrics":
metrics = arguments.get("metrics", []) metrics = data.get("metrics", [])
if not metrics: if not metrics:
raise ValueError("Missing required parameter: metrics") raise ValueError("Missing required parameter: data.metrics")
html = _build_dashboard_html(title, metrics) html = _build_dashboard_html(title, metrics)
return _create_ui_resource(uri, html, "100%", "auto") return _create_ui_resource(uri, html, "100%", "auto")
elif path == "chart": elif path == "chart":
chart_type = arguments.get("chart_type") chart_type = data.get("chart_type")
data = arguments.get("data", {}) chart_data = data.get("data", {})
if not chart_type: if not chart_type:
raise ValueError("Missing required parameter: chart_type") raise ValueError("Missing required parameter: data.chart_type")
if not data or not data.get("series"): if not chart_data or not chart_data.get("series"):
raise ValueError("Missing required parameter: data.series") raise ValueError("Missing required parameter: data.data.series")
width = arguments.get("width", "100%") width = data.get("width", "100%")
height = arguments.get("height", "400px") height = data.get("height", "400px")
stacked = arguments.get("stacked", False) stacked = data.get("stacked", False)
smooth = arguments.get("smooth", False) smooth = data.get("smooth", False)
show_label = arguments.get("show_label", False) show_label = data.get("show_label", False)
html = _build_chart_html( html = _build_chart_html(
title, chart_type, data, title, chart_type, chart_data,
width=width, height=height, theme=theme, width=width, height=height, theme=theme,
stacked=stacked, smooth=smooth, show_label=show_label, stacked=stacked, smooth=smooth, show_label=show_label,
) )
return _create_ui_resource(uri, html, width, height) return _create_ui_resource(uri, html, width, height)
elif path == "multi-chart": elif path == "multi-chart":
charts = arguments.get("charts", []) charts = data.get("charts", [])
if not charts: if not charts:
raise ValueError("Missing required parameter: charts") raise ValueError("Missing required parameter: data.charts")
for i, c in enumerate(charts): for i, c in enumerate(charts):
if not c.get("chart_type"): if not c.get("chart_type"):
raise ValueError(f"charts[{i}]: missing chart_type") raise ValueError(f"data.charts[{i}]: missing chart_type")
if not c.get("data", {}).get("series"): if not c.get("data", {}).get("series"):
raise ValueError(f"charts[{i}]: missing data.series") raise ValueError(f"data.charts[{i}]: missing data.series")
columns = arguments.get("columns", 2) columns = data.get("columns", 2)
html = _build_multi_chart_html(title, charts, columns=columns, theme=theme) html = _build_multi_chart_html(title, charts, columns=columns, theme=theme)
num_rows = (len(charts) + columns - 1) // columns num_rows = (len(charts) + columns - 1) // columns
total_height = f"{80 + num_rows * 420}px" total_height = f"{80 + num_rows * 420}px"
@ -493,6 +493,7 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
if tool_name == "render_data_viz": if tool_name == "render_data_viz":
uri = arguments.get("uri", "") uri = arguments.get("uri", "")
data = arguments.get("data", {})
if not uri or not uri.startswith("ui://data-dashboard/"): if not uri or not uri.startswith("ui://data-dashboard/"):
return create_error_response( return create_error_response(
request_id, -32602, request_id, -32602,
@ -500,7 +501,7 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
) )
try: try:
result = _handle_data_viz(uri, arguments) result = _handle_data_viz(uri, data)
except ValueError as e: except ValueError as e:
return create_error_response(request_id, -32602, str(e)) return create_error_response(request_id, -32602, str(e))

View File

@ -1,7 +1,7 @@
[ [
{ {
"name": "render_data_viz", "name": "render_data_viz",
"description": "Render data visualization resources. Use the `uri` field to specify the visualization type. All charts use ECharts library.\n\nSupported URIs:\n- `ui://data-dashboard/metrics` — Metric card dashboard (provide metrics array)\n- `ui://data-dashboard/chart` — Single chart: line, bar, pie, radar, scatter, or gauge (provide chart_type and data)\n- `ui://data-dashboard/multi-chart` — Multiple charts in a grid layout (provide charts array)", "description": "Render data visualization resources. Use the `uri` field to specify the visualization type, and pass all parameters in the `data` object. All charts use ECharts library.\n\nSupported URIs:\n- `ui://data-dashboard/metrics` — Metric card dashboard. data: {title, metrics: [{label, value, change?, change_type?}]}\n- `ui://data-dashboard/chart` — Single chart. data: {title, chart_type, data: {categories?, series}, width?, height?, stacked?, smooth?, show_label?, theme?}\n- `ui://data-dashboard/multi-chart` — Multiple charts in grid. data: {title, charts: [{title, chart_type, data, height?, stacked?, smooth?, show_label?}], columns?, theme?}",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -10,128 +10,134 @@
"enum": ["ui://data-dashboard/metrics", "ui://data-dashboard/chart", "ui://data-dashboard/multi-chart"], "enum": ["ui://data-dashboard/metrics", "ui://data-dashboard/chart", "ui://data-dashboard/multi-chart"],
"description": "Resource URI that determines the visualization type" "description": "Resource URI that determines the visualization type"
}, },
"title": {
"type": "string",
"description": "Title displayed at the top of the visualization"
},
"metrics": {
"type": "array",
"description": "[uri=ui://data-dashboard/metrics] Array of metric objects to display as cards",
"items": {
"type": "object",
"properties": {
"label": { "type": "string", "description": "Metric name" },
"value": { "type": "string", "description": "Metric value" },
"change": { "type": "string", "description": "Change indicator, e.g. '+12.5%'" },
"change_type": { "type": "string", "enum": ["up", "down", "neutral"], "description": "Direction of change" }
},
"required": ["label", "value"]
}
},
"chart_type": {
"type": "string",
"enum": ["line", "bar", "pie", "radar", "scatter", "gauge"],
"description": "[uri=ui://data-dashboard/chart] Type of chart to render"
},
"data": { "data": {
"type": "object", "type": "object",
"description": "[uri=ui://data-dashboard/chart] Chart data object", "description": "Parameters for the specified URI. Structure depends on uri:\n- metrics: {title, metrics: [{label, value, change?, change_type?}]}\n- chart: {title, chart_type, data: {categories?, series}, width?, height?, stacked?, smooth?, show_label?, theme?}\n- multi-chart: {title, charts: [{title, chart_type, data, height?, stacked?, smooth?, show_label?}], columns?, theme?}",
"properties": { "properties": {
"categories": { "title": {
"type": "array", "type": "string",
"items": { "type": "string" }, "description": "Title displayed at the top of the visualization"
"description": "X-axis labels (for line, bar, radar)"
}, },
"series": { "metrics": {
"type": "array", "type": "array",
"description": "Array of data series", "description": "[uri=metrics] Array of metric objects to display as cards",
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "type": "string", "description": "Series name" }, "label": { "type": "string", "description": "Metric name" },
"data": { "type": "array", "description": "Data values. For line/bar/radar: numbers. For pie: {name,value}. For scatter: [x,y]. For gauge: [value]." } "value": { "type": "string", "description": "Metric value" },
"change": { "type": "string", "description": "Change indicator, e.g. '+12.5%'" },
"change_type": { "type": "string", "enum": ["up", "down", "neutral"], "description": "Direction of change" }
}, },
"required": ["name", "data"] "required": ["label", "value"]
}
},
"chart_type": {
"type": "string",
"enum": ["line", "bar", "pie", "radar", "scatter", "gauge"],
"description": "[uri=chart] Type of chart to render"
},
"data": {
"type": "object",
"description": "[uri=chart] Chart data object",
"properties": {
"categories": {
"type": "array",
"items": { "type": "string" },
"description": "X-axis labels (for line, bar, radar)"
},
"series": {
"type": "array",
"description": "Array of data series",
"items": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Series name" },
"data": { "type": "array", "description": "Data values" }
},
"required": ["name", "data"]
}
}
},
"required": ["series"]
},
"width": {
"type": "string",
"description": "[uri=chart] CSS width. Default: '100%'",
"default": "100%"
},
"height": {
"type": "string",
"description": "[uri=chart] CSS height. Default: '400px'",
"default": "400px"
},
"stacked": {
"type": "boolean",
"description": "[uri=chart] Stack series (line/bar). Default: false",
"default": false
},
"smooth": {
"type": "boolean",
"description": "[uri=chart] Smooth curves (line). Default: false",
"default": false
},
"show_label": {
"type": "boolean",
"description": "[uri=chart] Show data labels. Default: false",
"default": false
},
"theme": {
"type": "string",
"enum": ["light", "dark"],
"description": "Color theme. Default: 'light'",
"default": "light"
},
"columns": {
"type": "integer",
"description": "[uri=multi-chart] Grid columns (1-4). Default: 2",
"default": 2,
"minimum": 1,
"maximum": 4
},
"charts": {
"type": "array",
"description": "[uri=multi-chart] Array of chart objects",
"items": {
"type": "object",
"properties": {
"title": { "type": "string", "description": "Chart title" },
"chart_type": { "type": "string", "enum": ["line", "bar", "pie", "radar", "scatter", "gauge"], "description": "Type of chart" },
"data": {
"type": "object",
"description": "Chart data",
"properties": {
"categories": { "type": "array", "items": { "type": "string" } },
"series": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"data": { "type": "array" }
},
"required": ["name", "data"]
}
}
},
"required": ["series"]
},
"height": { "type": "string", "default": "350px" },
"stacked": { "type": "boolean", "default": false },
"smooth": { "type": "boolean", "default": false },
"show_label": { "type": "boolean", "default": false }
},
"required": ["title", "chart_type", "data"]
} }
} }
},
"required": ["series"]
},
"width": {
"type": "string",
"description": "[uri=ui://data-dashboard/chart] CSS width. Default: '100%'",
"default": "100%"
},
"height": {
"type": "string",
"description": "[uri=ui://data-dashboard/chart] CSS height. Default: '400px'",
"default": "400px"
},
"stacked": {
"type": "boolean",
"description": "[uri=ui://data-dashboard/chart] Stack series (line/bar). Default: false",
"default": false
},
"smooth": {
"type": "boolean",
"description": "[uri=ui://data-dashboard/chart] Smooth curves (line). Default: false",
"default": false
},
"show_label": {
"type": "boolean",
"description": "[uri=ui://data-dashboard/chart] Show data labels. Default: false",
"default": false
},
"theme": {
"type": "string",
"enum": ["light", "dark"],
"description": "Color theme. Default: 'light'",
"default": "light"
},
"columns": {
"type": "integer",
"description": "[uri=ui://data-dashboard/multi-chart] Grid columns (1-4). Default: 2",
"default": 2,
"minimum": 1,
"maximum": 4
},
"charts": {
"type": "array",
"description": "[uri=ui://data-dashboard/multi-chart] Array of chart objects",
"items": {
"type": "object",
"properties": {
"title": { "type": "string", "description": "Chart title" },
"chart_type": { "type": "string", "enum": ["line", "bar", "pie", "radar", "scatter", "gauge"], "description": "Type of chart" },
"data": {
"type": "object",
"description": "Chart data",
"properties": {
"categories": { "type": "array", "items": { "type": "string" } },
"series": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"data": { "type": "array" }
},
"required": ["name", "data"]
}
}
},
"required": ["series"]
},
"height": { "type": "string", "default": "350px" },
"stacked": { "type": "boolean", "default": false },
"smooth": { "type": "boolean", "default": false },
"show_label": { "type": "boolean", "default": false }
},
"required": ["title", "chart_type", "data"]
} }
} }
}, },
"required": ["uri", "title"] "required": ["uri", "data"]
} }
} }
] ]

View File

@ -1,6 +1,6 @@
## `render_data_viz` usage guide ## `render_data_viz` usage guide
This tool renders data visualization resources. Use the `uri` field to specify the visualization type. This tool renders data visualization resources. Use the `uri` field to specify the visualization type, and pass all parameters in the `data` object.
### Supported URIs: ### Supported URIs:
- `ui://data-dashboard/metrics` — Metric card dashboard - `ui://data-dashboard/metrics` — Metric card dashboard
@ -16,12 +16,14 @@ When to use: KPIs, stats, numerical summaries, side-by-side comparisons.
``` ```
render_data_viz( render_data_viz(
uri="ui://data-dashboard/metrics", uri="ui://data-dashboard/metrics",
title="Sales Overview", data={
metrics=[ "title": "Sales Overview",
{"label": "Revenue", "value": "$12,345", "change": "+12.5%", "change_type": "up"}, "metrics": [
{"label": "Users", "value": "1,234", "change": "-3.2%", "change_type": "down"}, {"label": "Revenue", "value": "$12,345", "change": "+12.5%", "change_type": "up"},
{"label": "Conversion", "value": "4.5%", "change_type": "neutral"} {"label": "Users", "value": "1,234", "change": "-3.2%", "change_type": "down"},
] {"label": "Conversion", "value": "4.5%", "change_type": "neutral"}
]
}
) )
``` ```
@ -35,16 +37,18 @@ When to use: trends, comparisons, distributions, compositions, single metric gau
``` ```
render_data_viz( render_data_viz(
uri="ui://data-dashboard/chart", uri="ui://data-dashboard/chart",
title="Monthly Revenue",
chart_type="line",
data={ data={
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "title": "Monthly Revenue",
"series": [ "chart_type": "line",
{"name": "2024", "data": [820, 932, 901, 934, 1290, 1330]}, "data": {
{"name": "2025", "data": [620, 732, 801, 1034, 1190, 1530]} "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
] "series": [
}, {"name": "2024", "data": [820, 932, 901, 934, 1290, 1330]},
smooth=true {"name": "2025", "data": [620, 732, 801, 1034, 1190, 1530]}
]
},
"smooth": true
}
) )
``` ```
@ -52,17 +56,19 @@ render_data_viz(
``` ```
render_data_viz( render_data_viz(
uri="ui://data-dashboard/chart", uri="ui://data-dashboard/chart",
title="Sales by Region",
chart_type="bar",
data={ data={
"categories": ["North", "South", "East", "West"], "title": "Sales by Region",
"series": [ "chart_type": "bar",
{"name": "Q1", "data": [320, 302, 341, 374]}, "data": {
{"name": "Q2", "data": [220, 182, 191, 234]} "categories": ["North", "South", "East", "West"],
] "series": [
}, {"name": "Q1", "data": [320, 302, 341, 374]},
stacked=true, {"name": "Q2", "data": [220, 182, 191, 234]}
show_label=true ]
},
"stacked": true,
"show_label": true
}
) )
``` ```
@ -70,18 +76,20 @@ render_data_viz(
``` ```
render_data_viz( render_data_viz(
uri="ui://data-dashboard/chart", uri="ui://data-dashboard/chart",
title="Traffic Sources",
chart_type="pie",
data={ data={
"series": [{ "title": "Traffic Sources",
"name": "Sources", "chart_type": "pie",
"data": [ "data": {
{"name": "Search", "value": 1048}, "series": [{
{"name": "Direct", "value": 735}, "name": "Sources",
{"name": "Email", "value": 580}, "data": [
{"name": "Social", "value": 484} {"name": "Search", "value": 1048},
] {"name": "Direct", "value": 735},
}] {"name": "Email", "value": 580},
{"name": "Social", "value": 484}
]
}]
}
} }
) )
``` ```
@ -90,14 +98,16 @@ render_data_viz(
``` ```
render_data_viz( render_data_viz(
uri="ui://data-dashboard/chart", uri="ui://data-dashboard/chart",
title="Skill Assessment",
chart_type="radar",
data={ data={
"categories": ["Sales", "Admin", "Tech", "Support", "Marketing"], "title": "Skill Assessment",
"series": [ "chart_type": "radar",
{"name": "Alice", "data": [4200, 3000, 20000, 35000, 50000]}, "data": {
{"name": "Bob", "data": [5000, 14000, 28000, 26000, 42000]} "categories": ["Sales", "Admin", "Tech", "Support", "Marketing"],
] "series": [
{"name": "Alice", "data": [4200, 3000, 20000, 35000, 50000]},
{"name": "Bob", "data": [5000, 14000, 28000, 26000, 42000]}
]
}
} }
) )
``` ```
@ -106,13 +116,15 @@ render_data_viz(
``` ```
render_data_viz( render_data_viz(
uri="ui://data-dashboard/chart", uri="ui://data-dashboard/chart",
title="Height vs Weight",
chart_type="scatter",
data={ data={
"series": [ "title": "Height vs Weight",
{"name": "Male", "data": [[161, 51], [167, 59], [159, 49], [175, 73]]}, "chart_type": "scatter",
{"name": "Female", "data": [[150, 45], [160, 55], [165, 60], [155, 50]]} "data": {
] "series": [
{"name": "Male", "data": [[161, 51], [167, 59], [159, 49], [175, 73]]},
{"name": "Female", "data": [[150, 45], [160, 55], [165, 60], [155, 50]]}
]
}
} }
) )
``` ```
@ -121,13 +133,15 @@ render_data_viz(
``` ```
render_data_viz( render_data_viz(
uri="ui://data-dashboard/chart", uri="ui://data-dashboard/chart",
title="CPU Usage", data={
chart_type="gauge", "title": "CPU Usage",
data={"series": [{"name": "CPU", "data": [72.5]}]} "chart_type": "gauge",
"data": {"series": [{"name": "CPU", "data": [72.5]}]}
}
) )
``` ```
#### Chart options: #### Chart options (inside data):
- `width`: CSS width, default "100%" - `width`: CSS width, default "100%"
- `height`: CSS height, default "400px" - `height`: CSS height, default "400px"
- `theme`: "light" (default) or "dark" - `theme`: "light" (default) or "dark"
@ -144,48 +158,50 @@ When to use: comprehensive overviews, multiple related charts, business reports.
``` ```
render_data_viz( render_data_viz(
uri="ui://data-dashboard/multi-chart", uri="ui://data-dashboard/multi-chart",
title="Monthly Business Report", data={
columns=2, "title": "Monthly Business Report",
theme="light", "columns": 2,
charts=[ "theme": "light",
{ "charts": [
"title": "Revenue Trend", {
"chart_type": "line", "title": "Revenue Trend",
"data": { "chart_type": "line",
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "data": {
"series": [{"name": "Revenue", "data": [820, 932, 901, 934, 1290, 1330]}] "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
"series": [{"name": "Revenue", "data": [820, 932, 901, 934, 1290, 1330]}]
},
"smooth": true
}, },
"smooth": true {
}, "title": "Sales by Region",
{ "chart_type": "bar",
"title": "Sales by Region", "data": {
"chart_type": "bar", "categories": ["North", "South", "East", "West"],
"data": { "series": [
"categories": ["North", "South", "East", "West"], {"name": "Q1", "data": [320, 302, 341, 374]},
"series": [ {"name": "Q2", "data": [220, 182, 191, 234]}
{"name": "Q1", "data": [320, 302, 341, 374]}, ]
{"name": "Q2", "data": [220, 182, 191, 234]} }
] },
{
"title": "Traffic Sources",
"chart_type": "pie",
"data": {
"series": [{"name": "Sources", "data": [
{"name": "Search", "value": 1048},
{"name": "Direct", "value": 735},
{"name": "Social", "value": 484}
]}]
}
},
{
"title": "Server Load",
"chart_type": "gauge",
"data": {"series": [{"name": "CPU", "data": [67]}]},
"height": "300px"
} }
}, ]
{ }
"title": "Traffic Sources",
"chart_type": "pie",
"data": {
"series": [{"name": "Sources", "data": [
{"name": "Search", "value": 1048},
{"name": "Direct", "value": 735},
{"name": "Social", "value": 484}
]}]
}
},
{
"title": "Server Load",
"chart_type": "gauge",
"data": {"series": [{"name": "CPU", "data": [67]}]},
"height": "300px"
}
]
) )
``` ```

View File

@ -17,27 +17,32 @@ Example: If the question is "What is the topic of the PPT?", do NOT leave option
Do NOT call ask_user with empty options arrays. Do NOT call ask_user with empty options arrays.
### How to call render_ui for ask-user
Use `uri` + `data` format:
```json
render_ui(
uri="ui://mcp-ui/ask-user",
data={
"title": "A descriptive title",
"questions": [
{
"question": "Who is the audience?",
"options": ["Leadership", "Team", "Client"]
},
{
"question": "How long is the presentation?",
"options": ["5-10 minutes", "15-20 minutes", "30+ minutes"]
}
]
}
)
```
### How to format options ### How to format options
- Options MUST be placed in the `options` array field, NOT embedded in the question text. - Options MUST be placed in the `options` array field, NOT embedded in the question text.
- Keep the question text short and clean — just the question itself. - Keep the question text short and clean — just the question itself.
- Each question MUST have at least 2 options in the options array. - Each question MUST have at least 2 options in the options array.
CORRECT example:
```json
{
"questions": [
{
"question": "Who is the audience?",
"options": ["Leadership", "Team", "Client"]
},
{
"question": "How long is the presentation?",
"options": ["5-10 minutes", "15-20 minutes", "30+ minutes"]
}
]
}
```
WRONG example (DO NOT do this): WRONG example (DO NOT do this):
```json ```json
{ {

View File

@ -1,7 +1,7 @@
[ [
{ {
"name": "render_ui", "name": "render_ui",
"description": "Render an interactive UI resource in the chat. Use the `uri` field to specify the resource type. Supported URIs:\n- `ui://mcp-ui/html` — Render custom HTML/CSS/JS (provide html_content)\n- `ui://mcp-ui/url` — Embed an external URL in an iframe (provide url)\n- `ui://mcp-ui/ask-user` — Present questions with selectable options to the user (provide questions)", "description": "Render an interactive UI resource in the chat. Use the `uri` field to specify the resource type, and pass all parameters in the `data` object.\n\nSupported URIs:\n- `ui://mcp-ui/html` — Render custom HTML/CSS/JS. data: {html_content, title?, width?, height?}\n- `ui://mcp-ui/url` — Embed an external URL in an iframe. data: {url, title?, width?, height?}\n- `ui://mcp-ui/ask-user` — Present questions with selectable options. data: {questions: [{question, options, multi_select?}], title?}",
"inputSchema": { "inputSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -10,55 +10,61 @@
"enum": ["ui://mcp-ui/html", "ui://mcp-ui/url", "ui://mcp-ui/ask-user"], "enum": ["ui://mcp-ui/html", "ui://mcp-ui/url", "ui://mcp-ui/ask-user"],
"description": "Resource URI that determines the rendering mode" "description": "Resource URI that determines the rendering mode"
}, },
"title": { "data": {
"type": "string", "type": "object",
"description": "A descriptive title for the UI widget" "description": "Parameters for the specified URI. Structure depends on uri:\n- html: {html_content: string, title?: string, width?: string, height?: string}\n- url: {url: string, title?: string, width?: string, height?: string}\n- ask-user: {questions: [{question: string, options: string[], multi_select?: boolean}], title?: string}",
}, "properties": {
"html_content": { "title": {
"type": "string", "type": "string",
"description": "[uri=ui://mcp-ui/html] Complete HTML content to render. Can include inline CSS and JavaScript." "description": "A descriptive title for the UI widget"
},
"url": {
"type": "string",
"description": "[uri=ui://mcp-ui/url] External URL to embed in an iframe."
},
"width": {
"type": "string",
"description": "[uri=ui://mcp-ui/html|url] CSS width for the iframe. Default: '100%'",
"default": "100%"
},
"height": {
"type": "string",
"description": "[uri=ui://mcp-ui/html|url] CSS height for the iframe. Default: 'auto'",
"default": "auto"
},
"questions": {
"type": "array",
"description": "[uri=ui://mcp-ui/ask-user] Array of questions to ask the user.",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to ask the user"
},
"options": {
"type": "array",
"minItems": 2,
"items": { "type": "string" },
"description": "REQUIRED array with at least 2 options."
},
"multi_select": {
"type": "boolean",
"description": "If true, the user can select multiple options. Default: false.",
"default": false
}
}, },
"required": ["question", "options"] "html_content": {
"type": "string",
"description": "[uri=html] Complete HTML content to render. Can include inline CSS and JavaScript."
},
"url": {
"type": "string",
"description": "[uri=url] External URL to embed in an iframe."
},
"width": {
"type": "string",
"description": "[uri=html|url] CSS width. Default: '100%'",
"default": "100%"
},
"height": {
"type": "string",
"description": "[uri=html|url] CSS height. Default: 'auto'",
"default": "auto"
},
"questions": {
"type": "array",
"description": "[uri=ask-user] Array of questions to ask the user.",
"items": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to ask the user"
},
"options": {
"type": "array",
"minItems": 2,
"items": { "type": "string" },
"description": "REQUIRED array with at least 2 options."
},
"multi_select": {
"type": "boolean",
"description": "If true, the user can select multiple options. Default: false.",
"default": false
}
},
"required": ["question", "options"]
}
}
} }
} }
}, },
"required": ["uri", "title"] "required": ["uri", "data"]
} }
} }
] ]

View File

@ -26,15 +26,15 @@ def _serialize_ui_resource(ui_resource) -> str:
return json.dumps(ui_resource.model_dump(mode="json"), ensure_ascii=False) return json.dumps(ui_resource.model_dump(mode="json"), ensure_ascii=False)
def _handle_render_ui(uri: str, arguments: Dict[str, Any]) -> Dict[str, Any]: def _handle_render_ui(uri: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""Unified handler for all render_ui URIs. """Unified handler for all render_ui URIs.
Content is NOT self-contained the actual html_content/url/questions Content is NOT self-contained the actual html_content/url/questions
stays in the TOOL_CALL args to save tokens. resource.text is a placeholder. stays in the TOOL_CALL args to save tokens. resource.text is a placeholder.
The frontend extracts content from TOOL_CALL args based on URI. The frontend extracts content from TOOL_CALL args based on URI.
""" """
width = arguments.get("width", "100%") width = data.get("width", "100%")
height = arguments.get("height", "auto") height = data.get("height", "auto")
if uri == "ui://mcp-ui/ask-user": if uri == "ui://mcp-ui/ask-user":
resource = create_ui_resource({ resource = create_ui_resource({
@ -46,7 +46,7 @@ def _handle_render_ui(uri: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
}, },
}) })
elif uri == "ui://mcp-ui/url": elif uri == "ui://mcp-ui/url":
url = arguments.get("url", "") url = data.get("url", "")
resource = create_ui_resource({ resource = create_ui_resource({
"uri": uri, "uri": uri,
"content": {"type": "externalUrl", "iframeUrl": url}, "content": {"type": "externalUrl", "iframeUrl": url},
@ -111,6 +111,7 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
if tool_name == "render_ui": if tool_name == "render_ui":
uri = arguments.get("uri", "") uri = arguments.get("uri", "")
data = arguments.get("data", {})
if not uri or not uri.startswith("ui://mcp-ui/"): if not uri or not uri.startswith("ui://mcp-ui/"):
return create_error_response( return create_error_response(
@ -120,20 +121,20 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
path = uri.replace("ui://mcp-ui/", "") path = uri.replace("ui://mcp-ui/", "")
if path == "html" and not arguments.get("html_content"): if path == "html" and not data.get("html_content"):
return create_error_response( return create_error_response(
request_id, -32602, "Missing required parameter: html_content (for uri=ui://mcp-ui/html)" request_id, -32602, "Missing required parameter: data.html_content (for uri=ui://mcp-ui/html)"
) )
elif path == "url" and not arguments.get("url"): elif path == "url" and not data.get("url"):
return create_error_response( return create_error_response(
request_id, -32602, "Missing required parameter: url (for uri=ui://mcp-ui/url)" request_id, -32602, "Missing required parameter: data.url (for uri=ui://mcp-ui/url)"
) )
elif path == "ask-user" and not arguments.get("questions"): elif path == "ask-user" and not data.get("questions"):
return create_error_response( return create_error_response(
request_id, -32602, "Missing required parameter: questions (for uri=ui://mcp-ui/ask-user)" request_id, -32602, "Missing required parameter: data.questions (for uri=ui://mcp-ui/ask-user)"
) )
result = _handle_render_ui(uri, arguments) result = _handle_render_ui(uri, data)
return {"jsonrpc": "2.0", "id": request_id, "result": result} return {"jsonrpc": "2.0", "id": request_id, "result": result}
else: else: