MCP: a transient `/v3/session` failure is reported to the client as an invalid token

Author: rajarshidattapyCreated Aug 19, 2026Updated Aug 29, 2026
Labelsbug

Severity: Low File: apps/mcp/src/server/auth/index.ts:70-93

Description

validateApiKey collapses every failure mode into null:

typescript
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):

json
{"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:

typescript
throw Object.assign(new Error(`Session request failed with status ${response.status}`),
    { status: response.status })

Suggested fix

Distinguish 401/403 (genuinely invalid → nullinvalid_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