"""Unit tests for the MCP Streamable HTTP client.""" import httpx import pytest import respx from ai_evals.clients import mcp _ENDPOINT = "http://localhost:8080/mcp" _TOOL_MANIFEST = [ { "name": "get_artist_fans", "description": "Fetch fan growth for an artist", "inputSchema": { "type": "object", "properties": {"artist_id": {"type": "string"}}, }, } ] def _initialize_response() -> httpx.Response: return httpx.Response( 200, json={"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18"}}, headers={"mcp-session-id": "session-abc"}, ) @respx.mock def test_list_tools_returns_bedrock_shaped_tool_specs() -> None: respx.post(_ENDPOINT).mock( side_effect=[ _initialize_response(), httpx.Response( 200, json={"jsonrpc": "2.0", "result": {}} ), # notifications/initialized httpx.Response( 200, json={"jsonrpc": "2.0", "id": 2, "result": {"tools": _TOOL_MANIFEST}}, ), ] ) with mcp.McpSession(_ENDPOINT, "test-jwt") as session: tools = session.list_tools() assert tools == _TOOL_MANIFEST @respx.mock def test_list_tools_sends_bearer_token_and_session_id() -> None: route = respx.post(_ENDPOINT).mock( side_effect=[ _initialize_response(), httpx.Response(200, json={"jsonrpc": "2.0", "result": {}}), httpx.Response( 200, json={"jsonrpc": "2.0", "id": 2, "result": {"tools": []}} ), ] ) with mcp.McpSession(_ENDPOINT, "test-jwt") as session: session.list_tools() tools_list_request = route.calls[2].request assert tools_list_request.headers["authorization"] == "Bearer test-jwt" assert tools_list_request.headers["mcp-session-id"] == "session-abc" @respx.mock def test_list_tools_parses_sse_response() -> None: sse_body = ( 'event: message\ndata: {"jsonrpc": "2.0", "id": 2, "result": {"tools": []}}\n\n' ) respx.post(_ENDPOINT).mock( side_effect=[ _initialize_response(), httpx.Response(200, json={"jsonrpc": "2.0", "result": {}}), httpx.Response( 200, content=sse_body, headers={"content-type": "text/event-stream"} ), ] ) with mcp.McpSession(_ENDPOINT, "test-jwt") as session: tools = session.list_tools() assert tools == [] @respx.mock def test_list_tools_raises_on_jsonrpc_error() -> None: respx.post(_ENDPOINT).mock( side_effect=[ _initialize_response(), httpx.Response(200, json={"jsonrpc": "2.0", "result": {}}), httpx.Response( 200, json={ "jsonrpc": "2.0", "id": 2, "error": {"code": -32601, "message": "boom"}, }, ), ] ) with mcp.McpSession(_ENDPOINT, "test-jwt") as session: with pytest.raises(mcp.McpError, match="boom"): session.list_tools() @respx.mock def test_call_tool_returns_concatenated_text_content() -> None: respx.post(_ENDPOINT).mock( side_effect=[ _initialize_response(), httpx.Response(200, json={"jsonrpc": "2.0", "result": {}}), httpx.Response( 200, json={ "jsonrpc": "2.0", "id": 2, "result": {"content": [{"type": "text", "text": "14,661 views"}]}, }, ), ] ) with mcp.McpSession(_ENDPOINT, "test-jwt") as session: result = session.call_tool("execute", {"query": "..."}) assert result == "14,661 views" @respx.mock def test_call_tool_reuses_the_same_session_across_multiple_calls() -> None: route = respx.post(_ENDPOINT).mock( side_effect=[ _initialize_response(), httpx.Response(200, json={"jsonrpc": "2.0", "result": {}}), httpx.Response( 200, json={"jsonrpc": "2.0", "id": 2, "result": {"content": []}} ), httpx.Response( 200, json={"jsonrpc": "2.0", "id": 3, "result": {"content": []}} ), ] ) with mcp.McpSession(_ENDPOINT, "test-jwt") as session: session.call_tool("search", {"query": "campaign views"}) session.call_tool("execute", {"query": "..."}) assert route.calls[2].request.headers["mcp-session-id"] == "session-abc" assert route.calls[3].request.headers["mcp-session-id"] == "session-abc" @respx.mock def test_call_tool_marks_isError_result_as_a_returned_error_text() -> None: respx.post(_ENDPOINT).mock( side_effect=[ _initialize_response(), httpx.Response(200, json={"jsonrpc": "2.0", "result": {}}), httpx.Response( 200, json={ "jsonrpc": "2.0", "id": 2, "result": { "isError": True, "content": [{"type": "text", "text": "campaign not found"}], }, }, ), ] ) with mcp.McpSession(_ENDPOINT, "test-jwt") as session: result = session.call_tool("execute", {"query": "..."}) assert "campaign not found" in result @respx.mock def test_call_tool_raises_on_jsonrpc_error() -> None: respx.post(_ENDPOINT).mock( side_effect=[ _initialize_response(), httpx.Response(200, json={"jsonrpc": "2.0", "result": {}}), httpx.Response( 200, json={ "jsonrpc": "2.0", "id": 2, "error": {"code": -32602, "message": "bad input"}, }, ), ] ) with mcp.McpSession(_ENDPOINT, "test-jwt") as session: with pytest.raises(mcp.McpError, match="bad input"): session.call_tool("execute", {"query": "..."})