MCP servers are network services. They accept input from untrusted clients, make outbound requests, handle credentials, and run on shared infrastructure. The protocol itself is well-designed, but the security posture of most deployments is an afterthought.
We have tested hundreds of MCP servers through our compliance suite and proxied production traffic for dozens more. This is the checklist we wish every server operator had before going live.
1. Block SSRF from tool handlers
If any of your tools make outbound HTTP requests based on user input - fetching a URL, calling an API, loading a resource - you are exposed to Server-Side Request Forgery.
An attacker sends a tool call with a URL pointing to http://169.254.169.254/latest/meta-data/ (AWS instance metadata), http://metadata.google.internal/ (GCP), or a private RFC 1918 address. Your server dutifully fetches it and returns the result.
The fix has two parts:
- Block private and reserved IP ranges - loopback (127.0.0.0/8), link-local (169.254.0.0/16), RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and cloud metadata endpoints
- Resolve DNS on every request - not just at configuration time. DNS rebinding attacks work by returning a public IP during validation, then switching to an internal IP before the actual request. Resolve, check, then connect.
// Validate resolved IP before connecting
import { isPrivateIP } from './network-utils';
async function safeFetch(url: string): Promise<Response> {
const resolved = await dns.resolve4(new URL(url).hostname);
for (const ip of resolved) {
if (isPrivateIP(ip)) {
throw new Error(`Blocked request to private IP: ${ip}`);
}
}
return fetch(url);
}We flagged this in our compliance findings. It remains the most dangerous gap in the ecosystem for any platform that accepts user-defined server URLs.
2. Never log credentials or tool arguments verbatim
MCP tool calls carry arguments in the params.argumentsfield. Those arguments frequently contain API keys, database connection strings, OAuth tokens, and other secrets that users paste into tool inputs.
If your logging pipeline writes tool call arguments to disk, to a log aggregator, or to an analytics store, you are storing credentials in plaintext. This is not hypothetical - we have seen it in production.
Rules:
- Log tool names and result status, not argument values
- If you must log arguments for debugging, redact fields that match common secret patterns (
*_KEY,*_TOKEN,*_SECRET,password,authorization) - Never include raw request/response bodies in error reports sent to third-party services
3. Validate tool input schemas strictly
The MCP spec defines inputSchema on each tool. Most servers declare the schema but never validate incoming arguments against it. The result: tools receive unexpected types, extra fields, or missing required parameters and either crash or behave unpredictably.
This matters for security because an overly permissive tool handler can be manipulated through prompt injection. An LLM that has been tricked into calling a tool with crafted arguments will send whatever the attacker dictates. Your validation layer is the last line of defense.
// Validate arguments against the declared schema
import Ajv from 'ajv';
const ajv = new Ajv();
function validateToolCall(tool: Tool, args: unknown): void {
const validate = ajv.compile(tool.inputSchema);
if (!validate(args)) {
throw {
code: -32602,
message: `Invalid params: ${ajv.errorsText(validate.errors)}`,
};
}
}4. Scope everything to the authenticated tenant
We covered this in the auth post, but it bears repeating as a security concern specifically. If your MCP server is multi-tenant, every data path needs tenant scoping:
- Database queries - always include the tenant ID in WHERE clauses, not just the API layer
- Cache keys - prefix with the tenant ID so Tenant A never reads Tenant B’s cached data
- File paths - if tools read or write files, constrain paths to the tenant’s directory. Validate that resolved paths do not escape the sandbox via
..traversal - Log streams - tag every log entry with the tenant ID so audit trails are isolated
The pattern: extract the tenant identifier from the auth token at the middleware layer and thread it through every downstream call. Do not rely on individual tool handlers to remember to scope their queries.
5. Rate limit at the session and tool level
API-level rate limiting (requests per second per key) is table stakes. MCP servers need additional limits that account for how AI agents behave:
- Per-session limits - an agent in a retry loop can fire hundreds of requests in seconds. Cap total tool calls per session.
- Per-tool limits - expensive tools (database queries, API calls with rate-limited upstream services) need their own limits separate from cheap tools (list, describe).
- Cost-based limits - some tools cost real money per invocation (sending emails, creating resources). These should have hard daily caps per tenant, not just rate limits.
The cleanest place to enforce these limits is at the infrastructure layer - a reverse proxy or API gateway in front of the MCP server - so individual servers do not need to implement it themselves.
6. Use TLS everywhere, including local development
Remote MCP servers must use HTTPS. This is non-negotiable. But even local stdio servers should be aware of transport security:
- If your server makes outbound API calls, verify TLS certificates. Do not set
NODE_TLS_REJECT_UNAUTHORIZED=0in production. - If you proxy through an intermediate service, ensure the proxy terminates TLS and re-encrypts to the backend. Do not send credentials in plaintext between the proxy and your server, even on a private network.
- Pin certificates for critical upstream services if your threat model warrants it.
7. Protect against prompt injection in tool results
Tool results are fed back into the LLM context. If a tool returns user-controlled content (web page text, database records, file contents), an attacker can embed instructions that manipulate the LLM’s behavior.
This is not something your MCP server can fully solve - it is fundamentally a client-side concern. But you can reduce the attack surface:
- Clearly delineate data boundaries in tool results (use structured content types, not raw text dumps)
- Truncate excessively large results rather than passing megabytes of text into context
- If your tool fetches web content, strip script tags and HTML comments that are common prompt injection vectors
8. Audit every tool call
Every tool invocation should produce an audit record: who called it, when, which tool, success or failure, and the tenant context. You do not need to log arguments (see point 2), but you need to know what happened.
This is not just for security incidents. When a user reports that “the AI did something unexpected,” your audit log is the only way to reconstruct what actually happened. MCP tool calls are the actions an AI agent takes in the world. You need to be able to trace them.
9. Pin dependency versions in production
MCP servers built with npx -y pull the latest version of the package on every invocation. This is convenient for development and dangerous for production. A compromised or buggy upstream release deploys automatically to every instance.
- Pin exact versions in your
package.json - Use a lockfile (
package-lock.json,yarn.lock) and commit it - Run
npm auditin CI and fail on high-severity vulnerabilities - If you publish an MCP server as an npm package, enable provenance signatures so users can verify the build
10. Run compliance tests continuously
Security and compliance are related. A server that does not handle unknown methods correctly (a compliance failure) is also a server that might leak error details to attackers. A server that does not validate inputSchema (a compliance failure) is also a server that is vulnerable to crafted inputs.
Run mcp-compliance in your CI pipeline. Use --strict to fail on required test failures. Compliance is the baseline. Security is what you build on top.
# In CI: fail if any required compliance test fails
npx -y @yawlabs/mcp-compliance test $MCP_SERVER_URL --strictThe checklist
Before putting an MCP server in production:
- SSRF protection with DNS rebinding defense
- No credentials in logs or error reports
- Tool input validation against declared schemas
- Tenant isolation at every data layer
- Session-level and tool-level rate limiting
- TLS end-to-end, certificates verified
- Prompt injection mitigations in tool results
- Audit logging for every tool call
- Pinned dependencies, lockfile committed
- Continuous compliance testing in CI
Most of these are standard web security practices applied to a new context. The MCP-specific risks - SSRF through tool handlers, prompt injection through tool results, credential leakage through tool arguments - are the ones that catch people off guard.
Most of these are straightforward to implement yourself. The hard part is catching when one of them silently regresses - a header middleware reordered, a dependency bump that changed validation behavior, a new transport path that skipped the auth gate. An 88-test compliance suite against any MCP server (HTTP or stdio) graded A–F is the right final gate before you ship - wire it into CI.
Jeff Yaw, Yaw Labs. Follow along at tokenlimit.news for weekly notes on AI infrastructure.