# MCP standardizes the wire format. Running it safely across an org is still yours to build. Source: https://customlabs.io/mcp/ Updated: 2026-09-20 MCP # MCP standardizes the wire format. Running it safely across an org is still yours to build. Six surfaces, 24 controls tagged by who owns each one and what the spec actually requires of it, five attack paths walked end to end, and a ten-question gate for the next server someone asks you to connect. Updated September 20, 2026 · First published August 15, 2026 · 42 min read · Key takeaways - → A poisoned tool description can steer a model without ever touching its prompt. - → Cross-server tool shadowing lets one server's tool definition override another's. - → A post-approval rug pull swaps a tool's behavior after a human already approved it. - → The catalog the model sees is named as a separate surface from connection and transport. - → An approved tool's return value is itself a named exfiltration path. - → On supply chain the spec mandates no registry and no signing scheme. A single [MCP](https://customlabs.io/glossary/model-context-protocol/) server connected to a single agent is easy to reason about. The failures in this page show up once a second server joins the same session, a gateway starts brokering identity for both, and the catalog the model reads gets built from tool descriptions three different teams wrote without ever talking to each other. None of that is a defect in the protocol. It's the integration layer the protocol assumes someone builds around it. [The Agent Tool Interface](https://customlabs.io/tool-design/) covers designing one tool well: a tight schema, a clear name, sane permissions. This page starts where that one stops: identity, the catalog, the trust boundary, supply chain, and operations, once a tool is reachable across a process boundary by more than the team that wrote it. **Current as of the 2026-07-28 specification revision.** The Model Context Protocol's own security guidance is narrower than most teams assume: it tells a host to treat tool descriptions and annotations as untrusted unless they come from a server you trust, requires explicit user consent before a tool runs, and requires an HTTP server to validate the Origin header against DNS rebinding. Authorization is optional, but a server that adopts it inherits real teeth — it must publish protected resource metadata, and a token issued for one server must never be accepted or forwarded by another. Past that, the spec is quiet. Catalog size, supply-chain provenance, logging, and incident response are policy questions the protocol leaves entirely to whoever operates the server. [MCP Specification: Authorization (2026-07-28) →](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) [MCP Security Cheat Sheet →](https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html) The six surfaces ## Where an MCP rollout actually breaks. Each surface fails a different way. The characteristic failure is one layer assuming another layer handled it. 01 ### Connection & transport **Covers:** Where a server actually runs — stdio versus a remote HTTP endpoint — and the network hygiene each choice demands: Origin-header validation, binding to loopback rather than every interface, TLS on anything remote, and isolating one session's state from the next. **Breaks when:** A server built for local development ends up bound to every interface on the box, or a remote endpoint skips the Origin check and lets a rebound DNS name reach it from a page the user merely visited. **Watch:** Share of servers reachable only over stdio or a loopback bind, versus reachable on a routable network interface 02 ### Identity & authorization **Covers:** A protected MCP server as an OAuth 2.1 resource server: protected resource metadata discovery, PKCE, audience-bound tokens scoped to one server, and short-lived credentials in place of a personal access token pasted into a config file. **Breaks when:** A gateway holds one broad, delegated token to keep integration simple, and forwards it unchanged to every server it proxies. Compromising the least-trusted server in the chain then hands over credentials scoped for every other one. **Watch:** Share of connected servers that reject a token whose audience doesn't name them specifically 03 ### The catalog the model sees **Covers:** How many servers and tools land in one [context window](https://customlabs.io/glossary/context-window/) at once, namespacing so two servers' identically named tools don't collide, and whether the model reads a static list or one that changes mid-session. **Breaks when:** Two connected servers both register a tool called `search`, and the model has no reliable way to tell which one a given call actually reached. **Watch:** Count of distinct tool names sharing a session, and how many collide before namespacing is applied 04 ### The trust boundary **Covers:** Tool descriptions, annotations, schemas, and return values, all of them untrusted input by default. Structured returns over raw HTML, no auto-approval, full call parameters shown before execution, and sensitive servers kept apart from general-purpose ones. **Breaks when:** A tool's return value renders as trusted HTML, or a repeated approval gets remembered as blanket consent instead of being asked again for the specific call about to run. **Watch:** Share of destructive or write-capable tool calls that show full parameters before execution, not a summarised description 05 ### Supply chain & change control **Covers:** A registry or allowlist, provenance and publisher identity, pinning a tool definition by hash, re-consent when a definition changes, typosquat checks, and sandboxing a server that runs locally. **Breaks when:** A tool a user approved last week gets silently redefined by its own server (a new parameter, a new destination), and every session that cached the original approval keeps calling it with no fresh consent prompt. **Watch:** Share of connected servers whose current tool definitions are pinned against a recorded hash from approval time 06 ### Operations **Covers:** Per-call logging with parameters and user context, redaction, quotas and rate limits per session or tenant, version pinning with a real deprecation window, and what an incident runbook contains when the compromised component is someone else's server. **Breaks when:** An MCP server misbehaves in production and the only record of what happened is whatever the model's own transcript happened to keep, because nothing logged the actual call. **Watch:** Share of tool calls with a logged parameter set and user identity retrievable after the fact The protocol/you split ## What the spec guarantees, and what's still yours. One row per surface. The right column is where almost every real incident on this page actually starts. ### Connection & transport The spec guarantees The spec defines stdio and Streamable HTTP as the two transports. On HTTP it requires Origin-header validation on every incoming connection, with a 403 on an invalid one, to defeat DNS rebinding, and says a local server should bind to 127.0.0.1 rather than every interface. Yours to build Whether a given server should be remote at all, TLS termination, the Host-header check OWASP asks for, and per-session state isolation are network-topology decisions the spec describes the wire format for but doesn't make for you. ### Identity & authorization The spec guarantees Authorization itself is optional, and a server on stdio is told to use its launch environment instead. Once an HTTP server is protected, though, the requirements are hard: it acts as an OAuth 2.1 resource server, must implement RFC 9728 protected resource metadata, clients must send RFC 8707 resource indicators on both the authorization and token request, and a server must reject any token whose audience doesn't name it — no exceptions for one that arrived from a trusted-looking gateway. Yours to build Which identity provider you trust, how short-lived a credential is, and whether one agent gets one distinct identity per server are policy calls the spec leaves entirely to you. ### The catalog the model sees The spec guarantees A server can declare its tool list as static or dynamic, and notify a connected client when that list changes. Yours to build Namespacing, how many servers share one session, and how much of the context window the catalog itself is allowed to spend are all your call — the spec has no opinion on catalog size or collision handling. ### The trust boundary The spec guarantees Tool descriptions and annotations should be treated as untrusted unless they come from an already-trusted server, and a host must get explicit user consent before a tool call runs. Yours to build What that consent prompt actually shows, whether a return value renders as trusted markup or sanitised structured data, and which servers are sensitive enough to isolate are all implementation decisions the spec leaves open. ### Supply chain & change control The spec guarantees Close to nothing: no required registry, no provenance format, no mandated signing scheme for a tool definition. Yours to build An allowlist, a way to detect a changed definition, and a sandbox for a locally run server are entirely your build — this is the surface where the protocol and a real supply chain are furthest apart. ### Operations The spec guarantees Version legibility, and that's about it. There is no initialization handshake in this revision: every request carries its own protocol version, a server must reject one it doesn't implement with an error listing the versions it does support, and every server must implement a discovery endpoint a client can query up front. Yours to build Logging, redaction, quotas, and a deprecation policy for your own [tool contracts](https://customlabs.io/glossary/tool-contract/) are entirely your design — knowing a version mismatch exists tells you nothing about what to do when a third-party server changes underneath you. The control bank ## 24 controls, six surfaces. Filter by which layer actually owns a control, then copy the visible list as a Markdown checklist. Host Server Gateway Identity Showing all 24 controls Copy as Markdown ### 01 Connection & transport MCP gives you two shapes of transport: stdio, a subprocess talking over its own stdin and stdout, and Streamable HTTP, a server reachable over a network. The choice isn't cosmetic. A stdio server inherits the process boundary of whatever launched it and is unreachable from anywhere else on the network by construction; an HTTP server is reachable from anything that can route to it, which means every piece of hygiene a web service needs (TLS, the Origin check the spec requires to block DNS rebinding, a bind address that isn't 0.0.0.0 by accident) now applies to a tool integration too. Most teams get this right for a service they'd call "real infrastructure" and skip it for an MCP server, because it started life as a weekend script and nobody revisited the bind address once it moved to production. #### Choose stdio or remote HTTP by where the real boundary is, not by habit Server Your policy **What it covers** A deliberate choice between a stdio subprocess and a remote HTTP server, made by naming the actual process or team boundary the tool needs to cross. **How to build it** Default to stdio for anything launched and used inside one process or one team's infrastructure, and reach for remote HTTP only once a genuinely separate team, product, or organisation needs to call the same server. **Produces** Fewer network-facing servers standing up TLS, auth, and hardening work for a boundary that never actually existed. **Tradeoff** Stdio is simpler to secure but harder to share; moving to remote HTTP later is a real migration, not a config flag. **Prove it** For any remote server, someone can name the specific other team or product that calls it — if the honest answer is nobody yet, it should still be stdio. [Glossary: In-process until you need MCP](https://customlabs.io/tool-design/#in-process-until-you-need-mcp) #### Bind a local server to loopback, never a wildcard address Server Spec recommended **What it covers** A local or development-only MCP server bound explicitly to 127.0.0.1, not to 0.0.0.0 or an interface reachable from the rest of the network. **How to build it** Set the bind address explicitly in the server's own configuration rather than accepting a framework default, and check it on every new server before it runs anywhere near a shared machine. **Produces** A server that's unreachable from anything else on the same network, by construction rather than by firewall rule someone has to remember to add. **Tradeoff** A loopback-only bind means a genuinely shared use case has to go through a deliberate remote-transport decision instead of just widening the bind address quietly. **Prove it** A port scan from another host on the same network reaches nothing for any server meant to stay local. #### Validate the Origin header, then add the Host check and TLS on top Server Spec required **What it covers** The one hard requirement the spec places on this surface: a Streamable HTTP server must check the Origin header on every incoming connection and reject an invalid one with 403, which is what stops a page the user merely visited from driving a local server through a rebound DNS name. TLS and a Host-header allowlist are not in the spec at all; both are ordinary web-service hardening, and the OWASP MCP cheat sheet asks for the Host check by name. **How to build it** Check Origin before a request reaches any handler and answer an unrecognised one with 403, rather than treating the header as advisory. Then terminate TLS in front of the server rather than inside a bespoke implementation, and reject a request whose Host header names something this server does not serve. The three defend different things and none substitutes for another. **Produces** A local or internal server a hostile web page cannot drive, even after it resolves a name it controls to your loopback address, and a remote one that no longer leaks credentials in plaintext on the wire. **Tradeoff** A strict allowlist on either header breaks a legitimate client connecting from an origin, or under a hostname, nobody wrote down. Enumerate the ones you actually serve rather than falling back to accepting anything. **Prove it** A request carrying an unrecognised Origin is answered with 403 before any tool handler runs, and one carrying an unexpected Host is rejected too, both tested directly rather than assumed from a framework default. #### Isolate one session's state from the next Server Your policy **What it covers** A server design where one client's session, its context, its in-flight calls, its cached results, never leaks into another session's view. **How to build it** Key every piece of server-side state by session identifier explicitly, and test with two concurrent sessions on purpose rather than assuming the framework already isolates them. **Produces** A server where one user's conversation can't surface another user's data by accident under concurrent load. **Tradeoff** Explicit per-session isolation costs real engineering time on a server that started as a single-user script; it's cheaper to build before the first second user than to retrofit after. **Prove it** Two concurrent sessions against the same server, driven with different inputs, produce results that never cross. ### 02 Identity & authorization Authorization is optional in MCP, and whether to protect a server at all is your call. Once you do, the 2026-07-28 spec is the one place it takes a real position instead of leaving the details to you: a protected server acts as an OAuth 2.1 resource server, must implement RFC 9728 protected resource metadata so a client can discover how to authenticate, and must validate that any token it receives was issued with that server named as the audience under RFC 8707. The line worth reading twice is the one that says a server must not accept or transit any other token — no passthrough, no "the gateway already checked this." Stdio servers sit outside that regime by design; the spec tells them to take credentials from the environment they were launched in instead, which pushes the identity question back to whatever process owns that environment. #### Implement PKCE and resource indicators on every authorization request Identity Spec required **What it covers** A client that always uses PKCE on the OAuth 2.1 authorization code flow, and always includes the RFC 8707 `resource` parameter naming the specific server the token is for. **How to build it** Wire both into the client's authorization request unconditionally, rather than treating them as optional hardening added later, and confirm the authorization server actually honours the resource parameter rather than silently ignoring it. **Produces** A token minted for exactly one server, with no authorization-code interception path left open. **Tradeoff** Not every authorization server implements resource indicators yet; a client that requires them can't talk to one that doesn't, which is the correct failure mode rather than a silent downgrade. **Prove it** A captured authorization request for any MCP connection shows both a PKCE challenge and a resource parameter naming the target server. #### Validate audience on every token, and never forward one to a different server Identity Spec required **What it covers** A hard check, on every request a server receives, that the token's audience names that server specifically — and a standing rule that no component in the chain ever forwards a token it received to a different downstream server. **How to build it** Reject a token whose audience claim doesn't match at the server itself, in addition to any check a gateway upstream already runs, and treat any code path that reuses an inbound token as an outbound credential as a design defect to fix immediately. **Produces** A chain where compromising the least-trusted server in a proxy setup doesn't hand over credentials valid anywhere else. **Tradeoff** Per-server audience-bound tokens mean a gateway can't just mint one credential and relay it everywhere; it has to broker a distinct token per downstream server, which is more moving parts. **Prove it** A token minted for server A, replayed against server B, is rejected, tested directly rather than assumed from the library's defaults. [Glossary: Confused deputy](https://customlabs.io/glossary/confused-deputy/) #### Scope every grant to the narrowest set of actions the workflow needs Identity Your policy **What it covers** A token scope that maps one-to-one to the tools a given integration actually calls, with a separate, explicit step-up prompt for anything more sensitive than the original grant covered. **How to build it** Request the minimum scope at connection time, and require a fresh, visible authorization step before any call that needs more than what was originally granted, rather than silently expanding what a cached token can do. **Produces** A blast radius, on a compromised or misbehaving server, bounded by what that integration actually needed rather than by whatever scope was convenient to request once. **Tradeoff** Narrow scopes mean more authorization prompts as a workflow's needs grow, which is real friction against a smooth first-run experience. **Prove it** The scope list on a live grant, diffed against the tools that integration actually calls, shows no unused scope. #### One identity per agent per server, on short-lived credentials Identity Your policy **What it covers** A distinct credential for each agent-to-server pairing, issued with a short lifetime, rather than one long-lived personal access token shared across every agent that needs to reach that server. **How to build it** Provision credentials per pairing through whatever identity system already issues short-lived tokens elsewhere in the org, and retire the practice of pasting a personal access token into a shared config file. **Produces** An audit trail that can actually say which agent made which call, and a compromised credential that expires on its own instead of staying valid indefinitely. **Tradeoff** Per-pairing short-lived credentials need real provisioning infrastructure; a shared long-lived token is faster to stand up and worse the moment anything goes wrong. **Prove it** A credential in active use is traceable to exactly one agent-server pairing, and its remaining lifetime is measured in hours or days, not in "until someone rotates it." ### 03 The catalog the model sees Connect three servers in one session and the model reads all of their tools, and all of their descriptions, in the same context window at once — nothing partitions one server's catalog from another's. That's what makes cross-server attacks possible in the first place: a tool from a server added an hour ago can describe itself as the "preferred" version of a tool the model has already been calling all session, and there's no structural signal telling the model those two claims come from different trust levels. Namespacing every tool by the server that owns it is the cheapest fix available, and it's on you to apply it — see /tool-design/ for the per-tool naming rules this surface assumes; this page is about what happens once more than one server's tools share the same room. #### Namespace every tool by the server that owns it Host Your policy **What it covers** A naming convention that prefixes every tool with the server it came from, so two servers can never register the same bare name and have the model unable to tell them apart. **How to build it** Apply the prefix at the client or gateway layer if a server doesn't do it itself, and refuse to connect a second server whose tools would otherwise collide with an already-connected one. **Produces** A catalog where a newly connected server's tools can never quietly shadow one the model has already been using all session. **Tradeoff** Namespacing lengthens every tool name the model has to read, a small, real cost against context budget for a catalog that stays legible as it grows. **Prove it** A search across the live catalog for duplicate bare tool names, ignoring the namespace prefix, returns none. [The Agent Tool Interface](https://customlabs.io/tool-design/) #### Budget how much of the context window the catalog itself is allowed to spend Host Your policy **What it covers** An explicit ceiling on how many servers and tools can be connected in one session before the tool catalog itself starts crowding out the actual task. **How to build it** Measure the token cost of the current catalog the same way you'd measure any other context spend, and cap the number of simultaneously connected servers rather than letting every available integration join by default. **Produces** A session where the model spends its context budget on the task, not on reading past a hundred tool descriptions it will use once or never. **Tradeoff** A hard cap on connected servers means a task that genuinely needs a dozen integrations has to request an exception, which is friction worth having. **Prove it** The token cost of the tool catalog in a live session is measured and tracked, not assumed to be negligible. #### Review what one server's description can see about another before connecting it Host Your policy **What it covers** A standing check, run before a new server joins a session with existing ones, for whether its tool descriptions could plausibly reference or impersonate an already-connected server's tools. **How to build it** Read a new server's tool descriptions specifically for claims about precedence, freshness, or preference ("use this instead of") before approving the connection, the same scrutiny a code reviewer gives an unfamiliar dependency. **Produces** A caught instance of a would-be shadowing tool before it ever reaches a live session, rather than after a user reports something odd. **Tradeoff** Manual review of every new server's descriptions doesn't scale past a small, curated set of integrations without also building the registry controls in the supply-chain surface below. **Prove it** The last new server connected has a recorded review of its tool descriptions against the existing catalog, beyond an install log entry. #### Gate a mid-session catalog change behind the same consent a new tool would need Host Spec recommended **What it covers** A rule that a server updating its tool list mid-session (`tools/list_changed`) triggers the same visible review a brand-new tool connection would, rather than silently expanding what's callable. **How to build it** Surface a mid-session catalog change to the user or operator explicitly, and treat an unreviewed expansion of the tool list as equivalent to connecting an unvetted new server. **Produces** A session where the set of things a model can call never grows without someone actually seeing that it grew. **Tradeoff** Gating every dynamic update interrupts a legitimate, frequently changing server's normal operation; scope the gate to genuinely new tools, not routine parameter tweaks on existing ones. **Prove it** A test server that adds a tool mid-session triggers a visible review before that tool becomes callable, checked directly against a live session. ### 04 The trust boundary The spec's own security guidance says a tool's description and annotations should be treated as untrusted unless they come from a server you already trust, and that a host must get explicit consent before a tool runs. Both of those are aimed at the same underlying fact: a model reads one undifferentiated stream of tokens, and nothing in the wire format marks a string as "instruction from us" versus "text a server sent." A return value is exactly as untrusted as a description — a ticket-lookup tool that's perfectly safe on its own can still hand back a record someone else was able to write to, and the model has no privileged channel to tell that record apart from a real instruction. /security-review/ is where this surface gets checked before a server ships; the prompt-injection-threat-model insight covers the general case this page narrows to one specific channel. #### Treat every tool description and annotation as untrusted by default Host Spec required **What it covers** A standing rule that a tool's description, its annotations, and its schema are all data from an untrusted source unless that specific server has been individually vetted and trusted. **How to build it** Render tool descriptions to the user as plain, inert text rather than executing or interpolating them anywhere, and don't grant a server "trusted" status by default just because it's already connected. **Produces** A model and a host that can't be steered by an instruction hidden inside a field nobody expected to carry one. **Tradeoff** Treating even long-standing integrations as untrusted by default means re-earning trust explicitly for each one, rather than assuming it from tenure. **Prove it** A tool description containing an embedded instruction, tested deliberately against the live host, produces no behaviour change beyond displaying the text. #### Return structured data, never rendered HTML, from a tool call Server Your policy **What it covers** A tool response format constrained to structured data (JSON, a typed object) that the calling application controls how to display, rather than raw markup the model or a client renders directly. **How to build it** Constrain every tool's return schema to structured fields, and reject a tool definition that hands back a raw HTML blob for direct rendering. **Produces** A return value that can carry a hidden script or instruction with nowhere for it to actually execute. **Tradeoff** Structured returns need an explicit rendering layer on the client side instead of just displaying whatever the server sent, which is more work upfront. **Prove it** Every live tool's return schema is inspected and confirmed structured, not a free-text or HTML field. #### Never auto-approve a tool call, and show the full parameters before it runs Host Spec required **What it covers** A consent step, shown fresh for every destructive or sensitive call, that displays the actual parameters about to be sent, not a friendly one-line summary of what the tool is supposed to do. **How to build it** Render the literal argument values in the consent prompt, and never let a prior approval of the same tool stand in for approval of this specific call's parameters. **Produces** A user who can actually catch a hallucinated or maliciously altered parameter before it's sent, because they saw it, not a paraphrase of it. **Tradeoff** Full-parameter prompts are more friction than a one-tap approval, and a user under time pressure may stop reading them closely; that's a training and UX problem to solve, not a reason to summarise. **Prove it** A destructive tool call's consent prompt, captured from a live session, shows the actual argument values rather than a generic description. #### Keep a sensitive server out of sessions that also carry general-purpose ones Host Your policy **What it covers** A rule that a server with access to genuinely sensitive data or actions runs in a session by itself, or alongside only other equally trusted servers, never mixed in with a general-purpose or exploratory one. **How to build it** Classify each server by what it can reach before connecting it, and route sensitive-server sessions through a separate, more restricted configuration than the one used for everyday browsing or search tools. **Produces** A blast radius, if a general-purpose server turns out to be malicious or compromised, that never reaches the sensitive server sitting in the same session. **Tradeoff** Isolating sensitive servers means a workflow that genuinely needs both a sensitive tool and a general one in the same task has to be redesigned around that separation, which is real friction. **Prove it** No live session mixes a classified-sensitive server with an unvetted general-purpose one, checked against the actual session configuration. ### 05 Supply chain & change control This is the surface where the spec has the least to say and your own policy carries the most weight. There's no standard registry, no required signing, no provenance format — a server is trustworthy because someone checked, not because the protocol enforces it. The specific failure worth naming is the rug pull: a definition that was safe when a user first approved it changes shape later, and nothing in the transport forces a new consent prompt unless you build that check yourself. Pinning a definition by hash at approval time, and requiring re-consent the moment it changes, is the cheapest version of the fix, and it has to be built on top of the protocol rather than assumed from it. #### Run every server past an allowlist that checks publisher identity Gateway Your policy **What it covers** A registry or allowlist that a server must clear, naming its publisher and confirming that identity, before it can be connected in any production session. **How to build it** Stand up a lightweight internal registry (even a reviewed spreadsheet to start) that records publisher, purpose, and approval date for every sanctioned server, and block anything not on it at the gateway. **Produces** A concrete answer to "who published this, and did we check" for every server in production, instead of an install history nobody can reconstruct. **Tradeoff** An allowlist is one more thing to maintain, and a genuinely useful new server waits on review before anyone can use it. **Prove it** Every server connected in production maps to an entry in the registry with a recorded publisher and approval date. #### Pin a tool's definition to a hash recorded at approval time Host Your policy **What it covers** A recorded hash of a tool's schema and description, taken the moment it's approved, checked against the live definition on every subsequent connection. **How to build it** Hash the tool definition at approval and store it alongside the registry entry, then compare on connect; treat any mismatch as a new, unapproved tool rather than an update to the old one. **Produces** A structural way to detect a rug pull the moment it happens, rather than relying on someone noticing the tool behaves differently. **Tradeoff** Hash pinning breaks on a legitimate, benign update just as readily as a malicious one, so it has to be paired with the re-consent flow below rather than used as a silent block. **Prove it** A deliberately modified tool definition, reconnected against a stored hash, is flagged as changed before it's callable. #### Require fresh consent the moment a tool's definition changes Host Spec recommended **What it covers** A rule that any change to a tool's schema, description, or destination invalidates a prior approval and triggers the full consent flow again, rather than letting the cached approval carry over. **How to build it** Wire the hash check above directly into the consent system, so a mismatch forces the same explicit approval step a brand-new tool would need. **Produces** Protection against the specific rug-pull attack path below: a tool that was safe when approved and gets redefined afterward. **Tradeoff** Frequent, legitimate tool updates from an actively maintained server mean more re-consent prompts, which a user may start clicking through without reading. **Prove it** A tool whose definition changed since last approval requires a new consent prompt before its next call, tested directly. #### Sandbox a locally run server and scan its dependencies Server Your policy **What it covers** Process-level isolation (containers, restricted filesystem access, no unscoped network egress) for any server running as a local process, plus a routine dependency scan on its package manifest. **How to build it** Run local servers in the same sandboxing regime a team would already require for untrusted third-party code, and add the server's dependency manifest to whatever scanning already runs on the rest of the codebase. **Produces** A compromised or malicious local server contained to its sandbox instead of holding the same filesystem and network access as the process that launched it. **Tradeoff** Sandboxing adds real setup overhead per server, and an overly restrictive sandbox breaks a server that genuinely needs broader access, which has to be granted deliberately rather than by default. **Prove it** A locally run server's process is confirmed sandboxed (restricted filesystem, no unscoped egress), and its last dependency scan is on record. ### 06 Operations What the spec gives you here is version legibility and nothing else. As of 2026-07-28 there is no initialization handshake at all: every request declares its own protocol version, and a server that doesn't implement the requested one has to say so explicitly and list what it does support. Servers must also implement a discovery endpoint a client can ask up front. That is genuinely useful for an operator, and it stops well short of logging, redaction, quotas, or a deprecation policy, all of which are your build. The gap matters more here than on most integration surfaces, because the thing you're operating is frequently someone else's software: a third-party server you don't control the release cadence of, running against your production data. An incident runbook that assumes you own every component in the chain doesn't survive contact with "the vendor pushed a new tool definition overnight and we found out from a support ticket." Write the runbook for that specific case, alongside the case where the broken thing is yours. #### Log every call with its full parameters and the calling user's identity Gateway Your policy **What it covers** A logging record for every tool call that captures the tool name, its parameters, the identity of the agent and user behind it, and the result, with sane redaction for anything genuinely sensitive. **How to build it** Instrument logging at the gateway or host layer so it's uniform across every connected server, rather than depending on each server to log its own calls consistently. **Produces** A specific, inspectable record to answer "what did this agent actually call, and with what," instead of reconstructing it from a model's own transcript after the fact. **Tradeoff** Full-parameter logging means sensitive data sometimes lands in logs; redaction rules need real design, not an afterthought once the first sensitive value shows up in a log line. **Prove it** A tool call picked at random from the last day has a log entry recoverable by tool name, parameters, and calling identity. #### Set quotas and rate limits per session and per tenant Gateway Your policy **What it covers** An enforced ceiling on call volume per session and per tenant, so a single misbehaving agent or a runaway loop can't exhaust a shared server's capacity for everyone else. **How to build it** Enforce limits at the gateway, keyed by session and tenant identity, with a defined response (backoff, hard stop) when a ceiling is hit rather than an unbounded queue. **Produces** A shared server that degrades for the one tenant that's misbehaving, not for every tenant connected to it. **Tradeoff** A rate limit set too low throttles a legitimate high-volume workload; calibrate it against real traffic rather than a round number picked in advance. **Prove it** A session artificially driven past its quota is throttled or stopped, and every other session on the same server is unaffected, tested directly. #### Pin the protocol and tool version you depend on, with a stated deprecation window Server Your policy **What it covers** An explicit version pin for the MCP protocol revision and every tool contract a client depends on, with a published window before an old version stops being supported. **How to build it** Record the pinned version alongside each server's registry entry, and require any breaking change to ship a deprecation notice with a real date, not an immediate cutover. **Produces** A client that fails predictably at a known date instead of breaking silently the moment a server updates. **Tradeoff** Supporting an old version through its deprecation window means running more than one shape of the same integration at once, real maintenance cost for a real safety margin. **Prove it** Every connected server's pinned version and its stated deprecation date, if any, are on record and checked against what's actually running. #### Write an incident runbook for the case where the broken component isn't yours Gateway Your policy **What it covers** A specific incident procedure for a compromised or misbehaving third-party MCP server: how to disconnect it fast, what to check for damage done while it was connected, and who has the authority to make that call. **How to build it** Write the runbook against the actual worst case (a vendor's server starts returning altered data, or gets compromised outright) rather than only against incidents in your own code, and name who can pull the plug without waiting for the vendor. **Produces** A response measured in minutes for a third-party server incident, instead of a scramble because the existing runbook assumed every component was one your own team could patch. **Tradeoff** Writing a runbook for a case you don't control the fix for means the best available action is sometimes just disconnect and wait, which can feel unsatisfying compared to a real fix. **Prove it** A tabletop exercise that simulates a third-party server compromise reaches disconnection inside the runbook's stated target time. No controls match that combination. Clear a filter to see more. Five attack paths ## Walked end to end, not left as a category. Each one is a specific sequence, the control that stops it, and the layer that owns that control. Three of the five attack paths run through the host; the other two run one each through identity and through the server. ### The poisoned tool description **How it runs:** A server, compromised or malicious from the start, publishes a tool whose name and parameters look ordinary. Its description field, though, carries an embedded instruction the model reads as context it should act on, since nothing in the transport marks that field as data rather than a directive. The model quietly follows the hidden instruction on its next turn, reading a file or calling a tool nobody asked it to. **Stopped by:** Treating every description as untrusted by default, and showing the full parameters of any resulting call before it executes, catches the instruction before it turns into an action. Host [See the control: Treat every tool description and annotation as untrusted by default →](https://customlabs.io/mcp/#treat-descriptions-as-untrusted) ### Cross-server tool shadowing **How it runs:** A session already has a legitimate `file_search` tool in active use. A newly connected server registers its own tool under the same bare name, its description claiming to be the "updated" or "preferred" version, and subsequent calls the model intended for the original tool get routed to a server that should never have seen those queries. **Stopped by:** Namespacing every tool by the server that owns it removes the name collision the shadowing attack depends on entirely. Host [See the control: Namespace every tool by the server that owns it →](https://customlabs.io/mcp/#namespace-tools-by-server) ### The OAuth-proxy confused deputy **How it runs:** A gateway sits in front of several MCP servers and holds one broad, delegated token to keep the integration simple. A request meant only for the least-privileged server in the chain carries that same token; the receiving server, with no audience check to fail, treats it as valid proof of authorization for actions the original grant never covered. **Stopped by:** Validating token audience at every server, and refusing to forward a received token to a different downstream server, closes exactly this path — it's the one attack the spec explicitly names as disallowed. Identity [See the control: Validate audience on every token, and never forward one to a different server →](https://customlabs.io/mcp/#audience-bound-tokens-no-passthrough) ### The post-approval rug pull **How it runs:** A user approves a tool the first time it appears, reasonably, because its description and schema look narrow and safe. Days later, the server that publishes it silently redefines the same tool name to accept a new parameter or reach a new destination, and every session that cached the original approval keeps calling the new, unreviewed version with no fresh consent prompt. **Stopped by:** Pinning the tool's definition by hash at approval time and requiring a new consent prompt the moment that hash changes catches the redefinition before it's ever called again unreviewed. Host [See the control: Require fresh consent the moment a tool's definition changes →](https://customlabs.io/mcp/#reconsent-on-definition-change) ### Exfiltration through an approved tool's return value **How it runs:** A perfectly legitimate, already-approved tool, a ticket lookup or a document fetch, returns content that itself contains an embedded instruction, because whoever populated that record was able to write arbitrary text into it. The model reads the return value as ordinary data on one call and as an instruction on the next, then forwards something it was never asked to forward to a second, also-approved tool that can act on it. **Stopped by:** Returning structured data instead of raw, renderable content limits what a return value can actually smuggle, and isolating sensitive servers bounds what a successful exfiltration can reach. Server [See the control: Return structured data, never rendered HTML, from a tool call →](https://customlabs.io/mcp/#structured-returns-over-html) The server intake gate ## Ten questions before the next server gets connected. What a platform team actually needs the day someone asks to add a third-party MCP server. - ### Who operates this server, and is there a named contact for it? **Why it's on the gate** An unowned server is a support and security dead end the day something breaks — there's nobody to ask what changed. **Passing answer** A named publisher or internal team, reachable, recorded before the server is added to any production session. **Artifact** An owner field in the server registry, filled in before approval, not after an incident. - ### Does the requested scope map one-to-one to the tools this workflow actually calls? **Why it's on the gate** An over-scoped grant is exactly what turns a compromised or misbehaving server into a bigger incident than it needed to be. **Passing answer** A scope list with no entry unused by the tools actually invoked. **Artifact** A diff of the granted scope against the tool list, run at approval and rechecked periodically. - ### Is the token this server receives bound to it specifically, and rejected if it isn't? **Why it's on the gate** This is the exact gap the confused-deputy attack path above exploits. **Passing answer** A test call with a token minted for a different server is confirmed rejected, rather than assumed to be. **Artifact** A recorded test result from a token-audience check, not a design document describing one. - ### What does the tool catalog look like once this server joins an existing session, and does anything collide? **Why it's on the gate** A new server can shadow an existing tool the moment its name or description overlaps with one already in use. **Passing answer** A reviewed diff of the catalog before and after connecting, with no unresolved name collision. **Artifact** The before/after catalog diff, attached to the approval record. - ### Can this server's tool definitions change without anyone finding out? **Why it's on the gate** An unpinned definition is exactly what the post-approval rug pull depends on. **Passing answer** Definitions pinned by a recorded hash at approval time, with re-consent required the moment that hash changes. **Artifact** The recorded hash from the approval, checked against the currently live definition. - ### What does a destructive or irreversible call from this server actually show the user before it runs? **Why it's on the gate** A summarised description hides exactly the parameter a user would have caught if they'd seen it in full. **Passing answer** The full, literal parameters of the call, shown fresh every time, never a cached blanket approval. **Artifact** A captured screenshot or spec of the consent prompt for this server's most destructive tool. - ### Where does this server actually run, and is it reachable from anything it shouldn't be? **Why it's on the gate** A server bound wider than intended is reachable from more of the network than anyone signed off on. **Passing answer** Bound to loopback or a private network segment, with TLS on anything remote. **Artifact** A port scan or network diagram confirming the actual reachable surface, not the intended one. - ### Are this server's calls logged with full parameters and the calling identity, and where does that log live? **Why it's on the gate** Without this, an incident review has nothing to reconstruct what actually happened. **Passing answer** Full parameters and calling identity logged centrally, with redaction rules for anything genuinely sensitive. **Artifact** A sample log line for a real call to this server. - ### What's the version and deprecation story if this server's tools change shape? **Why it's on the gate** A breaking change with no warning is a production incident; one with a stated window is a scheduled migration. **Passing answer** A version marker on the current definition and a stated notice window before an old shape stops working. **Artifact** The server's own published versioning or deprecation policy, or the internal equivalent if it doesn't have one. - ### If this server is compromised, what's the actual blast radius, and who responds? **Why it's on the gate** Every other question above is worth nothing if nobody's named to act when the answer to this one turns out badly. **Passing answer** A blast radius bounded by the scopes already granted, and a named on-call path in the incident runbook. **Artifact** The runbook entry naming this server specifically, not a generic incident-response document. What this is built from ## Grounded in the spec, not a vendor's incident count. No third-party prevalence statistics or CVE identifiers. Attack classes are stable; those numbers rot in weeks. - /tool-design/ covers how to design one tool's interface well: naming, schemas, permissions. This page is the layer above it — what has to be true about identity, the catalog, and the trust boundary before that well-designed tool is reachable across an organisation rather than inside one process. [The Agent Tool Interface](https://customlabs.io/tool-design/) - AI Security Review is the standing gate the trust-boundary surface above routes into — an untrusted tool description or an unreviewed return value is exactly the kind of finding a review has to catch before a server goes anywhere near production. [AI Security Review](https://customlabs.io/security-review/) - The model-agnostic inference gateway reference architecture is the same shared-broker shape this page argues for at the identity and operations surfaces: one place to put routing, logging, and rate-limit policy instead of reimplementing it per integration. [Model-agnostic inference gateway](https://customlabs.io/architectures/model-gateway/) - The prompt-injection threat model insight is the general case the trust boundary above narrows to one specific channel: an approved tool's own return value, not just the prompt a user typed. [Insight: Prompt injection threat model](https://customlabs.io/insights/prompt-injection-threat-model/) - The AI Governance Layer's inventory and third-party surfaces are where a connected MCP server actually gets recorded once it clears the intake gate below — this page is the gate itself; that page is the standing record a year later. [The AI Governance Layer](https://customlabs.io/governance/) ### Sources - [Model Context Protocol - MCP Specification: Authorization (2026-07-28)](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) The specification text our identity and authorization surface is checked against. Retrieved 2026-08-24. - [Model Context Protocol - MCP Specification: Tools (2026-07-28)](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) The specification text our tool-design and catalogue rules are checked against. Retrieved 2026-08-24. - [OWASP - MCP Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html) The MCP-specific checklist our intake gate is checked against. Retrieved 2026-08-24. Adding a third-party MCP server soon A Ship Audit checks the intake gate above against the specific server someone's asking you to connect, before it's in a production session. [Book a Ship Audit →](https://customlabs.io/diagnostic/ship-audit/) [See the tool interface guide →](https://customlabs.io/tool-design/) Questions ## Before you connect the next server. What platform teams ask us before they let MCP past a single pilot integration. 01 Isn't MCP itself the security boundary? + No. The spec standardizes the wire format for a tool call crossing a process boundary, and it does mandate a few real things: Origin validation on an HTTP transport, and, once you choose to protect a server at all, audience-bound tokens with no passthrough. Past that, the catalog you expose, which servers you trust, and the blast radius of a bad call are yours to design; the protocol doesn't guarantee any of it. 02 We only connect first-party servers we wrote ourselves. Does the trust boundary still apply? + Yes, though the risk shifts shape. A first-party server can't typosquat you, but its tool descriptions and return values still sit in the same context window as every other connected server's, so a bug in your own server's output can read like an instruction just as easily as a malicious one. Namespacing and structured returns aren't only for third parties. 03 What's the single fastest way to get burned on this surface? + A gateway that holds one broad, delegated token and forwards it unchanged to every server behind it. That's the confused-deputy path above, and it's the one mistake the spec explicitly calls out by name: a server must not accept or transit a token issued for someone else. 04 Do we need all 24 controls before connecting a single server? + No — scope the controls to what the server can actually do. A read-only, first-party lookup tool needs far less than a remote, third-party server with write access. The ten-question intake gate above is the fast version of that call. 05 How is this different from the trust-boundary surface on /tool-design/? + /tool-design/ is about designing one tool well — a tight schema, a clear name, sane permissions. This page is what happens once that tool is reachable across a process boundary by more people than the team that wrote it: identity, the catalog, and supply chain are organisation-level problems a single tool's design can't solve alone. 06 Does Dynamic Client Registration still work? + It's retained for backwards compatibility with authorization servers that predate Client ID Metadata Documents, but the 2026-07-28 revision deprecates it. Don't design a new integration around it. Take the review seriously A poisoned tool description can steer a model without ever touching its prompt. A security review walks that same attack surface for the system you are connecting a server to, not just the protocol. [Start a security review →](https://customlabs.io/security-review/) [Talk to us about integrations →](https://customlabs.io/services/ai-integrations/)