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.

LayerWeak defaultEnterprise contract
Tool contractOne generic run_query executorNarrow typed tools, reviewed descriptions
IdentityShared service accountOAuth 2.1 resource server, per-tool scopes
TransportLocal stdio for everythingStreamable HTTP behind a gateway
ErrorsThrown exceptionsTyped, documented outcomes
ObservabilityNo call logEvery 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.

ts
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:

  1. The server must publish Protected Resource Metadata (RFC 9728) so clients can discover which authorization server to talk to.
  2. 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.
  3. 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.

AttackDisclosureArchitecture layer that stops it
Tool poisoning: hidden instructions in descriptionsInvariant Labs, April 2025Tool contract: pin versions, review descriptions
MCPoison: an already-approved MCP config file is silently swapped for a malicious oneCVE-2025-54136, Cursor, CVSS 8.8, Check Point Research, patched July 2025Tool contract: pin configs and re-approve on any change
Token replay across serversAddressed by RFC 8707 binding in the 2026-07-28 specIdentity: resource-bound tokens
RCE via mcp-remote transport libraryCVE-2025-6514, CVSS 9.6, more than 437,000 downloads at disclosureTransport: patch and pin client libraries
Silent data exfiltration by a compromised serverCross-tenant isolation flaws reported in enterprise integrationsObservability: 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