MCP Authentication Guide
Secure your MCP servers with API keys, OAuth tokens, and environment-based credentials.
MCP servers often need to connect to external APIs and services that require authentication. This guide covers multiple authentication strategies, from simple API keys to OAuth tokens, with security best practices for production deployments.
What You'll Learn
- Environment variable-based authentication
- API key management and rotation
- OAuth token handling
- Secrets management best practices
- Multi-environment configurations
Environment Variables: The Foundation
The most common and recommended way to handle credentials in MCP servers is through environment variables. Claude Desktop and other clients support passing environment variables to MCP server processes.
Basic Environment Variable Usage
Here's a Python MCP server that uses environment variables for authentication:
# server.py
import os
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("authenticated-server")
# Read credentials from environment
API_KEY = os.environ.get("MY_SERVICE_API_KEY")
if not API_KEY:
raise ValueError("MY_SERVICE_API_KEY environment variable required")
@mcp.tool()
def fetch_data(query: str) -> str:
"""Fetch data from authenticated API."""
import httpx
response = httpx.get(
"https://api.example.com/data",
params={"q": query},
headers={"Authorization": f"Bearer {API_KEY}"}
)
return response.json()
if __name__ == "__main__":
mcp.run()Claude Desktop Configuration
Configure Claude Desktop to pass environment variables to your server:
{
"mcpServers": {
"my-authenticated-server": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {
"MY_SERVICE_API_KEY": "sk-your-api-key-here"
}
}
}
}⚠️ Security Note
Storing API keys directly in config files is convenient for development but not ideal for production. We'll cover better approaches below.
Managing Multiple Credentials
Real-world MCP servers often need to interact with multiple services. Here's a pattern for managing multiple credentials cleanly:
# config.py
import os
from dataclasses import dataclass
@dataclass
class Credentials:
github_token: str
openai_key: str
database_url: str
@classmethod
def from_env(cls) -> "Credentials":
"""Load credentials from environment variables."""
missing = []
github_token = os.environ.get("GITHUB_TOKEN")
if not github_token:
missing.append("GITHUB_TOKEN")
openai_key = os.environ.get("OPENAI_API_KEY")
if not openai_key:
missing.append("OPENAI_API_KEY")
database_url = os.environ.get("DATABASE_URL")
if not database_url:
missing.append("DATABASE_URL")
if missing:
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
return cls(
github_token=github_token,
openai_key=openai_key,
database_url=database_url
)
# server.py
from mcp.server.fastmcp import FastMCP
from config import Credentials
mcp = FastMCP("multi-auth-server")
creds = Credentials.from_env()
@mcp.tool()
def github_search(query: str) -> str:
"""Search GitHub repositories."""
import httpx
response = httpx.get(
"https://api.github.com/search/repositories",
params={"q": query},
headers={"Authorization": f"token {creds.github_token}"}
)
return response.json()
@mcp.tool()
def ai_summarize(text: str) -> str:
"""Summarize text using OpenAI."""
import httpx
response = httpx.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {creds.openai_key}"},
json={
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": f"Summarize: {text}"}]
}
)
return response.json()["choices"][0]["message"]["content"]OAuth Token Handling
For services that require OAuth (like Google APIs, Slack, etc.), you'll typically need to handle token refresh. Here's a pattern for that:
# oauth_handler.py
import os
import json
import time
from pathlib import Path
import httpx
class OAuthTokenManager:
"""Manage OAuth tokens with automatic refresh."""
def __init__(
self,
client_id: str,
client_secret: str,
token_url: str,
token_file: Path
):
self.client_id = client_id
self.client_secret = client_secret
self.token_url = token_url
self.token_file = token_file
self._token_data = None
self._load_token()
def _load_token(self):
"""Load token from file if it exists."""
if self.token_file.exists():
self._token_data = json.loads(self.token_file.read_text())
def _save_token(self):
"""Save token to file."""
self.token_file.parent.mkdir(parents=True, exist_ok=True)
self.token_file.write_text(json.dumps(self._token_data))
def _refresh_token(self):
"""Refresh the access token using refresh token."""
if not self._token_data or "refresh_token" not in self._token_data:
raise ValueError("No refresh token available. Re-authorize required.")
response = httpx.post(
self.token_url,
data={
"grant_type": "refresh_token",
"refresh_token": self._token_data["refresh_token"],
"client_id": self.client_id,
"client_secret": self.client_secret,
}
)
response.raise_for_status()
new_data = response.json()
self._token_data["access_token"] = new_data["access_token"]
self._token_data["expires_at"] = time.time() + new_data.get("expires_in", 3600)
if "refresh_token" in new_data:
self._token_data["refresh_token"] = new_data["refresh_token"]
self._save_token()
def get_access_token(self) -> str:
"""Get a valid access token, refreshing if necessary."""
if not self._token_data:
raise ValueError("No token available. Authorization required.")
# Refresh if expired or expiring soon (within 5 minutes)
expires_at = self._token_data.get("expires_at", 0)
if time.time() > expires_at - 300:
self._refresh_token()
return self._token_data["access_token"]
# Usage in MCP server
from mcp.server.fastmcp import FastMCP
from oauth_handler import OAuthTokenManager
from pathlib import Path
mcp = FastMCP("google-drive-server")
token_manager = OAuthTokenManager(
client_id=os.environ["GOOGLE_CLIENT_ID"],
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
token_url="https://oauth2.googleapis.com/token",
token_file=Path.home() / ".config" / "mcp" / "google_token.json"
)
@mcp.tool()
def list_drive_files(folder_id: str = "root") -> str:
"""List files in Google Drive folder."""
import httpx
token = token_manager.get_access_token()
response = httpx.get(
"https://www.googleapis.com/drive/v3/files",
params={"q": f"'{folder_id}' in parents"},
headers={"Authorization": f"Bearer {token}"}
)
return response.json()Using Secrets Managers
For production deployments, consider using a secrets manager instead of plain environment variables. Here are examples for popular options:
1Password CLI Integration
# Claude Desktop config using 1Password
{
"mcpServers": {
"secure-server": {
"command": "op",
"args": [
"run",
"--",
"python",
"/path/to/server.py"
],
"env": {
"MY_API_KEY": "op://vault/item/field"
}
}
}
}
# The 'op run' command automatically injects secrets
# from 1Password references like op://vault/item/fieldAWS Secrets Manager
# secrets_loader.py
import boto3
import json
from functools import lru_cache
@lru_cache(maxsize=1)
def get_secrets(secret_name: str, region: str = "us-east-1") -> dict:
"""Load secrets from AWS Secrets Manager."""
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
# Usage in server
from mcp.server.fastmcp import FastMCP
from secrets_loader import get_secrets
mcp = FastMCP("aws-secrets-server")
secrets = get_secrets("my-mcp-server-secrets")
@mcp.tool()
def secure_operation() -> str:
"""Perform operation using secrets from AWS."""
api_key = secrets["api_key"]
# Use the secret...
return "Done"HashiCorp Vault
# vault_loader.py
import hvac
import os
def get_vault_secrets(path: str) -> dict:
"""Load secrets from HashiCorp Vault."""
client = hvac.Client(
url=os.environ.get("VAULT_ADDR", "http://localhost:8200"),
token=os.environ.get("VAULT_TOKEN")
)
response = client.secrets.kv.v2.read_secret_version(path=path)
return response["data"]["data"]
# Usage
secrets = get_vault_secrets("mcp/my-server")
api_key = secrets["api_key"]Multi-Environment Configuration
Handle different credentials for development, staging, and production:
# config.py
import os
from dataclasses import dataclass
from typing import Literal
Environment = Literal["development", "staging", "production"]
@dataclass
class Config:
environment: Environment
api_key: str
api_base_url: str
debug: bool
@classmethod
def load(cls) -> "Config":
env = os.environ.get("MCP_ENV", "development")
if env == "production":
return cls(
environment="production",
api_key=os.environ["PROD_API_KEY"],
api_base_url="https://api.example.com",
debug=False
)
elif env == "staging":
return cls(
environment="staging",
api_key=os.environ["STAGING_API_KEY"],
api_base_url="https://staging-api.example.com",
debug=True
)
else:
return cls(
environment="development",
api_key=os.environ.get("DEV_API_KEY", "dev-key-for-testing"),
api_base_url="http://localhost:8000",
debug=True
)
# server.py
from mcp.server.fastmcp import FastMCP
from config import Config
config = Config.load()
mcp = FastMCP(f"my-server-{config.environment}")
@mcp.tool()
def get_environment() -> str:
"""Check which environment the server is running in."""
return f"Running in {config.environment} mode"Security Best Practices
✓ Do
- Use environment variables or secrets managers for credentials
- Implement least-privilege access (only request scopes you need)
- Rotate credentials regularly
- Use short-lived tokens when possible (OAuth)
- Log authentication failures (without logging the credentials)
- Validate all input before using in API calls
✗ Don't
- Hardcode credentials in source code
- Commit config files with real credentials to git
- Log credentials or tokens (even in debug mode)
- Share credentials between environments
- Use overly permissive API scopes
- Store credentials in plain text files
Credential Validation Pattern
# validation.py
import os
import sys
def validate_credentials():
"""Validate all required credentials at startup."""
required = {
"GITHUB_TOKEN": "GitHub Personal Access Token",
"OPENAI_API_KEY": "OpenAI API Key",
}
optional = {
"SLACK_BOT_TOKEN": "Slack Bot Token (for notifications)",
}
missing = []
for var, description in required.items():
if not os.environ.get(var):
missing.append(f" - {var}: {description}")
if missing:
print("❌ Missing required credentials:", file=sys.stderr)
print("\n".join(missing), file=sys.stderr)
print("\nPlease set these environment variables.", file=sys.stderr)
sys.exit(1)
# Warn about optional missing credentials
for var, description in optional.items():
if not os.environ.get(var):
print(f"⚠️ Optional: {var} not set ({description})", file=sys.stderr)
print("✅ All required credentials validated")
# Call at server startup
if __name__ == "__main__":
validate_credentials()
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("validated-server")
# ... rest of server
mcp.run()TypeScript Authentication
The same patterns work in TypeScript with the official MCP SDK:
// config.ts
import { z } from 'zod';
const ConfigSchema = z.object({
apiKey: z.string().min(1, 'API key is required'),
apiSecret: z.string().optional(),
environment: z.enum(['development', 'staging', 'production']).default('development'),
});
export type Config = z.infer<typeof ConfigSchema>;
export function loadConfig(): Config {
const result = ConfigSchema.safeParse({
apiKey: process.env.API_KEY,
apiSecret: process.env.API_SECRET,
environment: process.env.NODE_ENV,
});
if (!result.success) {
console.error('Configuration error:', result.error.format());
process.exit(1);
}
return result.data;
}
// server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { loadConfig } from './config.js';
const config = loadConfig();
const server = new McpServer({ name: 'authenticated-ts-server', version: '1.0.0' });
server.tool(
'secure_fetch',
{ url: z.string().url() },
async ({ url }) => {
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${config.apiKey}` }
});
return { content: [{ type: 'text', text: await response.text() }] };
}
);
const transport = new StdioServerTransport();
server.connect(transport);Summary
Authentication in MCP servers follows the same best practices as any backend service:
- Use environment variables as the foundation
- Validate credentials at startup to fail fast
- Consider secrets managers for production (1Password, AWS, Vault)
- Handle OAuth properly with token refresh
- Separate environments with different credentials
- Never log or commit actual credentials
The key insight: Claude Desktop (and other MCP clients) can pass environment variables to your server process. Build your authentication around that capability, and you can keep credentials secure while maintaining easy local development.
Get updates in your inbox
Tutorials, updates, and best practices for Model Context Protocol.
No spam. Unsubscribe anytime.