How it is built
Architecture overview
An MCP host is the AI application that connects a language model to many tool servers at once. It reads a list of servers from configuration, starts or dials each one, and keeps one client object per server, each with its own connection and its own negotiated capabilities (source: MCP architecture overview). It merges what the servers offer into one catalogue, shows the model a selection of it, routes each tool call the model makes back to the right server, and puts the result into the next prompt. Everything a server sends is outside the host's trust boundary: tool descriptions, results and prompts are text that will be read by a model that holds other tools.
The one idea that makes it manageable: the host owns the policy, the servers own the work. The protocol only moves messages. Which servers to trust, which tools the model sees, which calls need a human, how large a result may be, when to reconnect: all of that is host code, and each of those decisions is a safety or cost decision.
Parts:
- Connection manager — reads configuration, one client per server, connect, reconnect, shut down — Model Context Protocol (no page yet), JSON-RPC (no page yet), Process supervision, Exponential backoff with jitter, Graceful draining
- Transport clients — spawn a local process, or call a remote endpoint with streamed replies — Server-sent events
- Tool registry — merges, names, validates and selects tools from all servers — JSON Schema validation, Tool calling, Tool retrieval (selecting tools for a request),
embedding-inference(no page yet) - Model loop and context — the cycle of prompt, tool call, result; keeps results within budget — agent-loop (no page yet), Context window management
- Credentials — OAuth for remote servers, stored tokens, server secrets — OAuth 2 and OpenID Connect, Secrets management, Hardware-backed keystore (no page yet)
- Trust and confinement — untrusted text, local servers as code with the user's rights — Prompt injection defense, agent-sandboxing (no page yet), Least privilege and isolation, Capability-based security (object capabilities), Process sandboxing, Mutual TLS
- Hosted-host operations — for a host run as a service: durable state, latency, correctness of the client state machine — Point-in-time recovery, Primary-backup replication, HDR histogram (no page yet), Deterministic simulation testing, Coverage-guided fuzzing
config (user / project / org) ┌──────────────── host process ─────────────────┐
servers: A (stdio) B (https) C (https)│ connection manager ── client A ── spawn ──► server A (child)
│ │ reconnect, backoff ─ client B ── HTTPS ──► server B (remote)
│ ▼ client C ── HTTPS ──► server C (remote)
│ tool registry (A.x, B.y, C.z) ◄── tools/list per client
│ │ select + schema
user ◄── confirm ───────────────────► │ model loop ──► LLM API ──► tool_use ─► route ─► tools/call
│ ▲ result: size cap, provenance tag, append to context
└───────────────────────────────────────────────┘
How the parts fit together
Connection manager
The host holds a list of server definitions. In Claude Desktop that is a JSON file (claude_desktop_config.json) with an mcpServers map from a name to a command and args (source: MCP docs, Connect to local MCP servers). In Claude Code the same idea has three scopes: local (private to you in one project), project (a .mcp.json in the repository, shared by version control) and user (all your projects) (source: Claude Code docs, MCP). The scopes encode trust: a project file arrives from a cloned repository, so Claude Code does not connect a project server until the user has approved it, and an untrusted folder cannot approve its own servers.
Each definition gets one client. A stdio server is a child process serving one client; a remote server serves many. The lifecycle is the protocol's: under revision 2025-11-25 a client sends initialize with its version and capabilities, the server answers with its own, then notifications/initialized; under 2026-07-28 the handshake is gone and each request carries the version and capabilities, with server/discover available to learn them up front (source: MCP spec lifecycle and changelog). A host that talks to servers of both eras detects the era and falls back (the 2026-07-28 spec has a compatibility matrix).
Connections fail, and the manager is where that is handled. Exponential backoff with jitter is the rule: Claude Code retries a first HTTP connection up to three times on transient errors (5xx, refused, timeout) but not on authentication or not-found errors, which need a configuration change; it reconnects a dropped remote server up to five times starting at one second and doubling each time, then marks it failed and tells the user; stdio servers are local processes and are not reconnected automatically (source: Claude Code docs, MCP). Jitter matters for a hosted host: thousands of sessions reconnecting in step after a restart is a self-inflicted outage.
For a stdio server the manager is also a parent process, so Process supervision applies: reap exits, bound restarts, send SIGTERM then SIGKILL at the end. The spec's shutdown order for stdio is exactly that: close the child's stdin, wait, SIGTERM, then SIGKILL. Graceful draining is the host-side version: before exiting, let in-flight calls finish or cancel them, so a half-done write is not left behind.
Transport clients
The two standard transports need two kinds of client. The stdio client writes newline-delimited JSON to the child's stdin and reads the child's stdout; it must treat anything on stderr as logging, not as an error (source: spec, Transports). The Streamable HTTP client sends every message as a POST, with an Accept header naming both application/json and text/event-stream, and must handle either reply: one JSON object, or an event stream (Server-sent events) carrying progress and ending with the response. Under 2025-11-25 it may also open a GET stream and resume with Last-Event-ID; under 2026-07-28 both are gone, and a stream that breaks loses the request, which the client re-issues as a new one. The client times out every request and cancels when it gives up.
Tool registry
Each client reports its tools with tools/list (paginated with a cursor). The registry merges them into one catalogue and has to solve four problems the protocol leaves open:
- Names. Two servers may both offer
search. The registry namespaces by server (Claude Code usesmcp__<server>__<tool>) and enforces the model API's name rules. - Validation. The registry keeps each tool's
inputSchema, and checks the model's arguments against it (JSON Schema validation) before sending the call. A malformed call is answered locally with an error the model can fix, keeping bad input away from the server. - Selection. Every tool definition costs tokens on every request and a model chooses worse as the menu grows. Tool retrieval is the remedy: keep descriptions in an index, rank them against the request, expose the top few. Claude Code's tool search does this by loading only tool names and server instructions at session start and fetching definitions on demand; it cuts descriptions at 4,096 characters for upfront tools and 16,384 for deferred ones, so a server author should put the words a model would search for first (source: Claude Code docs, MCP tool search). Word ranking is cheap; Embeddings help when the user's wording differs from the description.
- Change. A server that declared
listChangednotifies when its tools change. Editing the exposed list mid-conversation breaks the prompt cache, so apply changes at turn boundaries and keep the order stable.
Model loop and context
The loop is Tool calling around the model: send the conversation and exposed tools, receive text or tool_use blocks, run the calls, return one tool_result per call, repeat (the agent-loop (no page yet), no page yet). MCP changes one thing here: results are produced by programs the host does not control, so their size and shape are unbounded. Context window management therefore starts at the registry boundary. Claude Code warns above 10,000 tokens per result, caps output at 25,000 by default (MAX_MCP_OUTPUT_TOKENS), saves text over 50,000 characters to a file, and moves a call still running after two minutes to a background task (source: Claude Code docs, MCP).
The spec adds interaction rules a host must implement. Hosts SHOULD show which tools are exposed to the model, show when one is invoked, and let a human deny calls. Elicitation lets a server ask the user for input as a form the host renders from a schema, or as a URL the user opens in a browser so secrets never pass through the host. Roots and sampling (a server asking the host's model for a completion) are deprecated as of 2026-07-28 and new hosts should not add them (source: MCP changelog).
Credentials
Remote servers use OAuth 2 and OpenID Connect as the MCP authorization spec defines it. The host is the OAuth client: it finds the authorization server from the server's protected-resource metadata (RFC 9728, usually announced in the WWW-Authenticate header of a 401), uses the authorization-code flow with PKCE, sends the resource parameter (RFC 8707) so the token is bound to that one server, and must never send a server a token that was not issued by that server's authorization server. A request that fails with insufficient scope triggers a step-up: request more scope and retry. Under 2026-07-28 the host must also key stored client credentials by the authorization server's issuer, not reuse them with another, and Client ID Metadata Documents are preferred over dynamic client registration (source: MCP spec, Authorization).
Where tokens live is the host's job. A refresh token in a plain config file is a long-lived credential; keep it in the operating system's store, ideally hardware-backed (Hardware-backed keystore, no page yet), and follow Secrets management for the server definitions themselves: environment variable expansion in a shared .mcp.json lets the file name a variable instead of containing the key (Claude Code supports this). A stdio server gets its credentials from the environment the host sets for it, not from OAuth.
Trust and confinement
A local stdio server is a program started with the user's rights: installing one is running code. Remote servers are not code on the machine but they supply text to the model. The host's defences therefore split in two.
Against what the servers say. Tool descriptions and results are untrusted input. A hostile server can put instructions in a description or result, or change a description after the user approved it; a result from one server can steer the model into calling a tool from another. Prompt injection defense is the answer, and it is structural: tag every value with the server it came from, do not let text from one server decide the arguments of a sensitive call to another without a check, and keep a human confirmation on actions with side effects. The spec says tool annotations must be treated as untrusted unless the server is trusted. Risk peaks when one session holds private data, untrusted content and a way to send data out; limiting that combination beats filtering text.
Against what a server can do. Run local servers under Process sandboxing with only the folders and network they need (agent-sandboxing (no page yet), no page yet, is the agent side) and Least privilege and isolation. Hand a server narrow handles, not ambient authority (Capability-based security): the filesystem server in the docs receives the directories to expose as arguments, which is this pattern. For hops between hosted components, Mutual TLS gives each side a verified identity.
Hosted-host operations
A desktop host keeps state in files. A host run as a service (a web assistant with connectors) also keeps per-user connections, tokens and conversations, so the generic blocks apply: Point-in-time recovery and Primary-backup replication for that store; an HDR histogram (no page yet) of tool-call latency per server so one slow server is visible in the tail; Deterministic simulation testing of the client state machine (connect, drop, reconnect, cancel, re-authorise) under injected faults; and Coverage-guided fuzzing of the message parsers, because every byte from a server is untrusted.
Data and control flow
Startup and one tool call:
- Read configuration from all scopes; ask approval for project servers; resolve duplicates by precedence.
- In parallel, spawn each stdio server and dial each remote one; one failure must not block the others.
- For each connected client, run the handshake (2025-11-25) or
server/discover(2026-07-28), thentools/list, following cursors; read the server's instructions text. - Build the registry with namespaced names. With tool search on, the model sees names and instructions only; otherwise full definitions.
- The user writes a request; the loop sends conversation plus the selected tools to the model API. Time: dominated by the model.
- The model emits
tool_useforserver_b.search. The registry validates arguments against the schema; the policy layer checks the allow list and, if needed, asks the user. - The client for server B sends
tools/call(a POST with a bearer token). A 401 starts the OAuth flow; a timeout sends a cancellation and returns an error result. - The result is capped in size, tagged with its origin, appended as a
tool_result; the loop calls the model again.
Server needs the user (2026-07-28): the server returns an InputRequiredResult with an elicitation request; the host renders a form or opens a URL and retries the same call with the answer attached (source: MCP changelog).
Scaling
In the order the limits arrive:
- Context cost of tools. Add servers and the tool list eats the window and degrades choice first. Lever: tool retrieval or deferred loading, stable ordering for the cache. This bites long before any CPU limit.
- Result size. Large results bury the context. Lever: caps, write-to-file, ask servers for paging, clear old results (Context window management).
- Latency of one slow server. The model waits for the slowest call in a turn. Lever: per-server timeouts, background tasks for long calls, parallel calls to different servers.
- Connection count in a hosted host: one client per server per session means many idle connections. Lever: pool per user (stateless HTTP in 2026-07-28 helps), connect lazily on first use.
- Refresh storms after an outage. Lever: jittered backoff, one refresh in flight per credential.
Failure modes
| Failure | Symptom | Cause | Fix |
|---|---|---|---|
| Server fails silently | the model does not know a tool exists and answers without it | connection failed; host never told the model | surface failed servers to the model and the user; /mcp-style status |
| Reconnect storm | a restarted server is hit by all clients at once | fixed retry interval | exponential backoff with jitter, cap on attempts |
| Tool-name clash | wrong server called, or request rejected | two servers share a tool name, name too long | namespace by server; validate names |
| Context flood | budget spent in one call, answers degrade | unbounded tool result | size cap, file offload, summarisation |
| Tool poisoning | model leaks data or calls an unrelated tool | instructions hidden in a tool description or result | provenance tags, confirm side effects, review before approving a server |
| Description drift | a tool behaves differently after approval | server changed its tool list or description | re-prompt on change, pin or hash definitions |
| Token sent to the wrong server | data exposed to a third party | token reused outside its audience | bind with the resource parameter; key credentials by server and issuer |
| Cache invalidation | cost jumps after adding a server mid-chat | exposed tool list changed between turns | apply tool changes at turn boundaries, keep order stable |
| Orphan child process | stdio server still running after the host exits | host died without the shutdown sequence | close stdin, SIGTERM, SIGKILL; supervise children |
Real implementations
| name | what it is | repo | licence | notes |
|---|---|---|---|---|
| Claude Desktop | desktop assistant, local servers from a JSON config file | none public; setup guide | proprietary | example host in the MCP local-server guide |
| Claude Code | terminal coding agent; three config scopes, OAuth, tool search, output limits | anthropics/claude-code | proprietary (all rights reserved) | docs: MCP |
| Visual Studio Code | editor with MCP support in its chat features | microsoft/vscode | MIT | named as an example host in the MCP architecture docs |
| Goose | open-source local agent with extensions over MCP | block/goose | Apache-2.0 | README: extensions via MCP |
| Cline | coding agent in the editor with MCP servers | cline/cline | Apache-2.0 | |
| LibreChat | self-hosted chat UI with MCP servers and OAuth sessions | danny-avila/LibreChat | MIT | multi-user, hosted-style |
| OpenAI Agents SDK | agent library that can attach MCP servers | openai/openai-agents-python | MIT | MCP docs |
The alternative: bake the few tools you need into the application as plain Tool calling functions. That removes third-party trust and suits a small, fixed tool set. Choose MCP when users bring their own tools or many applications share them.
Sources
- MCP specification: 2026-07-28 changelog, 2025-11-25 Lifecycle, Transports, Tools, Authorization.
- MCP docs: Architecture overview, Client concepts, Security Best Practices, Connect to local MCP servers.
- Claude Code documentation, Connect Claude Code to tools via MCP (scopes, reconnection, tool search, output limits, background calls).