Hosted Notion MCP (DCR): client re-registers per session → repeated full re-auth (orphaned refresh tokens)
Description
Authenticating the hosted Notion MCP server (https://mcp.notion.com/mcp) via the official notion plugin works, but the session repeatedly drops and demands a full interactive browser re-auth (open URL + paste callback). Notion uses Dynamic Client Registration (RFC 7591), which is supposed to persist client credentials for reuse — but Claude Code appears to register a new OAuth client on many sessions/resumes instead of reusing the persisted one, orphaning the prior refresh token.
This is a follow-on to the now-closed #52961 (which moved Notion off the static claude.ai metadata client onto DCR). DCR fixed the Invalid redirect_uri error, but surfaced this persistence problem.
Environment
- macOS (Darwin), zsh
- Hosted Notion MCP via official
notionplugin:{ "type": "http", "url": "https://mcp.notion.com/mcp" }(nooauthblock, unlike theslackplugin) - Runs under Conductor (headless/remote): logs show
Skipping browser open (skipBrowserOpen=true), so every re-auth is a manual open-URL-and-paste - Many concurrent Conductor workspaces active (~40+ in
~/Library/Caches/claude-cli-nodejs/)
Evidence (mcp-logs-plugin-Notion-notion, one workspace, several days)
- 5 distinct dynamically-registered
client_ids across sessions (a new one each re-auth) No client info found(×5),Cleared stored tokens(×5)Token refresh failed with invalid_grant: Grant not found→Redirecting to authorization URL→HTTP Connection failed: Unauthorized- Compounding idle drops:
Connection error: SSE stream disconnected: TimeoutError(×11, ~every 5 min) → after 3 strikesClosing transport (max consecutive terminal errors)→HTTP connection closed after ~900–1200s
Contrast — Slack hosted MCP (slack plugin, which does pin a fixed oauth.clientId + callbackPort): 1 client_id, 0 full re-auths, 0 logged transport teardowns over the same period.
Where credentials are stored (investigation)
- Nothing in
~/.claude.json(no oauth/client/token keys) and no~/.claude/*cred*file. - MCP OAuth state is in the macOS Keychain bundled inside the single
Claude Code-credentialsgeneric item — there is no per-host keychain item (mcp.notion.com,notion, etc. return nothing).
Hypothesis
Because every workspace shares the one Claude Code-credentials keychain blob and (apparently) a single per-host Notion entry, concurrent workspaces race / last-writer-wins on that entry: when workspace A re-auths after an SSE teardown, it rewrites the Notion client/token, so workspace B's stored refresh token + client-info no longer match → Grant not found / No client info found → re-register (new client_id) → full re-auth. This compounds with the ~5-min SSE idle teardown that forces frequent reconnects.
Ruled out
tengu_mcp_local_oauth_blocked_hostsdoes not include Notion (only microsoft365/gmail/gcal).token_refresh_buffer_ms= 600000 (10 min) — in-session refresh works fine.
Ask
- Persist and reuse the DCR client registration (
client_id/secret) + refresh token formcp.notion.comacross sessions; only re-register on a genuineinvalid_client. - Make per-host MCP OAuth entries concurrency-safe (merge, don't last-writer-wins clobber) so multiple simultaneous Claude Code instances / Conductor workspaces don't invalidate each other's grants.
- Consider a longer SSE keepalive / not tearing down the transport so aggressively on idle, to reduce reconnect churn that triggers the above.
Open question
Where is the DCR client info meant to be persisted, and why is No client info found hit on a stable workspace? If it's the shared Claude Code-credentials blob, is access serialized across concurrent processes?
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗