88import threading
99import time
1010import uuid
11+ from contextlib import asynccontextmanager
1112from http .server import BaseHTTPRequestHandler , HTTPServer
12- from typing import Any , AsyncGenerator
13+ from typing import Any , AsyncGenerator , AsyncIterator
1314from urllib .parse import parse_qs , quote , urlparse
1415
1516import anyio
@@ -118,14 +119,15 @@ async def wait_for_nextcloud(
118119 return False
119120
120121
122+ @asynccontextmanager
121123async def create_mcp_client_session (
122124 url : str ,
123125 token : str | None = None ,
124126 client_name : str = "MCP" ,
125127 elicitation_callback : Any = None ,
126128 sampling_callback : Any = None ,
127129 headers : dict [str , str ] | None = None ,
128- ) -> AsyncGenerator [ClientSession , Any ]:
130+ ) -> AsyncIterator [ClientSession ]:
129131 """
130132 Factory function to create an MCP client session with proper lifecycle management.
131133
@@ -227,10 +229,10 @@ async def nc_mcp_client(anyio_backend) -> AsyncGenerator[ClientSession, Any]:
227229
228230 Uses anyio pytest plugin for proper async fixture handling.
229231 """
230- async for session in create_mcp_client_session (
232+ async with create_mcp_client_session (
231233 url = "http://localhost:8000/mcp" ,
232234 client_name = "Basic MCP (HTTP)" ,
233- ):
235+ ) as session :
234236 yield session
235237
236238
@@ -246,11 +248,11 @@ async def nc_mcp_oauth_client(
246248 Uses headless browser automation suitable for CI/CD.
247249 Uses anyio pytest plugin for proper async fixture handling.
248250 """
249- async for session in create_mcp_client_session (
251+ async with create_mcp_client_session (
250252 url = "http://localhost:8001/mcp" ,
251253 token = playwright_oauth_token ,
252254 client_name = "OAuth MCP (Playwright)" ,
253- ):
255+ ) as session :
254256 yield session
255257
256258
@@ -271,11 +273,11 @@ async def nc_mcp_basic_auth_client(
271273 credentials = base64 .b64encode (b"admin:admin" ).decode ("utf-8" )
272274 auth_header = f"Basic { credentials } "
273275
274- async for session in create_mcp_client_session (
276+ async with create_mcp_client_session (
275277 url = "http://localhost:8003/mcp" ,
276278 headers = {"Authorization" : auth_header },
277279 client_name = "BasicAuth MCP (Multi-User)" ,
278- ):
280+ ) as session :
279281 yield session
280282
281283
@@ -296,11 +298,11 @@ async def nc_mcp_oauth_jwt_client(
296298 Uses headless browser automation suitable for CI/CD.
297299 Uses anyio pytest plugin for proper async fixture handling.
298300 """
299- async for session in create_mcp_client_session (
301+ async with create_mcp_client_session (
300302 url = "http://localhost:8001/mcp" ,
301303 token = playwright_oauth_token_jwt ,
302304 client_name = "OAuth JWT MCP (Playwright)" ,
303- ):
305+ ) as session :
304306 yield session
305307
306308
@@ -456,12 +458,12 @@ async def elicitation_callback(
456458 await page .close ()
457459
458460 # Create client session with elicitation callback
459- async for session in create_mcp_client_session (
461+ async with create_mcp_client_session (
460462 url = "http://localhost:8001/mcp" ,
461463 token = playwright_oauth_token ,
462464 client_name = "OAuth MCP with Elicitation" ,
463465 elicitation_callback = elicitation_callback ,
464- ):
466+ ) as session :
465467 # Attach elicitation metadata for test validation
466468 session .elicitation_triggered = elicitation_triggered
467469 yield session
@@ -482,11 +484,11 @@ async def nc_mcp_oauth_client_read_only(
482484 Uses JWT tokens because they embed scope information in claims,
483485 enabling proper scope-based tool filtering.
484486 """
485- async for session in create_mcp_client_session (
487+ async with create_mcp_client_session (
486488 url = "http://localhost:8001/mcp" ,
487489 token = playwright_oauth_token_read_only ,
488490 client_name = "OAuth JWT MCP Read-Only (Playwright)" ,
489- ):
491+ ) as session :
490492 yield session
491493
492494
@@ -505,11 +507,11 @@ async def nc_mcp_oauth_client_write_only(
505507 Uses JWT tokens because they embed scope information in claims,
506508 enabling proper scope-based tool filtering.
507509 """
508- async for session in create_mcp_client_session (
510+ async with create_mcp_client_session (
509511 url = "http://localhost:8001/mcp" ,
510512 token = playwright_oauth_token_write_only ,
511513 client_name = "OAuth JWT MCP Write-Only (Playwright)" ,
512- ):
514+ ) as session :
513515 yield session
514516
515517
@@ -527,11 +529,11 @@ async def nc_mcp_oauth_client_full_access(
527529 Uses JWT tokens because they embed scope information in claims,
528530 enabling proper scope-based tool filtering.
529531 """
530- async for session in create_mcp_client_session (
532+ async with create_mcp_client_session (
531533 url = "http://localhost:8001/mcp" ,
532534 token = playwright_oauth_token_full_access ,
533535 client_name = "OAuth JWT MCP Full Access (Playwright)" ,
534- ):
536+ ) as session :
535537 yield session
536538
537539
@@ -552,11 +554,11 @@ async def nc_mcp_oauth_client_no_custom_scopes(
552554 Uses JWT tokens because they embed scope information in claims,
553555 enabling proper scope-based tool filtering.
554556 """
555- async for session in create_mcp_client_session (
557+ async with create_mcp_client_session (
556558 url = "http://localhost:8001/mcp" ,
557559 token = playwright_oauth_token_no_custom_scopes ,
558560 client_name = "OAuth JWT MCP No Custom Scopes (Playwright)" ,
559- ):
561+ ) as session :
560562 yield session
561563
562564
@@ -2726,11 +2728,11 @@ async def alice_mcp_client(
27262728 alice_oauth_token : str ,
27272729) -> AsyncGenerator [ClientSession , Any ]:
27282730 """MCP client authenticated as alice (owner role)."""
2729- async for session in create_mcp_client_session (
2731+ async with create_mcp_client_session (
27302732 url = "http://localhost:8001/mcp" ,
27312733 token = alice_oauth_token ,
27322734 client_name = "Alice MCP" ,
2733- ):
2735+ ) as session :
27342736 yield session
27352737
27362738
@@ -2739,11 +2741,11 @@ async def bob_mcp_client(
27392741 anyio_backend , bob_oauth_token : str
27402742) -> AsyncGenerator [ClientSession , Any ]:
27412743 """MCP client authenticated as bob (viewer role)."""
2742- async for session in create_mcp_client_session (
2744+ async with create_mcp_client_session (
27432745 url = "http://localhost:8001/mcp" ,
27442746 token = bob_oauth_token ,
27452747 client_name = "Bob MCP" ,
2746- ):
2748+ ) as session :
27472749 yield session
27482750
27492751
@@ -2753,11 +2755,11 @@ async def charlie_mcp_client(
27532755 charlie_oauth_token : str ,
27542756) -> AsyncGenerator [ClientSession , Any ]:
27552757 """MCP client authenticated as charlie (editor role, in 'editors' group)."""
2756- async for session in create_mcp_client_session (
2758+ async with create_mcp_client_session (
27572759 url = "http://localhost:8001/mcp" ,
27582760 token = charlie_oauth_token ,
27592761 client_name = "Charlie MCP" ,
2760- ):
2762+ ) as session :
27612763 yield session
27622764
27632765
@@ -2767,11 +2769,11 @@ async def diana_mcp_client(
27672769 diana_oauth_token : str ,
27682770) -> AsyncGenerator [ClientSession , Any ]:
27692771 """MCP client authenticated as diana (no-access role)."""
2770- async for session in create_mcp_client_session (
2772+ async with create_mcp_client_session (
27712773 url = "http://localhost:8001/mcp" ,
27722774 token = diana_oauth_token ,
27732775 client_name = "Diana MCP" ,
2774- ):
2776+ ) as session :
27752777 yield session
27762778
27772779
0 commit comments