MCP standardizes how an AI application discovers and calls tools, reads resources, and uses prompts. It does not make a connected system secure by default.
That distinction matters because an MCP server often sits between a probabilistic model and systems with real authority: files, customer records, advertising accounts, databases, calendars, or internal APIs. A valid tools/call request can still be unauthorized, unsafe to repeat, too broad, or based on attacker-controlled content.
The production goal is not “secure because it uses MCP.” The goal is a small, understood trust boundary with controls at every transition.
First, separate protocol features from implementation controls
MCP provides a message model, capability negotiation, transports, schemas, and an authorization framework for HTTP. It also provides metadata such as tool annotations. Those pieces help compatible clients and servers communicate.
Your implementation remains responsible for:
- authenticating the caller;
- authorizing the caller for each operation and object;
- protecting downstream credentials;
- validating inputs and upstream responses;
- isolating tenants, sessions, files, and network destinations;
- controlling retries and side effects;
- logging safely and responding to incidents.
Tool annotations are a good example of the boundary. The current MCP schema defines readOnlyHint and destructiveHint, but explicitly calls annotations hints. Marking a tool read-only does not prevent its handler from writing. A server must enforce the rule, and a client should not trust annotations from an untrusted server.
Draw the data and authority paths
Before choosing middleware, map the participants: person, host application, MCP client, MCP server, authorization server, downstream API, data store, and any queue or cache. For each connection, record the identity presented, credentials used, operations allowed, data returned, and log destination.
This catches designs where one shared provider token gives every caller the same account access, or where a result from one tenant can be retrieved with another tenant's session ID. It also makes prompt injection easier to reason about. Content returned by a webpage, document, issue, or CRM note is untrusted data even when it arrived through a trusted API.
Choose the transport around the trust boundary
For a single-user process launched by a desktop or coding client, stdio reduces the network surface. The current transport specification defines stdio and Streamable HTTP as the standard transports. With stdio, credentials normally come from the process environment; stdout is reserved for protocol messages, and diagnostics should go to stderr.
Stdio is not a sandbox. The process can generally do whatever its operating-system identity can do. Restrict its filesystem, network access, environment, and executable provenance. The MCP security best practices recommend explicit consent before executing local server commands and minimal privileges or sandboxing where practical.
For remote or shared access, use Streamable HTTP over HTTPS and implement the protocol's HTTP requirements. Validate the Origin header, do not expose a local HTTP listener on every interface without a reason, and authenticate every protected request. The transport spec warns specifically about DNS rebinding against local servers.
Authenticate the MCP server as its own resource
The current MCP authorization specification uses OAuth-based standards for HTTP authorization and requires clients to identify the intended MCP resource when requesting tokens. Validate issuer, signature, audience, expiry, and any other claims your policy relies on. Use maintained libraries rather than writing token parsing or cryptography yourself.
Do not accept a token intended for a downstream service and simply pass it through. MCP's security guidance explicitly forbids token passthrough because it breaks token audience boundaries, weakens auditability, and can turn the server into an unintended proxy.
If the MCP server calls a third-party API, treat that as a separate authorization relationship. The server may use a service credential, a delegated user credential, or a token exchange supported by the provider. Whichever model you choose, document whose authority the call represents and keep the scopes narrow.
Authorization must happen inside every consequential operation
Authentication answers who the caller is. Authorization answers whether that caller may run this operation against this account, project, file, or record now.
Do not rely only on a route-level “logged in” check. Tool handlers should receive a verified identity and policy context, then check action and object scope before reading or writing. A user allowed to list their campaigns is not automatically allowed to edit every campaign ID supplied in a model-generated argument.
For multi-tenant deployments, derive tenant identity from verified server-side claims, not from a tool argument such as tenant_id. Include the tenant in cache keys, database predicates, queue messages, object-storage prefixes, and audit records. Test cross-tenant access as a negative case.
Sessions need the same discipline. The MCP security guidance says servers must not use session IDs as authentication. Use cryptographically secure session IDs, bind state to the verified user where state exists, expire it, and recheck authorization on every inbound request.
Use progressive, least-privilege scopes
A single “full access” scope is simple to implement and difficult to defend. Start with discovery and low-risk reads. Request or challenge for narrower elevated scopes when a caller first attempts a privileged operation.
The official security guidance recommends progressive scope elevation and warns that broad scopes increase blast radius and obscure the audit trail. Scopes are still only claims. The handler must map them to a real policy decision.
Separate read, additive write, destructive write, and administrative tools when they have different risk. Avoid a generic run_api_request tool that turns carefully reviewed scopes into arbitrary provider access.
Constrain every input, including paths and URLs
JSON Schema is the public contract, not the full validation strategy. Parse arguments with a runtime validator and enforce lengths, enums, numeric bounds, formats, relationships between fields, and business invariants.
For filesystem tools, resolve and canonicalize the path before checking it against an allowed root. Account for symbolic links and platform path behavior. Client-provided MCP roots communicate intended scope, but the client concepts guide notes that roots do not enforce a security boundary.
For tools that fetch URLs, prefer destination allowlists where the use case permits them. Block private, loopback, link-local, and cloud-metadata destinations unless they are explicitly required. Revalidate redirect targets and defend against DNS rebinding and time-of-check/time-of-use gaps. The official security guide recommends mature URL and network controls rather than hand-written IP parsing.
Bound request bodies, files, result sizes, pagination, concurrency, and execution time. A schema-valid request can still exhaust memory, consume provider quota, or keep a worker occupied indefinitely.
Design write tools for review and safe repetition
Give a write tool a narrow verb and an explicit target. Return a preview before applying a large or consequential change. Require a confirmation value tied to the exact proposed payload when human approval is part of the workflow.
Use provider idempotency keys for create operations when available. Do not automatically retry an ambiguous write after a timeout unless you can determine whether it committed. Record the downstream request identifier so an operator can reconcile uncertain outcomes.
Human approval belongs in the host experience, but the server should not assume a model statement such as “the user approved” is proof. If approval is a security requirement, represent it as a verifiable, short-lived server-side capability or a separate authenticated action.
Treat retrieved content as untrusted
A search result, scraped page, repository file, support ticket, and database note can all contain instructions aimed at the model. The server should return data as data, with source and provenance where useful, rather than silently concatenating it into privileged instructions.
Do not let retrieved text select hidden tools, change access policy, or supply unrestricted URLs and paths. Keep deterministic policy checks outside the model. If a workflow lets a model propose an action based on untrusted content, require tighter review before high-impact execution.
This is another protocol-versus-implementation distinction: MCP transports the content and call. Your host and server architecture decide which content is trusted and which actions require approval.
Protect credentials and sensitive output
Keep credentials out of tool arguments, descriptions, returned errors, and model-visible logs. For local servers, pass narrowly scoped secrets through the environment or an operating-system credential facility. For remote servers, use a managed secret store and workload identity when the platform supports it.
Redact authorization headers, cookies, API keys, session IDs, and sensitive fields from logs. Log identifiers and metadata needed for investigation: verified user, tenant, tool, policy decision, target identifier, result status, duration, correlation ID, and downstream request ID. Decide retention and access policy based on the data, not convenience.
Do not claim that MCP keeps data private. The server may call external APIs, and the host may place tool results into model context. The local versus remote guide shows how to trace the actual data path.
Add operational controls before launch
Rate limit by a verified identity or tenant where possible, not only by IP. Apply concurrency limits and provider budgets. Use timeouts and cancellation, health checks that do not expose secrets, and readiness checks that verify required dependencies without performing destructive work.
Set secure defaults. A missing production credential should normally fail closed, not enable mock or anonymous mode. Separate development configuration from production policy. Pin and update dependencies through a deliberate release process, and generate a software inventory appropriate to the deployment.
Plan revocation: how do you disable one client, one user, one tenant, one tool, or one downstream credential without taking the entire server offline? If the answer is “redeploy and hope,” the authorization model is incomplete.
Test abuse cases, not only happy paths
A production test plan should include:
- missing, expired, wrong-audience, and insufficient-scope tokens;
- cross-tenant identifiers and object-level authorization failures;
- malformed schemas, oversized inputs, and pagination extremes;
- path traversal, symbolic-link escape, private URLs, redirects, and DNS changes;
- prompt-injection strings inside tool results;
- repeated create calls, timeouts after downstream commit, and cancellation;
- stolen or mismatched session IDs across multiple server instances;
- secret redaction in errors, logs, traces, and metrics.
Run protocol checks through a real compatible client or the MCP Inspector. Then test the provider boundary with fixtures or a safe sandbox account. A protocol test cannot prove that business authorization or tenant isolation is correct.
A practical launch gate
Before exposure to real data, I want clear answers to these questions: Who is the caller? Which tenant and object can they access? Which tools can mutate state? What external systems receive the data? What happens if the call repeats? Which logs prove the outcome without exposing the payload? How is access revoked?
If any answer depends on the model behaving well, move that decision into deterministic code or a human approval boundary.
Our MCP development service uses these controls as implementation requirements, not as privacy or security guarantees. The exact design depends on the systems, identities, and consequences involved. For design lessons from the public codebase set, read what building seven MCP servers taught me.
