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
{{ message }}
Repository navigation
Authorization server selection is first-in-list for pre-provisioned providers #3844
With no issuer configured, _select_authorization_server returns advertised[0], and _preferred_authorization_server only filters by issuer when one is set, falling back to advertised[0] otherwise. So the resource server's list order decides which authorization server receives the pre-provisioned provider's Basic auth, which carries a durable client_secret rather than an expiring bearer.
Is first-in-list the intended selection when the list carries more than one server, and how would you classify a durable client secret placed with the first entry of a list the resource server controls?
The code answers the first question directly, and the second one is what the deprecation notice in the same file is about.
_preferred_authorization_server in src/mcp/client/auth/extensions/client_credentials.py is:
return next(
(server for server in advertised if issuer is not None and issuers_match(server, issuer)), advertised[0]
)
So with issuer=None the fallback is advertised[0] by construction, not by accident. The base provider's _select_authorization_server in src/mcp/client/auth/oauth2.py is more explicit still: it returns advertised[0] with a comment saying the list is never empty. Both pre-provisioned providers override it (ClientCredentialsOAuthProvider and the private_key_jwt one) and both delegate to the same helper, so there is exactly one selection policy in the tree.
Your classification question is answered by _checked_issuer in the same module: omitting issuer raises MCPDeprecationWarning with the text that the MCP server decides which authorization server receives the client's credentials, and that issuer becomes required in 3.0. The durable client_secret sent as Basic auth to a resource-server-chosen authorization server is precisely the hazard that deprecation exists to remove. I would classify it as a confused-deputy hazard rather than a bug: the resource server controls list order, that list is unauthenticated metadata, and a pre-provisioned credential that cannot be rotated per session is the highest-value asset in the whole flow.
Two consequences, and both are decisions rather than defects:
With issuer set, _require_metadata_for_configured_issuer drops discovered metadata that does not match the issuer, so the credential can only reach that issuer's token endpoint. That path already exists, which is why passing issuer is the entire fix and nothing else has to change.
When the advertised list has more than one entry and no issuer is configured, first-in-list is ordering by resource-server preference. If you want a defensive default before 3.0 makes the choice mandatory, refusing to send a pre-provisioned credential while the list is longer than one and no issuer is set would fail closed instead of picking silently. RFC 9728 leaves the choice to the client, so this is policy, but this is the one place where a wrong pick produces no error surface at all.
That last point is not theoretical for us: the connectors we run at corpusiq.io bind each connected account to its own authorization server, and the advertised list is exactly the field where a multi-tenant deployment could be pointed at the wrong tenant's token endpoint with nothing to show for it. Pinning issuer is the only thing that makes that structurally impossible rather than unlikely.
Thanks for confirming it's construction rather than accident. Two readings then, and I want to get them right.
Passing issuer is the fix and it's available now, since _require_metadata_for_configured_issuer already drops non-matching metadata. So the exposure is any deployment that hasn't set it, which is the default.
Fail-closed on a multi-entry list with no issuer is the part not in the tree. Worth knowing whether that's a 3.0 plan or still open, because until it ships, the safe default is a config choice the operator has to know to make, and the deprecation warning is the only thing telling them
Requiring issuer is where the 3.0 note points. Once it is required, the selection fallback can still return advertised[0], but the credentials are only sent after _require_metadata_for_configured_issuer accepts metadata whose issuer matches, and that raises OAuthFlowError on a mismatch. The guard is what fails closed, not the selector.
What the tree does not do today is refuse a multi-entry list while issuer is unset. It warns and returns None, so a client with several advertised servers and no issuer still takes the first. Whether a pre-3.0 hard error is added for that case is a maintainer call, not something the code answers.
Until then your read holds. Passing issuer is the only path that constrains where the credentials go, and it works now because the metadata guard drops non-matching servers, so the deployment that sets it is already covered while the default one is not.
Activity
media-ctrl commentedon Nov 9, 2026
The code answers the first question directly, and the second one is what the deprecation notice in the same file is about.
_preferred_authorization_serverinsrc/mcp/client/auth/extensions/client_credentials.pyis:So with
issuer=Nonethe fallback isadvertised[0]by construction, not by accident. The base provider's_select_authorization_serverinsrc/mcp/client/auth/oauth2.pyis more explicit still: it returnsadvertised[0]with a comment saying the list is never empty. Both pre-provisioned providers override it (ClientCredentialsOAuthProviderand theprivate_key_jwtone) and both delegate to the same helper, so there is exactly one selection policy in the tree.Your classification question is answered by
_checked_issuerin the same module: omittingissuerraisesMCPDeprecationWarningwith the text that the MCP server decides which authorization server receives the client's credentials, and thatissuerbecomes required in 3.0. The durableclient_secretsent as Basic auth to a resource-server-chosen authorization server is precisely the hazard that deprecation exists to remove. I would classify it as a confused-deputy hazard rather than a bug: the resource server controls list order, that list is unauthenticated metadata, and a pre-provisioned credential that cannot be rotated per session is the highest-value asset in the whole flow.Two consequences, and both are decisions rather than defects:
issuerset,_require_metadata_for_configured_issuerdrops discovered metadata that does not match the issuer, so the credential can only reach that issuer's token endpoint. That path already exists, which is why passingissueris the entire fix and nothing else has to change.That last point is not theoretical for us: the connectors we run at corpusiq.io bind each connected account to its own authorization server, and the advertised list is exactly the field where a multi-tenant deployment could be pointed at the wrong tenant's token endpoint with nothing to show for it. Pinning
issueris the only thing that makes that structurally impossible rather than unlikely.LHMisme420 commentedon Nov 9, 2026
Thanks for confirming it's construction rather than accident. Two readings then, and I want to get them right.
Passing issuer is the fix and it's available now, since _require_metadata_for_configured_issuer already drops non-matching metadata. So the exposure is any deployment that hasn't set it, which is the default.
Fail-closed on a multi-entry list with no issuer is the part not in the tree. Worth knowing whether that's a 3.0 plan or still open, because until it ships, the safe default is a config choice the operator has to know to make, and the deprecation warning is the only thing telling them
media-ctrl commentedon Nov 9, 2026
Requiring
issueris where the 3.0 note points. Once it is required, the selection fallback can still returnadvertised[0], but the credentials are only sent after_require_metadata_for_configured_issueraccepts metadata whose issuer matches, and that raisesOAuthFlowErroron a mismatch. The guard is what fails closed, not the selector.What the tree does not do today is refuse a multi-entry list while
issueris unset. It warns and returns None, so a client with several advertised servers and no issuer still takes the first. Whether a pre-3.0 hard error is added for that case is a maintainer call, not something the code answers.Until then your read holds. Passing
issueris the only path that constrains where the credentials go, and it works now because the metadata guard drops non-matching servers, so the deployment that sets it is already covered while the default one is not.