Error 401: invalid_token — OAuth token rejected
Your agent got a 401 from the tunnel:
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer error="invalid_token", error_description="the access token is missing, expired, or invalid", resource_metadata="https://<your-host>/.well-known/oauth-protected-resource"{ "error": "invalid_token", "error_description": "this tunnel requires an OAuth 2.1 access token", "resource_metadata": "https://<your-host>/.well-known/oauth-protected-resource"}This only happens on tunnels whose auth mode is OAuth. A tunnel in the default mode does not gate agent requests at all, so if you are seeing this and did not expect authentication, check Settings → the tunnel’s auth mode first.
There is only one challenge code
Section titled “There is only one challenge code”Every rejection reason collapses to invalid_token. Malformed token,
unsupported algorithm, unknown signing key, bad signature, expired, wrong
issuer, wrong audience — all of them. The code does not tell you which.
The one exception is a missing token: the WWW-Authenticate header then
carries no error= parameter at all, just the resource_metadata pointer.
(The JSON body still says "error": "unauthorized" in that case — the header
and the body use different words for the same situation.)
So do not try to branch on the code. Work through the causes below instead.
Why it was rejected
Section titled “Why it was rejected”No token at all. The most common case, and the one the challenge is designed for — see the discovery flow below.
Wrong audience. A token minted for a different host or tunnel. Each tunnel’s
tokens are bound to its own canonical https://<host>, so a token that works
for one tunnel is invalid on another by design. This is the failure to suspect
when a token that “definitely works” suddenly does not — check which host it
was issued for.
Expired. Access tokens are short-lived. Headless agents that cache a token and never refresh will hit this on a schedule.
Consent revoked. The tunnel owner can revoke an agent’s consent from the dashboard. Existing tokens stop being accepted.
Wrong issuer, bad signature, unknown key. Points at a token from an entirely different system, or a signing key that has been rotated out.
The resource_metadata pointer is the fix path
Section titled “The resource_metadata pointer is the fix path”That URL is a RFC 9728 Protected Resource Metadata document, and a spec-compliant MCP client uses it automatically: it reads the challenge, fetches the metadata, discovers where to get a token, obtains one and retries — without you configuring an issuer anywhere.
If your client supports that flow, a 401 on first contact is not an error to
fix; it is the handshake working. If your client does not, you need to attach a
token yourself.
Headless agents: use a Service Token
Section titled “Headless agents: use a Service Token”The interactive flow expects a human at a browser to approve the agent on a consent screen. For a cron job, a container or a server-side agent, mint a Service Token in the tunnel’s Settings instead. It is validated exactly like a regular access token — same signature, same audience binding, same revocation — but it is long-lived and needs no browser.
See Authentication: OAuth & Service Tokens.
A 500 is not a 401
Section titled “A 500 is not a 401”Two OAuth failures deliberately return 500, because they are service-side
problems and not something the caller can fix by presenting a better token:
"OAuth is not configured on this edge"— the tunnel asks for OAuth mode but no issuer is configured."token issuer is unreachable"— the signing keys could not be fetched, and mcppipe failed closed rather than accepting an unverifiable token.
If you see either, retrying with a different token will not help.
Not this error?
Section titled “Not this error?”- Request blocked before authentication ran: 403 — the IP allowlist is checked first.
- Token accepted, quota or rate limit refused it: 429.