From f7e90388a10ce93802cfa5f9b954555668b8b086 Mon Sep 17 00:00:00 2001 From: DeepSeek V4 Pro agent Date: Mon, 13 Jul 2026 15:01:43 +0300 Subject: [PATCH] fix: salvage list-typed structuredContent from non-conformant MCP servers Some MCP servers (e.g. memos) return structuredContent as a bare JSON array instead of an object, which the MCP SDK's CallToolResult Pydantic model rejects with a ValidationError (dict_type). This causes tool calls to silently fail with no results returned to the user. Catch pydantic.ValidationError in MCPClient.call_tool(). When the error is specifically about structuredContent being a list instead of a dict, extract the array and return it as MCP text content that the existing process_tool_result() pipeline already handles. Ref: #27021 Co-authored-by: atlarix-agent --- backend/open_webui/utils/mcp/client.py | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/utils/mcp/client.py b/backend/open_webui/utils/mcp/client.py index 39b7ae3b8e..11b07fd70e 100644 --- a/backend/open_webui/utils/mcp/client.py +++ b/backend/open_webui/utils/mcp/client.py @@ -1,8 +1,11 @@ import asyncio +import json import logging from contextlib import AsyncExitStack from typing import Optional +from pydantic import ValidationError + log = logging.getLogger(__name__) import anyio @@ -110,7 +113,29 @@ class MCPClient: if not self.session: raise RuntimeError('MCP client is not connected.') - result = await self.session.call_tool(function_name, function_args) + try: + result = await self.session.call_tool(function_name, function_args) + except ValidationError as e: + # Some MCP servers (e.g. memos) return structuredContent as a + # bare JSON array instead of an object, which the SDK's + # CallToolResult Pydantic model rejects with a dict_type error. + # Extract the list from the error and return it as MCP text + # content so the existing process_tool_result pipeline can handle it. + sc_error = next( + (err for err in e.errors() + if err.get('loc') == ('structuredContent',) and err.get('type') == 'dict_type'), + None + ) + if sc_error is not None: + content_list = sc_error.get('input', []) + if isinstance(content_list, list): + log.debug( + 'MCP server returned structuredContent as list for tool "%s"; salvaging as text content', + function_name, + ) + return [{'type': 'text', 'text': json.dumps(content_list, ensure_ascii=False)}] + raise + if not result: raise Exception('No result returned from MCP tool call.')