Which parts of OAuth does the Strands Agents MCP client handle?
Given nothing but a URL and client credentials, the v1.53.0 MCPClient handles OAuth (client_credentials) discovery, token acquisition, and reuse within an instance. The requested scopes, however, are not what you pass but the full set the server advertises. Verified locally with a self-hosted authorization server.
- Published
Originally written in Japanese. This is a translation of the same piece.
Introduction
Strands Agents Python SDK v1.53.0 (August 21, 2026) gave MCPClient a built-in way to connect to OAuth-protected MCP servers.
You could always connect by supplying your own transport; what is new in this version is that the SDK acquires the token and attaches it.
All you pass is a URL and client credentials; the scopes are optional.
from strands.tools.mcp import MCPClient
from strands.tools.mcp.mcp_types import MCPClientCredentials
client = MCPClient(
url="http://127.0.0.1:9000/mcp",
auth=MCPClientCredentials(
client_id="lab-client",
client_secret="...",
scopes=["mcp:tools"],
),
)
There is no token endpoint URL. If that is enough to connect, where does the missing information come from? I stood up both an authorization server (AS below) and an MCP server locally and checked the HTTP exchange. The verification code is in this repository. The versions used are strands-agents 1.53.0 and its dependency MCP SDK 1.29.0; the behavior below is of these versions.
This auth supports only the client_credentials grant (the M2M authorization grant where a client obtains an access token under permissions granted to the client itself, with no user involved), and only over streamable HTTP.
Any other flow is implemented by the caller, by passing an arbitrary httpx.Auth to the auth_provider argument.
Where the information you didn’t write comes from
On connect, the client sent HTTP requests in this order.
One line is added to the httpx log for the value of the 401 response’s WWW-Authenticate header.
POST http://127.0.0.1:9000/mcp "401 Unauthorized"
WWW-Authenticate: Bearer error="invalid_token",
resource_metadata="http://127.0.0.1:9000/.well-known/oauth-protected-resource/mcp"
GET http://127.0.0.1:9000/.well-known/oauth-protected-resource/mcp "200 OK"
GET http://127.0.0.1:9001/.well-known/oauth-authorization-server "200 OK"
POST http://127.0.0.1:9001/token "200 OK"
POST http://127.0.0.1:9000/mcp "200 OK"
The resource_metadata in the 401’s WWW-Authenticate points at the protected resource metadata (RFC 9728, the document where a resource server publishes its authorization details).
Its authorization_servers yields the AS, and the AS metadata (RFC 8414) yields the token endpoint1.
This order follows the discovery procedure defined by the MCP authorization spec.
The AS log recorded the token request like this.
POST /token grant_type=client_credentials auth=client_secret_basic
scope='mcp:tools' resource='http://127.0.0.1:9000/mcp'
In this setup, every token request to the AS includes the resource parameter (RFC 8707), so the AS can narrow the token’s audience to this MCP server.
What actually rejects reuse against another server is the audience validation performed by the MCP server that receives the token.
The MCP spec makes that validation mandatory for resource servers.
Client authentication was fixed to client_secret_basic.
Where the requested scope is decided
In OAuth as generally used, the client decides which scopes to request.
The scopes_supported in AS or resource server metadata is a list to choose from: the client requests only what it needs, and if it asks for a scope not on the list, the AS rejects the request or narrows what it issues.
Read with that model, the scopes=["mcp:tools"] passed to MCPClientCredentials in the opening example is that “what it needs.”
The MCP authorization spec does not adopt that model.
Its Scope Selection Strategy says to use the scope parameter of the 401’s WWW-Authenticate if present, and otherwise to request all scopes listed in the protected resource metadata’s scopes_supported.
scopes_supported is positioned not as a menu but as the minimal set needed for basic functionality, with additional scopes requested incrementally after a 403.
The scope='mcp:tools' the client requested in the log is also a value decided by this procedure.
The MCP SDK’s server implementation used for verification (FastMCP) publishes its configured required_scopes as scopes_supported in the protected resource metadata.
In this setup, the 401 response carries no scope hint, so the scopes the server demands became, verbatim, the scopes the client requests.
Then what happens if you write only what you need in MCPClientCredentials.scopes?
The docstring says it is ignored when the server advertises its own scopes.
In that case, it should at least be used when the server advertises nothing.
Checking shows that in neither case did the client’s setting appear in the token request.
- When the server advertises
mcp:toolsandmcp:adminand the client passes onlyscopes=["mcp:tools"]: the token request carriesscope='mcp:tools mcp:admin'(everything advertised) - When the server advertises nothing: the same setting produces a token request with no scope parameter at all
MCP SDK 1.29.0 selects the requested scope in the order “the 401 WWW-Authenticate value, then the protected resource metadata’s scopes_supported, then the AS metadata’s scopes_supported, else unspecified,” and overwrites its internal requested scope with the result.
The only departure from the spec is that the AS metadata is included as a candidate.
The client’s setting is a candidate in neither the spec nor the implementation, so whatever you write in MCPClientCredentials.scopes never reaches the token request.
The two remaining steps live elsewhere.
The AS issues a token with the scopes its own authorization policy allows, and the MCP server compares the token’s scopes against required_scopes, rejecting the call with 403 if any is missing.
This comparison runs independently of what the client requested.
Calling with a token that lacks the required scope did return 403.
The returned WWW-Authenticate, however, has no machine-readable scope parameter, which the spec recommends including on 403.
The required scope appears only in the human-oriented error_description.
When the 403’s WWW-Authenticate carries error="insufficient_scope" but no scope, the MCP SDK 1.29.0 client re-selects the requested scope from the protected resource metadata.
In a setup like this one, where that re-selection lands on the same scopes_supported, it just repeats the same request, so retries keep hitting 403 until the AS changes what it issues.
Token reuse, and what failure looks like
Tokens are stored in memory and tied to the MCPClient instance.
Reopening the session twice on one instance and making three tool calls in total cost exactly one token request.
This was checked within the token’s lifetime; on expiry, the implementation re-fetches even within the same instance.
Recreating the instance, on the other hand, discards the in-memory state.
A pattern that constructs an MCPClient per call therefore issues a token request on every call, even while the token is still valid.
With a wrong client_secret, the exception you receive is MCPClientInitializationError, and its message is the client initialization failed: unhandled errors in a TaskGroup (1 sub-exception).
The cause cannot be read from it.
Chained directly to this exception is the TaskGroup’s ExceptionGroup, and the invalid_client the AS returned survives in the message of the OAuthTokenError contained in it.
A logger that writes only the exception message never records the cause, so the traceback has to be recorded with the exception chain included, as logger.exception does.
Closing
In a setup that follows the current MCP spec, the conditions for using this auth as-is are these: both kinds of metadata are published in a shape the SDK can read, client_credentials and client_secret_basic are accepted, requesting the advertised set verbatim as the initial scope is acceptable, and in-memory token storage is enough.
In addition, the token this auth obtains represents the client’s own permissions and includes no user’s.
A connection that needs per-user authorization means passing your own implementation as auth_provider.
For any other connection that fits the conditions above, the few lines at the top of this article are all you write.
Footnotes
-
A caveat if you build your own AS to try this: the MCP SDK 1.29.0 client requires
authorization_endpointin the AS metadata, which RFC 8414 allows to be omitted. Even an AS that supports only client_credentials needed a dummy endpoint to satisfy that check. ↩