Hosted Notion MCP (DCR): client re-registers per session → repeated full re-auth (orphaned refresh tokens)

Resolved 💬 1 comment Opened Jun 5, 2026 by kelvSYC-ironclad Closed Jun 9, 2026

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 notion plugin: { "type": "http", "url": "https://mcp.notion.com/mcp" } (no oauth block, unlike the slack plugin)
  • 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 foundRedirecting to authorization URLHTTP Connection failed: Unauthorized
  • Compounding idle drops: Connection error: SSE stream disconnected: TimeoutError (×11, ~every 5 min) → after 3 strikes Closing 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-credentials generic 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_hosts does not include Notion (only microsoft365/gmail/gcal).
  • token_refresh_buffer_ms = 600000 (10 min) — in-session refresh works fine.

Ask

  1. Persist and reuse the DCR client registration (client_id/secret) + refresh token for mcp.notion.com across sessions; only re-register on a genuine invalid_client.
  2. 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.
  3. 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?

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗