One arrow, four layers

The model never calls the MCP server

In a recent workshop for a self-hosted Kubernetes deployment, a platform vendor's engineers kept drawing an arrow from the language model straight to the MCP server. I kept objecting, and by the third round I realised the argument was worth writing down. That arrow does not exist, and knowing why changes how you secure the design.

Updated July 2026 MCP spec revision 2025-11-25
L1 · The picture

A brilliant assistant sits in a locked room, and nobody ever hands them a key.

For anyone

Everything the assistant does in the world happens by note. The assistant slides a note under the door: please fetch the blue folder. Outside sits a clerk with a rulebook and the only ring of keys. The clerk reads the note, checks the rulebook, walks to the right room, and returns with a note describing what was found.

The assistant never touches a key. Never opens a door. If someone slips a trick note into the room, the assistant may write a foolish request, but the clerk and the rulebook decide what actually happens.

That is the whole post in one scene. The model is the assistant. The clerk is a piece of software called the MCP client. Every "tool call" you have heard about is a note, not a key.

Locked room The assistant, no keys Clerk Rulebook + keys Rooms Files, calendar, database Note Clerk walks No direct path
The vendor's whiteboard arrow ran along the dashed line. Nothing in the protocol runs there.
L2 · The parts

A language model reads text and writes text. That is the complete list of things it can do.

For a builder

The model has no network connection, no passwords, and no ability to press any button anywhere. When people say the model "calls a tool", what actually happens is that the model writes a block of structured text describing a call it would like made. The model provider's API calls this a tool use block. It is a note in a fixed format, nothing more.

The app you actually run, a chat window or a coding tool, is the host. Inside the host sits the MCP client, the clerk from the picture above. The client reads the model's note, decides whether to act on it, and if so makes the real network call to an MCP server. Each server is a small program standing in front of one real thing, such as your files or a calendar. What the server finds comes back as a tool result, appended to the conversation so the model can read it and carry on.

A common assumption is that self-hosting changes this. It does not. Self-hosting changes where the model runs, not what it can do: a vLLM pod on your own cluster is still a pure text-in, text-out endpoint. The confusion usually comes from packaging. Agent platforms bundle the model server, the agent loop, and the MCP client into one product, so on a whiteboard it all collapses into a single box labelled "LLM". That shorthand is fine for a product conversation. It is not fine for a security design, because inside the box the separation still exists, and it decides who holds the keys.

The test I apply to any agent platform is simple: name the exact component that holds the MCP client session. Whatever answers that question inherits every security requirement you would put on a gateway: authorisation, audit, credential handling. Until that question has an answer, the design has a governance hole.

My rule of thumb If a diagram shows an arrow from the model to anything other than the host, the diagram is wrong or it is hiding an ungoverned component. Ask which.
L3 · The loop

A tool call is a stop, not a command.

For an engineer

At the start of a session the client asks every server what tools it offers, using a call named tools/list, and puts those descriptions into the model's context. That is the only reason the model knows any tool exists.

Then the loop runs. The model writes. If the output contains a tool use block, generation halts. The client checks its policy, makes the real call with tools/call, and the answer is appended to the conversation before the model is asked to continue. One tool call means one full stop, execute, append, resume cycle. A task needing five tools runs the loop five times, and each cycle is a complete round trip through the model.

Model outputA note, not a network call
{
  "type": "tool_use",
  "name": "read_file",
  "input": { "path": "reports/q2.xlsx" }
}
// generation stops here. the client decides what happens next.
Client to serverThe real call, JSON-RPC
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "read_file",
    "arguments": { "path": "reports/q2.xlsx" }
  }
}
// made with the server's credentials. the model never sees them.

The policy check between those two blocks is the only moment anything can say no. Is this tool on the allow-list? Does a person need to approve it? Is this the fifth call in a row that looks strange? Every defence you will ever add to an agent design lives in that gap, because it is the only gap there is.

Three behaviours of this loop bite in production. Tool results re-enter the model's context, so a poisoned result can steer the next call: this is how prompt injection drives an agent. Tool descriptions are attack surface before any call is made, and a server can swap its definitions mid-session with a tools/list_changed notification, a move researchers named the rug pull. And every argument in a call was written by the model, so servers must treat arguments as untrusted input, the same way you treat anything a browser sends you.

L4 · The decision

Once you accept that a client makes every call, the real question is where that client lives.

For a practitioner

From what I see inside enterprises, four placements cover the market.

Embedded on the user's machine. The client lives inside a desktop app. Simplest possible setup, and the endpoint becomes the attack surface: a local server launched over stdio runs code as the user.

Behind central gateways. The client sits in your backend, with one gateway on the model channel for prompt and data controls, and one on the tool channel for authorisation and audit, credentials injected at the gateway so agents never hold them.

At the model provider. The provider runs the loop and calls your servers with tokens you shared. The least code of the four, and no inline inspection is possible.

As a sidecar on Kubernetes. One client proxy per workload. The mesh gives you identity, and the blast radius of a compromise is one pod.

1 · Embedded Host app Client inside Server The endpoint is the attack surface 2 · Dual gateway Agent Model gw LLM MCP gw Server Audit on both channels, gateways become crown jewels 3 · Provider-side Thin app Provider Loop + client Srv The seam leaves your boundary, no inline inspection 4 · Sidecar Agent pod Agent Sidecar Client Server Blast radius: one pod
The same clerk, four different desks. Every placement moves the trust boundary; none of them removes it.
How the placements compare on the risks I weigh first. h high, m medium, l low.
RiskEmbeddedDual gatewayProvider-sideSidecar
Injection via tool resultshmhm
Tool poisoning, rug pullhlml to m
Credential exposurehlhm
Audit and forensicshlhm
Blast radius of a compromisemhhl
Data residencymlhl

Every column has gaps. The difference is whether the risk is concentrated where you can see and harden it, or scattered where you cannot.

My view is that the dual gateway wins for regulated environments, and it will keep winning until inspection tooling matures, because it is the only placement where every hop can be seen, stopped, and replayed in an audit. It trades many invisible gaps for one visible one: the gateways become crown jewels, and a gateway outage stops every agent in the company. I take that trade with open eyes, then borrow the sidecar pattern's mechanics inside the cluster, network policies that force agent pods through the gateways and mutual TLS between components, so a compromised pod owns one session rather than the estate. This is the sequence I defend in reviews, end to end:

Agent Model gw Guardrails LLM MCP gw Server 1 Prompt + tools 2 Inspect request 3 Pass / block 4 Sanitised request 5 Tool use block 6 Inspect response 7 To agent 8 tools/call 9 Execute Creds injected here 10 Result 11 Result to agent, audited 12 Next pass The tool result rides inside message 12, so the guardrails inspect it at step 2 of the next pass. Steps 1 to 12 repeat once per tool call.
The dual gateway, end to end. Two enforcement points, every hop logged, and the loop pays the inspection cost on every iteration.

One honest caveat before anyone builds this: gateway inspection of MCP traffic is harder than it sounds. There is no static API contract to validate against. Schemas are dynamic and every argument was written by a model, so inspection is heuristic, never complete. Budget for that, and for the latency of running inspection inside a loop that may iterate ten times per user request.

The checks I now run before signing off any agent design:

  1. Name the component that holds the MCP client session, and treat it as the trust boundary.
  2. Pin the tool list per session, and fail loudly on a mid-session tools/list_changed: terminate or flag the session rather than silently ignoring it.
  3. Confirm the guardrail layer inspects tool use and tool result blocks, including nested JSON and base64, not only the user's latest message.
  4. Split approvals correctly: synchronous enforcement at the gateway, the asynchronous approval workflow in the agent runtime. Neither alone is enough.
  5. Fail closed when guardrails are down. An uninspected agent loop is worse than an outage.
  6. Cap loop iterations, my default is 5 to 10 per user turn, and alert on unusual call chains.
Where I think this is heading Provider-side execution will keep getting easier, and teams will keep reaching for it because it is the least code. For anything touching internal systems I would still refuse it: the client is the one control point this whole architecture gives you, and that option hands it to someone else. When a vendor draws that arrow from the model to the server, it is usually not deception, it is packaging: the arrow is where their product sits. Your job is to ask what is inside it.

References

  1. Model Context Protocol specification, architecture and lifecycle: the client-host-server roles, tools/list and tools/call. modelcontextprotocol.io/specification/2025-11-25
  2. Invariant Labs, tool poisoning attacks disclosure (April 2025): the rug pull via tools/list_changed. invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks
  3. OWASP MCP Top 10: tool poisoning classified as MCP03:2025. owasp.org/www-project-mcp-top-10
  4. Anthropic, MCP connector documentation: provider-side tool execution and its approval model. platform.claude.com/docs/en/agents-and-tools/mcp-connector
  5. OpenAI, connectors and remote MCP documentation: per-call approval defaults for remote servers. developers.openai.com/api/docs/guides/tools-connectors-mcp

The severity ratings in the comparison table and the loop-cap default are my own working judgments from enterprise reviews, not figures from a study. Wire payloads above are illustrative and match the spec's method names and shapes.