Skip to content

fix(mcp): report transient session-endpoint failures as 503, not invalid token - #1587

Open
Sravanjangam wants to merge 2 commits into
supermemoryai:mainfrom
Sravanjangam:fix/mcp-transient-session-errors
Open

fix(mcp): report transient session-endpoint failures as 503, not invalid token#1587
Sravanjangam wants to merge 2 commits into
supermemoryai:mainfrom
Sravanjangam:fix/mcp-transient-session-errors

Conversation

@Sravanjangam

@Sravanjangam Sravanjangam commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #1551.

Hi supermemory team 👋 Thanks for building such a great product! While running a security & quality audit of the repo we hit this issue and put together a small, tested fix — details below.

Problem

validateApiKey collapsed every fetchSession failure — network errors, timeouts, any non-OK status — into null, which the transport maps to JSON-RPC -32000 "Invalid or expired token". During an api.supermemory.ai/Hyperdrive outage this made MCP clients discard perfectly valid sm_ keys and forced users through browser re-authentication at exactly the wrong moment, amplifying incidents into support load.

Solution

Classify failures instead of collapsing them. A new TransientAuthError is thrown for timeouts (AbortError/TimeoutError) and upstream statuses other than 401/403; the transport maps it to a 503 JSON-RPC response with Retry-After: 5. Genuine rejections (401/403/schema failures/missing org) still resolve null → unchanged invalid_token behavior. Success-path caching untouched.

Changes

  • apps/mcp/src/server/auth/index.tsTransientAuthError class + typed catch in validateApiKey
  • apps/mcp/src/server/index.ts → try/catch around token validation → 503 response with Retry-After
  • apps/mcp/src/server/auth/index.test.ts → +2 tests (500 → TransientAuthError; timeout → TransientAuthError)
  • applied review feedback: auth resolution extracted into resolveAuthUser() returning a typed AuthResolution union — call site is all-const, behavior unchanged

Verification

Fresh from the committed branch: bunx vitest run src/server/auth15/15 pass, including the pre-existing revoked-key → null case proving rejection behavior is unchanged; tsc adds zero new errors vs baseline; Biome clean.


Happy to iterate on any of this — feedback and reworks very welcome! 🙏

Environment

  • macOS 26.1 (arm64) · bun 1.4.0 · node v26.7.0
  • vitest 3.2.4 (workspace-pinned) · Biome lint clean
  • Branch fix/mcp-transient-session-errors — all gates re-run fresh at commit b561e12a27c5

…lid token

validateApiKey collapsed every fetchSession failure (network error,
timeout, any non-OK status) to null, which the transport mapped to
'Invalid or expired token'. During api.supermemory.ai outages that made
MCP clients discard perfectly valid sm_ keys and forced users through
re-authentication at exactly the wrong moment (supermemoryai#1551).

- new TransientAuthError: validateApiKey rethrows it for timeouts and
  non-401/403 upstream statuses; only genuine rejections return null
- transport maps TransientAuthError to a 503 JSON-RPC response with
  Retry-After instead of the invalid_token error
- tests cover the 500 and timeout paths; 401 still resolves null
Comment thread apps/mcp/src/server/index.ts Outdated
const authUser = isApiKey(token)
? await validateApiKey(token, apiUrl)
: await validateOAuthToken(token, apiUrl, mcpResource)
let authUser: AuthUser | null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable authUser is declared with let on line 185. While let is technically required here because the variable is assigned inside a try block, the style guide states 'Use const by default. Use let only when reassignment is needed.' In this case the variable is not reassigned after initial assignment — it is only assigned once inside the try block. Consider restructuring to avoid let by extracting the try/catch into a helper function that returns the value, allowing const authUser = await resolveAuthUser(...) at the call site.

Spotted by Graphite (based on custom rule: TypeScript style guide (Google))

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Extracts resolveAuthUser() returning AuthResolution (ok/user vs
invalid vs transient), so the call site is all-const and the transient
503 mapping stays explicit. Behavior unchanged; addressed from
Graphite's inline review on supermemoryai#1587.
graphite-app Bot pushed a commit that referenced this pull request Aug 24, 2026
…#1591)

Cherry-picks #1587 from @Sravanjangam, plus the OAuth half on top.

When the auth backend is slow or returns a 5xx, the MCP server currently answers `invalid_token`. That is the protocol's signal to discard the credential and re-authenticate, so a brief upstream blip logs every connected client out, and `sm_` API key users have no automatic way back. These requests now return 503 with `Retry-After: 5` so clients retry instead.

His change covered the API key path only. This shares one `transientAuthErrorFor` helper between `validateApiKey` and `validateOAuthToken`, so a JWKS timeout or a 5xx also returns 503 on the OAuth path that Claude, Cursor and browser clients use.

Genuinely bad tokens are unaffected: bad signature, expired, and no-matching-key still resolve to 401. Verified across all seven cases.

Co-Authored-By: Sravanjangam <163002695+Sravanjangam@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant