Lesson 5 of 6·10 min read

Security & Best Practices

An MCP server that provides tools to an AI model has real power to act. Without security measures, a compromised server can leak data, perform unauthorized actions, or serve as an attack vector.

Authentication

Token-Based Authentication

server.tool(
  "get_customer",
  "Retrieve customer data",
  { customerId: z.string() },
  async ({ customerId }, context) => {
    // Check auth token from context
    const token = context.meta?.authToken;
    if (!token || !await verifyToken(token)) {
      return {
        content: [{ type: "text", text: "Error: Not authorized" }],
        isError: true
      };
    }
    const customer = await db.getCustomer(customerId);
    return { content: [{ type: "text", text: JSON.stringify(customer) }] };
  }
);

API Key Management

  • Never store API keys in code
  • Use environment variables or secret managers
  • Rotate keys regularly
  • Separate keys for development and production

Authorization

Role-Based Access Control (RBAC)

const permissions: Record<string, string[]> = {
  "read-only":  ["get_customer", "list_orders", "search_products"],
  "editor":     ["get_customer", "list_orders", "update_order", "create_ticket"],
  "admin":      ["*"]  // All tools
};

function checkPermission(role: string, toolName: string): boolean {
  const allowed = permissions[role];
  return allowed?.includes("*") || allowed?.includes(toolName) || false;
}

Granular Permissions

  • Read tools: Allowed by default
  • Write tools: Explicit approval required
  • Delete tools: Human approval required
  • Admin tools: Only for privileged users

Input Validation

Every tool input must be validated before the action is executed:

server.tool(
  "execute_query",
  "Execute SQL query on the database",
  {
    query: z.string()
      .max(1000)
      .refine(q => !q.toLowerCase().includes("drop"), "DROP not allowed")
      .refine(q => !q.toLowerCase().includes("delete"), "DELETE not allowed")
      .refine(q => q.toLowerCase().startsWith("select"), "Only SELECT queries allowed")
  },
  async ({ query }) => {
    const result = await db.query(query);
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  }
);

Sandboxing

Process Isolation

  • Run MCP servers in separate processes
  • Container-based isolation for critical servers
  • Restrict network access to the minimum necessary

Resource Limits

const serverConfig = {
  maxRequestsPerMinute: 60,
  maxTokensPerRequest: 10000,
  timeoutMs: 30000,
  maxConcurrentRequests: 5
};

Rate Limiting

import { RateLimiter } from "./rate-limiter";

const limiter = new RateLimiter({ maxRequests: 60, windowMs: 60000 });

server.tool("search", "Perform search", { query: z.string() },
  async ({ query }) => {
    if (!limiter.allow()) {
      return {
        content: [{ type: "text", text: "Rate limit reached. Please wait." }],
        isError: true
      };
    }
    return { content: [{ type: "text", text: await search(query) }] };
  }
);

Audit Logging

Every action must be logged:

async function auditLog(event: {
  tool: string;
  input: unknown;
  output: unknown;
  userId: string;
  timestamp: Date;
  success: boolean;
}) {
  await db.insert("audit_log", event);
  if (!event.success) {
    await alerting.notify(`Tool error: ${event.tool}`, event);
  }
}

Practical tip: Security is not an afterthought — it's built in from the start. Every MCP server should have at least authentication, input validation, and audit logging. For production, add rate limiting and sandboxing.