fix(tests): convert create_mcp_client_session to asynccontextmanager
The multi-user-basic integration job was consistently failing with `CancelledError: Cancelled via cancel scope ... by <async_generator_athrow>` followed by a cascade of `anyio.ClosedResourceError` in every subsequent test. Root cause: `create_mcp_client_session` was declared as an async generator driven by `async for session in ...:`, so Python's generator finalizer (`aclose`) ran under pytest-asyncio's cleanup task instead of the task that owned the nested `streamablehttp_client` cancel scope. anyio then raised when the inner task group saw its scope being exited from a foreign task, leaving the memory object streams half-closed and poisoning the rest of the session. Switching to `@asynccontextmanager` + `async with ... as session:` makes `__aenter__`/`__aexit__` run in the frame that owns the context manager, satisfying anyio's structured concurrency requirements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9b6b554155
commit
3935f45be8
+30
-28
@@ -8,8 +8,9 @@ import subprocess
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Any, AsyncGenerator
|
||||
from typing import Any, AsyncGenerator, AsyncIterator
|
||||
from urllib.parse import parse_qs, quote, urlparse
|
||||
|
||||
import anyio
|
||||
@@ -118,6 +119,7 @@ async def wait_for_nextcloud(
|
||||
return False
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def create_mcp_client_session(
|
||||
url: str,
|
||||
token: str | None = None,
|
||||
@@ -125,7 +127,7 @@ async def create_mcp_client_session(
|
||||
elicitation_callback: Any = None,
|
||||
sampling_callback: Any = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
) -> AsyncIterator[ClientSession]:
|
||||
"""
|
||||
Factory function to create an MCP client session with proper lifecycle management.
|
||||
|
||||
@@ -227,10 +229,10 @@ async def nc_mcp_client(anyio_backend) -> AsyncGenerator[ClientSession, Any]:
|
||||
|
||||
Uses anyio pytest plugin for proper async fixture handling.
|
||||
"""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8000/mcp",
|
||||
client_name="Basic MCP (HTTP)",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -246,11 +248,11 @@ async def nc_mcp_oauth_client(
|
||||
Uses headless browser automation suitable for CI/CD.
|
||||
Uses anyio pytest plugin for proper async fixture handling.
|
||||
"""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=playwright_oauth_token,
|
||||
client_name="OAuth MCP (Playwright)",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -271,11 +273,11 @@ async def nc_mcp_basic_auth_client(
|
||||
credentials = base64.b64encode(b"admin:admin").decode("utf-8")
|
||||
auth_header = f"Basic {credentials}"
|
||||
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8003/mcp",
|
||||
headers={"Authorization": auth_header},
|
||||
client_name="BasicAuth MCP (Multi-User)",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -296,11 +298,11 @@ async def nc_mcp_oauth_jwt_client(
|
||||
Uses headless browser automation suitable for CI/CD.
|
||||
Uses anyio pytest plugin for proper async fixture handling.
|
||||
"""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=playwright_oauth_token_jwt,
|
||||
client_name="OAuth JWT MCP (Playwright)",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -456,12 +458,12 @@ async def nc_mcp_oauth_client_with_elicitation(
|
||||
await page.close()
|
||||
|
||||
# Create client session with elicitation callback
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=playwright_oauth_token,
|
||||
client_name="OAuth MCP with Elicitation",
|
||||
elicitation_callback=elicitation_callback,
|
||||
):
|
||||
) as session:
|
||||
# Attach elicitation metadata for test validation
|
||||
session.elicitation_triggered = elicitation_triggered
|
||||
yield session
|
||||
@@ -482,11 +484,11 @@ async def nc_mcp_oauth_client_read_only(
|
||||
Uses JWT tokens because they embed scope information in claims,
|
||||
enabling proper scope-based tool filtering.
|
||||
"""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=playwright_oauth_token_read_only,
|
||||
client_name="OAuth JWT MCP Read-Only (Playwright)",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -505,11 +507,11 @@ async def nc_mcp_oauth_client_write_only(
|
||||
Uses JWT tokens because they embed scope information in claims,
|
||||
enabling proper scope-based tool filtering.
|
||||
"""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=playwright_oauth_token_write_only,
|
||||
client_name="OAuth JWT MCP Write-Only (Playwright)",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -527,11 +529,11 @@ async def nc_mcp_oauth_client_full_access(
|
||||
Uses JWT tokens because they embed scope information in claims,
|
||||
enabling proper scope-based tool filtering.
|
||||
"""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=playwright_oauth_token_full_access,
|
||||
client_name="OAuth JWT MCP Full Access (Playwright)",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -552,11 +554,11 @@ async def nc_mcp_oauth_client_no_custom_scopes(
|
||||
Uses JWT tokens because they embed scope information in claims,
|
||||
enabling proper scope-based tool filtering.
|
||||
"""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=playwright_oauth_token_no_custom_scopes,
|
||||
client_name="OAuth JWT MCP No Custom Scopes (Playwright)",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -2726,11 +2728,11 @@ async def alice_mcp_client(
|
||||
alice_oauth_token: str,
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""MCP client authenticated as alice (owner role)."""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=alice_oauth_token,
|
||||
client_name="Alice MCP",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -2739,11 +2741,11 @@ async def bob_mcp_client(
|
||||
anyio_backend, bob_oauth_token: str
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""MCP client authenticated as bob (viewer role)."""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=bob_oauth_token,
|
||||
client_name="Bob MCP",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -2753,11 +2755,11 @@ async def charlie_mcp_client(
|
||||
charlie_oauth_token: str,
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""MCP client authenticated as charlie (editor role, in 'editors' group)."""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=charlie_oauth_token,
|
||||
client_name="Charlie MCP",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
@@ -2767,11 +2769,11 @@ async def diana_mcp_client(
|
||||
diana_oauth_token: str,
|
||||
) -> AsyncGenerator[ClientSession, Any]:
|
||||
"""MCP client authenticated as diana (no-access role)."""
|
||||
async for session in create_mcp_client_session(
|
||||
async with create_mcp_client_session(
|
||||
url="http://localhost:8001/mcp",
|
||||
token=diana_oauth_token,
|
||||
client_name="Diana MCP",
|
||||
):
|
||||
) as session:
|
||||
yield session
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user