MCP Security and Authentication
Add authentication to your MCP server using OAuth 2.0 tokens, implement input validation to prevent injection attacks, and apply the principle of least privilege to tool permissions.
MCP Security and Authentication is a free AI Engineering Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why MCP Security Matters
An MCP server is a gateway into your systems. Without proper security, a compromised AI client or malicious prompt could read sensitive data, trigger destructive operations, or exfiltrate information through the tool call channel. Security for MCP servers must be defense in depth: authentication at the transport layer, authorization at the tool level, and input validation on every call.
Local vs. Remote Server Security
The stdio transport (used by Claude Desktop) has inherent security: the server runs as a local process, only accessible to the user who launched it. Network-exposed MCP servers using HTTP/SSE face the full range of web security threats: authentication bypass, injection attacks, and unauthorized access. Security requirements differ dramatically based on deployment mode.
- Local stdio: Trust the local user; focus on input validation
- Remote HTTP/SSE: Full authentication, TLS, rate limiting, input sanitization
API Key Authentication for Remote Servers
The simplest authentication for remote MCP servers is API key validation via an HTTP header. Check the Authorization: Bearer <token> header on every incoming request and reject unauthenticated requests with HTTP 401. Store valid API keys in a database with user metadata so you can revoke individual keys.
# For HTTP/SSE MCP servers using FastAPI or similar:
from fastapi import FastAPI, HTTPException, Depends, Header
from typing import Optional
import secrets
app_http = FastAPI()
# In production: store in database with user_id, created_at, last_used
VALID_KEYS = {'sk-mcp-abc123': {'user': 'alice', 'scopes': ['read']},
'sk-mcp-def456': {'user': 'bob', 'scopes': ['read', 'write']}}
async def verify_api_key(authorization: Optional[str] = Header(None)) -> dict:
if not authorization or not authorization.startswith('Bearer '):
raise HTTPException(status_code=401, detail='Missing API key')
key = authorization.removeprefix('Bearer ')
if key not in VALID_KEYS:
raise HTTPException(status_code=401, detail='Invalid API key')
return VALID_KEYS[key] # Returns user context
# Use in route handlers:
# @app_http.get('/sse')
# async def sse_endpoint(user=Depends(verify_api_key)):OAuth 2.0 for Enterprise MCP Servers
For enterprise deployments, use OAuth 2.0 so users authenticate with their corporate identity provider (Okta, Azure AD, Google Workspace). The MCP client obtains an OAuth access token, which it includes in tool call requests. Your server validates the token signature against the identity provider's public keys using python-jose or authlib.
from jose import jwt, JWTError
import httpx
AUTH_DOMAIN = 'your-tenant.auth0.com'
AUDIENCE = 'https://api.your-mcp-server.com'
async def get_jwks():
async with httpx.AsyncClient() as client:
resp = await client.get(f'https://{AUTH_DOMAIN}/.well-known/jwks.json')
return resp.json()
async def verify_oauth_token(token: str) -> dict:
jwks = await get_jwks()
try:
payload = jwt.decode(
token,
jwks,
algorithms=['RS256'],
audience=AUDIENCE,
issuer=f'https://{AUTH_DOMAIN}/'
)
return payload # Contains sub (user ID), scope, exp, etc.
except JWTError as e:
raise ValueError(f'Invalid token: {e}')Scope-Based Authorization
Not all MCP tools should be available to all users. Use OAuth scopes or role claims in the JWT token to determine which tools the authenticated user may call. Check authorization at the beginning of every tool execution, before any database or API call is made.
TOOL_REQUIRED_SCOPES = {
'list_products': ['read:products'],
'search_products': ['read:products'],
'create_order': ['write:orders'],
'delete_order': ['admin:orders']
}
def check_authorization(tool_name: str, token_payload: dict):
'''Raise ValueError if user lacks required scope for the tool.'''
required = TOOL_REQUIRED_SCOPES.get(tool_name, [])
if not required:
return # No scope required
user_scopes = set(token_payload.get('scope', '').split())
missing = [s for s in required if s not in user_scopes]
if missing:
raise ValueError(
f'Access denied. Tool "{tool_name}" requires scopes: {missing}. '
f'Your token has: {list(user_scopes)}'
)
# In call_tool handler:
# check_authorization(name, current_user_token)
# ... then execute the toolInput Validation and Injection Prevention
All tool inputs are ultimately strings generated by an LLM — treat them as untrusted. Validate every input against expected types and patterns before use. Specifically guard against: SQL injection (use parameterized queries, never string-interpolated SQL), command injection (never pass user input to shell commands), and path traversal (normalize and validate file paths).
import re
from pathlib import Path
BASE_DATA_DIR = Path('/data/mcp-files')
def safe_file_path(user_input: str) -> Path:
'''Validate and normalize a file path to prevent traversal attacks.'''
# Remove any path traversal sequences
clean = re.sub(r'\.\./', '', user_input)
clean = re.sub(r'\.\.\\\\', '', clean)
path = (BASE_DATA_DIR / clean).resolve()
# Ensure the resolved path is still within the allowed base directory
if not str(path).startswith(str(BASE_DATA_DIR)):
raise ValueError(f'Path traversal detected: {user_input}')
return path
def safe_identifier(value: str) -> str:
'''Validate a database identifier (table/column name).'''
if not re.match(r'^[a-z_][a-z0-9_]{0,63}$', value, re.IGNORECASE):
raise ValueError(f'Invalid identifier: {value}')
return valueRate Limiting Tool Calls
An LLM agent in a loop could call expensive tools hundreds of times per minute, exhausting your database connections, third-party API quotas, or compute budget. Implement per-user rate limits using a token bucket algorithm in Redis. Reject tool calls that exceed the limit with a descriptive error so the agent knows to wait.
import redis
import time
r = redis.Redis.from_url('redis://localhost:6379')
def check_rate_limit(user_id: str, tool_name: str, limit: int = 60, window: int = 60) -> None:
'''Allow at most `limit` calls per `window` seconds per user per tool.'''
key = f'rate:{user_id}:{tool_name}'
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window)
count, _ = pipe.execute()
if count > limit:
retry_after = r.ttl(key)
raise ValueError(
f'Rate limit exceeded for {tool_name}. '
f'Limit: {limit} calls/{window}s. '
f'Retry after {retry_after} seconds.'
)Principle of Least Privilege
Apply the principle of least privilege at every layer of your MCP server. The database user should have SELECT-only on the tables the server needs. The server process should run as a non-root OS user. Tools should request only the permissions they need. API keys should have the minimum scope for their purpose. Each permission you withhold is a potential attack that can't succeed.
-- PostgreSQL: Create a dedicated read-only database user for your MCP server
CREATE ROLE mcp_reader LOGIN PASSWORD 'strong_random_password';
-- Grant SELECT on only the tables the server needs
GRANT SELECT ON products, categories, public_content TO mcp_reader;
-- Explicitly deny access to sensitive tables
REVOKE ALL ON users, api_keys, payment_methods FROM mcp_reader;
-- Never grant: INSERT, UPDATE, DELETE, TRUNCATE, or DDL permissionsTLS and Transport Security
Remote MCP servers must use TLS to protect data in transit. Configure your server to only accept HTTPS connections. In production, use a reverse proxy (nginx, Caddy) to handle TLS termination and keep certificates up to date with automatic renewal (Let's Encrypt via Certbot or Caddy's built-in ACME support).
# Example Caddyfile for TLS-terminating MCP server at a subdomain:
#
# mcp.yourcompany.com {
# reverse_proxy localhost:8080
# encode gzip
# tls internal # Use Let's Encrypt in production
# header {
# Strict-Transport-Security 'max-age=31536000; includeSubDomains'
# X-Content-Type-Options nosniff
# X-Frame-Options DENY
# }
# }Prompt Injection via MCP Resources
A subtle attack vector: if your MCP server reads content from external sources (web pages, user-uploaded files, untrusted databases) and returns it as a tool result, an attacker could embed adversarial instructions in that content. The model might obey instructions hidden in retrieved data — this is indirect prompt injection. Sanitize retrieved content and never return raw untrusted text directly as tool output.
import re
def sanitize_for_mcp_output(text: str) -> str:
'''Remove patterns that look like instructions to the LLM.'''
# Remove common injection patterns
dangerous_patterns = [
r'ignore previous instructions',
r'ignore all prior instructions',
r'system:',
r'<\|.*?\|>', # Special tokens
r'\[INST\]',
r'<s>',
]
for pattern in dangerous_patterns:
text = re.sub(pattern, '[FILTERED]', text, flags=re.IGNORECASE)
return text[:10000] # Also cap length to prevent context stuffingSecurity Auditing and Monitoring
Log all security-relevant events: authentication successes and failures, rate limit violations, authorization denials, validation errors, and unusual access patterns. Set up alerts for: multiple failed auth attempts (brute force), a single user calling destructive tools rapidly, and any tool call with extremely large inputs. Review audit logs regularly and automate anomaly detection.
Quick Check
Test your understanding of MCP security and authentication concepts.
Lesson Recap
In this lesson you learned: remote MCP servers require OAuth 2.0 or API key authentication on every request, scope-based authorization controls which tools each authenticated user can call, and all tool inputs must be validated to prevent injection attacks and path traversal. This concludes the MCP module — next we explore advanced chunking strategies for high-precision RAG retrieval.
Frequently asked questions
Is the “MCP Security and Authentication” lesson free?
Yes — the full text of “MCP Security and Authentication” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.
What will I learn in “MCP Security and Authentication”?
Add authentication to your MCP server using OAuth 2.0 tokens, implement input validation to prevent injection attacks, and apply the principle of least privilege to tool permissions. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Engineering Academy?
No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “MCP Security and Authentication” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Engineering Academy lesson?
Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- What Is MCP and Why It Matters
- Building Your First MCP Server
- Exposing Database Resources via MCP
- MCP Security and Authentication