Skip to content
Model Context Protocol (MCP)Architecture

Mid · Core path · 7 chapters

Architecture

Hosts, clients and servers; connection lifecycle; capability negotiation on initialize

Most MCP failures are boundary and lifecycle mistakes, so a plausible tool integration can still fail before its first useful call.

Read these first

What you will know

  • Build a minimal host with a client connection to a server, and annotate which process owns lifecycle, transport, and capabilities

    Misplacing state or responsibility creates duplicated connections, unusable capabilities, and shutdown leaks.

  • Walk through a connection from initialize to first tool call

    A valid exchange includes initialization, the response, the initialized notification, and only then discovery or invocation; ordering errors often look like tool failures.

  • Compare the capabilities advertised during initialize with the operations actually attempted, using a captured message fixture

    A client must not infer support from product identity, and unsupported features need an explicit fallback rather than an assumed method.

  • Read a protocol trace and identify the failed stage, citing the message that proves it

    Separating transport, initialization, capability discovery, and invocation failures prevents retries from masking the real fault.

  • Reproduce a capability mismatch and patch the client to degrade to a supported path

    Servers and clients evolve independently, so unknown methods, missing features, and version skew must produce controlled behavior instead of broken sessions.


Chapters

  1. 1

    Hosts, clients and servers

    A host is the application; a client is the connection object inside it, bound to exactly one server for exactly one session; a server is the separate process it connects to. A desktop app joined to four servers therefore has four clients, not one — and consent policy, whether the user sees a dialog before a destructive call, is the host's job across all four and never the server's. This chapter fixes the counts and the labels, then annotates which process owns lifecycle, which owns the transport, and which owns the negotiated capabilities.

  2. 2

    The initialized notification

    A valid opening is three messages, not two: initialize, the server's response, and the initialized notification that carries no id and gets no reply. A client that sends tools/list straight after the response is still inside initialization, and the rejection it gets back — `-32601`, naming a tool that plainly exists — looks exactly like a broken tool. Expect the symptom to be intermittent, appearing only in the first second after connect, and resist the server-side fix that answers early.

  3. 3

    Capability and version negotiation

    What a session may do is whatever both sides declared during initialize, not whatever the product on the other end is known to support: a well-known desktop host can ship with sampling switched off for your server, and the capabilities object arrives as `{}`. The protocol version in the response is the server's counter-offer rather than an echo, and a client that keeps sending newer message shapes after receiving an older one has ignored it. Negotiation happens once, and nothing in the protocol reopens it.

  4. 4

    Reading a protocol trace

    Transport, initialization, capability negotiation and invocation fail differently, and each leaves a distinguishable message. An initialize response carrying an empty capabilities object followed by resources/read answered with -32601 is not a missing handler; it is a method that never existed in this session. Exactly one of the four stages benefits from a retry, which is why a generic retry wrapper turns a diagnosable error into an indefinite spinner — and why the habit worth building is citing the message that proves the diagnosis.

  5. 5

    Degrading on the capability declaration

    A server can declare resources without subscribe or listChanged, and a client that assumes otherwise renders a spinner that never resolves. Feature-testing by calling subscribe and catching the error is the wrong shape twice: the contract was already negotiated at initialize, and the failure that actually hurts is a subscription accepted and never notified. Branch on the capability set at connect time, paint from an unconditional first read, and show the staleness window the degraded path costs.

  6. 6

    Closing an MCP session

    A session ends as deliberately as it opens, and the protocol has no message for it: teardown is a transport act, closing a child's stdin or issuing an HTTP DELETE. In-flight requests have to be cancelled or abandoned, subscriptions drop with the transport, a local server's child process has to be reaped, and the host has to remove that server's tools from the aggregated definitions it sends the provider on the next turn. Nothing enforces any of it, which is why the failure mode is a leak with no exception rather than an error.

  7. 7

    The state a connection carries

    Reusing one long-lived client object across two servers to avoid reconnect cost is a design many hosts reach for, and re-running initialize on it is half the work. Pending request ids, active subscriptions, the workspace roots the previous server was told about, the cached tool list and the id counter all survive the swap, so a late response from the old server can be matched to a request sent to the new one. The three failures surface on three timescales, and the protocol is not the expense the design was avoiding.

External resources