Merge branch 'feature/mcp-ui' into bot_manager

This commit is contained in:
朱潮 2026-05-18 20:19:15 +08:00
commit 333bef1289
2 changed files with 55 additions and 31 deletions

View File

@ -144,16 +144,13 @@ async def enhanced_generate_stream_response(
elif isinstance(msg, ToolMessage) and msg.content: elif isinstance(msg, ToolMessage) and msg.content:
message_tag = "TOOL_RESPONSE" message_tag = "TOOL_RESPONSE"
waiting_for_answer_first_char = False 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 = ( is_ui_resource = (
msg.text msg.text
and msg.text.lstrip().startswith('{"') and msg.text.lstrip().startswith('{"')
and '"ui://' in msg.text 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' if config.tool_response or is_ui_resource:
is_render_ui = msg.name == 'render_ui'
if config.tool_response or is_ui_resource or is_ask_user or is_render_ui:
new_content = f"[{message_tag}] {msg.name}\n{msg.text}\n" new_content = f"[{message_tag}] {msg.name}\n{msg.text}\n"
# Collect full content # Collect full content

View File

@ -8,6 +8,8 @@ import json
import sys import sys
from typing import Any, Dict from typing import Any, Dict
from mcp_ui_server import create_ui_resource, UIMetadataKey
from mcp_common import ( from mcp_common import (
create_error_response, create_error_response,
create_initialize_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]: 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, The actual questions are in the TOOL_CALL arguments. The frontend
so the frontend parses them directly from there. This response only detects ui_type from the UIResource metadata and extracts content
serves to acknowledge the tool call and minimize token usage in the from the corresponding TOOL_CALL args.
subsequent LLM inference round.
""" """
return { resource = create_ui_resource({
"content": [ "uri": "ui://mcp-ui/ask-user",
{"type": "text", "text": ASK_USER_RESPONSE} "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.
The actual html_content/url is in the TOOL_CALL arguments. The frontend
def render_ui() -> Dict[str, Any]: detects ui_type from the UIResource metadata and extracts content
"""Return a minimal fixed response for render_ui tool. from the corresponding TOOL_CALL args.
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.
""" """
return { html_content = arguments.get("html_content", "")
"content": [ url = arguments.get("url", "")
{"type": "text", "text": RENDER_UI_RESPONSE} 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]: 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" 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} return {"jsonrpc": "2.0", "id": request_id, "result": result}
elif tool_name == "ask_user": elif tool_name == "ask_user":