Securing MCP server connections is critical — especially for remote servers that access sensitive data. This guide covers every authentication method available for MCP, from simple API keys to full OAuth 2.0 flows.
Local servers (stdio) — no auth needed
For servers running as subprocesses via stdio transport, the OS handles security. The server runs with the same permissions as the host process. There’s no network boundary, so no authentication is required.
This is the default for most MCP setups — your server starts as a child process, communicates over stdin/stdout, and inherits the user’s permissions. The attack surface is minimal because no ports are exposed.
However, you still need to be careful about what environment variables and file system access you grant to the subprocess.
Remote servers (SSE/HTTP) — auth required
When your MCP server runs over HTTP (using Server-Sent Events or the newer Streamable HTTP transport), you need authentication. Anyone who can reach the endpoint can potentially call your tools.
API key authentication (simplest)
The fastest way to secure a remote MCP server. Generate a random key, share it with authorized clients, and validate it on every request:
import { randomBytes } from 'crypto';
// Generate a secure API key (do this once, store securely)
const apiKey = randomBytes(32).toString('hex');
// Middleware to validate the key
app.use((req, res, next) => {
const provided = req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', '');
if (!provided || provided !== process.env.MCP_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
API keys work well for single-user or internal deployments. For multi-user or enterprise scenarios, use OAuth.
OAuth 2.0 flows for MCP
MCP supports OAuth 2.0 for enterprise deployments. The protocol defines a standard flow where the MCP host (like Claude Desktop or Cursor) handles the OAuth dance on behalf of the user.
Here’s how the flow works:
- The MCP client connects to the server and receives a
401with an OAuth discovery URL - The client fetches the authorization server metadata (
.well-known/oauth-authorization-server) - The client opens a browser for user consent
- After consent, the authorization server issues an access token
- The client includes the token in subsequent MCP requests
Server-side OAuth implementation
import jwt from 'jsonwebtoken';
function validateToken(req: Request): { userId: string; scopes: string[] } {
const token = req.headers['authorization']?.replace('Bearer ', '');
if (!token) throw new Error('No token provided');
const decoded = jwt.verify(token, process.env.JWT_SECRET!, {
audience: 'mcp-server',
issuer: process.env.OAUTH_ISSUER,
});
return {
userId: decoded.sub as string,
scopes: (decoded.scope as string || '').split(' '),
};
}
app.use('/mcp', (req, res, next) => {
try {
req.auth = validateToken(req);
next();
} catch (e) {
res.status(401).json({
error: 'invalid_token',
oauth_discovery: 'https://auth.example.com/.well-known/oauth-authorization-server',
});
}
});
OAuth discovery document
Your authorization server needs to expose metadata at /.well-known/oauth-authorization-server:
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"scopes_supported": ["mcp:read", "mcp:write", "mcp:admin"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"]
}
Per-user tokens and multi-tenancy
For multi-tenant servers, pass user identity through the validated token and enforce permissions per tool:
server.tool('query_data', { sql: z.string() }, async ({ sql }, { meta }) => {
const userId = meta?.authInfo?.userId;
const scopes = meta?.authInfo?.scopes || [];
if (!scopes.includes('mcp:read')) {
throw new Error('Insufficient scope: mcp:read required');
}
if (!await hasPermission(userId, 'read_data')) {
throw new Error('Permission denied for this dataset');
}
return await db.query(sql, { tenantId: userId });
});
Token management
Proper token lifecycle management prevents security incidents:
async function refreshAccessToken(refreshToken: string) {
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: process.env.CLIENT_ID!,
}),
});
const { access_token, refresh_token, expires_in } = await response.json();
await tokenStore.update({
accessToken: access_token,
refreshToken: refresh_token,
expiresAt: Date.now() + expires_in * 1000,
});
return access_token;
}
Token storage best practices
- Never store tokens in plain text config files
- Use OS keychains (macOS Keychain, Windows Credential Manager, Linux Secret Service)
- For server-to-server, use a secrets manager (AWS Secrets Manager, HashiCorp Vault)
- Set short expiry times (15 minutes for access tokens, 7 days for refresh tokens)
Security best practices
- Never hardcode secrets — use environment variables or a secrets manager
- Rotate API keys regularly — automate rotation on a 90-day cycle
- Use scoped tokens — grant
mcp:readfor read-only tools,mcp:writefor mutations - Log all authenticated requests — include user ID, tool called, and timestamp for audit
- Rate limit per identity — prevent abuse even from authenticated users
- Validate on every request — don’t cache auth decisions; tokens can be revoked
- Use HTTPS exclusively — never run remote MCP servers over plain HTTP
- Implement CORS properly — restrict origins to known MCP hosts
- For GDPR — ensure auth tokens don’t contain or leak PII
- Monitor for anomalies — alert on unusual patterns (new IPs, high request volumes)
Choosing the right auth method
| Method | Best for | Complexity | Security |
|---|---|---|---|
| None (stdio) | Local servers | None | OS-level |
| API key | Single user, internal | Low | Medium |
| OAuth 2.0 | Multi-user, enterprise | High | High |
| mTLS | Service-to-service | Medium | Very high |
For most developers building personal MCP servers, API keys are sufficient. If you’re deploying an MCP server that multiple users will connect to — especially in a corporate environment — invest in OAuth.
See the complete MCP developer guide for setup instructions, or learn more about how OAuth actually works if you’re new to the protocol. For a broader comparison of authentication methods, check our API authentication compared guide.
FAQ
Does MCP support OAuth?
Yes. The MCP specification includes native support for OAuth 2.0 authorization flows. Remote MCP servers can advertise their OAuth discovery endpoint, and MCP hosts (like Claude Desktop, Cursor, or custom clients) handle the authorization code flow automatically — opening a browser for user consent and managing token exchange. This is the recommended approach for any multi-user or enterprise MCP deployment.
How do I authenticate MCP servers?
It depends on the transport. For local stdio servers, no authentication is needed — the OS process model provides isolation. For remote servers over SSE or HTTP, you have three main options: API keys (simplest, pass via Authorization header or x-api-key), OAuth 2.0 (for multi-user scenarios), or mutual TLS (for service-to-service). Configure the auth method in your MCP host’s server configuration, typically in a JSON config file.
Is MCP authentication secure?
MCP authentication is as secure as your implementation. The protocol itself supports industry-standard mechanisms (OAuth 2.0, bearer tokens, TLS). The main risks come from misconfiguration: running remote servers without HTTPS, using weak API keys, not rotating tokens, or granting overly broad permissions. Follow the best practices above — use HTTPS, scope tokens narrowly, rotate credentials, and log all access — and your MCP authentication will be production-grade.
Can I use API keys with MCP?
Yes, and it’s the simplest approach for single-user or internal MCP servers. Generate a cryptographically random key (at least 32 bytes), store it in an environment variable, and validate it in middleware on every incoming request. Pass the key from your MCP host config using the env or headers field. API keys don’t support per-user identity or fine-grained scopes, so upgrade to OAuth if you need multi-tenancy or granular permissions.
Related: MCP Security Risks · MCP Security Checklist · MCP Complete Guide