feat(mcpserver): let ToolError carry content for is_error results by RaidLZ · Pull Request #2984 · modelcontextprotocol/python-sdk · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/mcp/server/mcpserver/exceptions.py
9 changes: 7 additions & 2 deletions src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from mcp.server.lowlevel.server import LifespanResultT, Server
from mcp.server.lowlevel.server import lifespan as default_lifespan
from mcp.server.mcpserver.context import Context
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError, ToolError
from mcp.server.mcpserver.prompts import Prompt, PromptManager
from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager
from mcp.server.mcpserver.tools import Tool, ToolManager
Expand Down Expand Up @@ -312,7 +312,12 @@ async def _handle_call_tool(
return await self.call_tool(params.name, params.arguments or {}, context)
except MCPError:
raise
except Exception as e:
except ToolError as e:
# Tool execution failures surface as `ToolError` (the tool layer wraps
# any non-`MCPError` exception). Use the content the tool attached, if
# any, otherwise fall back to the error message as text.
if e.content is not None:
return CallToolResult(content=e.content, is_error=True)
return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True)

async def _handle_list_resources(
Expand Down
5 changes: 5 additions & 0 deletions src/mcp/server/mcpserver/tools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,10 @@ async def run(
# it as a top-level JSON-RPC error rather than wrapping it as a
# `CallToolResult(isError=True)` execution failure.
raise
except ToolError as e:
# The tool deliberately signalled an error. Preserve any content it
# attached (e.g. an image) so it survives to the `CallToolResult`,
# while keeping the execution-failure prefix on the message.
raise ToolError(f"Error executing tool {self.name}: {e}", content=e.content) from e
except Exception as e:
raise ToolError(f"Error executing tool {self.name}: {e}") from e
28 changes: 28 additions & 0 deletions tests/server/mcpserver/tools/test_base.py
Loading