Why Claude Code MCP servers keep asking you to sign in again Running 40-60 concurrent Claude Code sessions against the same OAuth-protected remote MCP servers causes a refresh-token rotation race that repeatedly forces re-authentication, according to a DocSpring engineer's blog post. The MCP authorization specification (2025-11-25) requires public clients to rotate refresh tokens, and RFC 9700 lets servers revoke the active refresh token when a spent one is reused; a Shortcut server log from 8 November recorded "Refresh lock held by another process" 169 times, "Token refresh failed, returning current tokens" 143 times, and "Skipping connection (cached needs-auth)" 138 times. The author resolved it by routing all sessions through a single local MCPProxy process that signs in once per server and refreshes tokens for every session, eliminating the repeated sign-ins. I usually have 40-60 Claude Code sessions open at once, each working on its own branch. Most of them use the same remote MCP servers: Shortcut for our stories, Notion for docs, Intercom for support conversations, Sentry for errors, and Slack. Every one of those servers signs you in with OAuth. Many of my sessions would lose their sign-in to at least one server, and this would happen many times per day. Claude Code showed "needs authentication" next to Shortcut or Notion, I'd sign in again in one session, and the others would stay broken until I reconnected each one by hand. A few hours later it would happen again. It turned out to be a race between the sessions, and adding more sessions made it worse. The fix was to stop letting each session hold its own copy of the tokens. Now one local process, MCPProxy https://github.com/smart-mcp-proxy/mcpproxy-go?ref=docspring.com , signs in to each server once and refreshes the tokens for everyone, and every Claude Code session connects to it instead. I haven't had to sign in again since. I've seen plenty of people talking about this on X, so I thought I would write a blog post to cover what causes it, what we changed in MCPProxy, and how my sessions use it. Why the sessions sign each other out When you sign in to a remote MCP server, Claude Code gets two tokens. The access token is what it sends with each request, and it expires after a while: hours for some servers, much less for others. The refresh token is what Claude Code uses to get a new access token without opening the browser again. The MCP specification requires refresh token rotation for clients like Claude Code. Its authorization spec https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization?ref=docspring.com says "For public clients, authorization servers MUST rotate refresh tokens". Rotation means every refresh hands back a new refresh token and invalidates the old one, so each refresh token works once. Slack's token rotation docs https://docs.slack.dev/authentication/using-token-rotation?ref=docspring.com put it plainly: "Refresh tokens are designed to be used once." Rotation also lets a server spot a stolen token. If an old refresh token turns up again, someone else has a copy. RFC 9700 https://www.rfc-editor.org/rfc/rfc9700?ref=docspring.com section-4.14.2 , the OAuth security best practices, says the server can't tell which party is the attacker, so "it will revoke the active refresh token." Some servers revoke the whole sign-in when that happens. Now put 47 Claude Code processes on one Mac. They all read the same stored sign-in for Shortcut, and when its access token expires, many of them notice within the same few seconds: 1. The first session to refresh gets a new pair of tokens and saves them. The old refresh token is now spent. 2. Other sessions refresh with the spent token. The server answers invalid grant , and it may treat the reuse as theft and revoke the new tokens too. 3. A session that gets invalid grant decides the sign-in is gone and clears the stored tokens, which can wipe out the good tokens the first session just saved. 4. Each session remembers that the server needs authentication. Signing in again in one session doesn't fix the others until they reconnect. Claude Code does try to prevent this. It takes a lock file before refreshing, waits if another process holds it, and checks whether another process has already saved fresh tokens. That didn't hold up with dozens of sessions. Claude Code writes a log for each MCP server, and on 8 November the one for Shortcut showed how often the lock lost: - "Refresh lock held by another process": 169 times - "Token refresh failed, returning current tokens": 143 times - "Skipping connection cached needs-auth ": 138 times - "Token refresh failed with invalid grant", followed by "Invalidated credentials", with dozens of sessions refreshing within the same few seconds A session that waits five times for the lock gives up and keeps its expired token, and a session that loses the race clears the sign-in. Shortcut failed most often because its access tokens expire quickly, so the race came up many times a day. Notion's last longer, so it broke less often. Other people have reported the same race. Issue 84440 https://github.com/anthropics/claude-code/issues/84440?ref=docspring.com reproduces it with five claude mcp get commands run at once against a server that rotates refresh tokens, and shows the winning session's new token being stored and then wiped. It was closed as not planned in September, as was 43000 https://github.com/anthropics/claude-code/issues/43000?ref=docspring.com , "MCP OAuth with multiple terminals open causes re-auth". Claude Code 2.1.325 logs messages about keeping tokens that another process rotated, so it may handle this better now. In those issues, the only workaround anyone reported was running fewer sessions. One process holds the tokens If only one process ever refreshes a token, there's nothing to race. That process has to be something that runs all the time, holds the sign-in for every server, and lets every Claude Code session use those servers through it. MCPProxy does this. It's an open-source MIT MCP proxy written in Go: one binary that runs in the background on your machine, with a web UI and an optional macOS menu-bar app. You add your MCP servers to it once and sign in to each one through MCPProxy, which keeps the tokens in its own data directory. Its docs say refreshes are "serialized per server": however many requests notice an expiring token at once, MCPProxy sends one refresh to the server, and every waiting request gets the result. Claude Code then talks to MCPProxy at http://127.0.0.1:8080 instead of talking to Shortcut or Notion. Each session authenticates to MCPProxy with a static key, which never expires and never needs refreshing, so 47 sessions can share it safely. The OAuth tokens never leave MCPProxy. What we changed in our fork Out of the box, MCPProxy puts all your servers behind one endpoint and exposes them as a small set of its own tools. An agent calls retrieve tools to search across every server, then calls the tool it found through call tool read , call tool write or call tool destructive . There's also a mode that lists every tool directly, renamed to server tool . Both would have changed the name of every tool my sessions use. Claude Code names MCP tools after the server they come from, such as mcp shortcut get story , and those names are all over our setup: permission rules that allow mcp shortcut , skills that say which tool to call, and the notes in our AGENTS.md files. I wanted the sessions to see exactly what they saw before, with only the sign-in moved into MCPProxy. So we forked MCPProxy https://github.com/DocSpring/mcpproxy-go/tree/docspring/per-server-endpoints?ref=docspring.com and made a few changes. One URL per server /mcp/s/