Skip to main content
← Back to Articles
SecurityBest PracticesMCP DevelopmentChecklist

MCP Security Checklist: Server-Side Code Patterns for Developers (2026)

A practical, code-first MCP security checklist: input validation, rate limiting, least privilege, and output sanitization patterns you can paste into a server today.

By Web MCP GuideAugust 4, 202610 min read


Looking for the full threat-model deep dive instead? This page is the short, code-first version — copy-pasteable server-side patterns and a pre-deployment checklist. For prompt injection, tool poisoning, data exfiltration scenarios, and an enterprise security program, see the complete MCP security guide.

Security is paramount when building MCP integrations. You're essentially giving AI applications access to your systems — that power needs to come with responsibility. Before diving into security, make sure you understand how MCP works and its architecture. Here are the essential server-side code patterns every MCP developer should have on hand — think of it as the reference you paste from while writing a server, not the full explainer of why each risk exists.

The Security Mindset

MCP servers act as a bridge between AI and your systems. Unlike traditional APIs where users explicitly make requests, MCP tools can be invoked by AI models based on their interpretation of user requests. This creates unique security challenges.

Key Principle: Assume the AI might be convinced to do something unintended. Design your servers to be safe even when misused.

Server-Side Security

1. Validate All Inputs

Never trust input from AI applications. Always validate and sanitize:

server.tool(
  "query_database",
  "Query the database",
  {
    query: z.string()
      .max(1000)  // Limit length
      .refine(q => !q.toLowerCase().includes('drop'), {
        message: "Destructive queries not allowed"
      }),
  },
  async ({ query }) => {
    // Additional validation
    if (containsSqlInjection(query)) {
      throw new Error("Invalid query detected");
    }
    // ... execute safely
  }
);

2. Implement Rate Limiting

Prevent abuse by limiting how often tools can be called:

const rateLimiter = new Map<string, number[]>();

function checkRateLimit(toolName: string, maxCalls: number, windowMs: number): boolean {
  const now = Date.now();
  const calls = rateLimiter.get(toolName) || [];
  const recentCalls = calls.filter(t => t > now - windowMs);
  
  if (recentCalls.length >= maxCalls) {
    return false;
  }
  
  recentCalls.push(now);
  rateLimiter.set(toolName, recentCalls);
  return true;
}

3. Principle of Least Privilege

Only expose what's absolutely necessary:

// Bad: Expose entire filesystem
server.tool("read_file", ..., async ({ path }) => {
  return fs.readFile(path);  // Dangerous!
});

// Good: Restrict to specific directories
const ALLOWED_DIRS = ['/home/user/projects', '/tmp/workspace'];

server.tool("read_file", ..., async ({ path }) => {
  const resolved = path.resolve(path);
  const isAllowed = ALLOWED_DIRS.some(dir => resolved.startsWith(dir));
  
  if (!isAllowed) {
    throw new Error("Access denied: Path not in allowed directories");
  }
  
  return fs.readFile(resolved);
});

4. Sanitize Outputs

Don't leak sensitive information in responses:

function sanitizeOutput(data: any): any {
  // Remove sensitive fields
  const sensitiveKeys = ['password', 'token', 'apiKey', 'secret'];
  
  if (typeof data === 'object' && data !== null) {
    return Object.fromEntries(
      Object.entries(data)
        .filter(([key]) => !sensitiveKeys.some(s => 
          key.toLowerCase().includes(s)
        ))
        .map(([key, value]) => [key, sanitizeOutput(value)])
    );
  }
  
  return data;
}

5. Log Everything

Maintain audit trails for debugging and security analysis:

function logToolInvocation(toolName: string, args: any, result: any) {
  console.error(JSON.stringify({
    timestamp: new Date().toISOString(),
    tool: toolName,
    arguments: args,
    success: !result.isError,
    // Don't log full results to avoid leaking data
    resultSize: JSON.stringify(result).length,
  }));
}

Client-Side Security

1. Review Tool Descriptions

AI models use tool descriptions to decide when to invoke them. Malicious or poorly written descriptions could lead to unexpected behavior:

// Be specific about what tools do
server.tool(
  "delete_file",
  "PERMANENTLY deletes a file. Cannot be undone. Use with caution.",
  // ...
);

2. User Confirmation for Sensitive Operations

For operations with significant impact, implement confirmation:

server.tool(
  "send_email",
  "Send an email (requires user confirmation)",
  { to: z.string(), subject: z.string(), body: z.string() },
  async (args) => {
    // Return preview for confirmation
    return {
      content: [{
        type: "text",
        text: `Ready to send email:\nTo: ${args.to}\nSubject: ${args.subject}\n\nCall confirm_send_email to proceed.`,
      }],
    };
  }
);

3. Timeout Long Operations

Don't let tools run indefinitely:

async function withTimeout<T>(
  promise: Promise<T>, 
  timeoutMs: number
): Promise<T> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error("Operation timed out")), timeoutMs);
  });
  
  return Promise.race([promise, timeout]);
}

server.tool("long_operation", ..., async (args) => {
  return withTimeout(performOperation(args), 30000);  // 30 second timeout
});

Common Vulnerabilities to Avoid

Path Traversal

// Vulnerable
const content = await fs.readFile(`/data/${userInput}`);

// Safe
const safePath = path.join('/data', path.basename(userInput));

Command Injection

// Vulnerable
exec(`git log ${branch}`);

// Safe
execFile('git', ['log', branch]);

SQL Injection

// Vulnerable
db.query(`SELECT * FROM users WHERE id = ${userId}`);

// Safe
db.query('SELECT * FROM users WHERE id = $1', [userId]);

Information Disclosure

// Vulnerable - exposes system info
catch (error) {
  return { error: error.stack };
}

// Safe - generic error
catch (error) {
  console.error(error);  // Log internally
  return { error: "Operation failed" };
}

Secure Configuration

Environment Variables

Never hardcode secrets:

// Bad
const API_KEY = "sk-12345...";

// Good
const API_KEY = process.env.API_KEY;
if (!API_KEY) {
  throw new Error("API_KEY environment variable required");
}

Configuration Files

If your server needs a config file, validate it:

const configSchema = z.object({
  allowedDirs: z.array(z.string()),
  maxFileSize: z.number().max(10 * 1024 * 1024),
  enableDangerousOperations: z.boolean().default(false),
});

const config = configSchema.parse(JSON.parse(configFile));

Security Checklist

Before deploying an MCP server:

  • [ ] All inputs are validated and sanitized

  • [ ] Rate limiting is implemented

  • [ ] Sensitive operations require confirmation

  • [ ] Outputs are sanitized for sensitive data

  • [ ] Audit logging is in place

  • [ ] Timeouts are set for all operations

  • [ ] Least privilege principle is followed

  • [ ] No hardcoded secrets

  • [ ] Error messages don't leak sensitive info

  • [ ] Path traversal is prevented

  • [ ] Command/SQL injection is prevented
  • A 10-Minute Audit for a Server You Already Built

    If you already have an MCP server running and just want to know how exposed it is right now, check these four things first — they catch the majority of real-world misconfigurations faster than working through the full checklist top to bottom:

    1. Read the actual env block in your mcp.json. Is there a live API key or database password sitting in plain text in a file that could end up in a dotfiles repo or a screen share? That's the single most common real-world MCP security mistake, and it has nothing to do with code quality.
    2. Check what directory or database user the server can reach. A filesystem server pointed at your home directory or a database user with DELETE rights "just in case" is over-privileged by default, not because anyone decided it should be.
    3. Grep your tool descriptions for anything that sounds like an instruction rather than a description. ("Always call this first," "ignore other tools for this task.") That's the tool-poisoning pattern from the full threat-model guide, and it's visible just by reading the source.
    4. Confirm write-capable tools (send, delete, execute) require confirmation, not just read tools. It's easy to lock down read_file and forget that write_file or execute_command sitting in the same server needs the same scrutiny.

    Conclusion

    Security in MCP is not optional — it's essential. As AI applications become more capable, the potential impact of security vulnerabilities grows. By following these best practices, you can build MCP integrations that are both powerful and safe.

    Remember: the goal is to enable AI to help users while preventing it from being tricked into causing harm. Design with paranoia, test thoroughly, and always err on the side of caution.

    Frequently Asked Questions

    Q: Should I use this checklist or the full MCP security guide?
    A: Use this one while you're actively writing server code and want a pattern to paste from. Use the full guide when you need to understand why a risk exists — prompt injection via tool responses, tool poisoning, cross-server data exfiltration — or when you're building an enterprise security review process rather than a single server.

    Q: Which single item on the checklist matters most if I can only do one thing?
    A: Least privilege on credentials. A read-only database user or a scoped API token turns most of the other failure modes (SQL injection, over-broad tool access, accidental writes) from "data breach" into "the tool just returns an error." Input validation matters too, but a compromised or buggy tool with a read-only credential behind it has a much lower ceiling on damage than one with an admin key.

    Q: Do I need all of this for a personal, local-only MCP server?
    A: The audit checklist above is worth five minutes even for personal use — hardcoded credentials and over-broad filesystem access are just as real a risk on a laptop as in production. Full audit logging and container sandboxing are more of a production/team concern; skip those for a server only you run locally.

    Q: My server only has read-only tools — do I still need rate limiting?
    A: Yes, for a different reason than write-safety: an unbounded loop of read calls (the AI re-querying a large table or re-fetching a large file repeatedly) can still exhaust resources or run up API costs against a paid backend, even with zero risk of data modification.

    Q: How often should I re-run this checklist?
    A: Whenever you add a new tool, change a credential's scope, or update a dependency that touches how inputs are handled. A stale checklist pass from six months ago doesn't cover a tool you added last week.

    Ready to build secure MCP servers? Check out our guide to building your first server or explore real-world MCP use cases.

    ---


    Related guides