@@ -464,6 +464,83 @@ async def test_client_credentials_request_token_without_metadata(monkeypatch: py
464464 assert provider ._metadata is None
465465
466466
467+ @pytest .mark .anyio
468+ async def test_client_credentials_request_token_omits_scope_when_not_registered (
469+ monkeypatch : pytest .MonkeyPatch ,
470+ ) -> None :
471+ storage = InMemoryStorage ()
472+ client_metadata = OAuthClientMetadata (redirect_uris = _redirect_uris ())
473+ provider = ClientCredentialsProvider ("https://api.example.com/service" , client_metadata , storage )
474+
475+ metadata_json = _metadata_json ().copy ()
476+ metadata_json .pop ("scopes_supported" )
477+ metadata_response = _make_response (200 , json_data = metadata_json )
478+ registration_response = _make_response (200 , json_data = _registration_json ())
479+ token_response = _make_response (200 , json_data = _token_json ())
480+
481+ class CapturingAsyncClient (DummyAsyncClient ):
482+ def __init__ (self , * args : object , ** kwargs : object ) -> None :
483+ super ().__init__ (* args , ** kwargs )
484+ self .captured_data : dict [str , str ] | None = None
485+ self .captured_headers : dict [str , str ] | None = None
486+
487+ async def post (
488+ self ,
489+ url : str ,
490+ * ,
491+ data : dict [str , str ],
492+ headers : dict [str , str ],
493+ ) -> httpx .Response :
494+ self .captured_data = dict (data )
495+ self .captured_headers = dict (headers )
496+ assert self ._post_responses , "Unexpected post() call"
497+ return self ._post_responses .pop (0 )
498+
499+ capturing_client = CapturingAsyncClient (post_responses = [token_response ])
500+ clients = [
501+ DummyAsyncClient (send_responses = [metadata_response ]),
502+ DummyAsyncClient (send_responses = [registration_response ]),
503+ capturing_client ,
504+ ]
505+ monkeypatch .setattr ("mcp.client.auth.oauth2.httpx.AsyncClient" , AsyncClientFactory (clients ))
506+
507+ await provider ._request_token ()
508+
509+ assert capturing_client .captured_data is not None
510+ assert capturing_client .captured_headers == {
511+ "Content-Type" : "application/x-www-form-urlencoded"
512+ }
513+ assert capturing_client .captured_data ["grant_type" ] == "client_credentials"
514+ assert capturing_client .captured_data ["resource" ] == provider .resource
515+ assert "scope" not in capturing_client .captured_data
516+
517+
518+ @pytest .mark .anyio
519+ async def test_client_credentials_request_token_stops_on_server_error (
520+ monkeypatch : pytest .MonkeyPatch ,
521+ ) -> None :
522+ storage = InMemoryStorage ()
523+ client_metadata = OAuthClientMetadata (redirect_uris = _redirect_uris (), scope = "alpha" )
524+ provider = ClientCredentialsProvider ("https://api.example.com/service" , client_metadata , storage )
525+
526+ metadata_responses = [_make_response (503 )]
527+ registration_response = _make_response (200 , json_data = _registration_json ())
528+ token_response = _make_response (200 , json_data = _token_json ("alpha" ))
529+
530+ clients = [
531+ DummyAsyncClient (send_responses = metadata_responses ),
532+ DummyAsyncClient (send_responses = [registration_response ]),
533+ DummyAsyncClient (post_responses = [token_response ]),
534+ ]
535+ monkeypatch .setattr ("mcp.client.auth.oauth2.httpx.AsyncClient" , AsyncClientFactory (clients ))
536+
537+ await provider ._request_token ()
538+
539+ assert storage .tokens is not None
540+ assert storage .tokens .scope == "alpha"
541+ assert provider ._metadata is None
542+
543+
467544@pytest .mark .anyio
468545async def test_client_credentials_ensure_token_returns_when_valid () -> None :
469546 storage = InMemoryStorage ()
0 commit comments