How does MCP's architecture work?
MCP’s architecture has three roles. The host is the application you open, the one that talks to the model. The client lives inside the host and talks to exactly one server. The server is a separate process exposing the tools, resources and prompts of some system. The host creates one client per connected server and is the only party that sees the whole conversation.
The specification calls this a client-host-server architecture, and the sentence
right after it matters most: MCP is a stateless protocol, in which every request
is self-contained and carries its own protocol version and capabilities1. That
changed in revision 2026-07-28, and it reshuffled much of what follows.
Two things sit outside this article and have their own. How messages get from one end to the other belongs to MCP transports, and whether the server runs on your machine or somewhere else is the local versus remote question. What matters here is who is who, with the Model Context Protocol in general covered by the cluster’s own article.
What each role does
The host is the application process. It creates and destroys client instances, controls which connections exist and for how long, enforces security policies and consent requirements, resolves the user’s authorization decisions and aggregates context across every client1. It is also the party that talks to the model. No server reaches that layer.
The client is created by the host and talks to a single server. It attaches the protocol version and capabilities to every request, routes messages in both directions, manages notification subscriptions and maintains the boundary between one server and the rest1. In practice you never see a client: when somebody says “Claude Code is an MCP client”, what they are describing is the host, and the client is the internal component it instantiates once per server in your configuration.
The server exposes the three primitives, operates independently and can be a local process or a remote service1. It knows nothing about who is on the other end beyond what the request carries, and nothing about the existence of other servers.
One distinction trips up almost everyone on a first read: the client is a piece of software inside your editor, not the editor. A host with four configured servers has four live clients at once.
Why one client per server
Multiplexing everything over a single connection would be simpler to implement. The specification chose the opposite, and the design principle behind it is written out in full: servers should not be able to read the whole conversation, nor see into other servers1. Full history stays with the host, each server receives only the context it needs, and cross-server interaction is controlled by the host.
Isolation is the reason the drawing looks like this. Connect a filesystem server and a third-party SaaS server, and the second one has no way to observe what the first returned. Composition happens in the host, under the host’s policy.
That isolation has a practical consequence that shows up early in any aggregator.
Tool names are unique within a server, not across servers. Two servers can each
expose a search tool, and the specification explicitly recommends that whoever
aggregates implement a disambiguation strategy such as prefixing with a server
identifier. It also warns that serverInfo.name is not guaranteed to be unique
and should not be relied on for that1. If you are writing a host, that is the
first bug you will hit.
How the two ends find each other
Up to revision 2025-11-25, a connection opened with a handshake: the client sent
initialize with the version and capabilities it supported, the server answered
with its own, the client confirmed with notifications/initialized, and only then
could it call anything. The whole negotiation lived in connection state.
Revision 2026-07-28 took that apart. The client puts its capabilities in
_meta.io.modelcontextprotocol/clientCapabilities on each request, and the server
advertises its own in response to server/discover, a call the client may
make before any other to discover capabilities up front1. May, not must. A
client that already knows what it wants to call sends tools/call straight away.
The change swaps one economy for another. Before, negotiation cost two round trips and covered the whole session. Now there is no session, every request repeats a few bytes of metadata, and any server instance behind a load balancer can answer any request with no shared state. For a local stdio server the gain is nil; for a remote server running several instances, it is what makes horizontal scaling trivial.
The part nobody can ignore in 2026 is compatibility. A client supporting both eras
should probe with server/discover before anything else, and the specification
enumerates the three outcomes: a DiscoverResult means the server is modern; a
recognized modern error such as UnsupportedProtocolVersionError means the server
is modern but does not speak that version, and you must not fall back to
initialize; any other error, or no answer at all, means the server is legacy and
the handshake applies1. The warning attached to that rule explains why the
fallback cannot key off one specific error code: legacy servers answer unknown
pre-initialize methods with implementation-defined errors, commonly -32601 or
-32602, or with nothing.
The server does not ask questions
One detail of the design changed quietly and breaks older code: in this revision, servers do not initiate JSON-RPC requests. Not one.
A server that needed a model completion, a piece of input from the user or the
list of permitted directories used to send its own request over the channel. Now
it returns an InputRequiredResult inside the reply, with the asks in
inputRequests, and the client re-sends the original request carrying
inputResponses plus the requestState the server handed back1. The JSON-RPC
id on the retry has to differ from the first one.
Two consequences are worth writing down. The first is that every flow is now
client-initiated, which fits HTTP’s request-response model and removes the need
for a permanently open server-to-client channel. The second is that server logic
has to be resumable: it answers, dies, and gets back a fresh request that must
pick up where it left off. That is what requestState is for, and it is where a
hasty implementation parks state in memory and breaks the moment a second replica
comes up.
On top of that, roots, sampling and logging are all marked deprecated in
2026-07-28, eligible for removal in the first revision released on or after 28
July 2027. The architecture is shrinking what a server may ask for, not expanding
it.
Who trusts whom
The architecture has a single source of authority, and it is the user. The user consents to the host, the host applies that consent across its clients, and nothing below carries permission of its own.
What travels in the other direction is data, not orders. A tool’s description, its argument schema and the result of a call all enter the model’s context and steer what it decides to do next. The specification is blunt about the weight of that: clients MUST consider tool annotations untrusted unless they come from trusted servers1. That word covers everything the server writes.
The trouble is that a boundary only holds if somebody is standing on it. A 2026 study mined 1,723 MCP-consuming applications from GitHub and measured exactly this point: 90.8% do logging and 77.2% offer enable/disable controls for servers, but only 37.2% gate tool execution behind a blocking approval step2. In most of the applications studied, the model can invoke any enabled tool unconditionally. The policy layer exists in the design and sits empty in the implementation.
On the server side the evidence rhymes. A 2025 study analysed 67,057 servers across six public registries and described a two-stage attack surface: weak vetting at registry level lets an adversarial server reach a host, and after integration attacker-controlled tool metadata shapes the model’s reasoning and induces operations the host executes without independent verification3. The authors’ scanner flagged 833 vulnerable servers and 18 with suspicious descriptions. Another study that year evaluated 1,899 open-source servers and found 7.2% with general vulnerabilities and 5.5% exhibiting MCP-specific tool poisoning4.
Where the architecture breaks
Isolating servers does not isolate context. Each server is blind to the others, but all of them write into the same model context. A result returned by server A can carry text that leads the model to call a tool on server B. The concrete path is mundane: a comment on a ticket, a line in a README, the description field of a database row. None of it gets validated, because it is legitimate content from the source system and the server is doing its job by returning it. The host is where that could be stopped, and it is exactly where 62.8% of the measured applications have nothing2.
Policy lives in the host, and the host belongs to someone else. If you write a server, you do not control whether human approval exists, whether calls are logged, or whether the user sees the arguments before they go out. You can recommend it in the README. That is the whole of it.
Statelessness pushed state into the arguments. With no protocol session, a server that needs to relate calls returns an explicit handle and takes it back as an ordinary argument. The specification names the risk: for unauthenticated servers the handle is necessarily a bearer token and needs sufficient entropy and a bounded lifetime1. A sequential identifier there turns into enumeration of somebody else’s session.
One client per server multiplies processes. Ten stdio servers are ten live subprocesses on the user’s machine, each with its own memory footprint and its own life cycle to manage. The 2025 threat model that split a server’s life cycle into four phases exists because operating those pieces is its own problem, separate from the protocol5.
How to build without regretting it
- Decide early whether you are writing a host or a server. The two halves have nearly disjoint responsibilities, and most of the confusion comes from trying to hold both in one head.
- If you are the host, write name disambiguation before the second server.
Prefix with an identifier you control, not with
serverInfo.name. - If you are the host, put a blocking approval in front of every writing tool. It is the one item on this list the data says almost nobody does.
- If you are the server, write resumable logic from the start. With no session, in-memory state between calls only works with a single replica, and you will find that out in production.
- Probe with
server/discoverand handle all three outcomes. Falling back toinitializeon any old error code is the most likely interoperability bug of the second half of 2026. - Treat everything a server writes as text of unknown origin. Descriptions, schemas, results, annotations. The specification already classifies it that way.
Step 3 is what separates the architecture as drawn from the architecture as built, and the numbers available say nearly two thirds of applications stop short of it.
Footnotes
-
The architecture page of revision
2026-07-28is the source for the three roles, the 1:1 client-server relationship, the design principles, the capability discovery model, the compatibility rules against the old handshake, and the classification of tool annotations as untrusted. Verified on 30 July 2026. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 -
Majeed et al. (2026) mined 1,723 MCP-consuming applications from GitHub: 85.2% configure servers through files, 81.1% use an official SDK, 90.8% log, 77.2% allow enabling and disabling servers, and 37.2% require a blocking approval before executing a tool. ↩ ↩2
-
Li and Gao (2025) analysed 67,057 servers across six public registries and described a two-stage attack — registry entry followed by invocation manipulation through metadata — identifying 833 vulnerable servers and 18 with suspicious descriptions. ↩
-
Hasan et al. (2025) evaluated 1,899 open-source MCP servers: 7.2% with general vulnerabilities, 5.5% with MCP-specific tool poisoning and 66% with code smells. ↩
-
Hou et al. (2025) decomposed the life cycle of an MCP server into four phases and derived a threat taxonomy by attacker type from it. ↩
Frequently asked questions
- What is the host in MCP?
- The host is the application you open: the editor, the chat assistant, the agent you wrote. It creates and tears down clients, holds the whole conversation, decides which servers are connected, and is the only place where the user's authorization exists. No server sees that layer.
- What is the difference between an MCP client and server?
- The client lives inside the host and talks to exactly one server, translating between the protocol and the application. The server is a separate process exposing the tools, resources and prompts of one system. The relationship is 1:1, and that is what keeps servers isolated from each other.
- What is an example of an MCP client?
- The component inside VS Code that talks to the filesystem server you added to your configuration. It never appears in the interface: what you see is the host. Connect three servers to that editor and three clients are running, each isolated from the other two.
- How many clients can one host have?
- One per connected server, with no limit set by the specification. The practical cost is not the client count but what each server injects into the model's context: every advertised tool becomes a declaration in the prompt on every call. Ten connected servers cost context before anyone asks anything.
- Does MCP still have an initialization handshake?
- Not in revision 2026-07-28, which made the protocol stateless. The initialize and notifications/initialized pair is gone, and every request now carries its own protocol version and client capabilities in _meta. What remains is server/discover, an optional call returning the server's versions and capabilities.
- Can an MCP server see the whole conversation?
- No, and that is an explicit design principle in the specification. The full history stays with the host, and each server receives only what a given call needs. A server also cannot see what another server received or returned. Aggregation happens in the host, and only there.
References
- Agentic AI Foundation. Model Context Protocol specification, revision 2026-07-28 — Architecture (2026)
- Majeed, M. H. A., Mahmoud, M., Nadi, S.. An Empirical Study of Model Context Protocol Applications (2026)arXiv:2607.25635
- Li, X., Gao, X.. A First Look at the Security Issues in the Model Context Protocol Ecosystem (2025)arXiv:2510.16558
- Hasan, M. M. et al.. Model Context Protocol (MCP) at First Glance: Studying the Security and Maintainability of MCP Servers (2025)arXiv:2506.13538
- Hou, X. et al.. Model Context Protocol (MCP): Landscape, Security Threats, and Future Research Directions (2025)arXiv:2503.23278