Custom HTTP MCP server: OAuth authorization does not persist across launches, and server visibility is inconsistent across surfaces
Status Open
Maintainer reply None cached
Workaround ✓ Mentioned in description ↑
Activity 0 comments · opened Jul 25, 2026
Custom HTTP MCP server: OAuth authorization does not persist, and server visibility is inconsistent across surfaces
Summary
A custom HTTP-type MCP server (21st, https://21st.dev/api/mcp, header-based x-api-key auth plus an OAuth authorization step surfaced via /mcp) exhibits two related problems:
- OAuth authorization does not persist across separate
claudeprocess launches in the terminal, even though it is stored at user/global scope (~/.claude.json). - Server visibility is inconsistent across different Claude surfaces (terminal CLI vs. a separate hosted/managed session type) in ways that don't correlate with configuration changes.
Environment
- macOS (Darwin 25.5.0), zsh
- Claude Code CLI (terminal) + a separate hosted Claude session surface (non-interactive, curated tool set — Gmail/Drive/Office connectors, computer-use, DesignSync, etc.)
- MCP server config:
type: "http",url: "https://21st.dev/api/mcp",headers: { "x-api-key": "${API_KEY_21ST}" } API_KEY_21STexported via~/.zshrc
Chronology / reproduction steps
- Installed the server at project scope via the vendor's own installer (
21st init --client claude --write), which wrotemcpServers.21stinto<project>/.mcp.json. - In the terminal,
/mcpshowed21stunder Project asneeds authentication. Selected it → "Authenticate" → browser OAuth flow → "Authentication successful" →/mcpshowed✔ connected. - Opened a new conversation on the separate hosted surface, in the same project directory.
21stdid not appear in that session's tool list at all (confirmed via a tool search / diagnostic call — no21st-prefixed tools existed, whereas another user-scope stdio MCP server,firepuppet, configured in~/.claude.json, was available and functional in that same hosted session). - Hypothesis: the hosted surface only loads user/global-scope (
~/.claude.json) MCP servers, not project-scope (.mcp.json) ones — plausibly because project-scope servers require an interactive one-time trust prompt that a non-interactive surface can't satisfy. - Copied the same server definition into
~/.claude.jsonundermcpServers.21st(global/user scope), alongside the already-workingfirepuppetentry. - Opened another new conversation on the hosted surface:
21stnow did appear in the tool list, but asneeds authentication(progress — confirms the scope hypothesis above). - Went back to the terminal, ran
/mcp: it now listed21sttwice — once under Project (✔ connected, from step 2) and once under User MCPs (needs authentication, the new global entry). Selected the User MCPs entry, authenticated via browser again → "Authentication successful." - Asked the user to open yet another new conversation on the hosted surface to confirm persistence. Before that could be tested, the user separately closed their terminal and opened a brand new terminal, ran
claude, and/mcp→ User MCPs21stagain showedneeds authentication, despite having just been successfully authenticated in step 7.
- Inspected
~/.claude/mcp-needs-auth-cache.json: contained{"21st":{"timestamp":<matches the moment of this /mcp check>}}— confirms this is a fresh negative result, not a stale cache artifact. - No token/credential file was found for this server elsewhere under
~/.claude/; a keychain search was attempted but abandoned (see Notes) since it started prompting for the user's macOS login/keychain password mid-search, which should not be necessary to diagnose an application-level config issue. - Conclusion: the OAuth authorization for this HTTP MCP server does not persist across separate
claudeprocess launches, even at user/global scope. Contrast withfirepuppet(stdio server, no OAuth step) and with claude.ai-linked connectors (Gmail/Drive/Indeed), which stay connected indefinitely without re-authorization.
- Re-authenticated again in that terminal session (
/mcp→ Re-authenticate → succeeded: "Authentication successful. Reconnected to 21st."). - User then re-authenticated inside the hosted-surface conversation itself (not the terminal) that had been showing
needs authentication. Result, per that session's own tool search:21sthad disappeared from the tool list entirely — no longer listed even as "needs authentication." Two hypotheses proposed by that session (unverified): either the authorization succeeded but that session doesn't hot-reload its tool list, or the connector simply isn't attached to that project/session.
Why this matters
Step 8 alone (auth not persisting across terminal launches) makes any HTTP-type custom MCP server with an OAuth step effectively unusable without re-authenticating every single session — a materially worse experience than stdio servers or claude.ai-native connectors. Step 10 suggests the problem compounds on hosted/managed surfaces, where the failure mode isn't even consistent (auth-required vs. server-absent) across attempts, making it unclear to an end user what state the connector is actually in or what action would fix it.
What would help
- Confirmation of whether OAuth tokens for user-scope HTTP MCP servers are intended to persist across
claudeprocess restarts (if yes: this is a bug in token storage/retrieval; if no: the/mcpUI should say so instead of implying a normal one-time authorization). - Clarity on whether project-scope (
.mcp.json) MCP servers are intentionally excluded from non-interactive/hosted session surfaces, and if so, documentation of that behavior so users aren't led to duplicate config across scopes as a workaround. - Investigation into why the hosted surface's reported state for the same server flipped from "needs authentication" to "absent from tool list" between two consecutive conversations with no configuration change in between.
Notes
- A
security dump-keychaincommand run during diagnosis triggered a macOS keychain password prompt on the user's screen. This was an unnecessary diagnostic step and was aborted without the user entering their password — flagging only in case it's relevant to how/where this type of credential should be stored (it apparently isn't using the keychain today, based on the empty search results before it was aborted).