From add8a1fd180f7667c59805fb32088142ce35e562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Mon, 18 May 2026 20:19:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20mcp=5Fui=5Fserver=20?= =?UTF-8?q?=E5=BA=93=E6=9E=84=E5=BB=BA=20UIResource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/chat.py | 7 +-- skills/common/mcp-ui/ui_render_server.py | 79 ++++++++++++++++-------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/routes/chat.py b/routes/chat.py index bfa0732..61b3682 100644 --- a/routes/chat.py +++ b/routes/chat.py @@ -144,16 +144,13 @@ async def enhanced_generate_stream_response( elif isinstance(msg, ToolMessage) and msg.content: message_tag = "TOOL_RESPONSE" waiting_for_answer_first_char = False - # Always output UIResource, ask_user and render_ui responses even when tool_response is disabled + # Always output UIResource responses even when tool_response is disabled is_ui_resource = ( msg.text and msg.text.lstrip().startswith('{"') and '"ui://' in msg.text - and ('"text/html' in msg.text or '"text/uri-list' in msg.text) ) - is_ask_user = msg.name == 'ask_user' - is_render_ui = msg.name == 'render_ui' - if config.tool_response or is_ui_resource or is_ask_user or is_render_ui: + if config.tool_response or is_ui_resource: new_content = f"[{message_tag}] {msg.name}\n{msg.text}\n" # Collect full content diff --git a/skills/common/mcp-ui/ui_render_server.py b/skills/common/mcp-ui/ui_render_server.py index 0af200a..8af2930 100644 --- a/skills/common/mcp-ui/ui_render_server.py +++ b/skills/common/mcp-ui/ui_render_server.py @@ -8,6 +8,8 @@ import json import sys from typing import Any, Dict +from mcp_ui_server import create_ui_resource, UIMetadataKey + from mcp_common import ( create_error_response, create_initialize_response, @@ -18,40 +20,65 @@ from mcp_common import ( ) -ASK_USER_RESPONSE = "Questions sent to user." +def _serialize_ui_resource(ui_resource) -> str: + """Serialize a UIResource to JSON string.""" + return json.dumps(ui_resource.model_dump(mode="json"), ensure_ascii=False) def ask_user() -> Dict[str, Any]: - """Return a minimal fixed response for ask_user tool. + """Return a UIResource response for ask_user tool. - The actual questions/options are already in the TOOL_CALL arguments, - so the frontend parses them directly from there. This response only - serves to acknowledge the tool call and minimize token usage in the - subsequent LLM inference round. + 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. """ - return { - "content": [ - {"type": "text", "text": ASK_USER_RESPONSE} - ] - } + resource = create_ui_resource({ + "uri": "ui://mcp-ui/ask-user", + "content": {"type": "rawHtml", "htmlString": "Questions sent to user."}, + "encoding": "text", + "uiMetadata": { + "type": "ask_user", + "interactive": True, + }, + }) + return {"content": [{"type": "text", "text": _serialize_ui_resource(resource)}]} -RENDER_UI_RESPONSE = "UI rendered." +def render_ui(arguments: Dict[str, Any]) -> Dict[str, Any]: + """Return a UIResource response for render_ui tool. - -def render_ui() -> Dict[str, Any]: - """Return a minimal fixed response for render_ui tool. - - The actual html_content/url is already in the TOOL_CALL arguments, - so the frontend parses them directly from there. This response only - serves to acknowledge the tool call and minimize token usage in the - subsequent LLM inference round. + 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. """ - return { - "content": [ - {"type": "text", "text": RENDER_UI_RESPONSE} - ] - } + html_content = arguments.get("html_content", "") + url = arguments.get("url", "") + width = arguments.get("width", "100%") + height = arguments.get("height", "auto") + + if html_content: + 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", + "content": {"type": "externalUrl", "iframeUrl": url}, + "encoding": "text", + "uiMetadata": { + UIMetadataKey.PREFERRED_FRAME_SIZE: [width, height], + "type": "render_ui_url", + "interactive": False, + }, + }) + return {"content": [{"type": "text", "text": _serialize_ui_resource(resource)}]} async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]: @@ -109,7 +136,7 @@ async def handle_request(request: Dict[str, Any]) -> Dict[str, Any]: request_id, -32602, "Missing required parameter: html_content or url" ) - result = render_ui() + result = render_ui(arguments) return {"jsonrpc": "2.0", "id": request_id, "result": result} elif tool_name == "ask_user":