MCP Security Best Practices for Production Servers

Ship MCP servers with per-client consent, audience-bound tokens, strict schemas, approval gates, isolation, and logs that catch tool abuse.

Thursday, June 4, 2026Omid Saffari
MCP Security Best Practices for Production Servers

An MCP server is not safe because it speaks MCP. It is safe only when every client, token, tool schema, high-risk action, and session is bounded before the model can call it.

The Production Rule: MCP Security Starts At The Tool Boundary

The control point is the tool boundary, not the prompt. MCP tools are model-controlled, which means the model can discover and invoke tools from the context and the user's request. That is the feature. It is also the reason production MCP security has to live in the server, client registration, policy layer, approval UI, and audit log.

The MCP tools specification is direct about the trust model: applications should show which tools are exposed, show when tools are invoked, and keep a human able to deny tool invocations. Treat that as the minimum bar, not a nice interface extra.

The first design question is still whether MCP should exist for this integration at all. If one internal workflow needs one deterministic call path, direct function calling may be simpler. If many clients need a shared, discoverable tool surface, MCP earns its place. That architecture line is covered in MCP vs Function Calling: The Production Decision Rule. Once MCP is the right surface, the security model has to assume a model may select the wrong tool with valid syntax.

Gate Every Client Before OAuth Leaves Your Server

Remote MCP security starts with OAuth audience binding and client consent. The current MCP authorization page resolves to the 2025-11-25 specification, and that spec defines authorization for HTTP-based transports. Stdio is different: stdio implementations should retrieve credentials from the environment instead of following the HTTP authorization flow.

For a remote MCP server, build the handshake like a resource server, not like a generic webhook:

  1. Publish protected resource metadata

    The MCP server must implement OAuth 2.0 Protected Resource Metadata, and the client must use it to discover the authorization server. Return the metadata location in WWW-Authenticate when a request arrives without a valid token.

  2. Make scope explicit

    Include a scope parameter in the WWW-Authenticate challenge when the operation needs a known permission. The client has to treat that challenge scope as authoritative for the current request, which keeps it from asking for every available permission up front.

  3. Bind the token to the MCP resource

    The client must send the resource parameter in both authorization and token requests. The server then validates that the access token was issued for this MCP server as the intended audience.

  4. Require PKCE and exact redirects

    MCP clients must implement PKCE, verify PKCE support before authorization, and use S256 when technically capable. Authorization servers must validate exact redirect URIs against registered values.

  5. Fail closed on invalid tokens

    Tokens belong in the Authorization header, not the query string. Invalid or expired tokens should return the OAuth error path, and insufficient scopes should return a scoped challenge instead of a partial execution.

A useful remote-server policy object is small and explicit:

JSON
{
  "mcp_resource": "https://mcp.example.com",
  "required_scope": "tickets:read",
  "token_audience": "https://mcp.example.com",
  "pkce_method": "S256",
  "redirect_policy": "exact-match",
  "scope_escalation": "challenge-then-retry"
}

That object is not enough by itself. It has to sit beside per-client consent. The official MCP security guidance calls out the confused-deputy problem for MCP proxy servers using static client IDs. The fix is per-client consent before the third-party authorization flow. Store approved client_id values per user, show the client name, requested scopes, and redirect URI, protect the consent flow with CSRF controls, and do not set the consent cookie or state session until after the user has approved the MCP server consent screen.

Never Pass Through A User Token

Token passthrough is the fastest way to make a clean MCP design unsafe. The MCP server should not accept a token issued for some other resource and then forward it to a downstream API. That breaks audience validation, makes revocation harder, and turns the MCP server into a confused deputy.

The production pattern is two-token separation:

BoundaryToken acceptedToken rejectedWhy
MCP client to MCP serverToken issued for the MCP resourceToken issued for Gmail, Jira, Postgres, or another APIThe MCP server has to validate its own audience
MCP server to upstream APISeparate upstream credential or exchanged tokenClient token passed through unchangedDownstream APIs should not trust unvalidated client-origin credentials
Runtime scope upgradeScoped challenge with the required operation scopeBroad token minted for the whole tool catalogScope escalation should be incremental
LoggingToken metadata and audience resultRaw token valueLogs must prove policy without becoming credential storage

When a tool needs upstream access, the MCP server acts as an OAuth client to that upstream service and gets the right upstream credential. The incoming MCP token proves the user and client may call the MCP server. It does not become the credential for every internal system behind the server.

This is also where scope minimization matters. Start with the smallest scope that makes the server useful, then use step-up authorization for operations that need more. A customer support MCP server might start with tickets:read, challenge for tickets:write when drafting a reply, and require a separate approval path before a tool can send or delete anything.

Treat Tool Definitions As Executable Surface

Tool metadata is part of the attack surface. MCP clients discover tools with tools/list and invoke them with tools/call. A tool definition can include name, title, description, inputSchema, outputSchema, annotations, icons, and execution metadata. OWASP's MCP cheat sheet treats descriptions, parameter names, types, return schemas, and tool results as places where malicious instructions can hide.

The server should register fewer tools with stricter schemas. The official tool spec says inputSchema must be a valid JSON Schema object, and for a tool with no parameters it recommends an empty object schema with additionalProperties: false. Use that same posture on tools with parameters: no undeclared properties, bounded enums where possible, and input strings constrained to the formats the downstream system actually accepts.

JSON
{
  "name": "billing.invoice.read",
  "description": "Read invoice metadata for an approved customer account.",
  "inputSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "invoice_id": {
        "type": "string",
        "pattern": "^inv_[A-Za-z]+$"
      }
    },
    "required": ["invoice_id"]
  }
}

Do not treat annotations as trusted policy. The MCP tools spec says clients must consider tool annotations untrusted unless they come from trusted servers. In production, that means a tool annotation can help the UI, but the policy engine still has to own the decision.

Pin the canonical tool definition at approval time. Store a cryptographic hash of the tool name, description, input schema, output schema, and approval category. Re-hash before execution. If a server changes billing.invoice.read into a broader export tool after approval, the request should fail before the model sees the changed surface.

Put Approval Gates On Irreversible Work

Every high-impact tool needs an approval class. The MCP tools spec says there should always be a human in the loop with the ability to deny invocations. OWASP makes the production version sharper: require explicit confirmation for destructive, financial, or data-sharing operations, show the full tool call parameters, never auto-approve tool calls, and make the confirmation UI impossible to bypass with model-crafted output.

Do not make approval a generic modal. Make it a policy result:

Tool classExampleDefault policyApproval screen must show
Read-onlyFetch one ticket or invoiceAllow with valid scopeUser, client, tool, record identifier
Write-draftDraft a reply or create a pending recordAllow with auditDiff or generated content before commit
External sendSend email, notify customer, post to SlackRequire approvalDestination, message body, attachments, source context
DestructiveDelete, refund, revoke, overwriteRequire elevated approvalResource, irreversible effect, rollback path if any
Data exportExport rows or filesRequire approval and data classificationQuery, filters, row class, destination

Approval is not just for user trust. It is the place where the system can enforce a second check on arguments that are syntactically valid but operationally wrong. A model may correctly call billing.invoice.refund.request with a customer that should not be refundable. The policy layer should evaluate business state before the approval UI appears, then the human sees the exact action that remains.

Isolate Local And Remote Servers Differently

Stdio and Streamable HTTP need different hardening. Stdio launches the MCP server as a subprocess and exchanges newline-delimited JSON-RPC over stdin and stdout. In that mode, the server must not write anything to stdout that is not a valid MCP message. Logs belong on stderr or in a separate sink, because polluted stdout can corrupt protocol messages.

Local does not mean trusted. OWASP recommends running local MCP servers in containers, chroot, or application sandboxes, restricting filesystem access, disabling network access unless needed, and separating sensitive servers from general-purpose servers. A local filesystem server should not have the same host permissions as the IDE, password manager, browser profile, and production deploy credentials.

Streamable HTTP has a different failure mode. It uses a single MCP endpoint that supports POST and GET, and can use SSE for streaming. The transport spec says Streamable HTTP servers must validate the Origin header on all incoming connections to prevent DNS rebinding. If the origin is invalid, the server must return HTTP 403 Forbidden. When running locally, Streamable HTTP servers should bind only to localhost rather than all network interfaces, and they should authenticate all connections.

The local release profile should look like this:

  • Stdio by default for local developer tools.
  • No broad filesystem roots.
  • No network access unless the tool needs it.
  • Credentials from the environment or secure OS storage, not plaintext config.
  • Sensitive servers split by domain, for example billing separate from docs search.

The remote release profile should look like this:

  • HTTPS endpoint with authorization on every connection.
  • Protected resource metadata and authorization server discovery.
  • Token audience validation on every request.
  • Origin validation for Streamable HTTP.
  • Private network egress rules for upstream APIs.
  • SSRF defenses on every URL the server or authorization flow fetches.

Log Policy Decisions, Not Just Tool Calls

MCP logs need to explain why a tool was allowed, denied, or escalated. A plain tools/call succeeded log is not enough when a customer asks why an agent touched a record or a security engineer needs to reconstruct a prompt-injection attempt.

At minimum, log the caller, the client, the MCP resource, the token audience validation result, the requested scope, the tool name, the canonical argument hash, the schema hash, the policy decision, the approval result, and the downstream outcome. Do not log raw tokens, secrets, or full sensitive payloads unless a retention and redaction policy explicitly allows it.

JSON
{
  "event": "mcp.tool_policy_decision",
  "user_id": "usr_redacted",
  "client_id": "https://app.example.com/oauth/client.json",
  "resource": "https://mcp.example.com",
  "scope": "tickets:read",
  "token_audience_valid": true,
  "tool_name": "support.ticket.read",
  "arguments_hash": "hash:redacted",
  "schema_hash": "hash:redacted",
  "decision": "approved",
  "approval": "not_required",
  "downstream_status": "success"
}

This is where MCP becomes governable. The audit log should catch tool drift, unexpected scope escalation, a new client calling a sensitive tool, repeated insufficient-scope failures, and policy denials grouped by tool. If the system cannot answer "which client called which tool with which approval and which audience-bound token," it is not ready for production traffic.

The Release Gate Before A New MCP Server Goes Live

The release gate should combine tool design, server hosting, and governance. AWS frames those as the three MCP strategy pillars, and OWASP's February 16, 2026 secure MCP server guide covers the same production surface: architecture, authentication and authorization, strict validation, session isolation, and hardened deployment.

Use this gate before exposing a new MCP server to internal agents:

  1. Approve the tool catalog

    Review every tool name, description, input schema, output schema, annotation, and execution mode. Remove tools that are convenience wrappers for unrelated domains. Pin the approved catalog hash.

  2. Prove the authorization path

    For remote servers, verify protected resource metadata, authorization server discovery, PKCE, exact redirect URI validation, resource parameter handling, token audience validation, and scoped WWW-Authenticate challenges.

  3. Block token passthrough

    Confirm the MCP server rejects tokens that were not issued for its own resource. If it calls an upstream API, prove that the upstream credential is separate from the incoming MCP client token.

  4. Classify approval requirements

    Mark each tool as read-only, write-draft, external send, destructive, or data export. Wire approval screens for the last three classes, and show full parameters before execution.

  5. Harden the transport

    For stdio, keep protocol output clean and sandbox the process. For Streamable HTTP, validate Origin, bind local servers to localhost, authenticate every connection, and add SSRF controls around fetched URLs and redirects.

  6. Run the audit drill

    Trigger an allowed call, a denied call, a scope challenge, an approval-required call, a schema-drift failure, and a bad-origin request. The logs should show the policy reason for each outcome without exposing secrets.

That gate is the difference between an MCP demo and an MCP server a platform team can defend. The model gets a useful tool surface. The engineering team keeps identity, consent, scope, approval, isolation, and auditability outside the model, where production controls belong.

How do you handle security in an MCP server?

Start with identity and resource-bound authorization, then narrow the tool surface. A production MCP server should validate token audience, minimize scopes, enforce strict schemas, require approval for high-risk actions, isolate the runtime, and log policy decisions.

What is a good practice for MCP authorization?

Use the MCP HTTP authorization spec for remote servers: protected resource metadata, authorization server discovery, PKCE, exact redirect URI validation, the resource parameter, scoped challenges, and server-side audience validation. Do not pass a client token through to downstream APIs.

How do you make MCP more secure?

Minimize scopes, pin tool definitions, reject schema drift, separate sensitive servers, sandbox local processes, validate Streamable HTTP origins, require human approval for destructive or data-sharing operations, and audit every allow or deny decision.

What should you log for MCP security?

Log the caller, client, MCP resource, token audience result, requested scope, tool name, canonical argument hash, schema hash, policy decision, approval state, and downstream outcome. Do not log raw tokens or secrets.

Last Updated

Jun 4, 2026

CategoryMCP
Newsletter

One letter, every week. Working systems — not hot takes.

Build logs, agentic engineering decisions, agent failures, evals, and what survives real users. Sent weekly, never more.

Weekly. No spam. Unsubscribe anytime.