This post is a practical checklist for securing an MCP server you own — whether it exposes internal tools, a knowledge base, or a workflow engine. It assumes you have already decided MCP is the right transport; if not, evaluate stdio-only proprietary transports first, and adopt MCP once you need multi-client interoperability.
Transport: HTTP over TLS, not stdio
Stdio transport is a good fit for local development and for MCP servers that are part of a monolithic desktop app. It is a bad fit for production because:
- No natural insertion point for a gateway.
- Audit trail ends at the process boundary.
- Cannot centrally revoke.
- Cannot rate-limit across clients.
Production MCP servers should serve HTTP with TLS termination and stateless request handling.
Auth: EMA (OAuth via your IdP)
The Enterprise-Managed Authorization extension, stable as of July 2026, is the sanctioned auth path. Configure your MCP server to trust tokens issued by your IdP (Okta, Entra, Ping, Auth0). Verify the token signature, audience, and expiry on every request.
Two anti-patterns to avoid:
- Static bearer tokens embedded in client configs. Rotate-hostile, revoke-hostile.
- API keys with no expiry and no scope. Once leaked, they are leaked forever until manually invalidated.
Scope: least-privilege per tool
Do not expose a single omni-scope. Group tools by risk tier and require the token to carry the specific scope claim for the tools it invokes:
| Tier | Examples | Auth requirement |
|---|---|---|
| Read | list_files, get_document, search_kb | OAuth token with scope mcp.read |
| Write | update_document, create_ticket | Scope mcp.write + user consent |
| Destructive | delete_record, transfer_funds | Scope mcp.destructive + explicit approval flow |
The token scope is not a hint. The server must reject any tool call whose required scope is not present in the token.
Rate limiting
Every MCP server should limit requests per client per second. The failure mode without rate limiting: a runaway agent spawns hundreds of tool calls in a loop, quickly exhausting downstream resources (database connections, third-party API quotas).
Reasonable defaults for a mid-scale server:
- Read tools: 100 rps per client.
- Write tools: 10 rps per client.
- Destructive tools: 1 rps per client, or gated on explicit approval each time.
Input validation
Tool arguments are JSON. That does not make them safe. Validate every argument against a schema before invocation. Reject requests with unknown arguments. Reject arguments that fail type or range checks.
The specific attack to prevent: a compromised or manipulated agent calling a legitimate tool with a payload that abuses the downstream system (SQL injection via a "search_kb" tool that concatenates user input into a query, or path traversal via a "read_file" tool that does not normalize paths).
Audit: one row per tool invocation
Every tool call should produce an audit row containing:
- Timestamp (UTC, high resolution).
- Caller identity (from the token subject).
- Tool name.
- Redacted arguments (hash-log for size-bounded audit; keep the full args only if compliance requires).
- Outcome (success, failure, denied by policy).
- Downstream side-effect id (ticket id, transaction id) where applicable.
- Trace id linking to the enforcing gateway's decision, if any.
The audit table should be append-only and readable by SRE + Security roles, not writable by the server itself in a way that permits after-the-fact edits.
Secrets: never store credentials in MCP prompts
Prompts and MCP payloads are logged, forwarded, embedded, and sometimes accidentally sent to LLM providers as part of larger contexts. Do not embed API keys, database passwords, or PII in MCP request bodies. If the tool needs to authenticate downstream, the credential should be resolved server-side by the identity of the caller, not passed inline.
The specific defense: an MCP gateway should have a guardrail that inspects outgoing LLM payloads for patterns matching known credential formats (tnk_, sk-, pak_, Bearer ) and drops the context or masks the credential before forwarding.
Revocation
Assume every issued token will eventually leak. Design revocation so that:
- Token revocation propagates to every MCP server within a bounded window (≤60 s).
- Revoked tokens fail cleanly with a typed error, not a generic 401.
- Recent audit rows are queryable by token subject, so incident response can enumerate the blast radius.
The five questions to run against your MCP server today
- If a client's token is compromised, what is the maximum blast radius?
- Can I revoke that token without redeploying the server?
- Do I have an audit row for every tool call in the last 30 days?
- Do destructive tools require a scope claim that most tokens do not carry?
- Can I trace an arbitrary tool call to the LLM decision that triggered it?
If any answer is "no" or "not sure," you have a security debt. Prioritize the biggest blast-radius item first.
Related reading: What is MCP governance? · MCP registries compared · ThinkNEO MCP servers