fix(backend/mcp): Set loop_scope=session on all async MCP tests

Matches the pattern used by oauth_test.py to prevent event loop
conflicts with session-scoped fixtures (server, graph_cleanup).
This commit is contained in:
Zamil Majdy
2026-02-10 22:02:48 +04:00
parent 11fbb51a70
commit ff48f4335b
4 changed files with 49 additions and 49 deletions

View File

@@ -27,7 +27,7 @@ pytestmark = pytest.mark.skipif(
class TestRealMCPServer:
"""Tests against the live OpenAI docs MCP server."""
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_initialize(self):
"""Verify we can complete the MCP handshake with a real server."""
client = MCPClient(OPENAI_DOCS_MCP_URL)
@@ -38,7 +38,7 @@ class TestRealMCPServer:
assert result["serverInfo"]["name"] == "openai-docs-mcp"
assert "tools" in result.get("capabilities", {})
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_list_tools(self):
"""Verify we can discover tools from a real MCP server."""
client = MCPClient(OPENAI_DOCS_MCP_URL)
@@ -58,7 +58,7 @@ class TestRealMCPServer:
assert "query" in search_tool.input_schema.get("properties", {})
assert "query" in search_tool.input_schema.get("required", [])
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_tool_list_api_endpoints(self):
"""Call the list_api_endpoints tool and verify we get real data."""
client = MCPClient(OPENAI_DOCS_MCP_URL)
@@ -75,7 +75,7 @@ class TestRealMCPServer:
total = data.get("total", len(data.get("paths", [])))
assert total > 50
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_tool_search(self):
"""Search for docs and verify we get results."""
client = MCPClient(OPENAI_DOCS_MCP_URL)
@@ -87,7 +87,7 @@ class TestRealMCPServer:
assert not result.is_error
assert len(result.content) >= 1
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_sse_response_handling(self):
"""Verify the client correctly handles SSE responses from a real server.

View File

@@ -121,7 +121,7 @@ def _make_client(url: str, auth_token: str | None = None) -> MCPClient:
class TestMCPClientIntegration:
"""Test MCPClient against a real local MCP server."""
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_initialize(self, mcp_server):
client = _make_client(mcp_server)
result = await client.initialize()
@@ -130,7 +130,7 @@ class TestMCPClientIntegration:
assert result["serverInfo"]["name"] == "test-mcp-server"
assert "tools" in result["capabilities"]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_list_tools(self, mcp_server):
client = _make_client(mcp_server)
await client.initialize()
@@ -152,7 +152,7 @@ class TestMCPClientIntegration:
assert "a" in add.input_schema["properties"]
assert "b" in add.input_schema["properties"]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_tool_get_weather(self, mcp_server):
client = _make_client(mcp_server)
await client.initialize()
@@ -167,7 +167,7 @@ class TestMCPClientIntegration:
assert data["temperature"] == 22
assert data["condition"] == "sunny"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_tool_add_numbers(self, mcp_server):
client = _make_client(mcp_server)
await client.initialize()
@@ -177,7 +177,7 @@ class TestMCPClientIntegration:
data = json.loads(result.content[0]["text"])
assert data["result"] == 10
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_tool_echo(self, mcp_server):
client = _make_client(mcp_server)
await client.initialize()
@@ -186,7 +186,7 @@ class TestMCPClientIntegration:
assert not result.is_error
assert result.content[0]["text"] == "Hello MCP!"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_unknown_tool(self, mcp_server):
client = _make_client(mcp_server)
await client.initialize()
@@ -195,7 +195,7 @@ class TestMCPClientIntegration:
assert result.is_error
assert "Unknown tool" in result.content[0]["text"]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_auth_success(self, mcp_server_with_auth):
url, token = mcp_server_with_auth
client = _make_client(url, auth_token=token)
@@ -206,7 +206,7 @@ class TestMCPClientIntegration:
tools = await client.list_tools()
assert len(tools) == 3
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_auth_failure(self, mcp_server_with_auth):
url, _ = mcp_server_with_auth
client = _make_client(url, auth_token="wrong-token")
@@ -214,7 +214,7 @@ class TestMCPClientIntegration:
with pytest.raises(Exception):
await client.initialize()
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_auth_missing(self, mcp_server_with_auth):
url, _ = mcp_server_with_auth
client = _make_client(url)
@@ -229,7 +229,7 @@ class TestMCPClientIntegration:
class TestMCPToolBlockIntegration:
"""Test MCPToolBlock end-to-end against a real local MCP server."""
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_full_flow_get_weather(self, mcp_server):
"""Full flow: discover tools, select one, execute it."""
# Step 1: Discover tools (simulating what the frontend/API would do)
@@ -261,7 +261,7 @@ class TestMCPToolBlockIntegration:
assert result["temperature"] == 22
assert result["condition"] == "sunny"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_full_flow_add_numbers(self, mcp_server):
"""Full flow for add_numbers tool."""
client = _make_client(mcp_server)
@@ -285,7 +285,7 @@ class TestMCPToolBlockIntegration:
assert outputs[0][0] == "result"
assert outputs[0][1]["result"] == 100
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_full_flow_echo_plain_text(self, mcp_server):
"""Verify plain text (non-JSON) responses work."""
block = MCPToolBlock()
@@ -308,7 +308,7 @@ class TestMCPToolBlockIntegration:
assert outputs[0][0] == "result"
assert outputs[0][1] == "Hello from AutoGPT!"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_full_flow_unknown_tool_yields_error(self, mcp_server):
"""Calling an unknown tool should yield an error output."""
block = MCPToolBlock()
@@ -326,7 +326,7 @@ class TestMCPToolBlockIntegration:
assert outputs[0][0] == "error"
assert "returned an error" in outputs[0][1]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_full_flow_with_auth(self, mcp_server_with_auth):
"""Full flow with authentication via credentials kwarg."""
url, token = mcp_server_with_auth
@@ -363,7 +363,7 @@ class TestMCPToolBlockIntegration:
assert outputs[0][0] == "result"
assert outputs[0][1] == "Authenticated!"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_no_credentials_runs_without_auth(self, mcp_server):
"""Block runs without auth when no credentials are provided."""
block = MCPToolBlock()

View File

@@ -123,7 +123,7 @@ class TestMCPClient:
client = MCPClient("https://mcp.example.com/mcp/")
assert client.server_url == "https://mcp.example.com/mcp"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_send_request_success(self):
client = MCPClient("https://mcp.example.com")
@@ -138,7 +138,7 @@ class TestMCPClient:
result = await client._send_request("tools/list")
assert result == {"tools": []}
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_send_request_error(self):
client = MCPClient("https://mcp.example.com")
@@ -149,7 +149,7 @@ class TestMCPClient:
with pytest.raises(MCPClientError, match="Invalid Request"):
await client._send_request("tools/list")
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_list_tools(self):
client = MCPClient("https://mcp.example.com")
@@ -185,7 +185,7 @@ class TestMCPClient:
assert tools[0].input_schema["properties"]["city"]["type"] == "string"
assert tools[1].name == "search"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_list_tools_empty(self):
client = MCPClient("https://mcp.example.com")
@@ -194,7 +194,7 @@ class TestMCPClient:
assert tools == []
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_list_tools_none_result(self):
client = MCPClient("https://mcp.example.com")
@@ -203,7 +203,7 @@ class TestMCPClient:
assert tools == []
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_tool_success(self):
client = MCPClient("https://mcp.example.com")
@@ -221,7 +221,7 @@ class TestMCPClient:
assert len(result.content) == 1
assert result.content[0]["type"] == "text"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_tool_error(self):
client = MCPClient("https://mcp.example.com")
@@ -235,7 +235,7 @@ class TestMCPClient:
assert result.is_error
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_tool_none_result(self):
client = MCPClient("https://mcp.example.com")
@@ -244,7 +244,7 @@ class TestMCPClient:
assert result.is_error
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_initialize(self):
client = MCPClient("https://mcp.example.com")
@@ -340,13 +340,13 @@ class TestMCPToolBlock:
missing = MCPToolBlock.Input.get_missing_input(data)
assert missing == set()
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_run_with_mock(self):
"""Test the block using the built-in test infrastructure."""
block = MCPToolBlock()
await execute_block_test(block)
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_run_missing_server_url(self):
block = MCPToolBlock()
input_data = MCPToolBlock.Input(
@@ -358,7 +358,7 @@ class TestMCPToolBlock:
outputs.append((name, data))
assert outputs == [("error", "MCP server URL is required")]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_run_missing_tool(self):
block = MCPToolBlock()
input_data = MCPToolBlock.Input(
@@ -372,7 +372,7 @@ class TestMCPToolBlock:
("error", "No tool selected. Please select a tool from the dropdown.")
]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_run_success(self):
block = MCPToolBlock()
input_data = MCPToolBlock.Input(
@@ -398,7 +398,7 @@ class TestMCPToolBlock:
assert outputs[0][0] == "result"
assert outputs[0][1] == {"temp": 20, "city": "London"}
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_run_mcp_error(self):
block = MCPToolBlock()
input_data = MCPToolBlock.Input(
@@ -418,7 +418,7 @@ class TestMCPToolBlock:
assert outputs[0][0] == "error"
assert "Tool not found" in outputs[0][1]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_mcp_tool_parses_json_text(self):
block = MCPToolBlock()
@@ -445,7 +445,7 @@ class TestMCPToolBlock:
assert result == {"temp": 20}
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_mcp_tool_plain_text(self):
block = MCPToolBlock()
@@ -472,7 +472,7 @@ class TestMCPToolBlock:
assert result == "Hello, world!"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_mcp_tool_multiple_content(self):
block = MCPToolBlock()
@@ -500,7 +500,7 @@ class TestMCPToolBlock:
assert result == ["Part 1", {"part": 2}]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_mcp_tool_error_result(self):
block = MCPToolBlock()
@@ -522,7 +522,7 @@ class TestMCPToolBlock:
with pytest.raises(MCPClientError, match="returned an error"):
await block._call_mcp_tool("https://mcp.example.com", "test_tool", {})
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_call_mcp_tool_image_content(self):
block = MCPToolBlock()
@@ -557,7 +557,7 @@ class TestMCPToolBlock:
"mimeType": "image/png",
}
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_run_with_credentials(self):
"""Verify the block uses OAuth2Credentials and passes auth token."""
from pydantic import SecretStr
@@ -594,7 +594,7 @@ class TestMCPToolBlock:
assert captured_tokens == ["resolved-token"]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_run_without_credentials(self):
"""Verify the block works without credentials (public server)."""
block = MCPToolBlock()

View File

@@ -66,7 +66,7 @@ class TestMCPOAuthHandler:
assert "code_challenge" not in url
assert "code_challenge_method" not in url
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_exchange_code_for_tokens(self):
handler = self._make_handler()
@@ -96,7 +96,7 @@ class TestMCPOAuthHandler:
assert creds.scopes == ["read"]
assert creds.access_token_expires_at is not None
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_refresh_tokens(self):
handler = self._make_handler()
@@ -128,7 +128,7 @@ class TestMCPOAuthHandler:
assert refreshed.refresh_token is not None
assert refreshed.refresh_token.get_secret_value() == "new-refresh"
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_refresh_tokens_no_refresh_token(self):
handler = self._make_handler()
@@ -142,7 +142,7 @@ class TestMCPOAuthHandler:
with pytest.raises(ValueError, match="No refresh token"):
await handler._refresh_tokens(creds)
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_revoke_tokens_no_url(self):
handler = self._make_handler(revoke_url=None)
@@ -156,7 +156,7 @@ class TestMCPOAuthHandler:
result = await handler.revoke_tokens(creds)
assert result is False
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_revoke_tokens_with_url(self):
handler = self._make_handler(revoke_url="https://auth.example.com/revoke")
@@ -181,7 +181,7 @@ class TestMCPOAuthHandler:
class TestMCPClientDiscovery:
"""Tests for MCPClient OAuth metadata discovery."""
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_discover_auth_found(self):
client = MCPClient("https://mcp.example.com/mcp")
@@ -201,7 +201,7 @@ class TestMCPClientDiscovery:
assert result is not None
assert result["authorization_servers"] == ["https://auth.example.com"]
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_discover_auth_not_found(self):
client = MCPClient("https://mcp.example.com/mcp")
@@ -215,7 +215,7 @@ class TestMCPClientDiscovery:
assert result is None
@pytest.mark.asyncio
@pytest.mark.asyncio(loop_scope="session")
async def test_discover_auth_server_metadata(self):
client = MCPClient("https://mcp.example.com/mcp")