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 model in one table
Section titled “Threat model in one table”| 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 |
Local (stdio) server hardening
Section titled “Local (stdio) server hardening”- 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=Trueorexec(string); uses argument arrays - Destructive tools are annotated
destructiveHint: true, and ask for confirmation (elicitation) or need an explicitconfirm: trueargument - Caps output size and pagination
- Logs to stderr without secrets
- Pins its dependencies (lockfile committed)
Remote (Streamable HTTP) server
Section titled “Remote (Streamable HTTP) server”Switch the transport
Section titled “Switch the transport”- 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 wasStreamableHTTPServerTransport, wired by hand.) - Python:
mcp.run(transport="streamable-http"), or mountmcp.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.
Authentication & authorisation
Section titled “Authentication & authorisation”- 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
Authorizationheader (clients:headersin Cursor config,--headerin Claude Code) - Per-user authorisation inside every tool (whether this user may see or change this record)
- Validate the
Originheader 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/callbackandhttp://localhost:8787/callback)
Operations
Section titled “Operations”- 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)
Versioning & compatibility
Section titled “Versioning & compatibility”- Semantic versioning in the server
versionfield 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.
Distribution options
Section titled “Distribution options”| 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 |
Pre-release security review
Section titled “Pre-release security review”- 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)