MCP: a transient `/v3/session` failure is reported to the client as an invalid token
Severity: Low
File: apps/mcp/src/server/auth/index.ts:70-93
Description
validateApiKey collapses every failure mode into null:
try {
const session = await fetchSession(token, apiUrl)
...
} catch (error) {
console.error("API key validation error:", error)
return null
}fetchSession throws on any non-OK status and on the 30s AbortSignal.timeout.
handleMcpRequest then maps null to unauthorizedResponse(url, true):
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Invalid or expired token"},"id":null}with WWW-Authenticate: Bearer error="invalid_token".
So an API 500, a Hyperdrive blip, or a slow session endpoint tells the MCP client
its perfectly valid sm_ key is invalid. Clients act on invalid_token by
discarding the credential and starting a re-auth flow — users get pushed through
the browser connect flow to fix an outage on our side.
The OAuth path has the same shape, but there the failure is local JWT
verification (no network), so null genuinely does mean "bad token". The API-key
path added in #1537 is the first one where null conflates "rejected" with
"couldn't ask".
fetchSession already attaches status to the thrown error, so the information
needed to distinguish the cases is present:
throw Object.assign(new Error(`Session request failed with status ${response.status}`),
{ status: response.status })Suggested fix
Distinguish 401/403 (genuinely invalid → null → invalid_token) from
everything else (transient → propagate, and have handleMcpRequest return 503
with -32000 "Authentication service unavailable"). Serving a stale cache entry
past its TTL during an outage would be a reasonable second layer, though it
extends the revocation window the PR already flagged.
Source: supermemoryai/supermemory