← Back to blog

MCP OAuth is broken (and it's not going to be easy to fix)

2026-03-21 · 10 min read

I've been using MCP integrations — Linear, Todoist, GitHub, Notion — across multiple Claude surfaces for a few months now. When they work, they're genuinely transformative. You stop copy-pasting between apps and start talking to your tools through a single interface. "Create a Linear issue for the login bug we discussed." "Add milk to my Todoist inbox." It just works.

Except it doesn't. Not reliably.

I set up a Linear connection in Claude mobile, used it for a few hours, then got invalid_grant. Disconnected, reconnected, authorized again. Worked for a while. Broke again. On mobile it's especially painful — you can't re-authorize from the Claude mobile app at all. You have to switch to a browser, go to Claude.ai, find the connector settings, re-do the OAuth dance, then come back to the mobile app and start a new conversation to refresh tools.

I started digging into why this keeps happening, and it turns out the problem is much deeper than I initially thought.

// the scope of the problem

This isn't a niche issue affecting a handful of users. It's happening across every major MCP client and every OAuth-based service:

When I see the same class of bug reported independently across Claude's various surfaces and Atlassian's own forums, my instinct is that the problem isn't in any individual implementation — it's structural.

// why this is hard

OAuth token refresh sounds simple. Access token expires, you use the refresh token to get a new one, done. Most apps handle this transparently. But MCP clients face a combination of problems that make reliable refresh surprisingly difficult.

Every service breaks differently

Linear uses single-use refresh token rotation. Every refresh invalidates the old refresh token and issues a new one. If two clients try to refresh at the same time — or if a refresh response gets lost — the token chain is permanently broken. You get invalid_grant and have to re-authorize from scratch. This is particularly nasty because the failure mode is silent: there's no way to recover without user intervention.

GitHub enforces a 10-token limit per user per OAuth app. Create an 11th token and the oldest one is silently revoked. If you're using GitHub's MCP server across Claude Code and a few other clients, each one creates its own token. You blow past 10 without realizing it, and older clients silently lose access. This one took me a while to figure out.

Atlassian's Rovo MCP server uses OAuth 2.1 with tokens that expire every 30–55 minutes. Most MCP clients can't refresh fast enough, so you're re-authorizing constantly. Thirty minutes! That's barely enough time to get into a flow state.

Notion's real problem is Dynamic Client Registration. Notion uses DCR for OAuth, and DCR client registrations are short-lived. This means MCP clients may need to rotate their client IDs while still keeping track of legacy IDs for existing access and refresh tokens. Most clients don't handle this correctly.

Todoist is the most stable of the bunch — tokens don't expire for 10 years as of November 2025. But Todoist doesn't issue a refresh_token in the OAuth response, which confuses some MCP clients that expect one. They see the missing field and treat the session as broken.

Every client reimplements token handling

This is the part that really gets me. Traditional OAuth apps have one client surface. A web app refreshes tokens in the background and you never notice.

MCP has... a lot of client surfaces. Claude Code, Claude Desktop, Claude.ai, Claude mobile, Cowork mode — each one implements its own token management. And each ships with its own bugs.

Claude Code's token refresh was missing entirely until v2.1.63. That fix landed in March 2026, and users confirmed it works. But Cowork mode loses auth after context compaction with a This connector requires authentication error — which is a completely unrelated failure mechanism.

AI clients are growing at such a fast rate that they cannot harden against all OAuth edge cases before hitting production. Every new client surface that adds MCP support has to solve token refresh from scratch.

Multi-client and race conditions

The real pain point is when you use more than one client surface — which, if you're using MCP at all, you probably do. Claude.ai on desktop, Claude mobile on your phone, Claude Code in the terminal. Each one independently manages its own OAuth tokens against the same services. They race each other for refreshes. They hit per-app token limits. They have no way to share a session.

But it's not just multi-client. Even within a single client, you get race conditions. Open three Claude Code terminal windows, or have two chat sessions in Claude.ai, and each one might fire parallel tool calls against the same connection. With Linear's single-use refresh token rotation, that's a recipe for disaster: session A refreshes and gets a new refresh token. Session B tries to refresh with the old one — which has already been invalidated. Session B is now permanently broken with invalid_grant. And you might not even notice which session caused it.

Recent progress (but not enough)

Claude Code landed a token refresh fix in v2.1.63 (March 2026) that helps significantly for single-client use. Users in the thread confirmed it's working. But this only fixes Claude Code — not Claude.ai, not Claude mobile. And it doesn't solve the multi-client token collision problem or the intra-client race conditions.

// things I tried that didn't work

"Just use API keys"

If you're on Claude Code, you can often configure MCP servers with API keys instead of OAuth. Much more stable. But Claude.ai and Claude mobile only support OAuth-based MCP connections. There's no place to paste an API key in Claude.ai's connector settings.

Running a local MCP server

You can run an MCP server on your own machine that handles token refresh — several open-source options exist. This works for Claude Code and Claude Desktop, but it doesn't help with Claude.ai or Claude mobile, which can only connect to remote MCP servers.

Self-hosting a cloud MCP proxy

You can deploy your own proxy to a cloud provider, which solves the mobile/web problem. But now you're maintaining infrastructure, handling security, worrying about uptime. For most people that's more complexity than the problem warrants.

// what I ended up building

I have a specific workflow that depends on this: I use Claude to look at all my Todoist tasks, ensure they have deadlines and durations, then triage them into my day and add time blocks on my calendar. The problem is that most times I would tell Claude to "Plan my day", it would spin for 30 seconds, then tell me I had to reauthenticate with Todoist. The one with 10-year access tokens!

After getting frustrated enough, I built Bindify — an MCP auth proxy that takes the client completely out of the OAuth equation.

The idea is simple: you authenticate once through Bindify's dashboard. Bindify gives you a secret URL — a permanent MCP endpoint that never expires and requires no OAuth from the client side. You paste that URL into any MCP client, and from the client's perspective it's just hitting a URL. No tokens to manage, no refresh to handle, no re-auth prompts.

Bindify handles the token refresh behind the scenes. When your OAuth token expires, Bindify refreshes it automatically. The client never knows.

This also solves the multi-client problem, which I think is the more interesting part. All your clients — Claude.ai, mobile, Claude Code — share the same Bindify connection. One token, many clients. GitHub's 10-token limit becomes irrelevant because there's only one token being used, not one per client.

The security model

I spent more time on this than anything else. Your OAuth tokens are encrypted with AES-256-GCM, and the encryption key exists only in your secret URL — Bindify never stores it. Each URL contains two 256-bit secrets: one for lookup, one for decryption. Even a complete database breach would yield only encrypted ciphertext. Without your secret URL, the tokens are unreadable.

(I'm aware this means losing your URL means losing access, and I'm aware that URL-based secrets have their own threat model. I think it's the right trade-off for this use case, but I'd love to hear counterarguments.)

What it supports today

Service Auth Method Notes
Linear OAuth or API Key Handles single-use refresh token rotation
Todoist OAuth or API Key Proactive refresh before expiry
Notion OAuth Handles DCR client rotation
GitHub OAuth or API Key One token shared across all clients
Jira API Token Bypasses Rovo OAuth entirely
Confluence API Token Bypasses Rovo OAuth entirely

The proxy is open source. The hosted service at bindify.dev adds user management, a dashboard, and the OAuth callback flows. It runs on Cloudflare Workers, so it's edge-deployed and token refresh happens proactively via cron triggers.

// will MCP clients eventually fix this?

I hope so! Claude Code already made real progress with v2.1.63. If every client eventually handles OAuth correctly and reliably, Bindify becomes unnecessary — and that would be fine.

But "every client surface handles OAuth correctly across every service" is a coordination problem involving Anthropic, Linear, Atlassian, GitHub, Notion, Todoist, and the MCP spec itself. Each service has its own token lifetime, rotation policy, and error behavior. Each client surface has its own architecture and failure modes. Getting all of them right simultaneously is not a small task.

In the meantime, if you're hitting invalid_grant errors across your MCP integrations and you're tired of re-authorizing, Bindify is the thing I built to solve this for myself. Free trial, no credit card required. The docs cover setup for each client.