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.
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.
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.
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.
{
"type": "tool_use",
"name": "read_file",
"input": { "path": "reports/q2.xlsx" }
}
// generation stops here. the client decides what happens next.
{
"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.
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.
| Risk | Embedded | Dual gateway | Provider-side | Sidecar |
|---|---|---|---|---|
| Injection via tool results | h | m | h | m |
| Tool poisoning, rug pull | h | l | m | l to m |
| Credential exposure | h | l | h | m |
| Audit and forensics | h | l | h | m |
| Blast radius of a compromise | m | h | h | l |
| Data residency | m | l | h | l |
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:
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:
tools/list_changed: terminate or flag the session rather than silently ignoring it.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.