09/17/2026

Writing an MCP Server That Doesn't Leak

Most getting-started guides for the Model Context Protocol (MCP) focus on wiring up your first tool and seeing an agent parse the JSON output. Few address what happens when an autonomous agent chains tool calls beyond your intended boundaries.

If an agent can execute an action, it eventually will. Relying on system prompts to keep an LLM from calling a destructive or over-scoped tool is a security failure waiting to happen. Boundaries must be enforced at the server layer.

This checklist provides a tactical, testable framework for auditing an MCP server—whether you built it yourself or are integrating a third-party server into your workflow.


The Three-Tier Permission Model

Before running through the checklist, categorize every tool your MCP server exposes into one of three execution tiers:

  1. Tier 1: Autonomous / Idempotent (Regular)

    • Characteristics: Read-only, side-effect-free, bounded output.
    • Examples: Reading a specific file, querying a read-only database view, fetching public API data.
    • Access: Automatically approved for agent execution.
  2. Tier 2: Interactive / Guarded (Requires Explicit Confirmation)

    • Characteristics: State-mutating, additive, or constrained write operations.
    • Examples: Writing a new file, creating a database record, committing a patch, triggering a build pipeline.
    • Access: Halts execution until an explicit human-in-the-loop (HITL) confirmation or token validation is granted.
  3. Tier 3: Prohibited / Unbounded (Blocked)

    • Characteristics: High-blast-radius, arbitrary execution, or destructive operations.
    • Examples: Executing arbitrary shell commands (bash), unrestricted raw SQL queries (DROP, DELETE), reading /etc/passwd or process environment variables.
    • Access: Hard-disabled at the server registration level.

The Audit Checklist

Run these five tests against your server before granting an agent access to its capabilities.

1. Granularity & Scope Audit

  • Are tools single-purpose? Avoid "god tools" like execute_query(sql) or run_script(cmd). Replace them with atomic endpoints like get_user_by_id(id) or create_draft_post(title, body).
  • Are parameters strictly typed and validated? Ensure arguments use strict JSON Schema definitions with enums, regex patterns, and length limits rather than open-ended strings where possible.

2. Path & Resource Boundary Audit

  • Is filesystem access strictly jailed? If the server reads or writes files, verify that paths are canonicalized (realpath) and checked against an absolute allowlist directory.
  • Are directory traversal attacks blocked? Test inputs containing ../, ..%2F, or symlinks to ensure the server rejects requests targeting parent directories.
  • Are database queries scoped to specific schemas/tenants? Verify that query parameters cannot break out of their intended database scope via unescaped inputs.

3. State & Error Sanitization Audit

  • Are error messages sanitized? Inspect stack traces. Ensure database connection strings, local file paths, internal network IPs, and environment details are stripped before returning an error payload to the client.
  • Is secret leakage prevented in tool output? Verify that tools returning raw HTTP responses or configuration files explicitly redact tokens, API keys, and auth headers.
  • Is execution state isolated between calls? Confirm that one tool call cannot alter global process state in a way that affects subsequent calls from different sessions or tenants.

4. Tier 2 Confirmation Hooks

  • Does the server signal mutating tools correctly? Tools tagged as state-changing must require positive confirmation via the host client before execution.
  • Is dry-run mode available? For complex mutating operations, provide a dry_run: true parameter by default so the agent can inspect expected side effects before execution.

5. Transport & Network Security

  • Is transport encrypted and authenticated? If running over HTTP/SSE rather than STDIO, ensure strict authentication headers are required for all endpoint connections.
  • Is rate-limiting enforced? Ensure an agent stuck in an infinite loop cannot overwhelm backend services or exhaust API quotas.

Code Example: Enforcing Boundaries in Handler Code

Here is a simplified example of wrapping a tool execution handler to strictly enforce path boundaries and permission tiers before running an operation:

import path from "node:path";

const ALLOWED_ROOT = "/var/www/app/storage/logs";

interface ReadLogArgs {
  filename: string;
}

export async function handleReadLog(args: ReadLogArgs) {
  // 1. Sanitize and resolve input path
  const safePath = path.normalize(args.filename).replace(/^(\.\.[\/\\])+/, "");
  const fullPath = path.join(ALLOWED_ROOT, safePath);

  // 2. Verify path stays within the boundary
  if (!fullPath.startsWith(ALLOWED_ROOT)) {
    throw new Error("Access denied: Target path outside allowed boundary.");
  }

  // 3. Perform idempotent Tier 1 operation
  const content = await fs.promises.readFile(fullPath, "utf-8");
  return { content };
}

Summary

Agents do not possess intuition about risk. If an MCP server exposes an unbounded tool, an agent will eventually use it in an unexpected context. By enforcing strict parameter schemas, jailing filesystem access, and hard-blocking Tier 3 capabilities at the server level, you ensure your agent remains a productive assistant rather than a security vector.

Filed under