@@ -928,3 +928,152 @@ async def test_handle_error_message_with_percent_in_message():
928928
929929 # This should not raise TypeError and should not raise the exception (since it's non-fatal)
930930 await client ._handle_error_message (error_with_percent )
931+
932+
933+ def test_call_tool_sync_elicitation_error (mock_transport , mock_session ):
934+ """Test that call_tool_sync correctly handles elicitation required errors."""
935+ from mcp .shared .exceptions import McpError
936+ from mcp .types import ElicitationRequiredErrorData , ElicitRequestURLParams
937+
938+ elicitation_data = ElicitationRequiredErrorData (
939+ elicitations = [
940+ ElicitRequestURLParams (
941+ url = "https://example.com/auth" , message = "Please authorize the application" , elicitationId = "elicit-123"
942+ )
943+ ]
944+ )
945+
946+ error = McpError (error = MagicMock (code = - 32042 , data = elicitation_data .model_dump ()))
947+ mock_session .call_tool .side_effect = error
948+
949+ with MCPClient (mock_transport ["transport_callable" ]) as client :
950+ result = client .call_tool_sync (tool_use_id = "test-123" , name = "test_tool" , arguments = {"param" : "value" })
951+
952+ assert result ["status" ] == "error"
953+ assert result ["toolUseId" ] == "test-123"
954+ assert len (result ["content" ]) == 1
955+ assert "MCP Elicitation required" in result ["content" ][0 ]["text" ]
956+ assert "https://example.com/auth" in result ["content" ][0 ]["text" ]
957+ assert "Please authorize the application" in result ["content" ][0 ]["text" ]
958+ assert "elicit-123" in result ["content" ][0 ]["text" ]
959+
960+
961+ def test_call_tool_sync_elicitation_error_multiple_urls (mock_transport , mock_session ):
962+ """Test that call_tool_sync correctly handles elicitation errors with multiple elicitations."""
963+ from mcp .shared .exceptions import McpError
964+ from mcp .types import ElicitationRequiredErrorData , ElicitRequestURLParams
965+
966+ elicitation_data = ElicitationRequiredErrorData (
967+ elicitations = [
968+ ElicitRequestURLParams (
969+ url = "https://example.com/auth1" , message = "First authorization" , elicitationId = "elicit-1"
970+ ),
971+ ElicitRequestURLParams (
972+ url = "https://example.com/auth2" , message = "Second authorization" , elicitationId = "elicit-2"
973+ ),
974+ ]
975+ )
976+
977+ error = McpError (error = MagicMock (code = - 32042 , data = elicitation_data .model_dump ()))
978+ mock_session .call_tool .side_effect = error
979+
980+ with MCPClient (mock_transport ["transport_callable" ]) as client :
981+ result = client .call_tool_sync (tool_use_id = "test-123" , name = "test_tool" , arguments = {"param" : "value" })
982+
983+ assert result ["status" ] == "error"
984+ assert result ["toolUseId" ] == "test-123"
985+ assert len (result ["content" ]) == 1
986+ assert "MCP Elicitation required" in result ["content" ][0 ]["text" ]
987+ assert "https://example.com/auth1" in result ["content" ][0 ]["text" ]
988+ assert "https://example.com/auth2" in result ["content" ][0 ]["text" ]
989+ assert "First authorization" in result ["content" ][0 ]["text" ]
990+ assert "Second authorization" in result ["content" ][0 ]["text" ]
991+ assert "elicit-1" in result ["content" ][0 ]["text" ]
992+ assert "elicit-2" in result ["content" ][0 ]["text" ]
993+
994+
995+ def test_call_tool_sync_elicitation_error_no_urls (mock_transport , mock_session ):
996+ """Test that -32042 error with empty URL still returns generic elicitation result."""
997+ from mcp .shared .exceptions import McpError
998+ from mcp .types import ElicitationRequiredErrorData , ElicitRequestURLParams
999+
1000+ elicitation_data = ElicitationRequiredErrorData (
1001+ elicitations = [ElicitRequestURLParams (url = "" , message = "No URL provided" , elicitationId = "elicit-1" )]
1002+ )
1003+ error = McpError (error = MagicMock (code = - 32042 , data = elicitation_data .model_dump ()))
1004+ mock_session .call_tool .side_effect = error
1005+
1006+ with MCPClient (mock_transport ["transport_callable" ]) as client :
1007+ result = client .call_tool_sync (tool_use_id = "test-123" , name = "test_tool" , arguments = {})
1008+ assert result ["status" ] == "error"
1009+ assert "MCP Elicitation required" in result ["content" ][0 ]["text" ]
1010+ assert "elicit-1" in result ["content" ][0 ]["text" ]
1011+ assert "No URL provided" in result ["content" ][0 ]["text" ]
1012+
1013+
1014+ def test_call_tool_sync_other_mcp_error_code (mock_transport , mock_session ):
1015+ """Test that non-32042 McpError falls through to generic error."""
1016+ from mcp .shared .exceptions import McpError
1017+
1018+ error = McpError (error = MagicMock (code = - 32600 , message = "Invalid request" ))
1019+ mock_session .call_tool .side_effect = error
1020+
1021+ with MCPClient (mock_transport ["transport_callable" ]) as client :
1022+ result = client .call_tool_sync (tool_use_id = "test-123" , name = "test_tool" , arguments = {})
1023+ assert result ["status" ] == "error"
1024+ assert "Tool execution failed" in result ["content" ][0 ]["text" ]
1025+
1026+
1027+ def test_call_tool_sync_elicitation_error_malformed_data (mock_transport , mock_session ):
1028+ """Test that -32042 with unparseable data falls through to generic error."""
1029+ from mcp .shared .exceptions import McpError
1030+
1031+ error = McpError (error = MagicMock (code = - 32042 , data = {"garbage" : True }))
1032+ mock_session .call_tool .side_effect = error
1033+
1034+ with MCPClient (mock_transport ["transport_callable" ]) as client :
1035+ result = client .call_tool_sync (tool_use_id = "test-123" , name = "test_tool" , arguments = {})
1036+ assert result ["status" ] == "error"
1037+ assert "Tool execution failed" in result ["content" ][0 ]["text" ]
1038+
1039+
1040+ @pytest .mark .asyncio
1041+ async def test_call_tool_async_elicitation_error (mock_transport , mock_session ):
1042+ """Test that call_tool_async correctly handles elicitation required errors."""
1043+ from mcp .shared .exceptions import McpError
1044+ from mcp .types import ElicitationRequiredErrorData , ElicitRequestURLParams
1045+
1046+ elicitation_data = ElicitationRequiredErrorData (
1047+ elicitations = [
1048+ ElicitRequestURLParams (
1049+ url = "https://example.com/auth" , message = "Please authorize the application" , elicitationId = "elicit-123"
1050+ )
1051+ ]
1052+ )
1053+
1054+ error = McpError (error = MagicMock (code = - 32042 , data = elicitation_data .model_dump ()))
1055+
1056+ with MCPClient (mock_transport ["transport_callable" ]) as client :
1057+ with (
1058+ patch ("asyncio.run_coroutine_threadsafe" ) as mock_run_coroutine_threadsafe ,
1059+ patch ("asyncio.wrap_future" ) as mock_wrap_future ,
1060+ ):
1061+ mock_future = MagicMock ()
1062+ mock_run_coroutine_threadsafe .return_value = mock_future
1063+
1064+ async def mock_awaitable ():
1065+ raise error
1066+
1067+ mock_wrap_future .return_value = mock_awaitable ()
1068+
1069+ result = await client .call_tool_async (
1070+ tool_use_id = "test-123" , name = "test_tool" , arguments = {"param" : "value" }
1071+ )
1072+
1073+ assert result ["status" ] == "error"
1074+ assert result ["toolUseId" ] == "test-123"
1075+ assert len (result ["content" ]) == 1
1076+ assert "MCP Elicitation required" in result ["content" ][0 ]["text" ]
1077+ assert "https://example.com/auth" in result ["content" ][0 ]["text" ]
1078+ assert "Please authorize the application" in result ["content" ][0 ]["text" ]
1079+ assert "elicit-123" in result ["content" ][0 ]["text" ]
0 commit comments