Skip to content

Commit f132b28

Browse files
SachinSachin
authored andcommitted
Allow ToolError to carry arbitrary content for isError=True results
Previously there was no way to return non-text content (e.g. images, embedded resources) with isError=True. A plain ToolError("msg") only produced a text error. Now ToolError accepts an optional `content` keyword argument with a list of ContentBlock items. When present, _handle_call_tool uses that content in the CallToolResult instead of wrapping the message as TextContent. A plain `raise ToolError("message")` still works exactly as before. ToolError is now re-exported from mcp.server.mcpserver for easier imports. Fixes #348
1 parent 57394b0 commit f132b28

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

‎src/mcp/server/mcpserver/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414

1515
from .context import Context
16+
from .exceptions import ToolError
1617
from .prompts.base import AssistantMessage, Message, UserMessage
1718
from .resolve import (
1819
AcceptedElicitation,
@@ -31,6 +32,7 @@
3132
__all__ = [
3233
"MCPServer",
3334
"Context",
35+
"ToolError",
3436
"Image",
3537
"Audio",
3638
"Message",

‎src/mcp/server/mcpserver/exceptions.py‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
"""Custom exceptions for MCPServer."""
22

3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from mcp_types import ContentBlock
9+
310

411
class MCPServerError(Exception):
512
"""Base error for MCPServer."""
@@ -19,7 +26,29 @@ class ResourceNotFoundError(ResourceError):
1926

2027

2128
class ToolError(MCPServerError):
22-
"""Error in tool operations."""
29+
"""Error in tool operations.
30+
31+
Raise this from a tool function to signal an execution error that
32+
the LLM should see (``CallToolResult(is_error=True)``).
33+
34+
A plain ``ToolError("message")`` produces a text-only error result.
35+
To return arbitrary content (images, embedded resources, etc.)
36+
alongside the error flag, pass a ``content`` list::
37+
38+
raise ToolError(
39+
"screenshot shows the failure",
40+
content=[
41+
TextContent(type="text", text="see attached"),
42+
ImageContent(type="image", data=b64, mime_type="image/png"),
43+
],
44+
)
45+
"""
46+
47+
content: list[ContentBlock] | None
48+
49+
def __init__(self, message: str = "", *, content: list[ContentBlock] | None = None) -> None:
50+
super().__init__(message)
51+
self.content = content
2352

2453

2554
class InvalidSignature(Exception):

‎src/mcp/server/mcpserver/server.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
from mcp.server.lowlevel.server import LifespanResultT, Server
7272
from mcp.server.lowlevel.server import lifespan as default_lifespan
7373
from mcp.server.mcpserver.context import Context
74-
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError
74+
from mcp.server.mcpserver.exceptions import ResourceError, ResourceNotFoundError, ToolError
7575
from mcp.server.mcpserver.prompts import Prompt, PromptManager
7676
from mcp.server.mcpserver.resources import (
7777
DEFAULT_RESOURCE_SECURITY,
@@ -421,6 +421,11 @@ async def _handle_call_tool(
421421
return await self.call_tool(params.name, params.arguments or {}, context)
422422
except MCPError:
423423
raise
424+
except ToolError as e:
425+
logger.exception(f"Error calling tool {params.name}")
426+
if e.content is not None:
427+
return CallToolResult(content=e.content, is_error=True)
428+
return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True)
424429
except Exception as e:
425430
return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True)
426431

0 commit comments

Comments
 (0)