Three roles, not two
The two-party mental model — an app calls a service — is what most developers bring to MCP, and it is subtly wrong. The specification names three participants. A host is the LLM application the user actually interacts with: an IDE assistant, a chat product, an agent runtime. A client is a component the host creates and owns, and its entire job is to hold one protocol connection. A server is a separate program that exposes capabilities — tools, resources, prompts — over that connection.
Collapsing host and client into one word is the error that causes real damage, because they answer different questions. The host answers “should this happen at all?” The client answers “does the peer on the other end of my connection support this?” Those are policy and plumbing respectively, and they belong in different places. Once you can say which of the three you are writing, most architectural questions in MCP answer themselves.
The host owns the user and the model
The host is the only participant with a relationship to the human. It renders the interface, it holds the conversation, and critically, it holds the model — the API key, the completion budget, the system prompt, the context window. No server has an LLM of its own in this architecture; when a server needs inference, it must ask the host for it.
That monopoly makes the host the natural home for everything that requires judgment. Consent is the clearest case: only the host can show a user a prompt and read their answer, so only the host can legitimately authorize a consequential action. Policy sits there too — which servers are permitted to run, which tools are exposed to the model at all, which operations need per-invocation approval, what the model is allowed to see. So does aggregation: the host is where several servers’ capabilities are merged into the single surface the model reasons over. If a decision requires knowing what the user wants or what the organization allows, it belongs to the host and nowhere else.
A client is a connection, not an application
This is the definition that surprises people. In MCP a client is not “the app”; it is a connector object the host instantiates, and the relationship with a server is deliberately one client per server. Connect a host to five servers and it runs five clients internally, each holding exactly one session.
What a client owns is narrow and mechanical: it establishes the connection over the chosen transport, performs the initialization exchange, remembers what that particular peer declared it can do, routes requests and responses by their JSON-RPC identifiers, surfaces notifications, and tears the connection down cleanly when the host says so. It tracks the capability set of its server — not a global one — because two servers in the same host may support entirely different feature sets and even negotiate different protocol revisions. What a client should not own is judgment. A client that decides on its own to approve a destructive tool call has quietly stolen the host’s job, and the user’s.
Why the one-to-one isolation matters
The one-client-per-server rule looks like an implementation detail and is actually the security and reliability backbone of the model. Because each connection is a separate object with separate state, a server sees only its own session: its own negotiated capabilities, its own request identifiers, its own notifications, its own lifecycle. It has no channel to another server and no visibility into one.
The consequences are concrete. A malicious or merely buggy server cannot read or inject into another server’s traffic, because there is no shared bus to reach. A server that hangs, crashes, or floods its connection degrades one client inside the host rather than the whole application — the host can drop that client and keep the others alive. Credentials scoped to one integration stay in that integration’s connection. And because the host sits between every pair, it can enforce different policy per server: full trust for a first-party server, tight confirmation gating for one a user installed yesterday. Take away the isolation and every one of those guarantees goes with it.
What a server owns, and what it must not assume
A server’s side of the contract is refreshingly small: implement capabilities and declare them honestly. It exposes tools the model may invoke, resources the host may read as context, and prompts the user may choose to trigger. It validates its own inputs, enforces its own authorization against the system it fronts, and reports failures in the form the protocol expects.
The harder half of the contract is the list of things a server must not assume. It does not know which host it is talking to, or whether that host is a chat UI, an IDE, or a headless agent with no human present at all. It does not know which model is in play, or whether one exists. It cannot assume a user will see anything it returns, or that an approval dialog will be shown, or that any particular optional feature is available — that is precisely what capability declaration exists to settle. A server that hard-codes assumptions about the host is not a server; it is one application’s plugin wearing protocol clothing.
Bidirectionality breaks the usual intuition
In HTTP, clients ask and servers answer, permanently. MCP is built on JSON-RPC 2.0, where either side of an established connection may originate a request, and the protocol uses that fully. The direction of the connection and the direction of a request are independent facts.
Three features make this unmistakable. With sampling, a server asks the host to run a model completion on its behalf — the server has no model, so it borrows the host’s. With elicitation, a server asks the host to collect additional input from the user, because the server has no user of its own. With roots, a server asks the client which filesystem or workspace boundaries it is allowed to operate within. In each case the arrow points from server back toward host, and in each case the reason is the same: the server needs something only the host owns. Read the role model this way and bidirectionality stops being an oddity — it is the direct consequence of putting the model, the user, and the workspace on one side and the capability on the other.