Skip to content

Commit e90a66b

Browse files
authored
Rename scopes= to scope= on the client-credentials OAuth providers (#3166)
1 parent 5dd062d commit e90a66b

7 files changed

Lines changed: 34 additions & 18 deletions

File tree

docs/client/oauth-clients.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ A nightly job, a CI step, another service. There is no browser and nobody to cli
112112
What changed:
113113

114114
* No `OAuthClientMetadata`, no handlers. You pass `client_id` and `client_secret`; the provider builds a minimal `client_credentials` registration around them and skips dynamic registration entirely.
115-
* `scopes` is a space-separated string, the OAuth wire format.
115+
* `scope` is a space-separated string, the OAuth wire format.
116116
* Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`.
117117

118118
By default the secret travels as HTTP Basic auth on the token request (`client_secret_basic`). Pass `token_endpoint_auth_method="client_secret_post"` to put it in the form body instead. Some authorization servers only accept one of the two.

docs/migration.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,6 +1975,22 @@ async def callback_handler() -> AuthorizationCodeResult:
19751975

19761976
Forward the `iss` query parameter from the redirect so the validation can run: omitting it makes the flow fail with `OAuthFlowError` against servers that advertise `authorization_response_iss_parameter_supported`, and silently skips the check for servers that send `iss` without advertising it.
19771977

1978+
### `scopes=` renamed to `scope=` on the client-credentials providers
1979+
1980+
`ClientCredentialsOAuthProvider` and `PrivateKeyJWTOAuthProvider` took the requested scope as a keyword named `scopes`, even though the value is a single space-separated string, not a list. The parameter is now `scope`, matching the RFC 6749 wire parameter, `OAuthClientMetadata.scope`, and the newer `IdentityAssertionOAuthProvider`.
1981+
1982+
**Before (v1):**
1983+
1984+
```python
1985+
ClientCredentialsOAuthProvider(..., scopes="read write")
1986+
```
1987+
1988+
**After (v2):**
1989+
1990+
```python
1991+
ClientCredentialsOAuthProvider(..., scope="read write")
1992+
```
1993+
19781994
### Client rejects authorization server metadata with a mismatched `issuer`
19791995

19801996
During OAuth discovery, `OAuthClientProvider` now validates that the authorization server

docs_src/oauth_clients/tutorial002.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None
2929
storage=InMemoryTokenStorage(),
3030
client_id="reporting-agent",
3131
client_secret="...",
32-
scopes="user",
32+
scope="user",
3333
)
3434

3535

examples/stories/oauth_client_credentials/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth:
2626
storage=InMemoryTokenStorage(),
2727
client_id=DEMO_CLIENT_ID,
2828
client_secret=DEMO_CLIENT_SECRET,
29-
scopes=DEMO_SCOPE,
29+
scope=DEMO_SCOPE,
3030
)
3131

3232

src/mcp/client/auth/extensions/client_credentials.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def __init__(
4646
client_id: str,
4747
client_secret: str,
4848
token_endpoint_auth_method: Literal["client_secret_basic", "client_secret_post"] = "client_secret_basic",
49-
scopes: str | None = None,
49+
scope: str | None = None,
5050
) -> None:
5151
"""Initialize client_credentials OAuth provider.
5252
@@ -57,14 +57,14 @@ def __init__(
5757
client_secret: The OAuth client secret.
5858
token_endpoint_auth_method: Authentication method for token endpoint.
5959
Either "client_secret_basic" (default) or "client_secret_post".
60-
scopes: Optional space-separated list of scopes to request.
60+
scope: Optional space-separated list of scopes to request.
6161
"""
6262
# Build minimal client_metadata for the base class
6363
client_metadata = OAuthClientMetadata(
6464
redirect_uris=None,
6565
grant_types=["client_credentials"],
6666
token_endpoint_auth_method=token_endpoint_auth_method,
67-
scope=scopes,
67+
scope=scope,
6868
)
6969
super().__init__(server_url, client_metadata, storage, None, None, 300.0)
7070
# Store client_info to be set during _initialize - no dynamic registration needed
@@ -74,7 +74,7 @@ def __init__(
7474
client_secret=client_secret,
7575
grant_types=["client_credentials"],
7676
token_endpoint_auth_method=token_endpoint_auth_method,
77-
scope=scopes,
77+
scope=scope,
7878
)
7979

8080
async def _initialize(self) -> None:
@@ -258,7 +258,7 @@ def __init__(
258258
storage: TokenStorage,
259259
client_id: str,
260260
assertion_provider: Callable[[str], Awaitable[str]],
261-
scopes: str | None = None,
261+
scope: str | None = None,
262262
) -> None:
263263
"""Initialize private_key_jwt OAuth provider.
264264
@@ -271,14 +271,14 @@ def __init__(
271271
`SignedJWTParameters.create_assertion_provider()` for SDK-signed JWTs,
272272
`static_assertion_provider()` for pre-built JWTs, or provide your own
273273
callback for workload identity federation.
274-
scopes: Optional space-separated list of scopes to request.
274+
scope: Optional space-separated list of scopes to request.
275275
"""
276276
# Build minimal client_metadata for the base class
277277
client_metadata = OAuthClientMetadata(
278278
redirect_uris=None,
279279
grant_types=["client_credentials"],
280280
token_endpoint_auth_method="private_key_jwt",
281-
scope=scopes,
281+
scope=scope,
282282
)
283283
super().__init__(server_url, client_metadata, storage, None, None, 300.0)
284284
self._assertion_provider = assertion_provider
@@ -288,7 +288,7 @@ def __init__(
288288
client_id=client_id,
289289
grant_types=["client_credentials"],
290290
token_endpoint_auth_method="private_key_jwt",
291-
scope=scopes,
291+
scope=scope,
292292
)
293293

294294
async def _initialize(self) -> None:

tests/client/auth/extensions/test_client_credentials.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ async def test_init_with_scopes(self, mock_storage: MockTokenStorage):
210210
storage=mock_storage,
211211
client_id="test-client-id",
212212
client_secret="test-client-secret",
213-
scopes="read write",
213+
scope="read write",
214214
)
215215

216216
await provider._initialize()
@@ -240,7 +240,7 @@ async def test_exchange_token_client_credentials(self, mock_storage: MockTokenSt
240240
storage=mock_storage,
241241
client_id="test-client-id",
242242
client_secret="test-client-secret",
243-
scopes="read write",
243+
scope="read write",
244244
)
245245
provider.context.oauth_metadata = OAuthMetadata(
246246
issuer=AnyHttpUrl("https://api.example.com"),
@@ -268,7 +268,7 @@ async def test_exchange_token_client_secret_post_includes_client_id(self, mock_s
268268
client_id="test-client-id",
269269
client_secret="test-client-secret",
270270
token_endpoint_auth_method="client_secret_post",
271-
scopes="read write",
271+
scope="read write",
272272
)
273273
await provider._initialize()
274274
provider.context.oauth_metadata = OAuthMetadata(
@@ -296,7 +296,7 @@ async def test_exchange_token_client_secret_post_without_client_id(self, mock_st
296296
client_id="placeholder",
297297
client_secret="test-client-secret",
298298
token_endpoint_auth_method="client_secret_post",
299-
scopes="read write",
299+
scope="read write",
300300
)
301301
await provider._initialize()
302302
provider.context.oauth_metadata = OAuthMetadata(
@@ -386,7 +386,7 @@ async def mock_assertion_provider(audience: str) -> str:
386386
storage=mock_storage,
387387
client_id="test-client-id",
388388
assertion_provider=mock_assertion_provider,
389-
scopes="read write",
389+
scope="read write",
390390
)
391391
provider.context.oauth_metadata = OAuthMetadata(
392392
issuer=AnyHttpUrl("https://auth.example.com"),

tests/interaction/auth/test_lifecycle.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ async def test_client_credentials_provider_obtains_a_token_without_an_authorize_
372372
storage=InMemoryTokenStorage(),
373373
client_id="m2m-client",
374374
client_secret="m2m-secret",
375-
scopes="mcp",
375+
scope="mcp",
376376
)
377377

378378
with anyio.fail_after(5):
@@ -423,7 +423,7 @@ async def assertion_provider(audience: str) -> str:
423423
storage=InMemoryTokenStorage(),
424424
client_id="m2m-jwt-client",
425425
assertion_provider=assertion_provider,
426-
scopes="mcp",
426+
scope="mcp",
427427
)
428428

429429
with anyio.fail_after(5):

0 commit comments

Comments
 (0)