Your MCP server checks that every request carries a valid bearer token: the signature verifies, the issuer is your authorization server, the token has not expired. That sounds complete. It leaves one question unasked: was this token issued for this server?
If the answer is never checked, a token issued for any other service behind the same authorization server works on your MCP server too.
The setup that makes this matter
One authorization server usually issues tokens for several resources. A company runs an internal API, a public API, a billing service and more recently an MCP server, all trusting the same issuer. Every token carries an aud (audience) claim naming the resource it was meant for:
{
"iss": "https://auth.example.com",
"aud": "https://billing.example.com",
"sub": "user-4821",
"exp": 1794400000
}That token is genuine. Its signature is valid and its issuer is the one your MCP server trusts. If the MCP server does not compare aud against its own identifier, it accepts the token and the holder can call your tools with a credential that was only ever meant for billing.
The practical consequence is that your MCP server is as hard to get into as the least protected service sharing its issuer. A token leaked from a log file of a low-value service, or obtained by a client that was only granted access to that service, becomes a key to an endpoint that can run tools.
What the specs require
RFC 8707 (Resource Indicators) lets a client name the resource it wants a token for, so the authorization server can set the audience accordingly. RFC 9068, the JWT profile for access tokens, requires the resource server to check that aud contains its own identifier and to reject the token if it does not. The MCP authorization spec makes both mandatory from revision 2025-06-18: clients must send the resource parameter and servers must only accept tokens issued for themselves. The earlier 2025-03-26 revision does not mention audience, so a server built against it may never have added the check.
Why metadata does not tell you
There is no standard metadata field that says a server validates the audience. RFC 8707 defines the resource request parameter and nothing a server could publish about it. Some servers put a resource_indicators_supported flag in their metadata, but no specification defines that flag and it says nothing about what happens at runtime. The only way to know is to present a token with the wrong audience and see what comes back.
Test it
patchbay audit does this as an opt-in probe. You supply a token that is valid on the same authorization server and was issued for a different resource:
npx -y @patchbay-nebius/agent audit https://mcp.example.com/mcp --probe-audience=<token>The probe sends tools/list with that token. A 401 or 403 means the server rejected it and the check passes. If the request succeeds you get AUDIENCE-001 at critical severity. Anything else is reported as inconclusive. The token can also be passed through the PATCHBAY_PROBE_AUDIENCE_TOKEN environment variable, which keeps it out of shell history and CI logs.
Because the probe needs a token you are entitled to hold, it is not part of public sweeps and there is no ecosystem-wide number for this finding. Run it against servers you operate.
The fix
- Decide on one canonical resource identifier for the MCP server (normally its public URL) and publish it as
resourcein/.well-known/oauth-protected-resource. - On every request, compare the token's
audclaim with that identifier.audmay be a string or an array; accept the token only if your identifier is in it. - Reject everything else with HTTP 401. Do not fall back to accepting tokens that have no
audat all. - If you use opaque tokens, the same rule applies to the audience returned by token introspection.
Most JWT libraries take the expected audience as a verification option and do nothing with the claim unless you pass it. It is worth checking that the option is set in the code path your MCP endpoint actually uses.