|
7 | 7 |
|
8 | 8 | import pytest |
9 | 9 | from langchain_core.tools import BaseTool |
10 | | -from mcp.types import ListToolsResult, Tool |
| 10 | +from mcp.shared.exceptions import McpError |
| 11 | +from mcp.types import ErrorData, ListToolsResult, Tool |
11 | 12 | from uipath.agent.models.agent import ( |
12 | 13 | AgentMcpResourceConfig, |
13 | 14 | AgentMcpTool, |
|
16 | 17 | DynamicToolsConfig, |
17 | 18 | ToolsConfiguration, |
18 | 19 | ) |
| 20 | +from uipath.runtime.errors import UiPathErrorCategory |
19 | 21 |
|
| 22 | +from uipath_langchain.agent.exceptions import ( |
| 23 | + AgentRuntimeError, |
| 24 | + AgentRuntimeErrorCode, |
| 25 | +) |
20 | 26 | from uipath_langchain.agent.tools.mcp import McpClient |
21 | 27 | from uipath_langchain.agent.tools.mcp.mcp_tool import ( |
22 | 28 | _schema_change_message, |
@@ -669,6 +675,68 @@ async def test_plain_value_returned_as_is(self, mcp_tool): |
669 | 675 | assert result == "plain string" |
670 | 676 |
|
671 | 677 |
|
| 678 | +class TestMcpToolErrorHandling: |
| 679 | + """Test that protocol-level McpErrors are mapped to categorized AgentRuntimeErrors.""" |
| 680 | + |
| 681 | + @pytest.fixture |
| 682 | + def mcp_tool(self): |
| 683 | + return AgentMcpTool( |
| 684 | + name="test_tool", |
| 685 | + description="Test tool", |
| 686 | + input_schema={"type": "object", "properties": {}}, |
| 687 | + ) |
| 688 | + |
| 689 | + def _mock_client(self, error: McpError) -> MagicMock: |
| 690 | + client = MagicMock(spec=McpClient) |
| 691 | + client.server_slug = "my-mcp-server" |
| 692 | + client.call_tool = AsyncMock(side_effect=error) |
| 693 | + return client |
| 694 | + |
| 695 | + @pytest.mark.asyncio |
| 696 | + async def test_session_terminated_raises_system_error_with_retry_hint( |
| 697 | + self, mcp_tool |
| 698 | + ): |
| 699 | + error = McpError(ErrorData(code=32600, message="Session terminated")) |
| 700 | + client = self._mock_client(error) |
| 701 | + |
| 702 | + tool_fn = build_mcp_tool(mcp_tool, client) |
| 703 | + |
| 704 | + with pytest.raises(AgentRuntimeError) as exc_info: |
| 705 | + await tool_fn() |
| 706 | + assert exc_info.value.error_info.category == UiPathErrorCategory.SYSTEM |
| 707 | + assert exc_info.value.error_info.code == AgentRuntimeError.full_code( |
| 708 | + AgentRuntimeErrorCode.HTTP_ERROR |
| 709 | + ) |
| 710 | + assert "my-mcp-server" in exc_info.value.error_info.detail |
| 711 | + assert "terminated" in exc_info.value.error_info.detail |
| 712 | + assert "retry" in exc_info.value.error_info.detail |
| 713 | + assert exc_info.value.__cause__ is error |
| 714 | + |
| 715 | + @pytest.mark.asyncio |
| 716 | + async def test_non_session_mcp_error_includes_server_message(self, mcp_tool): |
| 717 | + error = McpError(ErrorData(code=-32601, message="Method not found")) |
| 718 | + client = self._mock_client(error) |
| 719 | + |
| 720 | + tool_fn = build_mcp_tool(mcp_tool, client) |
| 721 | + |
| 722 | + with pytest.raises(AgentRuntimeError) as exc_info: |
| 723 | + await tool_fn() |
| 724 | + assert exc_info.value.error_info.category == UiPathErrorCategory.SYSTEM |
| 725 | + assert "Method not found" in exc_info.value.error_info.detail |
| 726 | + assert "test_tool" in exc_info.value.error_info.detail |
| 727 | + |
| 728 | + @pytest.mark.asyncio |
| 729 | + async def test_non_mcp_error_propagates_unchanged(self, mcp_tool): |
| 730 | + client = MagicMock(spec=McpClient) |
| 731 | + client.server_slug = "my-mcp-server" |
| 732 | + client.call_tool = AsyncMock(side_effect=RuntimeError("boom")) |
| 733 | + |
| 734 | + tool_fn = build_mcp_tool(mcp_tool, client) |
| 735 | + |
| 736 | + with pytest.raises(RuntimeError, match="boom"): |
| 737 | + await tool_fn() |
| 738 | + |
| 739 | + |
672 | 740 | class TestMcpToolNameSanitization: |
673 | 741 | """Test that MCP tool names are properly sanitized.""" |
674 | 742 |
|
|
0 commit comments