Skip to content

How to resolve MCP authentication hell across multiple servers

I was setting up a new AI agent system with Model Context Protocol (MCP) servers. Everything looked great on paper. I had my GitHub MCP, my Slack MCP, my Jira MCP, my Linear MCP, and about six others. Then I hit the wall.

The Auth Hell

I saw this Reddit comment that perfectly captured my pain:

“I dropped a bunch of individual MCP servers because managing auth tokens across 10+ integrations was a nightmare.”

And they were right. Here’s what my token rotation schedule looked like:

Token refresh schedule before centralization
06:00 - GitHub MCP token refresh (expires in 1 hour)
06:30 - Slack MCP token refresh (expires in 30 min)
07:00 - Jira MCP token refresh (expires in 1 hour)
07:15 - Linear MCP token refresh (expires in 15 min)
08:00 - Notion MCP token refresh (expires in 1 hour)
08:30 - Google Drive MCP token refresh (expires in 30 min)
... and 4 more

Each server had its own OAuth flow. Each had different token expiry times. Each required its own refresh logic. When one token expired mid-task, my agent would fail spectacularly.

I spent more time debugging auth issues than building actual features. Something had to change.

What Caused This Mess?

The MCP specification requires audience validation for tokens. This means a token issued for github-mcp cannot be used for slack-mcp. Here’s the relevant part from the spec:

MCP token validation example
{
"iss": "auth-provider",
"aud": "github-mcp-server",
"sub": "user-123",
"exp": 1712345678,
"scope": "repo:read repo:write"
}

If I try to use this token for Slack MCP, the server rejects it because the aud claim doesn’t match. This is by design for security, but it creates the token management nightmare when you have many MCP servers.

I tried several approaches before finding what actually works.

Solution 1: Centralized OAuth Provider

The first thing I tried was building a centralized OAuth provider that all my MCP servers could trust. This is the “proper” enterprise solution.

central_auth_provider.py
from authlib.integrations.flask_client import OAuth
from flask import Flask, jsonify
app = Flask(__name__)
oauth = OAuth(app)
# Single OAuth registration for all MCP servers
oauth.register(
name='mcp_gateway',
client_id='your-client-id',
client_secret='your-client-secret',
server_metadata_url='https://auth.example.com/.well-known/oauth-authorization-server',
client_kwargs={'scope': 'openid profile email'}
)
@app.route('/token/<mcp_server>')
def get_mcp_token(mcp_server):
"""Generate audience-scoped tokens for specific MCP servers."""
user_token = validate_current_user_session()
# Create a new token with specific audience
mcp_token = create_scoped_token(
subject=user_token['sub'],
audience=f'{mcp_server}-mcp',
expires_in=3600
)
return jsonify({'access_token': mcp_token, 'expires_in': 3600})

The MCP servers then trust this central provider:

mcp_server_config.py
# In each MCP server configuration
AUTH_CONFIG = {
'issuer': 'https://auth.example.com',
'audience': 'github-mcp', # This server's specific audience
'jwks_uri': 'https://auth.example.com/.well-known/jwks.json'
}

Pros:

  • Clean, standards-compliant architecture
  • Single point of user authentication
  • Easy to audit and revoke access

Cons:

  • Requires building and maintaining an OAuth provider
  • Still need to manage token refresh per server
  • Overkill for personal or small team setups

I found this works well for enterprise deployments but felt heavy for my use case.

Solution 2: API Key Management Layer

For servers that support API keys (and many do), I created a unified key management layer. This bypassed OAuth entirely for some integrations.

key-manager.js
class MCPKeyManager {
constructor() {
this.keys = new Map();
this.loadKeys();
}
loadKeys() {
// Load from secure storage (env vars, vault, etc.)
const keyConfigs = [
{ server: 'github-mcp', key: process.env.GITHUB_API_KEY, type: 'api-key' },
{ server: 'linear-mcp', key: process.env.LINEAR_API_KEY, type: 'api-key' },
{ server: 'slack-mcp', key: process.env.SLACK_BOT_TOKEN, type: 'bot-token' }
];
keyConfigs.forEach(config => {
this.keys.set(config.server, {
key: config.key,
type: config.type,
lastRotated: Date.now()
});
});
}
getKeyForServer(serverName) {
const keyData = this.keys.get(serverName);
if (!keyData) {
throw new Error(`No key configured for ${serverName}`);
}
return keyData.key;
}
async rotateKey(serverName) {
// Implement key rotation logic
const newKey = await this.requestNewKey(serverName);
this.keys.set(serverName, {
key: newKey,
type: this.keys.get(serverName).type,
lastRotated: Date.now()
});
}
}
export const keyManager = new MCPKeyManager();

Then each MCP server gets initialized with its key:

mcp-initialization.js
import { keyManager } from './key-manager.js';
async function initializeMCPServers() {
const servers = ['github-mcp', 'linear-mcp', 'slack-mcp'];
const connections = await Promise.all(
servers.map(async (server) => {
const key = keyManager.getKeyForServer(server);
return connectToMCP(server, {
auth: { type: 'api-key', key }
});
})
);
return connections;
}

Pros:

  • Simpler than OAuth for API-key compatible services
  • No token refresh cycles
  • Easier to rotate keys from one place

Cons:

  • Not all MCP servers support API keys
  • API keys don’t have fine-grained permissions
  • Can’t scope access per-request

This worked for about 60% of my MCP servers. For the rest, I needed something else.

Solution 3: Browser Session Routing

This is the clever hack I discovered. Instead of managing tokens, I piggyback on existing browser sessions using a Chrome extension.

The idea: route MCP requests through an extension that injects cookies from an authenticated browser session.

chrome-extension/background.js
// Chrome extension background script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'getAuthCookies') {
const domains = {
'github-mcp': 'github.com',
'slack-mcp': 'slack.com',
'notion-mcp': 'notion.so'
};
const targetDomain = domains[request.serverName];
if (!targetDomain) {
sendResponse({ error: 'Unknown server' });
return;
}
chrome.cookies.getAll({ domain: targetDomain }, (cookies) => {
const cookieString = cookies
.map(c => `${c.name}=${c.value}`)
.join('; ');
sendResponse({ cookies: cookieString });
});
return true; // Required for async response
}
});

The MCP client then uses these cookies:

session-router.js
class BrowserSessionRouter {
constructor(extensionId) {
this.extensionId = extensionId;
}
async getAuthCookies(serverName) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
this.extensionId,
{ type: 'getAuthCookies', serverName },
(response) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response.cookies);
}
}
);
});
}
async makeAuthenticatedRequest(serverName, endpoint, options = {}) {
const cookies = await this.getAuthCookies(serverName);
return fetch(endpoint, {
...options,
headers: {
...options.headers,
'Cookie': cookies
}
});
}
}

Pros:

  • No token management at all
  • Uses existing login sessions
  • Works with any service you’re logged into

Cons:

  • Requires Chrome extension
  • Depends on browser being open
  • Session can expire without warning
  • Security considerations around cookie access

What I Actually Use

I ended up with a hybrid approach:

Final architecture decision
API Key Layer: github-mcp, linear-mcp, jira-mcp (services with good API key support)
Browser Session: slack-mcp, notion-mcp, google-mcp (services where I'm already logged in)
Centralized OAuth: Only for custom internal MCP servers

The key insight: don’t try to solve everything with one pattern. Match the authentication method to the service’s capabilities.

Reason This Works

The MCP spec’s audience requirement is good security practice. But it forces you to think about token management at scale. By combining three approaches, I reduced my token refresh cycles from 10+ to just 2 (for my internal OAuth servers).

The browser session routing is particularly useful for services you already use interactively. Why maintain a separate OAuth flow when you already have a valid session in your browser?

Summary

Managing auth across multiple MCP servers doesn’t have to be hell. The key is recognizing that different servers have different auth capabilities:

  1. Centralized OAuth: Enterprise-grade, proper token scoping, most maintenance
  2. API Key Layer: Simple, works well for services that support it, limited permissions
  3. Browser Session Routing: Zero token management, requires extension, best for personal use

Mix and match based on what each MCP server supports. Your future self will thank you when you’re not debugging token refresh failures at 2 AM.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments