title: 'Security Considerations' description: 'Securing your MCP servers and protecting sensitive operations'

Security Considerations

MCP servers often access sensitive data and perform critical operations. Security must be a core consideration from the start. In this lesson, we'll explore essential security practices for MCP servers.

The AI Trust Boundary

A critical concept: Never trust AI-generated input without validation.

When an AI calls your tools, the arguments it provides are AI-generated. They might be:

  • Malformed (the AI misunderstood the schema)
  • Malicious (if the AI was manipulated through prompt injection)
  • Inappropriate (the AI made poor decisions)

Always validate inputs as if they came from an untrusted external API.

Input Validation

Schema Validation

JSON Schema provides a first line of defense, but don't rely on it exclusively:

// JSON Schema ensures structure
const schema = {
  type: "object",
  properties: {
    email: { type: "string", format: "email" },
    age: { type: "integer", minimum: 0, maximum: 150 }
  },
  required: ["email", "age"]
};

// But add application-level validation too
function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  if (!emailRegex.test(email)) {
    throw new Error("Invalid email format");
  }

  // Additional checks: length, domain whitelist, etc.
  if (email.length > 254) {
    throw new Error("Email too long");
  }

  return true;
}

SQL Injection Prevention

Always use parameterized queries:

// WRONG - Vulnerable to SQL injection
const query = `SELECT * FROM users WHERE email = '${email}'`;

// RIGHT - Parameterized query
const query = "SELECT * FROM users WHERE email = ?";
const results = await db.query(query, [email]);

In Python with SQLite:

# WRONG
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")

# RIGHT
cursor.execute("SELECT * FROM users WHERE email = ?", (email,))

Path Traversal Prevention

Validate file paths to prevent directory traversal:

import path from "path";

function validateFilePath(userPath: string, baseDir: string): string {
  // Resolve to absolute path
  const resolvedPath = path.resolve(baseDir, userPath);

  // Ensure it's within baseDir
  if (!resolvedPath.startsWith(baseDir)) {
    throw new Error("Access denied: path outside allowed directory");
  }

  return resolvedPath;
}

// Usage
const safePath = validateFilePath(
  request.arguments.path,
  "/allowed/directory"
);
const content = await fs.readFile(safePath, "utf-8");

Authentication & Authorization

Client Authentication

For remote MCP servers (HTTP/SSE), implement authentication:

import { createServer } from "http";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";

const server = createServer(async (req, res) => {
  // Check authentication header
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    res.writeHead(401, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "Unauthorized" }));
    return;
  }

  const token = authHeader.substring(7);

  // Validate token
  const user = await validateToken(token);
  if (!user) {
    res.writeHead(401, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: "Invalid token" }));
    return;
  }

  // Proceed with MCP handling
  // ...
});

Tool-Level Authorization

Different users may have different permissions:

server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
  const { name, arguments: args } = request.params;

  // Extract user from context
  const user = extra.user;

  if (name === "delete_database") {
    // Only admins can delete
    if (!user.roles.includes("admin")) {
      throw new Error("Permission denied: admin role required");
    }
  }

  if (name === "view_salary") {
    // Only HR or the user themselves
    const targetUserId = args.userId;
    if (!user.roles.includes("hr") && user.id !== targetUserId) {
      throw new Error("Permission denied: cannot view other salaries");
    }
  }

  // Execute tool
  // ...
});

Secrets Management

Never hardcode secrets in your server code:

// WRONG
const apiKey = "sk-1234567890abcdef";

// RIGHT
const apiKey = process.env.API_KEY;

if (!apiKey) {
  throw new Error("API_KEY environment variable not set");
}

For local development, use .env files (and add .env to .gitignore):

# .env
DATABASE_URL=postgresql://localhost/mydb
API_KEY=sk-1234567890abcdef
import dotenv from "dotenv";
dotenv.config();

const dbUrl = process.env.DATABASE_URL;

Rate Limiting

Prevent abuse through rate limiting:

const rateLimits = new Map();

function checkRateLimit(userId: string): boolean {
  const now = Date.now();
  const userLimits = rateLimits.get(userId) || { count: 0, resetAt: now + 60000 };

  if (now > userLimits.resetAt) {
    // Reset window
    userLimits.count = 0;
    userLimits.resetAt = now + 60000;
  }

  if (userLimits.count >= 100) { // 100 requests per minute
    return false;
  }

  userLimits.count++;
  rateLimits.set(userId, userLimits);
  return true;
}

server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
  const userId = extra.user.id;

  if (!checkRateLimit(userId)) {
    throw new Error("Rate limit exceeded. Please try again later.");
  }

  // Process request
  // ...
});

Logging & Audit Trails

Log security-relevant events:

import winston from "winston";

const logger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: "audit.log" }),
  ],
});

server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
  const { name, arguments: args } = request.params;
  const user = extra.user;

  // Log tool calls
  logger.info("Tool called", {
    tool: name,
    user: user.id,
    timestamp: new Date().toISOString(),
    arguments: args, // Be careful logging sensitive data
  });

  // Execute tool
  // ...
});

Transport Security

Use TLS for Remote Servers

Always use HTTPS for remote MCP servers:

import https from "https";
import fs from "fs";

const options = {
  key: fs.readFileSync("private-key.pem"),
  cert: fs.readFileSync("certificate.pem"),
};

https.createServer(options, app).listen(443);

stdio Security

For stdio servers, security comes from process isolation. Ensure:

  • The server process runs with minimal privileges
  • File system access is restricted to necessary directories
  • Environment variables don't expose secrets to subprocesses

Principle of Least Privilege

Only expose what's necessary:

// WRONG - Overly powerful tool
{
  name: "execute_sql",
  description: "Execute arbitrary SQL"
}

// RIGHT - Specific, limited tools
{
  name: "search_users",
  description: "Search users by name or email"
},
{
  name: "update_user_email",
  description: "Update a user's email address"
}

Security Checklist

Before deploying an MCP server:

  • [ ] All inputs validated beyond JSON Schema
  • [ ] SQL queries use parameters, not string concatenation
  • [ ] File paths validated against directory traversal
  • [ ] Secrets loaded from environment, not hardcoded
  • [ ] Authentication required for remote access
  • [ ] Authorization checks on sensitive operations
  • [ ] Rate limiting implemented
  • [ ] Security events logged
  • [ ] TLS/HTTPS for remote servers
  • [ ] Dependencies up to date (run npm audit or pip check)

Security is not optional. In the next module, we'll explore production patterns including error handling, observability, and deployment strategies.

Security Considerations - Compass | Nick Treffiletti — MCP, AI Agents & Platform Engineering