Skip to content

Commit 5b69422

Browse files
authored
Merge pull request #711 from cbcoutinho/fix/mcp-client-session-cancel-scope
fix(tests): convert create_mcp_client_session to asynccontextmanager
2 parents 9b6b554 + ea7e86f commit 5b69422

6 files changed

Lines changed: 51 additions & 49 deletions

File tree

tests/conftest.py

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import threading
99
import time
1010
import uuid
11+
from contextlib import asynccontextmanager
1112
from http.server import BaseHTTPRequestHandler, HTTPServer
12-
from typing import Any, AsyncGenerator
13+
from typing import Any, AsyncGenerator, AsyncIterator
1314
from urllib.parse import parse_qs, quote, urlparse
1415

1516
import anyio
@@ -118,14 +119,15 @@ async def wait_for_nextcloud(
118119
return False
119120

120121

122+
@asynccontextmanager
121123
async 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

tests/integration/sampling_support.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ def create_sampling_callback(provider: Provider):
3939
if provider.supports_generation:
4040
callback = create_sampling_callback(provider)
4141
42-
async for session in create_mcp_client_session(
42+
async with create_mcp_client_session(
4343
url="http://localhost:8000/mcp",
4444
sampling_callback=callback,
45-
):
45+
) as session:
4646
# Session now supports sampling
4747
pass
4848
```

tests/integration/test_astrolabe_plotly_visualization.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,11 @@ async def test_astrolabe_plotly_visualization_with_basic_auth(
145145
logger.info(f"Authorization result: {auth_result}")
146146

147147
# Create MCP client session as alice - all MCP operations inside this block
148-
async for alice_mcp_client in create_mcp_client_session(
148+
async with create_mcp_client_session(
149149
url="http://localhost:8003/mcp",
150150
headers={"Authorization": auth_header},
151151
client_name="Alice BasicAuth MCP",
152-
):
152+
) as alice_mcp_client:
153153
# Phase 3: Get initial indexed count
154154
initial_sync = await alice_mcp_client.call_tool(
155155
"nc_get_vector_sync_status", {}
@@ -355,11 +355,11 @@ async def test_astrolabe_plotly_visualization_with_basic_auth(
355355
# Cleanup note if not already cleaned (create new client for cleanup)
356356
if note_id:
357357
try:
358-
async for cleanup_client in create_mcp_client_session(
358+
async with create_mcp_client_session(
359359
url="http://localhost:8003/mcp",
360360
headers={"Authorization": auth_header},
361361
client_name="Cleanup MCP",
362-
):
362+
) as cleanup_client:
363363
delete_response = await cleanup_client.call_tool(
364364
"nc_notes_delete_note", {"note_id": note_id}
365365
)

tests/integration/test_rag.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,11 @@ async def nc_mcp_client_with_sampling(
227227
"""
228228
sampling_callback = create_sampling_callback(generation_provider)
229229

230-
async for session in create_mcp_client_session(
230+
async with create_mcp_client_session(
231231
url="http://localhost:8000/mcp",
232232
client_name=f"Sampling MCP ({provider_name})",
233233
sampling_callback=sampling_callback,
234-
):
234+
) as session:
235235
yield session
236236

237237

tests/server/login_flow/conftest.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -367,12 +367,12 @@ async def elicitation_callback(
367367
content={"acknowledged": True},
368368
)
369369

370-
async for session in create_mcp_client_session(
370+
async with create_mcp_client_session(
371371
url=LOGIN_FLOW_MCP_URL,
372372
token=login_flow_oauth_token,
373373
client_name="Login Flow MCP",
374374
elicitation_callback=elicitation_callback,
375-
):
375+
) as session:
376376
# Step 1: Provision access via Login Flow v2
377377
logger.info("Starting Login Flow v2 provisioning...")
378378
provision_result = await session.call_tool(
@@ -499,11 +499,11 @@ async def nc_mcp_login_flow_client_read_only(
499499
anyio_backend, login_flow_read_only_token: str
500500
) -> AsyncGenerator[ClientSession, Any]:
501501
"""MCP client with read-only scopes on the login-flow server."""
502-
async for session in create_mcp_client_session(
502+
async with create_mcp_client_session(
503503
url=LOGIN_FLOW_MCP_URL,
504504
token=login_flow_read_only_token,
505505
client_name="Login Flow MCP Read-Only",
506-
):
506+
) as session:
507507
yield session
508508

509509

@@ -512,11 +512,11 @@ async def nc_mcp_login_flow_client_write_only(
512512
anyio_backend, login_flow_write_only_token: str
513513
) -> AsyncGenerator[ClientSession, Any]:
514514
"""MCP client with write-only scopes on the login-flow server."""
515-
async for session in create_mcp_client_session(
515+
async with create_mcp_client_session(
516516
url=LOGIN_FLOW_MCP_URL,
517517
token=login_flow_write_only_token,
518518
client_name="Login Flow MCP Write-Only",
519-
):
519+
) as session:
520520
yield session
521521

522522

@@ -525,11 +525,11 @@ async def nc_mcp_login_flow_client_full_access(
525525
anyio_backend, login_flow_full_access_token: str
526526
) -> AsyncGenerator[ClientSession, Any]:
527527
"""MCP client with full access scopes on the login-flow server."""
528-
async for session in create_mcp_client_session(
528+
async with create_mcp_client_session(
529529
url=LOGIN_FLOW_MCP_URL,
530530
token=login_flow_full_access_token,
531531
client_name="Login Flow MCP Full Access",
532-
):
532+
) as session:
533533
yield session
534534

535535

@@ -538,11 +538,11 @@ async def nc_mcp_login_flow_client_no_custom_scopes(
538538
anyio_backend, login_flow_no_custom_scopes_token: str
539539
) -> AsyncGenerator[ClientSession, Any]:
540540
"""MCP client with no custom scopes on the login-flow server."""
541-
async for session in create_mcp_client_session(
541+
async with create_mcp_client_session(
542542
url=LOGIN_FLOW_MCP_URL,
543543
token=login_flow_no_custom_scopes_token,
544544
client_name="Login Flow MCP No Custom Scopes",
545-
):
545+
) as session:
546546
yield session
547547

548548

@@ -724,12 +724,12 @@ async def elicitation_callback(
724724

725725
return ElicitResult(action="accept", content={"acknowledged": True})
726726

727-
async for session in create_mcp_client_session(
727+
async with create_mcp_client_session(
728728
url=LOGIN_FLOW_MCP_URL,
729729
token=token,
730730
client_name=f"Login Flow MCP ({username})",
731731
elicitation_callback=elicitation_callback,
732-
):
732+
) as session:
733733
# Provision access
734734
provision_result = await session.call_tool(
735735
"nc_auth_provision_access", {"scopes": None}

0 commit comments

Comments
 (0)