Skip to content

Commit 9086244

Browse files
committed
capture errors correctly
1 parent 6578c04 commit 9086244

File tree

3 files changed

+82
-4
lines changed

3 files changed

+82
-4
lines changed

src/mcpcat/modules/compatibility.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from typing import Any, Protocol, runtime_checkable
44

5+
from mcp import ServerResult
6+
57

68
@runtime_checkable
79
class MCPServerProtocol(Protocol):
@@ -68,3 +70,36 @@ def get_mcp_compatible_error_message(error: Any) -> str:
6870
if isinstance(error, Exception):
6971
return str(error)
7072
return str(error)
73+
74+
def is_mcp_error_response(response: ServerResult) -> tuple[bool, str]:
75+
"""Check if the response is an MCP error."""
76+
try:
77+
# ServerResult is a RootModel, so we need to access its root attribute
78+
if hasattr(response, 'root'):
79+
result = response.root
80+
# Check if it's a CallToolResult with an error
81+
if hasattr(result, 'isError') and result.isError:
82+
# Extract error message from content
83+
if hasattr(result, 'content') and result.content:
84+
# content is a list of TextContent/ImageContent/EmbeddedResource
85+
for content_item in result.content:
86+
# Check if it has a text attribute (TextContent)
87+
if hasattr(content_item, 'text'):
88+
return True, str(content_item.text)
89+
# Check if it has type and content attributes
90+
elif hasattr(content_item, 'type') and hasattr(content_item, 'content'):
91+
if content_item.type == 'text':
92+
return True, str(content_item.content)
93+
94+
# If no text content found, stringify the first item
95+
if result.content and len(result.content) > 0:
96+
return True, str(result.content[0])
97+
return True, "Unknown error"
98+
return True, "Unknown error"
99+
return False, ""
100+
except (AttributeError, IndexError):
101+
# Handle specific exceptions more precisely
102+
return False, ""
103+
except Exception as e:
104+
# Log unexpected errors but still return a valid response
105+
return False, f"Error checking response: {str(e)}"

src/mcpcat/modules/overrides/mcp_server.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from mcp.shared.context import RequestContext
88

99
from mcpcat.modules import event_queue
10+
from mcpcat.modules.compatibility import is_mcp_error_response
1011
from mcpcat.modules.identify import identify_session
1112
from mcpcat.modules.logging import write_to_log
1213
from mcpcat.modules.tools import handle_report_missing
@@ -68,7 +69,7 @@ async def wrapped_list_tools_handler(request: ListToolsRequest) -> ServerResult:
6869
parameters=request.params.model_dump() if request.params else {},
6970
event_type=EventType.MCP_TOOLS_LIST.value,
7071
)
71-
72+
7273
# Call the original handler to get the tools
7374
original_result = await original_list_tools_handler(request)
7475
if not original_result or not hasattr(original_result, 'root') or not hasattr(original_result.root, 'tools'):
@@ -168,7 +169,9 @@ async def wrapped_call_tool_handler(request: CallToolRequest) -> ServerResult:
168169
try:
169170
# Call the original handler
170171
result = await original_call_tool_handler(request)
171-
172+
is_error, error_message = is_mcp_error_response(result)
173+
event.is_error = is_error
174+
event.error = {"message": error_message} if is_error else None
172175
# Record the trace using existing infrastructure
173176
event.response = result.model_dump() if result else None
174177
event_queue.publish_event(server, event)
@@ -187,4 +190,3 @@ async def wrapped_call_tool_handler(request: CallToolRequest) -> ServerResult:
187190
server.request_handlers[CallToolRequest] = wrapped_call_tool_handler
188191
server.request_handlers[ListToolsRequest] = wrapped_list_tools_handler
189192
server.request_handlers[InitializeRequest] = wrapped_initialize_handler
190-

tests/test_event_capture_completeness.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,4 +476,45 @@ def custom_identify(request, server):
476476
# Should have actor info from custom identify
477477
assert event.identify_actor_given_id == "custom123"
478478
assert event.identify_actor_name == "Custom User"
479-
assert event.identify_data == {"source": "custom_identify"}
479+
assert event.identify_data == {"source": "custom_identify"}
480+
481+
@pytest.mark.asyncio
482+
async def test_server_error_capture_in_event(self):
483+
"""Test that ServerResult errors are captured in the event's is_error and error fields."""
484+
mock_api_client = MagicMock()
485+
captured_events = []
486+
487+
def capture_event(publish_event_request):
488+
captured_events.append(publish_event_request)
489+
490+
mock_api_client.publish_event = MagicMock(side_effect=capture_event)
491+
492+
test_queue = EventQueue(api_client=mock_api_client)
493+
set_event_queue(test_queue)
494+
495+
server = create_todo_server()
496+
options = MCPCatOptions(enable_tracing=True)
497+
track(server, "test_project", options)
498+
499+
async with create_test_client(server) as client:
500+
# Try to complete a non-existent todo to trigger an error
501+
try:
502+
await client.call_tool("complete_todo", {"id": 999})
503+
except Exception:
504+
# The client might raise an exception, but we're interested in the event
505+
pass
506+
507+
time.sleep(1.0)
508+
509+
# Find the tool call event for complete_todo
510+
tool_events = [e for e in captured_events if e.event_type == "mcp:tools/call" and e.resource_name == "complete_todo"]
511+
assert len(tool_events) > 0, "No complete_todo tool call event captured"
512+
513+
event = tool_events[0]
514+
515+
# Verify error fields are populated
516+
assert event.is_error is True, "Event should be marked as error"
517+
assert event.error is not None, "Event should have error details"
518+
assert isinstance(event.error, dict), "Error should be a dictionary"
519+
assert "message" in event.error, "Error should have a message"
520+
assert "Todo with ID 999 not found" in event.error["message"], "Error message should contain the ValueError message"

0 commit comments

Comments
 (0)