From 4e87ee0698570aab9cbde00d11799cca65e26674 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:09:42 +0200 Subject: [PATCH] fix: model misses the actual result of MCP tools that send it as structured content Some MCP servers reply with only a short summary as text and put the actual result in the structured content part of the reply, which the MCP spec allows. Open WebUI kept only the text, so the model saw something like "Found 3 results" without the results themselves. The structured result is now passed to the model too whenever the text does not already contain it, so servers that already repeat it as text are not sent twice. Fixes #28926 --- backend/open_webui/utils/mcp/client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/utils/mcp/client.py b/backend/open_webui/utils/mcp/client.py index 29e67f04fb..b70c59aef2 100644 --- a/backend/open_webui/utils/mcp/client.py +++ b/backend/open_webui/utils/mcp/client.py @@ -1,7 +1,7 @@ import asyncio import logging from contextlib import AsyncExitStack -from typing import Optional +from typing import Any, Optional log = logging.getLogger(__name__) @@ -16,6 +16,7 @@ from open_webui.env import ( AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER, MCP_INITIALIZE_TIMEOUT, ) +from open_webui.utils.json_codec import JSONCodec def _build_httpx_client(headers=None, timeout=None, auth=None, verify=True): @@ -56,6 +57,24 @@ def create_insecure_httpx_client(headers=None, timeout=None, auth=None): return _build_httpx_client(headers=headers, timeout=timeout, auth=auth, verify=False) +def _parse_json_text(text: str) -> Any: + try: + return JSONCodec.loads(text) + except JSONCodec.JSONDecodeError: + return text + + +def _content_includes_structured(content: list, structured_content: dict) -> bool: + texts = [item.get('text') for item in content if item.get('type') == 'text'] + values = [_parse_json_text(text) for text in texts] + payload = structured_content + # The Python SDK wraps non-object returns as {'result': ...} + if structured_content.keys() == {'result'}: + payload = structured_content['result'] + candidates = (texts, values, *texts, *values) + return payload in candidates or structured_content in candidates + + class MCPClient: def __init__(self): self.session: Optional[ClientSession] = None @@ -122,10 +141,13 @@ class MCPClient: result_dict = result.model_dump(mode='json') result_content = result_dict.get('content', {}) + structured_content = result_dict.get('structuredContent') if result.isError: raise Exception(result_content) else: + if structured_content and not _content_includes_structured(result_content, structured_content): + result_content.append({'type': 'text', 'text': JSONCodec.dumps(structured_content)}) return result_content async def list_resources(self, cursor: Optional[str] = None) -> Optional[dict]: