Skip to content

MCP: Security & Production

An MCP server runs with real credentials and takes actions on behalf of a model that can be manipulated. Design as if the model’s inputs are hostile.

Threat Example Mitigation
Prompt injection A web page or ticket the agent reads says “call delete_all_projects” Least-privilege tools; confirmations or elicitation for destructive actions; no destructive tools on servers that also read untrusted content
Over-privileged credentials A server uses an admin token for a read-only job Scoped tokens per server; read-only credentials where possible
Secret leakage A token in mcp.json committed to git; secrets echoed in tool output ${env:…} / envFile / a secret manager; redact secrets in outputs and logs
Path traversal read_file("../../.ssh/id_rsa") Resolve and check paths against allowed roots
Command injection A tool builds a shell string from input Never pass input to a shell; use argument arrays; allowlist commands
SSRF A fetch_url tool used to reach internal services Allowlist domains; block private IP ranges
Tool poisoning / rug pull A third-party server changes its tool descriptions to manipulate the model Only install trusted servers; pin versions; review changes
Confused deputy (remote) A server passes the user’s token through to other APIs Validate the token audience; don’t forward client tokens
Denial of wallet / runaway A loop calls an expensive API thousands of times Rate limits, quotas, and result-size caps
  • Reads all credentials from environment variables; none in code or committed config
  • Validates every input (schemas, plus server-side checks for paths and IDs)
  • Restricts filesystem access to configured roots
  • No shell=True or exec(string); uses argument arrays
  • Destructive tools are annotated destructiveHint: true, and ask for confirmation (elicitation) or need an explicit confirm: true argument
  • Caps output size and pagination
  • Logs to stderr without secrets
  • Pins its dependencies (lockfile committed)
  • TypeScript (v2): mount the server with a framework adapter: @modelcontextprotocol/express, @modelcontextprotocol/hono, @modelcontextprotocol/fastify, or @modelcontextprotocol/node, on a route such as /mcp. (In v1 this was StreamableHTTPServerTransport, wired by hand.)
  • Python: mcp.run(transport="streamable-http"), or mount mcp.streamable_http_app() in your ASGI app.
  • Stateless mode (no session affinity) is simplest to scale horizontally. Use sessions only if you need server-initiated messages.
  • HTTPS only
  • OAuth 2.1 following the MCP authorization spec: the server acts as a resource server, publishes Protected Resource Metadata, and validates bearer tokens (issuer, audience, expiry, scopes)
  • Or, for internal or simple cases, API keys in an Authorization header (clients: headers in Cursor config, --header in Claude Code)
  • Per-user authorisation inside every tool (whether this user may see or change this record)
  • Validate the Origin header and bind to localhost when running locally, to prevent DNS rebinding
  • Register the client callback URLs you support (for Cursor: https://www.cursor.com/agents/mcp/oauth/callback and http://localhost:8787/callback)
  • Rate limiting per user or token
  • Timeouts on every upstream call
  • Structured logs: tool name, user, latency, and outcome (no secrets, and PII minimised)
  • Metrics and alerts on error rate and latency
  • Health endpoint for the load balancer
  • Horizontal scaling (stateless) or sticky sessions (stateful)
  • Semantic versioning in the server version field and package.
  • Adding a tool or an optional parameter is a minor change. Renaming or removing a tool, or adding a required parameter, is a major change.
  • Deprecate a tool before removing it: keep it for one release and say “Deprecated: use X” in its description.
  • The SDKs negotiate the protocol version automatically. Upgrade the SDK regularly and re-run your smoke tests and evals.
  • Keep a CHANGELOG.md.
Channel Audience How
Git repo + README Team Clone, build, and paste the config snippet
npm / PyPI package Anyone npx -y your-server / uvx your-server in the config
MCP Bundle (.mcpb) Claude Desktop users npx @anthropic-ai/mcpb init → mcpb pack; users open the file to install
Docker image Teams / isolation "command": "docker", "args": ["run", "-i", "--rm", "your/image"]
Remote URL Anyone, including Claude.ai Deploy it; share the URL; users add it as a connector or url entry
MCP Registry / directories Public Publish metadata to the official MCP registry, Cursor’s directory, or Claude’s connectors directory
Plugin Team / org Bundle the MCP config with skills and agents in a Claude Code or Cursor plugin
  • Threat-model table reviewed for this server
  • Every tool classified as read-only, write, or destructive; the annotations match
  • Destructive actions need confirmation
  • Credentials are scoped to the minimum
  • Injection tests: tool inputs containing ../, ; rm -rf, $(…), internal URLs
  • Output reviewed for secret or PII leakage
  • Dependencies scanned (npm audit / pip-audit)
  • For a remote server: auth tested with an expired, wrong-audience, or missing token (all must be rejected)