You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Let pre-provisioned OAuth clients name their authorization server
ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider take an
optional issuer keyword: the issuer identifier of the authorization
server the fixed client_id (and secret) were issued by. When set,
discovery only proceeds with that server: the protected resource has to
advertise it (with several advertised, its entry is the one used), or on
the legacy no-PRM path the resource server's origin has to be it;
anything else stops the flow with OAuthFlowError before any of that
server's metadata is fetched. Token requests are then only built from
metadata discovered for that issuer, never from the 2025-03-26 default
endpoints: the client_credentials exchange requires it, and a refresh
waits for it (OAuthContext.configured_issuer, can_refresh_token). A value
that is not an http(s) URL is a ValueError; omitting it keeps the current
behaviour.
This is the same "the authorization server is configuration" model that
IdentityAssertionOAuthProvider already uses, made available to the two
older machine-to-machine providers without changing their defaults.
Copy file name to clipboardExpand all lines: docs/client/oauth-clients.md
+3-2Lines changed: 3 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -105,13 +105,14 @@ A nightly job, a CI step, another service. There is no browser and nobody to cli
105
105
106
106
`ClientCredentialsOAuthProvider` is the same `httpx2.Auth`, minus the human:
107
107
108
-
```python title="client.py" hl_lines="4 27-33"
108
+
```python title="client.py" hl_lines="4 27-34"
109
109
--8<--"docs_src/oauth_clients/tutorial002.py"
110
110
```
111
111
112
112
What changed:
113
113
114
114
* 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
+
*`issuer` names the authorization server that issued those credentials. Discovery still runs as above, but the token request is only ever built from metadata for *that* issuer; if the MCP server points anywhere else, the flow stops with an `OAuthFlowError` instead. Leave it out and the provider uses whichever authorization server discovery finds.
115
116
*`scope` is a space-separated string, the OAuth wire format.
116
117
* Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`.
117
118
@@ -124,7 +125,7 @@ By default the secret travels as HTTP Basic auth on the token request (`client_s
124
125
One more provider lives in `mcp.client.auth.extensions.client_credentials`:
125
126
**`PrivateKeyJWTOAuthProvider`**, for clients that authenticate with a JWT instead of a
126
127
shared secret (`private_key_jwt`, the key-pair and workload-identity flavour). It follows
127
-
the same pattern: construct one, put it on `auth=`. The same module ships
128
+
the same pattern: construct one (it takes the same optional `issuer`), put it on `auth=`. The same module ships
128
129
`SignedJWTParameters` and `static_assertion_provider`, two helpers that build its assertion.
129
130
130
131
There is one more no-human situation: the client belongs to an enterprise whose identity provider, not the user, decides which MCP servers it may reach. That is a different grant with its own trust model and its own page, **[Identity assertion](identity-assertion.md)**.
0 commit comments