Troubleshooting
This page covers the issues people hit most often with the MCP Gateway, organized by symptom. Each entry lists what you'll see, the likely cause, and the fix.
401 with no WWW-Authenticate header
Symptom. The MCP client gets a 401 Unauthorized response when it first
tries POST /v1/mcp/<slug>, but the response has no WWW-Authenticate: Bearer
header. The client never discovers the authorization server.
Likely cause. The route in routes.oas.json doesn't have an MCP OAuth
policy in its inbound chain. Without mcp-oauth-inbound or
mcp-auth0-oauth-inbound, the route returns a plain 401 from another policy or
handler that doesn't speak the MCP authorization spec.
Fix. Add the OAuth policy to the route's policies.inbound array. For
Portal-managed projects, the Catalog page handles this automatically;
double-check that the project's identity provider is configured. For code-config
projects:
Code
A single MCP OAuth policy can (and should) be shared across every MCP route in the project.
403 with error="insufficient_scope"
Symptom. An authenticated request to a Virtual MCP returns 403 with
WWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:tools", resource_metadata=....
Likely cause. The access token was issued for a different scope set than the
gateway expects. The only scope the gateway issues is mcp:tools. A DCR client
registered with a different scope, or a stale token from a previous
configuration, will fail this check.
Fix. Re-register the client and let it discover the scope through the AS
metadata document, or use step-up authorization to re-issue the token with
scope=mcp:tools. For MCP clients that support incremental scope consent, the
gateway's 403 response includes the required scope in WWW-Authenticate for the
client to re-request.
Connect-required errors
Symptom. The first tool call after authentication returns a JSON-RPC error
with code: -32042 and a payload containing "state": "authenticating" and an
authUrl. The MCP client either opens a browser tab (modern clients) or
surfaces the URL for the user to copy (older clients).
Likely cause. This isn't an error — it's the gateway asking the user to complete the upstream OAuth flow for the first time. Each (user, upstream) pair connects once; after that, requests skip this step entirely.
Fix. Open the authUrl in a browser and complete the upstream provider's
login. The page returns to the gateway, which stores the encrypted tokens, and
the next tool call succeeds.
Reconsent prompts
Symptom. A user who connected an upstream weeks ago suddenly sees a
connect-required error again, this time with "state": "reconsent_required" and
a message like "Linear authorization must be renewed."
Likely cause. The upstream provider revoked the gateway's OAuth client, or the user revoked the connection from the upstream's dashboard, or the refresh token expired according to the upstream's policy. The stored connection record still exists, but its tokens are no longer usable.
Fix. Follow the same flow as a first-time connect — the user re-authorizes the upstream and the gateway replaces the stored tokens. No admin action is required.
Compatibility date too old
Symptom. Calls work most of the time, but a request that triggers an upstream 401 (for example, an upstream token expired mid-session) returns the raw 401 to the client instead of refreshing and retrying.
Likely cause. compatibilityDate in zuplo.jsonc is older than
2026-03-01. MCP Gateway features require 2026-03-01 or later.
Fix. Bump the compatibility date:
Code
Then redeploy. See the reference page for context.
Auth0 policy rejects domain with "https://" prefix
Symptom. The runtime rejects the project's MCP Auth0 policy with a
configuration error that mentions an invalid auth0Domain value.
Likely cause. McpAuth0OAuthInboundPolicy requires a bare hostname — not a
URL. Passing https://my-tenant.us.auth0.com/ fails validation.
Fix. Set auth0Domain to just the hostname:
Code
Custom domain → wrong issuer in AS metadata
Symptom. The Authorization Server metadata document advertises an issuer
like https://my-project.zuplosite.com instead of your custom domain
(https://api.example.com). MCP clients fail audience validation because the
token's iss doesn't match the URL they expect.
Likely cause. The reverse proxy or CDN in front of the gateway isn't
propagating the original Host header, and isn't setting X-Forwarded-Host
either. The gateway derives its issuer from the incoming request's effective
host.
Fix. Configure your edge proxy to forward one of these:
- Preserve the original
Hostheader end-to-end. - Or set
X-Forwarded-Host: api.example.comon requests to the gateway.
The gateway uses X-Forwarded-Host when present, falling back to Host. After
updating the proxy, fetch
https://api.example.com/.well-known/oauth-authorization-server and confirm the
issuer field matches.
Dev server needs a restart after the first MCP client connects
Symptom. When running zuplo dev locally, the first MCP client connection
succeeds, but subsequent requests hang or return unexpected errors. Restarting
zuplo dev makes it work again.
Fix. When in doubt, restart zuplo dev. This is a local development quirk
only — the production runtime is unaffected.
GET on /v1/mcp/<slug> returns 405
Symptom. A client (or a browser, or a probe) sends GET /v1/mcp/linear-prod
and gets back 405 Method Not Allowed with Allow: POST and a message about
Streamable HTTP.
Likely cause. This is by design. The gateway implements the Streamable HTTP transport as POST-only and doesn't open SSE streams for server-initiated messages.
Fix. Use POST for all MCP requests. Browser-based health checks or uptime
monitors should hit a different endpoint — pointing them at the well-known PRM
URL (/.well-known/oauth-protected-resource/<path>) is a good lightweight
option.
Tool name doesn't match in capability filter
Symptom. A capability-filter policy lists a tool by name, but the upstream still appears to return everything (or returns nothing).
Likely cause. The filter matches case-sensitively and exactly. A typo, a stray space, or a capitalization difference makes the entry not match, and the policy treats the tool as if it weren't on the allow-list.
Fix. Run tools/list against the unfiltered upstream first and copy the
name exactly. For example:
Code
Then paste the exact name values into the policy's tools array.
Browser session expires after 8 hours
Symptom. A long-running MCP client that's been idle for most of a day suddenly redirects the user to the identity provider's login page the next time it makes a request.
Likely cause. The __mcp_session cookie has an 8-hour default lifetime.
Once it expires, the next interaction that hits the consent or login flow has to
re-authenticate the user against the IdP.
Fix. Either accept the re-login (this matches a typical workday) or extend
the session by setting browserLogin.sessionTtlSeconds on the OAuth policy to a
longer value. The trade-off is the usual one: longer sessions mean a longer
window where a stolen cookie is useful.
Where to look when none of the above match
- Open the project's Logs in the Portal and filter on the request's
zuplo-request-id. Gateway log entries include the relevantoperationIdandsubjectId. - Open the Analytics dashboard and switch to the MCP tab. The "Top Reason Codes" and "Failure Origins" panels surface the highest- cardinality failure modes for the current time window.
- For OAuth-specific issues, the MCP Inspector reproduces the full discovery and authorization flow against any gateway URL and gives a step-by-step view of where it breaks.