Building Secure MCP Servers: Best Practices for Engineering Teams
MCP's inverted trust model creates new attack paths that conventional API security doesn't address.

MCP flips the trust model that conventional API security was built on, and that inversion is the root cause of every threat this piece covers. A REST API works as a passive responder. A request comes in, the server validates it, returns data, and the exchange ends there. Decades of API security practice rest on that one assumption: the server waits, the client asks.
MCP servers don't wait. They act on the client's behalf, fetching context, calling tools, and feeding fresh instructions back into the model so it can decide what to do next. The NSA warned about this directly in its May 2026 security guidance: the inversion opens new attack paths most teams haven't mapped yet.
The protocol doesn't govern identity on its own. Nothing in MCP requires bidirectional authentication, and nothing forces a server to prove what it's actually allowed to do before it does it. A server gets whatever permissions it's handed at setup, and from that point on, every request from an agent runs through unchecked unless someone bolts on controls from outside the protocol.
Adoption has outpaced the controls meant to secure it. Practical DevSecOps's 2026 MCP security report puts MCP SDK downloads above 97 million a month, while only a small share of live servers use OAuth. Independent scans still find a meaningful share of deployed servers running with no authentication whatsoever.
Where a server runs changes what's at stake. Local servers run OS commands or custom code on hardware the team already controls, so when a bug hits, it stays on that machine. A remote server runs on infrastructure someone else operates, so you get supply-chain exposure on top of everything else. Red Hat's Principal Product Security Architect, Florencio Cano Gabarda, draws that line in the company's MCP security guidance, and you can use it to sort risk before you write a single policy. None of this means a generic API security checklist is wrong, just that it's incomplete. Engineers need to reason through MCP's specific threats one at a time, because the protocol's design, not any one team's mistake, is what opens the door.
The six threat classes that exploit MCP's trust gaps
An agent reads a tool's description with the same trust it gives its own system prompt, and tool poisoning and rug pulls exploit that. A malicious server can bury hidden instructions inside that description, and the model follows them while the user only ever sees the harmless label on screen. The MCPTox benchmark tested adversarial versions of hundreds of real tools pulled from dozens of live MCP servers against twenty language models, and the average attack succeeded more than a third of the time. Against the top-performing reasoning model, the success rate climbed well past that average, and even the model with the strongest refusal behavior went along with the attack almost every time. The rug-pull variant is sneakier still: a tool's description or behavior changes quietly after it's already been approved, so it turns malicious without ever triggering a new review. UltraViolet Cyber's MCP threat advisory frames this as the gap between a one-time check at connection time and the tool's actual behavior at runtime, which never gets checked again.
Prompt injection via external content hides instructions in something the agent reads, not something the user types: a document, a database record, or a web page the agent pulls in during a task. The model can't tell the difference, so it executes the hidden instruction as if the user had given it directly. This threat is MCP06 (Prompt Injection via Contextual Payloads) on the OWASP MCP Top 10 and holds the #1 spot on the OWASP Top 10 for LLM Applications. Practical DevSecOps reports a sharp rise in prompt-injection disclosures logged through HackerOne over the past year.
Confused deputy and token passthrough attacks trick a server into using its own legitimate permissions for an attacker. Because the server already holds real authority and nothing checks who's actually behind a given request, the attacker never needs permissions of their own, just a way to borrow the server's. Token passthrough is the most common setup for this: a server takes a token a user issued for one service and reuses it to call a completely different downstream API. The MCP security best practices specification names this an anti-pattern and forbids it outright in the authorization spec, yet it persists because it's the easiest thing to build. The NSA's AI Security Center treats ending token passthrough as the single most urgent change MCP deployments need to make.
Session hijacking has moved from a deployment mistake into the SDKs themselves. A flaw baked into an official SDK ships automatically into every project that imports the affected version, with no misconfiguration required on the developer's part. CVE-2026-25536, found in the TypeScript SDK and scored 7.1 on CVSS, came from a shared server and transport instance getting reused across multiple client connections. Responses ended up routed to the wrong client, leaking one user's data straight into another user's session.
SSRF and path traversal attacks happen when the LLM passes inputs along without sanitizing them, so an attacker can reach internal network resources or read files that should never be exposed. Anthropic's own Git MCP server produced three CVEs in an exploit chain: unrestricted git_init and arbitrary path creation (CVE-2025-68143), argument injection (CVE-2025-68144), and a bypass of repository scoping and path validation (CVE-2025-68145), documented by Cyata researcher Yarden Porat. Wiz Research disclosed two separate flaws in the Amazon Q VS Code extension. One, CVE-2026-12957, let a crafted workspace MCP configuration file in a malicious repository achieve arbitrary code execution and steal cloud credentials. The other, CVE-2026-12958, came from missing symlink validation. A related misconfiguration researchers call "NeighborJack" happens when a server binds to all network interfaces (0.0.0.0) instead of localhost, letting anyone on the same network connect and run commands.
Supply chain and registry-level attacks go after the registries themselves, since they often accept a submission without checking its source code or verifying who published it. In the SmartLoader operation, disclosed by Straiker STAR Labs in February 2026, attackers spent three months building five fake GitHub accounts with AI-generated personas, cross-forked each other's repositories to simulate a real community, then submitted a trojanized Oura Ring MCP server. Nine of eleven major MCP directories accepted and published it, and not one ran an automated security check first. Typosquatting compounds the risk: attackers register servers under names deceptively close to legitimate ones, so an agent can be led to the wrong tool without anyone noticing.
Authentication: implementing OAuth 2.1 and the current spec requirements
The MCP spec now lays out a concrete OAuth 2.1 authorization model for HTTP-based transports. Transports should implement it, but the spec stops short of requiring it, and that's the gap that leaves so many servers running without authentication today. Get the implementation right, including a July 2026 revision to how client identity works, and you close the largest single category of exploitable gaps covered in the last section.
That July 28, 2026 revision made Client ID Metadata Documents, or CIMD, the preferred way for a client to establish identity. Dynamic Client Registration is now formally deprecated in favor of CIMD and survives only as a fallback for authorization servers that haven't added CIMD support yet. Teams should publish a CIMD document at a stable URL and keep Dynamic Client Registration available behind it for compatibility.
The spec sets firm requirements on top of that model. Authorization servers must implement OAuth 2.1, and its security measures need to cover both confidential and public clients. MCP servers must implement RFC 9728, and MCP clients must use it to discover the authorization server. These aren't optional hardening steps; they're the baseline the spec defines.
Local stdio servers work differently, and OAuth isn't the right tool for them. Read credentials from the environment and let the operating system's process boundary do the isolation work instead. Still validate the Origin header on every request, and bind the server to 127.0.0.1 to block DNS rebinding attacks, a practice The SaaS Library's analysis calls out specifically for stdio deployments.
Short-lived access tokens matter regardless of transport. If a credential leaks, a short expiration window limits how long an attacker can actually use it. The same logic extends to multi-agent setups: when one agent delegates work to another, verify the delegation chain at every hop in the sequence. Each agent-to-agent call needs its own verified identity. An agent shouldn't get to act with authority just because it inherited trust from whichever agent kicked off the task, and that distinction is what the credential architecture in the next section has to enforce.
Least-privilege scoping: credentials, agent identities, and tool manifests
Authentication answers who's calling. Least-privilege scoping answers what they're allowed to do once they're in, and in MCP deployments, where machine identities already outnumber human ones by a wide margin, getting that scoping architecture right is what keeps one compromised agent from turning into a pivot point across the whole system.
Palo Alto Networks' research found that machine identities now outnumber human employees by far inside the organizations it studied, and most of those machine identities are AI agents. That number alone means agent identity management has to be core engineering work, not something bolted on after a system is already live.
Every agent should exist as its own identity with a managed lifecycle: a defined role, tightly scoped permissions, and a fixed manifest of tools it's allowed to use. An agent that has no business touching a given tool shouldn't be able to see that the tool exists, let alone call it.
OAuth scope granularity is the lever that makes this enforceable in practice. Define separate scopes for distinct actions, so email.send and contacts.read become two different permissions instead of one broad grant, and make the server check that an incoming token carries the scope an action needs before it runs. The MCP security best practices specification frames this as the mechanism that keeps the blast radius small if a token gets stolen: a stolen token with one narrow scope can only do one narrow thing.
When an agent acts on a human user's behalf, it should carry a delegated token with its own limited scope rather than the user's actual credentials. Sharing user credentials directly with an agent erases any clear line back to the person responsible for an action; delegated, scoped tokens preserve that chain of accountability.
If you issue short-lived, scoped credentials just before an agent needs them for a task, that beats handing agents long-lived secrets that sit around waiting to be stolen. Token passthrough, where a server reuses a token issued for one service to call a completely different one, needs to be the first anti-pattern eliminated. The spec already forbids it by design. It survives anyway because it's the path of least resistance for a team trying to ship fast.
Standing access is the other problem. Many MCP servers keep access to file systems, APIs, and databases open whether or not an active task needs it, and that turns every one of those connections into a standing liability. Least-privilege scoping means revoking access when a task ends; it has to be an ongoing check, not a one-time review at provisioning time. MCP has no native identity governance, so if nothing enforces this from outside, every agent request runs through unverified, with no check on the server's authority or the user's actual intent behind the call. Platforms like Runlayer address that gap by binding identity to each MCP action through SSO and enforcing policy at the level of individual tool calls, so a trust inversion in the protocol doesn't translate into open-ended access in practice. Whatever platform a team chooses, the underlying discipline stays the same: authenticate every caller, scope every credential tightly, and never let standing access outlive the task it was granted for.


