Compare commits
8 Commits
944933d6b6
...
495b8031bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
495b8031bb | ||
|
|
c87daabd31 | ||
|
|
65f3fba9ea | ||
|
|
7615bd36ca | ||
|
|
35ceca1af3 | ||
|
|
2a243202c9 | ||
|
|
ece8b5b7d2 | ||
|
|
7f66279311 |
@ -114,8 +114,7 @@ async def enhanced_generate_stream_response(
|
||||
chunk_args = tool_call_chunk.get("args") if isinstance(tool_call_chunk, dict) else getattr(tool_call_chunk, "args", None)
|
||||
if chunk_name:
|
||||
current_tool_name = chunk_name
|
||||
# Always output ask_user and render_ui tool calls even when tool_response is disabled
|
||||
if config.tool_response or current_tool_name in ('ask_user', 'render_ui'):
|
||||
if config.tool_response:
|
||||
if chunk_name:
|
||||
new_content = f"[{message_tag}] {chunk_name}\n"
|
||||
if chunk_args:
|
||||
|
||||
@ -74,32 +74,385 @@ def _esc(text: str) -> str:
|
||||
return text.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
||||
|
||||
|
||||
def render_dashboard(title: str, metrics: List[Dict[str, str]]) -> Dict[str, Any]:
|
||||
"""Create a UIResource dashboard and serialize it as JSON for TOOL_RESPONSE."""
|
||||
try:
|
||||
html = _build_dashboard_html(title, metrics)
|
||||
uri_slug = title.replace(" ", "-").lower()[:50]
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chart HTML builder (ECharts)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ui_resource = create_ui_resource(
|
||||
ECHARTS_CDN = "https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"
|
||||
|
||||
|
||||
def _build_chart_html(
|
||||
title: str,
|
||||
chart_type: str,
|
||||
data: Dict[str, Any],
|
||||
width: str = "100%",
|
||||
height: str = "400px",
|
||||
theme: str = "light",
|
||||
stacked: bool = False,
|
||||
smooth: bool = False,
|
||||
show_label: bool = False,
|
||||
) -> str:
|
||||
"""Build a self-contained HTML page with an ECharts chart."""
|
||||
series = data.get("series", [])
|
||||
categories = data.get("categories", [])
|
||||
|
||||
option = _build_echarts_option(
|
||||
title, chart_type, categories, series,
|
||||
stacked=stacked, smooth=smooth, show_label=show_label,
|
||||
)
|
||||
option_json = json.dumps(option, ensure_ascii=False)
|
||||
|
||||
bg_color = "#1a1a2e" if theme == "dark" else "#f8fafc"
|
||||
text_color = "#e0e0e0" if theme == "dark" else "#0f172a"
|
||||
echarts_theme = "'dark'" if theme == "dark" else "null"
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{_esc(title)}</title>
|
||||
<script src="{ECHARTS_CDN}"></script>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: {bg_color}; padding: 16px; }}
|
||||
#chart {{ width: {width}; height: {height}; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart"></div>
|
||||
<script>
|
||||
var chart = echarts.init(document.getElementById('chart'), {echarts_theme});
|
||||
var option = {option_json};
|
||||
chart.setOption(option);
|
||||
window.addEventListener('resize', function() {{ chart.resize(); }});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def _build_echarts_option(
|
||||
title: str,
|
||||
chart_type: str,
|
||||
categories: List,
|
||||
series_data: List[Dict[str, Any]],
|
||||
stacked: bool = False,
|
||||
smooth: bool = False,
|
||||
show_label: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build an ECharts option dict based on chart_type."""
|
||||
builders = {
|
||||
"line": _option_line,
|
||||
"bar": _option_bar,
|
||||
"pie": _option_pie,
|
||||
"radar": _option_radar,
|
||||
"scatter": _option_scatter,
|
||||
"gauge": _option_gauge,
|
||||
}
|
||||
builder = builders.get(chart_type, _option_line)
|
||||
return builder(title, categories, series_data, stacked=stacked, smooth=smooth, show_label=show_label)
|
||||
|
||||
|
||||
def _base_option(title: str) -> Dict[str, Any]:
|
||||
"""Shared base option for all chart types."""
|
||||
return {
|
||||
"title": {"text": title, "left": "center", "top": 8, "textStyle": {"fontSize": 16, "fontWeight": 600}},
|
||||
"tooltip": {"trigger": "axis"},
|
||||
"legend": {"top": 36},
|
||||
"grid": {"top": 80, "left": 60, "right": 30, "bottom": 40},
|
||||
"color": ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de", "#3ba272", "#fc8452", "#9a60b4"],
|
||||
}
|
||||
|
||||
|
||||
def _label_cfg(show: bool) -> Dict[str, Any]:
|
||||
return {"show": show, "position": "top", "fontSize": 11}
|
||||
|
||||
|
||||
def _option_line(title, categories, series_data, stacked=False, smooth=False, show_label=False):
|
||||
opt = _base_option(title)
|
||||
opt["tooltip"]["trigger"] = "axis"
|
||||
opt["xAxis"] = {"type": "category", "data": categories, "boundaryGap": False}
|
||||
opt["yAxis"] = {"type": "value"}
|
||||
opt["series"] = [
|
||||
{
|
||||
"uri": f"ui://data-dashboard/{uri_slug}",
|
||||
"name": s["name"],
|
||||
"type": "line",
|
||||
"data": s["data"],
|
||||
"smooth": smooth,
|
||||
"stack": "total" if stacked else None,
|
||||
"areaStyle": {} if stacked else None,
|
||||
"label": _label_cfg(show_label),
|
||||
}
|
||||
for s in series_data
|
||||
]
|
||||
return opt
|
||||
|
||||
|
||||
def _option_bar(title, categories, series_data, stacked=False, smooth=False, show_label=False):
|
||||
opt = _base_option(title)
|
||||
opt["xAxis"] = {"type": "category", "data": categories}
|
||||
opt["yAxis"] = {"type": "value"}
|
||||
opt["series"] = [
|
||||
{
|
||||
"name": s["name"],
|
||||
"type": "bar",
|
||||
"data": s["data"],
|
||||
"stack": "total" if stacked else None,
|
||||
"label": _label_cfg(show_label),
|
||||
"barMaxWidth": 50,
|
||||
}
|
||||
for s in series_data
|
||||
]
|
||||
return opt
|
||||
|
||||
|
||||
def _option_pie(title, categories, series_data, stacked=False, smooth=False, show_label=False):
|
||||
opt = _base_option(title)
|
||||
del opt["grid"]
|
||||
opt["tooltip"] = {"trigger": "item", "formatter": "{b}: {c} ({d}%)"}
|
||||
# Pie uses the first series; data items are {name, value}
|
||||
pie_data = series_data[0]["data"] if series_data else []
|
||||
opt["series"] = [
|
||||
{
|
||||
"name": series_data[0]["name"] if series_data else title,
|
||||
"type": "pie",
|
||||
"radius": ["40%", "70%"],
|
||||
"center": ["50%", "55%"],
|
||||
"data": pie_data,
|
||||
"emphasis": {"itemStyle": {"shadowBlur": 10, "shadowOffsetX": 0, "shadowColor": "rgba(0,0,0,0.5)"}},
|
||||
"label": {"show": True, "formatter": "{b}: {d}%"},
|
||||
"itemStyle": {"borderRadius": 6, "borderColor": "#fff", "borderWidth": 2},
|
||||
}
|
||||
]
|
||||
return opt
|
||||
|
||||
|
||||
def _option_radar(title, categories, series_data, stacked=False, smooth=False, show_label=False):
|
||||
opt = _base_option(title)
|
||||
del opt["grid"]
|
||||
opt["tooltip"] = {"trigger": "item"}
|
||||
# Determine max value per indicator from all series
|
||||
max_vals = [0] * len(categories)
|
||||
for s in series_data:
|
||||
for i, v in enumerate(s["data"]):
|
||||
if i < len(max_vals) and v > max_vals[i]:
|
||||
max_vals[i] = v
|
||||
indicator = [{"name": c, "max": int(m * 1.2) or 100} for c, m in zip(categories, max_vals)]
|
||||
opt["radar"] = {"indicator": indicator, "center": ["50%", "55%"]}
|
||||
opt["series"] = [
|
||||
{
|
||||
"type": "radar",
|
||||
"data": [{"value": s["data"], "name": s["name"]} for s in series_data],
|
||||
"areaStyle": {"opacity": 0.15},
|
||||
}
|
||||
]
|
||||
return opt
|
||||
|
||||
|
||||
def _option_scatter(title, categories, series_data, stacked=False, smooth=False, show_label=False):
|
||||
opt = _base_option(title)
|
||||
opt["tooltip"] = {"trigger": "item", "formatter": "{a}: ({c})"}
|
||||
opt["xAxis"] = {"type": "value", "scale": True}
|
||||
opt["yAxis"] = {"type": "value", "scale": True}
|
||||
opt["series"] = [
|
||||
{
|
||||
"name": s["name"],
|
||||
"type": "scatter",
|
||||
"data": s["data"],
|
||||
"symbolSize": 10,
|
||||
"label": _label_cfg(show_label),
|
||||
}
|
||||
for s in series_data
|
||||
]
|
||||
return opt
|
||||
|
||||
|
||||
def _option_gauge(title, categories, series_data, stacked=False, smooth=False, show_label=False):
|
||||
opt = _base_option(title)
|
||||
del opt["grid"]
|
||||
opt["tooltip"] = {"trigger": "item"}
|
||||
# Gauge uses first series, first data value
|
||||
value = 0
|
||||
name = title
|
||||
if series_data:
|
||||
name = series_data[0]["name"]
|
||||
data_arr = series_data[0]["data"]
|
||||
if data_arr:
|
||||
value = data_arr[0] if isinstance(data_arr[0], (int, float)) else 0
|
||||
opt["series"] = [
|
||||
{
|
||||
"type": "gauge",
|
||||
"center": ["50%", "60%"],
|
||||
"startAngle": 200,
|
||||
"endAngle": -20,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"detail": {"formatter": "{value}%", "fontSize": 24, "offsetCenter": [0, "60%"]},
|
||||
"data": [{"value": value, "name": name}],
|
||||
"axisLine": {"lineStyle": {"width": 20}},
|
||||
"progress": {"show": True, "width": 20},
|
||||
"pointer": {"show": True},
|
||||
}
|
||||
]
|
||||
return opt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multi-chart HTML builder
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_multi_chart_html(
|
||||
title: str,
|
||||
charts: List[Dict[str, Any]],
|
||||
columns: int = 2,
|
||||
theme: str = "light",
|
||||
) -> str:
|
||||
"""Build a single HTML page containing multiple ECharts in a grid layout."""
|
||||
bg_color = "#1a1a2e" if theme == "dark" else "#f8fafc"
|
||||
text_color = "#e0e0e0" if theme == "dark" else "#0f172a"
|
||||
card_bg = "#252547" if theme == "dark" else "#ffffff"
|
||||
border_color = "#3a3a5c" if theme == "dark" else "#e2e8f0"
|
||||
echarts_theme = "'dark'" if theme == "dark" else "null"
|
||||
|
||||
# Build chart divs and init scripts
|
||||
chart_divs = ""
|
||||
chart_scripts = ""
|
||||
for idx, chart in enumerate(charts):
|
||||
chart_id = f"chart_{idx}"
|
||||
chart_title = chart.get("title", f"Chart {idx + 1}")
|
||||
chart_type = chart.get("chart_type", "line")
|
||||
chart_data = chart.get("data", {})
|
||||
chart_height = chart.get("height", "350px")
|
||||
stacked = chart.get("stacked", False)
|
||||
smooth = chart.get("smooth", False)
|
||||
show_label = chart.get("show_label", False)
|
||||
|
||||
categories = chart_data.get("categories", [])
|
||||
series = chart_data.get("series", [])
|
||||
|
||||
option = _build_echarts_option(
|
||||
chart_title, chart_type, categories, series,
|
||||
stacked=stacked, smooth=smooth, show_label=show_label,
|
||||
)
|
||||
option_json = json.dumps(option, ensure_ascii=False)
|
||||
|
||||
chart_divs += f"""
|
||||
<div class="chart-card">
|
||||
<div id="{chart_id}" style="width:100%;height:{chart_height};"></div>
|
||||
</div>"""
|
||||
|
||||
chart_scripts += f"""
|
||||
(function() {{
|
||||
var el = document.getElementById('{chart_id}');
|
||||
var c = echarts.init(el, {echarts_theme});
|
||||
c.setOption({option_json});
|
||||
charts.push(c);
|
||||
}})();"""
|
||||
|
||||
# Calculate total height based on rows
|
||||
num_rows = (len(charts) + columns - 1) // columns
|
||||
# 350px default chart + 32px card padding + 16px gap
|
||||
est_row_height = 400
|
||||
total_height = 80 + num_rows * est_row_height
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{_esc(title)}</title>
|
||||
<script src="{ECHARTS_CDN}"></script>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: {bg_color}; padding: 24px; color: {text_color}; }}
|
||||
h1 {{ font-size: 22px; font-weight: 600; margin-bottom: 20px; text-align: center; }}
|
||||
.grid {{ display: grid; grid-template-columns: repeat({columns}, 1fr); gap: 16px; }}
|
||||
.chart-card {{ background: {card_bg}; border: 1px solid {border_color}; border-radius: 12px; padding: 16px; }}
|
||||
@media (max-width: 768px) {{ .grid {{ grid-template-columns: 1fr; }} }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{_esc(title)}</h1>
|
||||
<div class="grid">{chart_divs}
|
||||
</div>
|
||||
<script>
|
||||
var charts = [];{chart_scripts}
|
||||
window.addEventListener('resize', function() {{ charts.forEach(function(c) {{ c.resize(); }}); }});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def _create_ui_resource(uri: str, html: str, width: str = "100%", height: str = "auto") -> Dict[str, Any]:
|
||||
"""Create a self-contained UIResource with HTML in resource.text."""
|
||||
ui_resource = create_ui_resource({
|
||||
"uri": uri,
|
||||
"content": {"type": "rawHtml", "htmlString": html},
|
||||
"encoding": "text",
|
||||
"uiMetadata": {
|
||||
UIMetadataKey.PREFERRED_FRAME_SIZE: ["100%", "auto"],
|
||||
UIMetadataKey.PREFERRED_FRAME_SIZE: [width, height],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
resource_json = json.dumps(
|
||||
ui_resource.model_dump(mode="json"), ensure_ascii=False
|
||||
)
|
||||
})
|
||||
resource_json = json.dumps(ui_resource.model_dump(mode="json"), ensure_ascii=False)
|
||||
return {"content": [{"type": "text", "text": resource_json}]}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"content": [{"type": "text", "text": f"Error creating dashboard: {str(e)}"}]
|
||||
}
|
||||
|
||||
def _handle_data_viz(uri: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Unified handler for all data visualization URIs.
|
||||
|
||||
Parses URI path to route to the appropriate HTML builder.
|
||||
All content is self-contained in resource.text.
|
||||
"""
|
||||
title = data.get("title", "Dashboard")
|
||||
theme = data.get("theme", "light")
|
||||
path = uri.replace("ui://data-dashboard/", "")
|
||||
|
||||
if path == "metrics":
|
||||
metrics = data.get("metrics", [])
|
||||
if not metrics:
|
||||
raise ValueError("Missing required parameter: data.metrics")
|
||||
html = _build_dashboard_html(title, metrics)
|
||||
return _create_ui_resource(uri, html, "100%", "auto")
|
||||
|
||||
elif path == "chart":
|
||||
chart_type = data.get("chart_type")
|
||||
chart_data = data.get("data", {})
|
||||
if not chart_type:
|
||||
raise ValueError("Missing required parameter: data.chart_type")
|
||||
if not chart_data or not chart_data.get("series"):
|
||||
raise ValueError("Missing required parameter: data.data.series")
|
||||
|
||||
width = data.get("width", "100%")
|
||||
height = data.get("height", "400px")
|
||||
stacked = data.get("stacked", False)
|
||||
smooth = data.get("smooth", False)
|
||||
show_label = data.get("show_label", False)
|
||||
|
||||
html = _build_chart_html(
|
||||
title, chart_type, chart_data,
|
||||
width=width, height=height, theme=theme,
|
||||
stacked=stacked, smooth=smooth, show_label=show_label,
|
||||
)
|
||||
return _create_ui_resource(uri, html, width, height)
|
||||
|
||||
elif path == "multi-chart":
|
||||
charts = data.get("charts", [])
|
||||
if not charts:
|
||||
raise ValueError("Missing required parameter: data.charts")
|
||||
for i, c in enumerate(charts):
|
||||
if not c.get("chart_type"):
|
||||
raise ValueError(f"data.charts[{i}]: missing chart_type")
|
||||
if not c.get("data", {}).get("series"):
|
||||
raise ValueError(f"data.charts[{i}]: missing data.series")
|
||||
|
||||
columns = data.get("columns", 2)
|
||||
html = _build_multi_chart_html(title, charts, columns=columns, theme=theme)
|
||||
num_rows = (len(charts) + columns - 1) // columns
|
||||
total_height = f"{80 + num_rows * 420}px"
|
||||
return _create_ui_resource(uri, html, "100%", total_height)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown URI path: {path}")
|
||||
|
||||
|
||||
async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@ -120,27 +473,15 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not tools:
|
||||
tools = [
|
||||
{
|
||||
"name": "render_dashboard",
|
||||
"description": "Render a data dashboard with metric cards.",
|
||||
"name": "render_data_viz",
|
||||
"description": "Render data visualization. Use uri to specify type.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uri": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"metrics": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {"type": "string"},
|
||||
"value": {"type": "string"},
|
||||
"change": {"type": "string"},
|
||||
"change_type": {"type": "string", "enum": ["up", "down", "neutral"]},
|
||||
},
|
||||
"required": ["label", "value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["title", "metrics"],
|
||||
"required": ["uri", "title"],
|
||||
},
|
||||
}
|
||||
]
|
||||
@ -150,16 +491,20 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
tool_name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if tool_name == "render_dashboard":
|
||||
title = arguments.get("title", "Dashboard")
|
||||
metrics = arguments.get("metrics", [])
|
||||
|
||||
if not metrics:
|
||||
if tool_name == "render_data_viz":
|
||||
uri = arguments.get("uri", "")
|
||||
data = arguments.get("data", {})
|
||||
if not uri or not uri.startswith("ui://data-dashboard/"):
|
||||
return create_error_response(
|
||||
request_id, -32602, "Missing required parameter: metrics"
|
||||
request_id, -32602,
|
||||
"Invalid uri. Must be one of: ui://data-dashboard/metrics, ui://data-dashboard/chart, ui://data-dashboard/multi-chart"
|
||||
)
|
||||
|
||||
result = render_dashboard(title, metrics)
|
||||
try:
|
||||
result = _handle_data_viz(uri, data)
|
||||
except ValueError as e:
|
||||
return create_error_response(request_id, -32602, str(e))
|
||||
|
||||
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
||||
|
||||
else:
|
||||
|
||||
@ -1,43 +1,143 @@
|
||||
[
|
||||
{
|
||||
"name": "render_dashboard",
|
||||
"description": "Render an interactive data dashboard with metric cards. Pass an array of metrics, each with label, value, and optional change/change_type fields. The dashboard is rendered as an HTML card UI.",
|
||||
"name": "render_data_viz",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uri": {
|
||||
"type": "string",
|
||||
"enum": ["ui://data-dashboard/metrics", "ui://data-dashboard/chart", "ui://data-dashboard/multi-chart"],
|
||||
"description": "Resource URI that determines the visualization type"
|
||||
},
|
||||
"data": {
|
||||
"type": "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": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Dashboard title, e.g. 'Sales Overview'"
|
||||
"description": "Title displayed at the top of the visualization"
|
||||
},
|
||||
"metrics": {
|
||||
"type": "array",
|
||||
"description": "Array of metric objects to display as cards",
|
||||
"description": "[uri=metrics] Array of metric objects to display as cards",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Metric name, e.g. 'Revenue'"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "Metric value, e.g. '$12,345'"
|
||||
},
|
||||
"change": {
|
||||
"type": "string",
|
||||
"description": "Change indicator, e.g. '+12.5%' or '-3.2%'"
|
||||
},
|
||||
"change_type": {
|
||||
"type": "string",
|
||||
"enum": ["up", "down", "neutral"],
|
||||
"description": "Direction of change"
|
||||
}
|
||||
"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=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": ["title", "metrics"]
|
||||
"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": ["uri", "data"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,13 +1,214 @@
|
||||
## `render_dashboard` usage guide
|
||||
## `render_data_viz` usage guide
|
||||
|
||||
### When to use render_dashboard:
|
||||
- When the user wants to visualize data as metric cards (KPIs, stats, numbers)
|
||||
- When summarizing numerical results (sales, traffic, performance metrics)
|
||||
- When comparing multiple data points side by side
|
||||
This tool renders data visualization resources. Use the `uri` field to specify the visualization type, and pass all parameters in the `data` object.
|
||||
|
||||
### How to call:
|
||||
render_dashboard(title="Sales Overview", metrics=[
|
||||
### Supported URIs:
|
||||
- `ui://data-dashboard/metrics` — Metric card dashboard
|
||||
- `ui://data-dashboard/chart` — Single chart (line, bar, pie, radar, scatter, gauge)
|
||||
- `ui://data-dashboard/multi-chart` — Multiple charts in a grid layout
|
||||
|
||||
---
|
||||
|
||||
### 1. Metrics — `ui://data-dashboard/metrics`
|
||||
|
||||
When to use: KPIs, stats, numerical summaries, side-by-side comparisons.
|
||||
|
||||
```
|
||||
render_data_viz(
|
||||
uri="ui://data-dashboard/metrics",
|
||||
data={
|
||||
"title": "Sales Overview",
|
||||
"metrics": [
|
||||
{"label": "Revenue", "value": "$12,345", "change": "+12.5%", "change_type": "up"},
|
||||
{"label": "Users", "value": "1,234", "change": "-3.2%", "change_type": "down"},
|
||||
{"label": "Conversion", "value": "4.5%", "change_type": "neutral"}
|
||||
])
|
||||
]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Chart — `ui://data-dashboard/chart`
|
||||
|
||||
When to use: trends, comparisons, distributions, compositions, single metric gauges.
|
||||
|
||||
#### Line chart — trends over time
|
||||
```
|
||||
render_data_viz(
|
||||
uri="ui://data-dashboard/chart",
|
||||
data={
|
||||
"title": "Monthly Revenue",
|
||||
"chart_type": "line",
|
||||
"data": {
|
||||
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
|
||||
"series": [
|
||||
{"name": "2024", "data": [820, 932, 901, 934, 1290, 1330]},
|
||||
{"name": "2025", "data": [620, 732, 801, 1034, 1190, 1530]}
|
||||
]
|
||||
},
|
||||
"smooth": true
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Bar chart — comparisons
|
||||
```
|
||||
render_data_viz(
|
||||
uri="ui://data-dashboard/chart",
|
||||
data={
|
||||
"title": "Sales by Region",
|
||||
"chart_type": "bar",
|
||||
"data": {
|
||||
"categories": ["North", "South", "East", "West"],
|
||||
"series": [
|
||||
{"name": "Q1", "data": [320, 302, 341, 374]},
|
||||
{"name": "Q2", "data": [220, 182, 191, 234]}
|
||||
]
|
||||
},
|
||||
"stacked": true,
|
||||
"show_label": true
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Pie chart — proportions
|
||||
```
|
||||
render_data_viz(
|
||||
uri="ui://data-dashboard/chart",
|
||||
data={
|
||||
"title": "Traffic Sources",
|
||||
"chart_type": "pie",
|
||||
"data": {
|
||||
"series": [{
|
||||
"name": "Sources",
|
||||
"data": [
|
||||
{"name": "Search", "value": 1048},
|
||||
{"name": "Direct", "value": 735},
|
||||
{"name": "Email", "value": 580},
|
||||
{"name": "Social", "value": 484}
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Radar chart — multi-dimensional
|
||||
```
|
||||
render_data_viz(
|
||||
uri="ui://data-dashboard/chart",
|
||||
data={
|
||||
"title": "Skill Assessment",
|
||||
"chart_type": "radar",
|
||||
"data": {
|
||||
"categories": ["Sales", "Admin", "Tech", "Support", "Marketing"],
|
||||
"series": [
|
||||
{"name": "Alice", "data": [4200, 3000, 20000, 35000, 50000]},
|
||||
{"name": "Bob", "data": [5000, 14000, 28000, 26000, 42000]}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Scatter chart — correlation
|
||||
```
|
||||
render_data_viz(
|
||||
uri="ui://data-dashboard/chart",
|
||||
data={
|
||||
"title": "Height vs Weight",
|
||||
"chart_type": "scatter",
|
||||
"data": {
|
||||
"series": [
|
||||
{"name": "Male", "data": [[161, 51], [167, 59], [159, 49], [175, 73]]},
|
||||
{"name": "Female", "data": [[150, 45], [160, 55], [165, 60], [155, 50]]}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Gauge chart — single KPI
|
||||
```
|
||||
render_data_viz(
|
||||
uri="ui://data-dashboard/chart",
|
||||
data={
|
||||
"title": "CPU Usage",
|
||||
"chart_type": "gauge",
|
||||
"data": {"series": [{"name": "CPU", "data": [72.5]}]}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Chart options (inside data):
|
||||
- `width`: CSS width, default "100%"
|
||||
- `height`: CSS height, default "400px"
|
||||
- `theme`: "light" (default) or "dark"
|
||||
- `stacked`: stack series (line/bar), default false
|
||||
- `smooth`: smooth curves (line), default false
|
||||
- `show_label`: show data labels, default false
|
||||
|
||||
---
|
||||
|
||||
### 3. Multi-chart — `ui://data-dashboard/multi-chart`
|
||||
|
||||
When to use: comprehensive overviews, multiple related charts, business reports.
|
||||
|
||||
```
|
||||
render_data_viz(
|
||||
uri="ui://data-dashboard/multi-chart",
|
||||
data={
|
||||
"title": "Monthly Business Report",
|
||||
"columns": 2,
|
||||
"theme": "light",
|
||||
"charts": [
|
||||
{
|
||||
"title": "Revenue Trend",
|
||||
"chart_type": "line",
|
||||
"data": {
|
||||
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
|
||||
"series": [{"name": "Revenue", "data": [820, 932, 901, 934, 1290, 1330]}]
|
||||
},
|
||||
"smooth": true
|
||||
},
|
||||
{
|
||||
"title": "Sales by Region",
|
||||
"chart_type": "bar",
|
||||
"data": {
|
||||
"categories": ["North", "South", "East", "West"],
|
||||
"series": [
|
||||
{"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Tips:
|
||||
- Always choose the most appropriate chart_type for the data
|
||||
- Use line for time-series trends, bar for category comparisons
|
||||
- Use pie for proportions (limit ~7 segments), radar for multi-dimensional scores
|
||||
- Use scatter for correlation, gauge for single KPI (0-100)
|
||||
- Set height: "400px" for most charts, "300px" for gauge
|
||||
- Multi-chart: columns=2 for 2-4 charts, keep 4-6 max per call
|
||||
|
||||
@ -17,14 +17,13 @@ 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.
|
||||
|
||||
### How to format options
|
||||
- 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.
|
||||
- Each question MUST have at least 2 options in the options array.
|
||||
|
||||
CORRECT example:
|
||||
### 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?",
|
||||
@ -36,8 +35,14 @@ CORRECT example:
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### How to format options
|
||||
- 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.
|
||||
- Each question MUST have at least 2 options in the options array.
|
||||
|
||||
WRONG example (DO NOT do this):
|
||||
```json
|
||||
{
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
[
|
||||
{
|
||||
"name": "render_ui",
|
||||
"description": "Render an interactive UI widget in the chat. Supports two modes: (1) raw HTML — provide html_content to render custom HTML/CSS/JS, (2) external URL — provide url to embed an external webpage in an iframe. Use html_content OR url, not both.",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"uri": {
|
||||
"type": "string",
|
||||
"enum": ["ui://mcp-ui/html", "ui://mcp-ui/url", "ui://mcp-ui/ask-user"],
|
||||
"description": "Resource URI that determines the rendering mode"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
@ -11,35 +20,25 @@
|
||||
},
|
||||
"html_content": {
|
||||
"type": "string",
|
||||
"description": "Complete HTML content to render. Can include inline CSS and JavaScript within <style> and <script> tags. Use this OR url, not both."
|
||||
"description": "[uri=html] Complete HTML content to render. Can include inline CSS and JavaScript."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "External URL to embed in an iframe. Use this OR html_content, not both."
|
||||
"description": "[uri=url] External URL to embed in an iframe."
|
||||
},
|
||||
"width": {
|
||||
"type": "string",
|
||||
"description": "CSS width for the iframe. Default: '100%'",
|
||||
"description": "[uri=html|url] CSS width. Default: '100%'",
|
||||
"default": "100%"
|
||||
},
|
||||
"height": {
|
||||
"type": "string",
|
||||
"description": "CSS height for the iframe. Set to a fixed value (e.g. '300px', '600px') matching the HTML content height. Use 'auto' only when the HTML is responsive and has no fixed height. Default: 'auto'",
|
||||
"description": "[uri=html|url] CSS height. Default: 'auto'",
|
||||
"default": "auto"
|
||||
}
|
||||
},
|
||||
"required": ["title", "html_content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ask_user",
|
||||
"description": "Present questions with selectable options to the user. CRITICAL: Every question MUST have at least 2 options in the options array — generate reasonable suggestions based on context. Do NOT call this tool with empty options arrays. See the ask_user usage guide in system prompt for detailed rules.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"description": "Array of questions to ask the user. Each question is an object with its own question text, options, and multi_select setting.",
|
||||
"description": "[uri=ask-user] Array of questions to ask the user.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -50,22 +49,22 @@
|
||||
"options": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "REQUIRED array with at least 2 options. Put choices here, NOT in the question text."
|
||||
"items": { "type": "string" },
|
||||
"description": "REQUIRED array with at least 2 options."
|
||||
},
|
||||
"multi_select": {
|
||||
"type": "boolean",
|
||||
"description": "If true, the user can select multiple options for this question. Default: false.",
|
||||
"description": "If true, the user can select multiple options. Default: false.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": ["question", "options"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["questions"]
|
||||
"required": ["uri", "data"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP UI Server - provides interactive UI rendering tools.
|
||||
Uses URI-based routing: ui://mcp-ui/[resource-type]
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@ -25,59 +26,49 @@ def _serialize_ui_resource(ui_resource) -> str:
|
||||
return json.dumps(ui_resource.model_dump(mode="json"), ensure_ascii=False)
|
||||
|
||||
|
||||
def ask_user() -> Dict[str, Any]:
|
||||
"""Return a UIResource response for ask_user tool.
|
||||
def _handle_render_ui(uri: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Unified handler for all render_ui URIs.
|
||||
|
||||
The actual questions are in the TOOL_CALL arguments. The frontend
|
||||
detects ui_type from the UIResource metadata and extracts content
|
||||
from the corresponding TOOL_CALL args.
|
||||
Content is self-contained in the TOOL_RESPONSE resource —
|
||||
the frontend only needs TOOL_RESPONSE to render, no TOOL_CALL reference needed.
|
||||
"""
|
||||
width = data.get("width", "100%")
|
||||
height = data.get("height", "auto")
|
||||
|
||||
if uri == "ui://mcp-ui/ask-user":
|
||||
questions = data.get("questions", [])
|
||||
resource = create_ui_resource({
|
||||
"uri": "ui://mcp-ui/ask-user",
|
||||
"content": {"type": "rawHtml", "htmlString": "Questions sent to user."},
|
||||
"uri": uri,
|
||||
"content": {"type": "rawHtml", "htmlString": json.dumps({"questions": questions}, ensure_ascii=False)},
|
||||
"encoding": "text",
|
||||
"uiMetadata": {
|
||||
"type": "ask_user",
|
||||
"interactive": True,
|
||||
},
|
||||
})
|
||||
return {"content": [{"type": "text", "text": _serialize_ui_resource(resource)}]}
|
||||
|
||||
|
||||
def render_ui(arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Return a UIResource response for render_ui tool.
|
||||
|
||||
The actual html_content/url is in the TOOL_CALL arguments. The frontend
|
||||
detects ui_type from the UIResource metadata and extracts content
|
||||
from the corresponding TOOL_CALL args.
|
||||
"""
|
||||
html_content = arguments.get("html_content", "")
|
||||
url = arguments.get("url", "")
|
||||
width = arguments.get("width", "100%")
|
||||
height = arguments.get("height", "auto")
|
||||
|
||||
if html_content:
|
||||
elif uri == "ui://mcp-ui/url":
|
||||
url = data.get("url", "")
|
||||
resource = create_ui_resource({
|
||||
"uri": "ui://mcp-ui/render-ui",
|
||||
"content": {"type": "rawHtml", "htmlString": "UI rendered."},
|
||||
"encoding": "text",
|
||||
"uiMetadata": {
|
||||
UIMetadataKey.PREFERRED_FRAME_SIZE: [width, height],
|
||||
"type": "render_ui_html",
|
||||
"interactive": False,
|
||||
},
|
||||
})
|
||||
else:
|
||||
resource = create_ui_resource({
|
||||
"uri": "ui://mcp-ui/render-ui",
|
||||
"uri": uri,
|
||||
"content": {"type": "externalUrl", "iframeUrl": url},
|
||||
"encoding": "text",
|
||||
"uiMetadata": {
|
||||
UIMetadataKey.PREFERRED_FRAME_SIZE: [width, height],
|
||||
"type": "render_ui_url",
|
||||
"interactive": False,
|
||||
},
|
||||
})
|
||||
else:
|
||||
# ui://mcp-ui/html (default)
|
||||
html_content = data.get("html_content", "")
|
||||
resource = create_ui_resource({
|
||||
"uri": uri,
|
||||
"content": {"type": "rawHtml", "htmlString": html_content},
|
||||
"encoding": "text",
|
||||
"uiMetadata": {
|
||||
UIMetadataKey.PREFERRED_FRAME_SIZE: [width, height],
|
||||
"interactive": False,
|
||||
},
|
||||
})
|
||||
|
||||
return {"content": [{"type": "text", "text": _serialize_ui_resource(resource)}]}
|
||||
|
||||
|
||||
@ -100,24 +91,16 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
tools = [
|
||||
{
|
||||
"name": "render_ui",
|
||||
"description": "Render an interactive UI widget in the chat.",
|
||||
"description": "Render an interactive UI resource in the chat.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A descriptive title for the UI widget",
|
||||
"uri": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"html_content": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
},
|
||||
"html_content": {
|
||||
"type": "string",
|
||||
"description": "Complete HTML content to render. Use this OR url.",
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "External URL to embed. Use this OR html_content.",
|
||||
},
|
||||
},
|
||||
"required": ["title"],
|
||||
"required": ["uri", "title"],
|
||||
},
|
||||
}
|
||||
]
|
||||
@ -128,26 +111,31 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if tool_name == "render_ui":
|
||||
html_content = arguments.get("html_content", "")
|
||||
url = arguments.get("url", "")
|
||||
uri = arguments.get("uri", "")
|
||||
data = arguments.get("data", {})
|
||||
|
||||
if not html_content and not url:
|
||||
if not uri or not uri.startswith("ui://mcp-ui/"):
|
||||
return create_error_response(
|
||||
request_id, -32602, "Missing required parameter: html_content or url"
|
||||
request_id, -32602,
|
||||
"Invalid uri. Must be one of: ui://mcp-ui/html, ui://mcp-ui/url, ui://mcp-ui/ask-user"
|
||||
)
|
||||
|
||||
result = render_ui(arguments)
|
||||
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
||||
path = uri.replace("ui://mcp-ui/", "")
|
||||
|
||||
elif tool_name == "ask_user":
|
||||
questions = arguments.get("questions", [])
|
||||
|
||||
if not questions:
|
||||
if path == "html" and not data.get("html_content"):
|
||||
return create_error_response(
|
||||
request_id, -32602, "Missing required parameter: questions"
|
||||
request_id, -32602, "Missing required parameter: data.html_content (for uri=ui://mcp-ui/html)"
|
||||
)
|
||||
elif path == "url" and not data.get("url"):
|
||||
return create_error_response(
|
||||
request_id, -32602, "Missing required parameter: data.url (for uri=ui://mcp-ui/url)"
|
||||
)
|
||||
elif path == "ask-user" and not data.get("questions"):
|
||||
return create_error_response(
|
||||
request_id, -32602, "Missing required parameter: data.questions (for uri=ui://mcp-ui/ask-user)"
|
||||
)
|
||||
|
||||
result = ask_user()
|
||||
result = _handle_render_ui(uri, data)
|
||||
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
||||
|
||||
else:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user