11import time
22
3+ import anyio
34import httpx2
45import pytest
56from mcp_types import (
67 CallToolRequestParams ,
78 CallToolResult ,
89 ListToolsResult ,
910 PaginatedRequestParams ,
11+ ProgressNotificationParams ,
1012 TextContent ,
1113 Tool ,
1214)
@@ -28,7 +30,7 @@ class _EchoTokenVerifier:
2830 """Accepts any bearer token and echoes it back as the verified AccessToken."""
2931
3032 async def verify_token (self , token : str ) -> AccessToken | None :
31- return AccessToken (token = token , client_id = token , scopes = [], expires_at = int (time .time ()) + 3600 )
33+ return AccessToken (token = token , client_id = "test-client" , scopes = [], expires_at = int (time .time ()) + 3600 )
3234
3335
3436async def _handle_whoami (ctx : ServerRequestContext , params : CallToolRequestParams ) -> CallToolResult :
@@ -51,23 +53,10 @@ def auth_flow(self, request: httpx2.Request):
5153 yield request
5254
5355
54- async def _call_whoami (asgi_app : Starlette , host : str , token : str | None ) -> str :
55- auth = _MutableBearerAuth (token )
56- async with (
57- httpx2 .ASGITransport (asgi_app ) as transport ,
58- httpx2 .AsyncClient (
59- transport = transport ,
60- base_url = f"http://{ host } " ,
61- auth = auth ,
62- timeout = httpx2 .Timeout (30 , read = 30 ),
63- follow_redirects = True ,
64- ) as http_client ,
65- ):
66- transport_ctx = streamable_http_client (f"http://{ host } /mcp" , http_client = http_client )
67- async with Client (transport_ctx ) as client : # pragma: no branch
68- result = await client .call_tool ("whoami" , {})
69- assert isinstance (result .content [0 ], TextContent )
70- return result .content [0 ].text
56+ async def _call_whoami (client : Client ) -> str :
57+ result = await client .call_tool ("whoami" , {})
58+ assert isinstance (result .content [0 ], TextContent )
59+ return result .content [0 ].text
7160
7261
7362@pytest .mark .anyio
@@ -92,6 +81,69 @@ async def test_get_access_token_reflects_current_request_in_stateful_session() -
9281 )
9382
9483 async with asgi_app .router .lifespan_context (asgi_app ):
95- assert await _call_whoami (asgi_app , host , "token-A" ) == "token-A"
96- assert await _call_whoami (asgi_app , host , "token-B" ) == "token-B"
97- assert await _call_whoami (asgi_app , host , None ) == "<none>"
84+ auth = _MutableBearerAuth ("token-A" )
85+ async with (
86+ httpx2 .ASGITransport (asgi_app ) as transport ,
87+ httpx2 .AsyncClient (
88+ transport = transport ,
89+ base_url = f"http://{ host } " ,
90+ auth = auth ,
91+ timeout = httpx2 .Timeout (30 , read = 30 ),
92+ follow_redirects = True ,
93+ ) as http_client ,
94+ Client (streamable_http_client (f"http://{ host } /mcp" , http_client = http_client ), mode = "legacy" ) as client ,
95+ ):
96+ assert await _call_whoami (client ) == "token-A"
97+
98+ auth .token = "token-B"
99+ assert await _call_whoami (client ) == "token-B"
100+
101+
102+ @pytest .mark .anyio
103+ async def test_notification_handler_get_access_token_reflects_current_request_in_stateful_session () -> None :
104+ host = "testserver"
105+ send_token , receive_token = anyio .create_memory_object_stream [str ](10 )
106+
107+ async def handle_progress (ctx : ServerRequestContext , params : ProgressNotificationParams ) -> None :
108+ access = get_access_token ()
109+ await send_token .send (access .token if access else "<none>" )
110+
111+ server = Server (
112+ "auth-test-server" ,
113+ on_call_tool = _handle_whoami ,
114+ on_list_tools = _handle_list_tools ,
115+ )
116+ server .add_notification_handler ("notifications/progress" , ProgressNotificationParams , handle_progress )
117+
118+ session_manager = StreamableHTTPSessionManager (app = server , stateless = False )
119+
120+ asgi_app = Starlette (
121+ routes = [Mount ("/mcp" , app = session_manager .handle_request )],
122+ middleware = [
123+ Middleware (AuthenticationMiddleware , backend = BearerAuthBackend (_EchoTokenVerifier ())),
124+ Middleware (AuthContextMiddleware ),
125+ ],
126+ lifespan = lambda app : session_manager .run (),
127+ )
128+
129+ async with send_token , receive_token , asgi_app .router .lifespan_context (asgi_app ):
130+ auth = _MutableBearerAuth ("token-A" )
131+ async with (
132+ httpx2 .ASGITransport (asgi_app ) as transport ,
133+ httpx2 .AsyncClient (
134+ transport = transport ,
135+ base_url = f"http://{ host } " ,
136+ auth = auth ,
137+ timeout = httpx2 .Timeout (30 , read = 30 ),
138+ follow_redirects = True ,
139+ ) as http_client ,
140+ Client (streamable_http_client (f"http://{ host } /mcp" , http_client = http_client ), mode = "legacy" ) as client ,
141+ ):
142+ await client .send_progress_notification ("token-A" , 0.1 ) # pyright: ignore[reportDeprecated]
143+ with anyio .fail_after (5 ):
144+ assert await receive_token .receive () == "token-A"
145+
146+ auth .token = "token-B"
147+ await client .send_progress_notification ("token-B" , 0.2 ) # pyright: ignore[reportDeprecated]
148+ with anyio .fail_after (5 ):
149+ assert await receive_token .receive () == "token-B"
0 commit comments