How authentication works
Why the server checks the token audience, and why the scope is the second half.
/mcp accepts an OAuth 2.0 bearer token. Two things about that token are
checked, and neither of them is which client asked for it.
Why the client identity is not checked
Ordinary applications register with the authorization server in advance, get a fixed client identifier, and the server can then pin that identifier.
MCP clients cannot work that way. A client registers itself, at the moment you add the server, through Dynamic Client Registration. It receives a fresh identifier nobody could have written down beforehand, and a different one next time. No static list could ever name it.
So the identifier is not what binds the token. The audience is.
Source: services/runtime/src/runtime/mcp/verifier.py:13-20 and
services/runtime/src/runtime/config.py:524-535.
First half: the audience
The realm stamps a custom audience onto the tokens it issues. The server verifies the token's signature, its issuer and its validity window, and requires that audience to be present.
Source: services/runtime/src/runtime/mcp/verifier.py:120-124, with the
required value read from
services/runtime/src/runtime/config.py:535.
What that proves is narrow and worth stating plainly: the token was minted for this resource. It says nothing about which client asked for it, because the mapper that stamps the audience is a realm-level thing and applies to tokens broadly, self-registered clients included.
Source: services/runtime/src/runtime/mcp/verifier.py:22-30.
Second half: the scope
The second check is the granted scope. The verifier returns the token's real
granted scopes, read from the token's own scope claim rather than assumed, and
the transport requires the configured scope to be among them.
Source: services/runtime/src/runtime/mcp/verifier.py:136 for the scopes, and
services/runtime/src/runtime/mcp/asgi.py:78-82 for the requirement.
The scope is set up in the realm as an optional client scope, not a default one. That distinction is the whole point. A default scope is added to every token the realm issues, so a client inherits it without asking. An optional scope is granted only when the client's own authorization request explicitly asks for it.
Put the two together: the audience says the token was minted for this resource, and the scope says the client deliberately asked to use it.
How a client knows the scope exists
It reads it. The protected resource metadata publishes the required scope as
scopes_supported, and the MCP authentication specification has clients read
that field before they send an authorization request.
Source: services/runtime/src/runtime/mcp/well_known.py:43-56, with the
published value at services/runtime/src/runtime/mcp/well_known.py:55.
Omitting that field would leave the scope requirement enforced but undiscoverable: every compliant client would fail, because nothing would have told it the scope was there to ask for.
The two discovery documents
Both are public, and both carry identical content.
| Path | Who fetches it |
|---|---|
/.well-known/oauth-protected-resource/mcp | The resource-scoped path an MCP client is expected to use, and the one named in the WWW-Authenticate header of a 401 |
/.well-known/oauth-protected-resource | The same document at the origin's own well-known path, for a client that has not adopted the resource-scoped one |
Source: services/runtime/src/runtime/mcp/well_known.py:1-21 and
services/runtime/src/runtime/mcp/well_known.py:62-68.
Both documents are built once from configuration, never from the request's
Host header. A metadata document asserts things about itself, so it must not
take those assertions from something the caller supplies.
Source: services/runtime/src/runtime/mcp/well_known.py:18-20 and
services/runtime/src/runtime/config.py:553-559.
What is checked once, and what is checked every time
Authentication happens once per connection. Everything about the workspace happens per call.
| Check | When |
|---|---|
| Signature, issuer, validity window, audience | Once, when the connection authenticates |
| Required scope | Once, when the connection authenticates |
| Which workspace this call acts in, and membership of it | Every tool call |
| Role floor for this tool in this workspace | Every tool call |
| MCP access not revoked here | Every tool call |
Source: services/runtime/src/runtime/mcp/verifier.py:32-50 for why no workspace
is resolved at authentication time, and
services/runtime/src/runtime/mcp/tools.py:132-166 for what happens on each
call instead.
What this does not prove
Registration is anonymous. Any client that registers can ask for the scope and, if the person signing in has an account and a workspace, will get a working token. See Operator setup for the full statement of what this arrangement stops and what it does not.