|
6 | 6 | from datetime import datetime |
7 | 7 | import logging |
8 | 8 | import time |
9 | | -from typing import Any |
| 9 | +from typing import Any, NamedTuple |
10 | 10 |
|
11 | 11 | from typing_extensions import Self |
12 | 12 |
|
|
34 | 34 | from crewai.utilities.string_utils import sanitize_tool_name |
35 | 35 |
|
36 | 36 |
|
| 37 | +class _MCPToolResult(NamedTuple): |
| 38 | + """Internal result from an MCP tool call, carrying the ``isError`` flag.""" |
| 39 | + |
| 40 | + content: str |
| 41 | + is_error: bool |
| 42 | + |
| 43 | + |
37 | 44 | # MCP Connection timeout constants (in seconds) |
38 | 45 | MCP_CONNECTION_TIMEOUT = 30 # Increased for slow servers |
39 | 46 | MCP_TOOL_EXECUTION_TIMEOUT = 30 |
@@ -420,6 +427,7 @@ async def _list_tools_impl(self) -> list[dict[str, Any]]: |
420 | 427 | return [ |
421 | 428 | { |
422 | 429 | "name": sanitize_tool_name(tool.name), |
| 430 | + "original_name": tool.name, |
423 | 431 | "description": getattr(tool, "description", ""), |
424 | 432 | "inputSchema": getattr(tool, "inputSchema", {}), |
425 | 433 | } |
@@ -461,29 +469,46 @@ async def call_tool( |
461 | 469 | ) |
462 | 470 |
|
463 | 471 | try: |
464 | | - result = await self._retry_operation( |
| 472 | + tool_result: _MCPToolResult = await self._retry_operation( |
465 | 473 | lambda: self._call_tool_impl(tool_name, cleaned_arguments), |
466 | 474 | timeout=self.execution_timeout, |
467 | 475 | ) |
468 | 476 |
|
469 | | - completed_at = datetime.now() |
470 | | - execution_duration_ms = (completed_at - started_at).total_seconds() * 1000 |
471 | | - crewai_event_bus.emit( |
472 | | - self, |
473 | | - MCPToolExecutionCompletedEvent( |
474 | | - server_name=server_name, |
475 | | - server_url=server_url, |
476 | | - transport_type=transport_type, |
477 | | - tool_name=tool_name, |
478 | | - tool_args=cleaned_arguments, |
479 | | - result=result, |
480 | | - started_at=started_at, |
481 | | - completed_at=completed_at, |
482 | | - execution_duration_ms=execution_duration_ms, |
483 | | - ), |
484 | | - ) |
| 477 | + finished_at = datetime.now() |
| 478 | + execution_duration_ms = (finished_at - started_at).total_seconds() * 1000 |
| 479 | + |
| 480 | + if tool_result.is_error: |
| 481 | + crewai_event_bus.emit( |
| 482 | + self, |
| 483 | + MCPToolExecutionFailedEvent( |
| 484 | + server_name=server_name, |
| 485 | + server_url=server_url, |
| 486 | + transport_type=transport_type, |
| 487 | + tool_name=tool_name, |
| 488 | + tool_args=cleaned_arguments, |
| 489 | + error=tool_result.content, |
| 490 | + error_type="tool_error", |
| 491 | + started_at=started_at, |
| 492 | + failed_at=finished_at, |
| 493 | + ), |
| 494 | + ) |
| 495 | + else: |
| 496 | + crewai_event_bus.emit( |
| 497 | + self, |
| 498 | + MCPToolExecutionCompletedEvent( |
| 499 | + server_name=server_name, |
| 500 | + server_url=server_url, |
| 501 | + transport_type=transport_type, |
| 502 | + tool_name=tool_name, |
| 503 | + tool_args=cleaned_arguments, |
| 504 | + result=tool_result.content, |
| 505 | + started_at=started_at, |
| 506 | + completed_at=finished_at, |
| 507 | + execution_duration_ms=execution_duration_ms, |
| 508 | + ), |
| 509 | + ) |
485 | 510 |
|
486 | | - return result |
| 511 | + return tool_result.content |
487 | 512 | except Exception as e: |
488 | 513 | failed_at = datetime.now() |
489 | 514 | error_type = ( |
@@ -564,23 +589,27 @@ def _clean_tool_arguments(self, arguments: dict[str, Any]) -> dict[str, Any]: |
564 | 589 |
|
565 | 590 | return cleaned |
566 | 591 |
|
567 | | - async def _call_tool_impl(self, tool_name: str, arguments: dict[str, Any]) -> Any: |
| 592 | + async def _call_tool_impl( |
| 593 | + self, tool_name: str, arguments: dict[str, Any] |
| 594 | + ) -> _MCPToolResult: |
568 | 595 | """Internal implementation of call_tool.""" |
569 | 596 | result = await asyncio.wait_for( |
570 | 597 | self.session.call_tool(tool_name, arguments), |
571 | 598 | timeout=self.execution_timeout, |
572 | 599 | ) |
573 | 600 |
|
| 601 | + is_error = getattr(result, "isError", False) or False |
| 602 | + |
574 | 603 | # Extract result content |
575 | 604 | if hasattr(result, "content") and result.content: |
576 | 605 | if isinstance(result.content, list) and len(result.content) > 0: |
577 | 606 | content_item = result.content[0] |
578 | 607 | if hasattr(content_item, "text"): |
579 | | - return str(content_item.text) |
580 | | - return str(content_item) |
581 | | - return str(result.content) |
| 608 | + return _MCPToolResult(str(content_item.text), is_error) |
| 609 | + return _MCPToolResult(str(content_item), is_error) |
| 610 | + return _MCPToolResult(str(result.content), is_error) |
582 | 611 |
|
583 | | - return str(result) |
| 612 | + return _MCPToolResult(str(result), is_error) |
584 | 613 |
|
585 | 614 | async def list_prompts(self) -> list[dict[str, Any]]: |
586 | 615 | """List available prompts from MCP server. |
|
0 commit comments