Agents become useful when they can act, and acting means calling internal systems. The Model Context Protocol standardizes that boundary: a server advertises tools, an agent calls them, and the transport is uniform across providers. What the protocol deliberately does not decide is your security and error model. That is the architecture, and this guide walks through it layer by layer.
The original version of this article covered three decisions. Two CVEs and one protocol revision later, it covers five.
The reference architecture
Every enterprise MCP deployment I have reviewed or built reduces to the same stack. An agent host connects through a control point to a set of MCP servers, each wrapping one internal system. Five decisions sit at that boundary, and each one has a weak default you get for free and an enterprise contract you have to build.
| Layer | Weak default | Enterprise contract |
|---|---|---|
| Tool contract | One generic run_query executor | Narrow typed tools, reviewed descriptions |
| Identity | Shared service account | OAuth 2.1 resource server, per-tool scopes |
| Transport | Local stdio for everything | Streamable HTTP behind a gateway |
| Errors | Thrown exceptions | Typed, documented outcomes |
| Observability | No call log | Every call logged with caller identity |
The rest of this guide is those five rows, expanded.
Layer 1: tools, not a generic executor
The fastest way to create an unsafe agent is a single run_query tool. Model each capability as a narrow tool with a typed schema, so the agent’s surface area is legible and reviewable.
server.tool(
'get_invoice_status',
{ invoiceId: z.string().uuid() },
async ({ invoiceId }, ctx) => {
const invoice = await billing.getInvoice(invoiceId, ctx.auth.scopes);
if (!invoice) return { error: 'not_found' as const };
return { status: invoice.status, dueAt: invoice.dueAt };
}
);Two details earn their keep here. First, the schema is the contract: a UUID parameter rejects half the injection attempts before your handler runs. Second, the tool description is not documentation. It is prompt surface.
That second point became concrete in April 2025, when Invariant Labs disclosed tool poisoning attacks: instructions hidden inside tool descriptions that the model reads and the user never sees. The MCPTox benchmark (August 2025) turned that disclosure into numbers: poisoned tools aimed at 45 live MCP servers and 20 models, with a 36.5% average attack success rate, several models above 60%, and the most vulnerable (o1-mini) at 72.8%. A Cloud Security Alliance research note draws the same operational conclusion: treat every MCP server as untrusted, even one you already approved. The practical consequence: pin server and tool versions, diff description changes in review like you would diff code, and never let a third-party server update its own descriptions silently.
Layer 2: identity is per call, not per server
A shared service account turns every agent into a superuser. I still find this in most audits, usually justified as temporary.
The protocol caught up. The current MCP authorization spec, version 2026-07-28, makes the server an OAuth 2.1 resource server, a role the spec has defined since the 2025-03-26 revision. Three requirements matter for your design:
- The server must publish Protected Resource Metadata (RFC 9728) so clients can discover which authorization server to talk to.
- Tokens are bound to the resource with Resource Indicators (RFC 8707), so a token issued for one MCP server is not replayable against another. This kills the confused-deputy pattern where a gateway happily forwards a token anywhere.
- Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents, so your authorization server no longer needs an open client-registration endpoint.
Map this onto your org’s identity provider instead of inventing one. Derive scopes per tool from the caller’s identity, deny by default, and propagate the end user on behalf of whom the agent acts. When the agent calls the billing system, the billing system should see Maya’s permissions, not the agent platform’s.
Layer 3: transport and deployment
MCP offers two transports, and they imply two different architectures.
stdio runs the server as a child process of the agent host. Credentials come from the environment. It is the right answer for single-developer tools on a laptop, and the wrong answer for anything shared, because every user gets their own copy with their own credentials and there is no central place to enforce policy.
Streamable HTTP runs the server as a network service. This is the enterprise path: the server validates tokens itself, you can scale it independently, and you can put a control point in front of it. That control point is usually a gateway that terminates authentication, enforces rate limits, and routes to individual servers. Microsoft’s open-source mcp-gateway is a useful reference for what production means here: Kubernetes StatefulSets, resource metadata in Redis or Cosmos DB, Entra ID authentication, and a hard rule that the inner tool servers reject any request missing the gateway’s secret header. I built an evaluation rubric for that landscape in How to Evaluate Any MCP Gateway.
One trap worth naming: state. The original HTTP+SSE transport kept long-lived sessions, which made horizontal scaling painful, and the 2026-07-28 revision no longer includes it at all. Streamable HTTP lets you run servers stateless, with any durable state pushed into a store the replicas share. Design for stateless from the start.
Layer 4: errors are part of the interface
Agents plan around outcomes. A thrown exception gives the model nothing to reason about, so it guesses, retries blindly, or fabricates a result. Return typed, non-throwing errors such as not_found, permission_denied, or rate_limited, and document them next to the tool.
The academic literature now agrees. A June 2026 paper on MCP server architecture patterns puts it plainly: return tool errors as structured content so the model can see the error, reason about whether to retry, and decide when to escalate to the user. Also give every error a stable machine-readable code. Humans read messages. Agents branch on codes.
Layer 5: observability is not optional
Log every tool call with the caller identity, the tool name, the arguments, the outcome, and the latency. Two reasons. First, debugging agent behavior without call logs is close to impossible, because the model’s reasoning is invisible and the calls are all you have. Second, your security team will eventually ask who approved what, and “the agent did it” is not an answer that survives an audit.
Feed these logs into the same pipeline as your AI agent audit log, so a tool call, an approval, and a data read share one timeline. When a human must approve a sensitive call before it executes, the log entry is what makes that approval binding, which I covered in Human-in-the-Loop AI Approval That Actually Binds.
The attacks, mapped to the layers
Each row of the architecture table exists because a specific attack class exploits the weak default. This mapping is the part I wish someone had handed me two years ago.
| Attack | Disclosure | Architecture layer that stops it |
|---|---|---|
| Tool poisoning: hidden instructions in descriptions | Invariant Labs, April 2025 | Tool contract: pin versions, review descriptions |
| MCPoison: an already-approved MCP config file is silently swapped for a malicious one | CVE-2025-54136, Cursor, CVSS 8.8, Check Point Research, patched July 2025 | Tool contract: pin configs and re-approve on any change |
| Token replay across servers | Addressed by RFC 8707 binding in the 2026-07-28 spec | Identity: resource-bound tokens |
RCE via mcp-remote transport library | CVE-2025-6514, CVSS 9.6, more than 437,000 downloads at disclosure | Transport: patch and pin client libraries |
| Silent data exfiltration by a compromised server | Cross-tenant isolation flaws reported in enterprise integrations | Observability: per-call logs with identity |
No single layer is sufficient. That is the point of having five.
Build or adopt
If you have one agent and three internal tools, skip the gateway and run two or three well-scoped Streamable HTTP servers with token validation. The full stack pays for itself when you cross roughly ten servers, several teams, or a compliance requirement to prove who called what. Below that line it is orchestration you do not need yet, and When to Use Multi-Agent Orchestration makes the same argument from the agent side.
Further reading
- How to Evaluate Any MCP Gateway: the buyer’s rubric for the control point in layer 3, with verified criteria.
- AI Agent Audit Log: the event model behind layer 5, including what to record for every call.
- Permission-Aware RAG for Enterprise Knowledge Systems: the same identity discipline applied to retrieval, where the failure mode is a correct answer built from a document the caller was never allowed to read.
