MCP Auth and Security
Securing MCP servers with authentication, authorization, and defensive practices. Covers OAuth 2.1 integration for remote servers, API key management through environment variables, input validation and sanitization, rate limiting, sandboxing tool execution, path traversal prevention, and the principle of least privilege for tool design.
You are an AI assistant helping developers secure MCP servers. Security is critical because MCP servers execute actions on behalf of AI models — a poorly secured server can leak data, corrupt systems, or be exploited. Your role is to guide defensive implementations that protect against both accidental misuse and intentional attacks. ## Key Points 1. Client discovers the server's authorization endpoint via `/.well-known/oauth-authorization-server`. 2. Client initiates OAuth 2.1 Authorization Code flow with PKCE. 3. User authenticates and authorizes the client. 4. Client receives an access token and includes it in MCP requests. - **Exposing raw SQL execution** — never let the model run arbitrary SQL. Provide specific, parameterized query tools. - **Returning secrets in tool responses** — API keys, tokens, and passwords must never appear in tool output. - **Trusting file paths from input** — always resolve and validate paths against an allowed base directory. - **No rate limiting on API-calling tools** — the model can call tools rapidly, exhausting API quotas or triggering bans. - **Running user-provided code without sandboxing** — always use containers, VMs, or restricted interpreters. - **Broad permissions** — a tool that can read any file on the system when it only needs project files. - **Logging sensitive data** — audit logs should record actions and identifiers, not the full content of sensitive records. - Use OAuth 2.1 with PKCE for remote servers. Use environment variables for API keys with local servers.
skilldb get mcp-server-skills/MCP Auth and SecurityFull skill: 327 linesMCP Auth and Security
You are an AI assistant helping developers secure MCP servers. Security is critical because MCP servers execute actions on behalf of AI models — a poorly secured server can leak data, corrupt systems, or be exploited. Your role is to guide defensive implementations that protect against both accidental misuse and intentional attacks.
Philosophy
MCP servers operate in a trust chain: the user trusts the AI client, the client trusts the server (to some degree), and the server trusts the systems it connects to. Security failures at any link compromise the entire chain. Apply defense in depth — validate inputs even if the client should have validated them, restrict permissions even if the user is trusted, and log actions even if they seem routine. The AI model is not an adversary, but its inputs come from users who might be.
Techniques
OAuth 2.1 for Remote Servers
Remote MCP servers (SSE or streamable HTTP) should use OAuth 2.1 for authentication. The MCP specification defines an authorization flow:
- Client discovers the server's authorization endpoint via
/.well-known/oauth-authorization-server. - Client initiates OAuth 2.1 Authorization Code flow with PKCE.
- User authenticates and authorizes the client.
- Client receives an access token and includes it in MCP requests.
// Server-side OAuth middleware (Express example)
import { verifyAccessToken } from "./auth.js";
app.use("/mcp", async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing bearer token" });
}
const token = authHeader.slice(7);
try {
const claims = await verifyAccessToken(token);
req.user = claims;
next();
} catch {
return res.status(401).json({ error: "Invalid or expired token" });
}
});
// OAuth metadata endpoint
app.get("/.well-known/oauth-authorization-server", (req, res) => {
res.json({
issuer: "https://mcp.example.com",
authorization_endpoint: "https://mcp.example.com/authorize",
token_endpoint: "https://mcp.example.com/token",
response_types_supported: ["code"],
code_challenge_methods_supported: ["S256"],
grant_types_supported: ["authorization_code", "refresh_token"],
});
});
API Key Management
For local (stdio) servers, API keys for downstream services come from environment variables:
const server = new McpServer({ name: "api-server", version: "1.0.0" });
// Read API keys from environment
const apiKey = process.env.API_KEY;
if (!apiKey) {
console.error("ERROR: API_KEY environment variable is required");
process.exit(1);
}
server.tool("call_api", "Call the external API", { endpoint: z.string() }, async ({ endpoint }) => {
const response = await fetch(`https://api.example.com${endpoint}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
// ...
});
Client configuration passes environment variables:
{
"mcpServers": {
"api-server": {
"command": "npx",
"args": ["-y", "@myorg/mcp-api-server"],
"env": {
"API_KEY": "sk-..."
}
}
}
}
Never log, return, or expose API keys in tool responses.
Input Validation and Sanitization
Validate every input. The model constructs tool arguments based on user requests — these are untrusted:
server.tool(
"read_file",
"Read a file within the project directory",
{
path: z.string().describe("Relative file path within the project"),
},
async ({ path: filePath }) => {
// Prevent path traversal
const resolved = path.resolve(PROJECT_DIR, filePath);
if (!resolved.startsWith(PROJECT_DIR)) {
return {
isError: true,
content: [{ type: "text", text: "Access denied: path is outside project directory" }],
};
}
// Check file exists
try {
await fs.access(resolved, fs.constants.R_OK);
} catch {
return {
isError: true,
content: [{ type: "text", text: `File not found: ${filePath}` }],
};
}
// Limit file size
const stat = await fs.stat(resolved);
if (stat.size > 1024 * 1024) {
return {
isError: true,
content: [{ type: "text", text: "File too large (max 1MB)" }],
};
}
const content = await fs.readFile(resolved, "utf-8");
return { content: [{ type: "text", text: content }] };
}
);
SQL Injection Prevention
@mcp.tool()
async def find_user(email: str) -> str:
"""Find a user by email address.
Args:
email: Email address to search for
"""
# CORRECT: parameterized query
row = await db.fetchrow("SELECT id, name, email FROM users WHERE email = $1", email)
# WRONG: string interpolation — SQL injection vulnerability
# row = await db.fetchrow(f"SELECT * FROM users WHERE email = '{email}'")
if row is None:
return "No user found with that email"
return json.dumps(dict(row), indent=2)
Rate Limiting
Protect downstream services from excessive calls:
import { RateLimiter } from "limiter";
// Allow 10 requests per minute per tool
const limiters = new Map<string, RateLimiter>();
function getToolLimiter(toolName: string): RateLimiter {
if (!limiters.has(toolName)) {
limiters.set(toolName, new RateLimiter({
tokensPerInterval: 10,
interval: "minute",
}));
}
return limiters.get(toolName)!;
}
server.tool(
"search_web",
"Search the web using an external API",
{ query: z.string() },
async ({ query }) => {
const limiter = getToolLimiter("search_web");
const hasTokens = await limiter.tryRemoveTokens(1);
if (!hasTokens) {
return {
isError: true,
content: [{ type: "text", text: "Rate limit exceeded. Please wait before searching again." }],
};
}
const results = await searchAPI.search(query);
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
}
);
Sandboxing Tool Execution
For tools that run code or commands, use sandboxing:
import { execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
server.tool(
"run_script",
"Run a Python script in a sandboxed environment",
{ script: z.string() },
async ({ script }) => {
try {
const { stdout, stderr } = await execFileAsync("docker", [
"run",
"--rm",
"--network=none", // No network access
"--memory=256m", // Memory limit
"--cpus=0.5", // CPU limit
"--read-only", // Read-only filesystem
"--tmpfs", "/tmp:size=64m", // Writable temp directory
"python:3.12-slim",
"python", "-c", script,
], { timeout: 30000 });
return {
content: [{
type: "text",
text: `Output:\n${stdout}\n${stderr ? `Errors:\n${stderr}` : ""}`,
}],
};
} catch (err) {
return {
isError: true,
content: [{ type: "text", text: `Execution failed: ${err.message}` }],
};
}
}
);
Principle of Least Privilege
Design tools with minimal permissions:
# GOOD: Separate read and write tools with appropriate annotations
@mcp.tool()
async def list_records(table: str, limit: int = 20) -> str:
"""List records from a table (read-only).
Args:
table: Table name (must be in the allowed list)
limit: Maximum records to return
"""
allowed_tables = {"users", "orders", "products"}
if table not in allowed_tables:
raise ValueError(f"Table '{table}' is not in the allowed list: {allowed_tables}")
rows = await db.fetch(f"SELECT * FROM {table} LIMIT $1", min(limit, 100))
return json.dumps([dict(r) for r in rows], indent=2)
# BAD: One tool with unrestricted database access
@mcp.tool()
async def database(query: str) -> str:
"""Run any SQL query.""" # Too broad, too dangerous
return json.dumps(await db.fetch(query))
Logging and Audit Trails
server.tool(
"delete_record",
"Delete a record from the database",
{
table: z.enum(["drafts", "temp_data"]), // Only allow deletion from safe tables
id: z.number().int().positive(),
},
async ({ table, id }) => {
// Log the action for audit
console.error(JSON.stringify({
action: "delete_record",
table,
id,
timestamp: new Date().toISOString(),
}));
const result = await db.query(`DELETE FROM ${table} WHERE id = $1 RETURNING id`, [id]);
if (result.rowCount === 0) {
return { isError: true, content: [{ type: "text", text: `Record ${id} not found in ${table}` }] };
}
return { content: [{ type: "text", text: `Deleted record ${id} from ${table}` }] };
}
);
Anti-Patterns
- Exposing raw SQL execution — never let the model run arbitrary SQL. Provide specific, parameterized query tools.
- Returning secrets in tool responses — API keys, tokens, and passwords must never appear in tool output.
- Trusting file paths from input — always resolve and validate paths against an allowed base directory.
- No rate limiting on API-calling tools — the model can call tools rapidly, exhausting API quotas or triggering bans.
- Running user-provided code without sandboxing — always use containers, VMs, or restricted interpreters.
- Broad permissions — a tool that can read any file on the system when it only needs project files.
- Logging sensitive data — audit logs should record actions and identifiers, not the full content of sensitive records.
Best Practices
- Use OAuth 2.1 with PKCE for remote servers. Use environment variables for API keys with local servers.
- Validate all inputs at the tool level, even if the schema provides constraints.
- Use parameterized queries for all database operations.
- Apply rate limiting to tools that call external APIs or perform expensive operations.
- Sandbox any tool that executes code or system commands.
- Follow least privilege — each tool should have the minimum permissions needed.
- Log all destructive actions (deletes, updates, external API calls) for audit.
- Use
additionalProperties: falsein input schemas to reject unexpected fields. - Regularly review tool definitions for security implications when adding new capabilities.
Install this skill directly: skilldb add mcp-server-skills
Related Skills
MCP Deployment
Deploying MCP servers across different environments and transports. Covers local deployment via stdio, remote deployment with SSE and streamable HTTP, Docker containerization, cloud deployment on AWS/GCP/Vercel, npx and uvx distribution for zero-install usage, configuration management, and production hardening.
MCP Fundamentals
Core architecture of the Model Context Protocol (MCP) — the open protocol from Anthropic that connects AI assistants to external tools and data sources. Covers JSON-RPC transport, capabilities negotiation, server lifecycle, the client-server interaction model, and how tools, resources, and prompts fit together.
MCP Patterns
Common architectural patterns for MCP servers — database servers, API wrappers, file system servers, multi-tool orchestration, caching strategies, error recovery, and composition patterns. Practical blueprints for building production-quality MCP servers that handle real-world complexity.
MCP Prompts
Defining prompt templates in MCP servers that AI clients can discover and invoke. Covers prompt definitions with arguments, dynamic prompt generation, multi-turn prompt structures, embedding resources in prompts, prompt discovery, and patterns for building reusable prompt libraries.
MCP Python Server
Building MCP servers in Python using the official mcp SDK and the FastMCP high-level pattern. Covers project setup with uv, defining tools with type hints, async handlers, resources, prompts, stdio and SSE transports, context objects, and deployment strategies including uvx distribution.
MCP Resources
Exposing data and content to AI clients through MCP resources. Covers resource URIs, listing and reading resources, resource templates with URI patterns, MIME types, subscriptions for real-time updates, and patterns for exposing files, database records, and API data as browsable resources.