@@ -189,6 +189,7 @@ async def test_init_sets_client_info(self, mock_storage: MockTokenStorage):
189189 storage = mock_storage ,
190190 client_id = "test-client-id" ,
191191 client_secret = "test-client-secret" ,
192+ issuer = "https://api.example.com" ,
192193 )
193194
194195 # client_info is set during _initialize
@@ -209,6 +210,7 @@ async def test_init_with_scopes(self, mock_storage: MockTokenStorage):
209210 client_id = "test-client-id" ,
210211 client_secret = "test-client-secret" ,
211212 scopes = "read write" ,
213+ issuer = "https://api.example.com" ,
212214 )
213215
214216 await provider ._initialize ()
@@ -224,6 +226,7 @@ async def test_init_with_client_secret_post(self, mock_storage: MockTokenStorage
224226 client_id = "test-client-id" ,
225227 client_secret = "test-client-secret" ,
226228 token_endpoint_auth_method = "client_secret_post" ,
229+ issuer = "https://api.example.com" ,
227230 )
228231
229232 await provider ._initialize ()
@@ -239,6 +242,7 @@ async def test_exchange_token_client_credentials(self, mock_storage: MockTokenSt
239242 client_id = "test-client-id" ,
240243 client_secret = "test-client-secret" ,
241244 scopes = "read write" ,
245+ issuer = "https://api.example.com" ,
242246 )
243247 provider .context .oauth_metadata = OAuthMetadata (
244248 issuer = AnyHttpUrl ("https://api.example.com" ),
@@ -265,6 +269,7 @@ async def test_exchange_token_without_scopes(self, mock_storage: MockTokenStorag
265269 storage = mock_storage ,
266270 client_id = "test-client-id" ,
267271 client_secret = "test-client-secret" ,
272+ issuer = "https://api.example.com" ,
268273 )
269274 provider .context .oauth_metadata = OAuthMetadata (
270275 issuer = AnyHttpUrl ("https://api.example.com" ),
@@ -296,6 +301,7 @@ async def mock_assertion_provider(audience: str) -> str: # pragma: no cover
296301 storage = mock_storage ,
297302 client_id = "test-client-id" ,
298303 assertion_provider = mock_assertion_provider ,
304+ issuer = "https://api.example.com" ,
299305 )
300306
301307 # client_info is set during _initialize
@@ -319,6 +325,7 @@ async def mock_assertion_provider(audience: str) -> str:
319325 client_id = "test-client-id" ,
320326 assertion_provider = mock_assertion_provider ,
321327 scopes = "read write" ,
328+ issuer = "https://auth.example.com" ,
322329 )
323330 provider .context .oauth_metadata = OAuthMetadata (
324331 issuer = AnyHttpUrl ("https://auth.example.com" ),
@@ -350,6 +357,7 @@ async def mock_assertion_provider(audience: str) -> str:
350357 storage = mock_storage ,
351358 client_id = "test-client-id" ,
352359 assertion_provider = mock_assertion_provider ,
360+ issuer = "https://auth.example.com" ,
353361 )
354362 provider .context .oauth_metadata = OAuthMetadata (
355363 issuer = AnyHttpUrl ("https://auth.example.com" ),
@@ -541,6 +549,65 @@ async def test_provider_picks_its_configured_issuer_among_several_advertised_ser
541549 await flow .aclose ()
542550
543551
552+ @pytest .mark .parametrize ("kind" , ["secret" , "jwt" ])
553+ def test_constructing_without_issuer_is_deprecated (mock_storage : MockTokenStorage , kind : str ) -> None :
554+ """SDK-defined: leaving `issuer` out is allowed but deprecated, and the provider says so at
555+ construction."""
556+
557+ async def assertion_provider (audience : str ) -> str :
558+ raise NotImplementedError
559+
560+ with pytest .warns (DeprecationWarning ) as recorded :
561+ if kind == "secret" :
562+ ClientCredentialsOAuthProvider (
563+ server_url = _SERVER_URL , storage = mock_storage , client_id = "c" , client_secret = "s"
564+ )
565+ else :
566+ PrivateKeyJWTOAuthProvider (
567+ server_url = _SERVER_URL , storage = mock_storage , client_id = "c" , assertion_provider = assertion_provider
568+ )
569+
570+ [warning ] = recorded
571+ assert warning .filename == __file__
572+ assert str (warning .message ) == (
573+ "Omitting `issuer` is deprecated and it will be required in 3.0. Without it, the MCP server "
574+ "decides which authorization server receives this client's credentials; pass "
575+ "issuer=<your authorization server's issuer URL> so they are only ever sent there."
576+ )
577+
578+
579+ @pytest .mark .anyio
580+ @pytest .mark .parametrize ("kind" , ["secret" , "jwt" ])
581+ async def test_without_issuer_the_exchange_follows_whichever_server_was_discovered (
582+ mock_storage : MockTokenStorage , kind : str
583+ ) -> None :
584+ """SDK-defined: with no `issuer` configured the token request is built from whatever metadata
585+ discovery produced, as before."""
586+
587+ async def assertion_provider (audience : str ) -> str :
588+ return "jwt"
589+
590+ with pytest .warns (DeprecationWarning , match = "Omitting `issuer` is deprecated" ):
591+ if kind == "secret" :
592+ provider : OAuthClientProvider = ClientCredentialsOAuthProvider (
593+ server_url = _SERVER_URL , storage = mock_storage , client_id = "c" , client_secret = "s"
594+ )
595+ else :
596+ provider = PrivateKeyJWTOAuthProvider (
597+ server_url = _SERVER_URL , storage = mock_storage , client_id = "c" , assertion_provider = assertion_provider
598+ )
599+ flow = provider .async_auth_flow (httpx .Request ("POST" , _SERVER_URL ))
600+
601+ token_request = await _answer_discovery (
602+ flow ,
603+ authorization_server = "https://elsewhere.example.com" ,
604+ metadata = _metadata_for ("https://elsewhere.example.com" ),
605+ )
606+
607+ assert (token_request .method , str (token_request .url )) == ("POST" , "https://elsewhere.example.com/token" )
608+ await flow .aclose ()
609+
610+
544611def test_an_issuer_that_is_not_an_http_url_is_rejected_at_construction (mock_storage : MockTokenStorage ) -> None :
545612 """SDK-defined: `issuer=` is the authorization server's issuer URL; anything else is a configuration
546613 error on both machine-to-machine providers."""
0 commit comments