|
5 | 5 | import anyio |
6 | 6 | import pytest |
7 | 7 |
|
| 8 | +from mcp import Client |
| 9 | +from mcp.server import Server |
8 | 10 | from mcp.server.auth.middleware.auth_context import auth_context_var, get_tenant_id |
9 | 11 | from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser |
10 | 12 | from mcp.server.auth.provider import AccessToken |
|
15 | 17 | from mcp.shared._context import RequestContext |
16 | 18 | from mcp.shared.message import SessionMessage |
17 | 19 | from mcp.shared.session import BaseSession |
18 | | -from mcp.types import ServerCapabilities |
| 20 | +from mcp.types import ListToolsResult, NotificationParams, PaginatedRequestParams, ServerCapabilities |
19 | 21 |
|
20 | 22 |
|
21 | 23 | def _simulate_tenant_binding(session: ServerSession, tenant_id_value: str) -> None: |
@@ -321,3 +323,85 @@ async def test_server_session_isolation_between_instances(init_options: Initiali |
321 | 323 | # Verify the other session is unaffected |
322 | 324 | assert session1.tenant_id == "tenant-gamma" |
323 | 325 | assert session2.tenant_id == "tenant-beta" # Still beta, not gamma |
| 326 | + |
| 327 | + |
| 328 | +@pytest.mark.anyio |
| 329 | +async def test_handle_request_populates_session_tenant_id(): |
| 330 | + """E2E: session.tenant_id is set from auth context during request handling. |
| 331 | +
|
| 332 | + This exercises the set-once tenant binding in lowlevel/server.py |
| 333 | + _handle_request, covering the branch where get_tenant_id() returns |
| 334 | + a non-None value. |
| 335 | + """ |
| 336 | + captured_ctx_tenant: str | None = None |
| 337 | + captured_session_tenant: str | None = None |
| 338 | + |
| 339 | + async def handle_list_tools( |
| 340 | + ctx: ServerRequestContext, params: PaginatedRequestParams | None |
| 341 | + ) -> ListToolsResult: |
| 342 | + nonlocal captured_ctx_tenant, captured_session_tenant |
| 343 | + captured_ctx_tenant = ctx.tenant_id |
| 344 | + captured_session_tenant = ctx.session.tenant_id |
| 345 | + return ListToolsResult(tools=[]) |
| 346 | + |
| 347 | + server = Server("test", on_list_tools=handle_list_tools) |
| 348 | + |
| 349 | + # Set auth context with tenant before entering the Client — |
| 350 | + # contextvars are inherited by child tasks, so the server will see it |
| 351 | + access_token = AccessToken( |
| 352 | + token="test-token", |
| 353 | + client_id="test-client", |
| 354 | + scopes=["read"], |
| 355 | + expires_at=int(time.time()) + 3600, |
| 356 | + tenant_id="tenant-e2e", |
| 357 | + ) |
| 358 | + user = AuthenticatedUser(access_token) |
| 359 | + token = auth_context_var.set(user) |
| 360 | + try: |
| 361 | + async with Client(server) as client: |
| 362 | + await client.list_tools() |
| 363 | + finally: |
| 364 | + auth_context_var.reset(token) |
| 365 | + |
| 366 | + assert captured_ctx_tenant == "tenant-e2e" |
| 367 | + assert captured_session_tenant == "tenant-e2e" |
| 368 | + |
| 369 | + |
| 370 | +@pytest.mark.anyio |
| 371 | +async def test_handle_notification_populates_session_tenant_id(): |
| 372 | + """E2E: session.tenant_id is set from auth context during notification handling. |
| 373 | +
|
| 374 | + This exercises the set-once tenant binding in lowlevel/server.py |
| 375 | + _handle_notification, covering the branch where get_tenant_id() returns |
| 376 | + a non-None value. |
| 377 | + """ |
| 378 | + notification_tenant: str | None = None |
| 379 | + notification_received = anyio.Event() |
| 380 | + |
| 381 | + async def handle_roots_list_changed( |
| 382 | + ctx: ServerRequestContext, params: NotificationParams | None |
| 383 | + ) -> None: |
| 384 | + nonlocal notification_tenant |
| 385 | + notification_tenant = ctx.tenant_id |
| 386 | + notification_received.set() |
| 387 | + |
| 388 | + server = Server("test", on_roots_list_changed=handle_roots_list_changed) |
| 389 | + |
| 390 | + access_token = AccessToken( |
| 391 | + token="test-token", |
| 392 | + client_id="test-client", |
| 393 | + scopes=["read"], |
| 394 | + expires_at=int(time.time()) + 3600, |
| 395 | + tenant_id="tenant-notify", |
| 396 | + ) |
| 397 | + user = AuthenticatedUser(access_token) |
| 398 | + token = auth_context_var.set(user) |
| 399 | + try: |
| 400 | + async with Client(server) as client: |
| 401 | + await client.session.send_roots_list_changed() |
| 402 | + with anyio.fail_after(5): |
| 403 | + await notification_received.wait() |
| 404 | + finally: |
| 405 | + auth_context_var.reset(token) |
| 406 | + |
| 407 | + assert notification_tenant == "tenant-notify" |
0 commit comments