Module 03

Lifecycle, Capabilities & Transports

Building Systems Model Context Protocol (MCP)

Module 3 — Lifecycle, Capabilities & Transports

Phase 1 · Foundations (the capstone of Phase 1). Module 2 gave you the messages. This module gives you the envelope around the session: how a connection is born, negotiates what each side can do, lives, and dies — and the physical pipes (transports) those messages travel through. Crucially, the transport you choose in this module is what you’ll secure with OAuth in Phase 3, so pay special attention to Streamable HTTP.

By the end you can:

  1. Walk the full connection lifecycle and say what each phase guarantees.
  2. Explain capability negotiation and why it lets the protocol evolve without breaking peers.
  3. Explain protocol version negotiation and what happens on mismatch.
  4. Choose correctly between stdio and Streamable HTTP, and explain the deprecated HTTP+SSE transport it replaced (a near-certain design-review question).

1. The lifecycle: birth → operation → death

An MCP session has three phases. The discipline of separating them is what makes connections predictable.

Phase A — Initialization (the handshake)

Nothing else may happen until this completes. It’s a strict three-step:

  1. Client → initialize request. The client states the protocol version it wants, the capabilities it supports (sampling, roots, elicitation…), and clientInfo (name/version).
  2. Server → initialize response. The server returns the protocol version it will use, its capabilities (tools, resources, prompts, and sub-features like listChanged), and serverInfo.
  3. Client → notifications/initialized. A one-way signal: “handshake acknowledged, let’s operate.” (It’s a notification — recall Module 2: no id, no reply.)

The rule the spec is strict about: before initialization completes, the client should send only initialize (and may send ping), and the server should send only the initialize response (and may send ping/logging). No tools/call before initialized. This guarantees both sides know the negotiated version and capability set before any real work.

Phase B — Operation

The open-ended middle. Now the negotiated capabilities are in force and either side sends the requests and notifications it’s allowed to: tools/list, tools/call, resources/read, prompts/get, progress and listChanged notifications, ping, etc. Both sides must respect what was negotiated — a client must not call tools/* if the server never advertised a tools capability.

Phase C — Shutdown

The connection ends. MCP keeps this deliberately simple and transport-specific rather than defining shutdown messages:

  • stdio: the client closes the child process’s stdin and the server exits (with escalation to terminate/kill if needed). Closing the pipe is the shutdown signal.
  • HTTP: there’s no long-lived process to kill; shutdown is just the end of the HTTP connection(s)/session. The client stops issuing requests and/or closes the stream.
   CLIENT                              SERVER
     │  ── initialize (id:1) ───────────▶ │   Phase A
     │  ◀── initialize result (id:1) ──── │   (handshake)
     │  ── notifications/initialized ───▶ │
     │                                    │
     │  ── tools/list (id:2) ───────────▶ │   Phase B
     │  ◀── result (id:2) ─────────────── │   (operation)
     │  ── tools/call (id:3) ───────────▶ │
     │  ◀── result (id:3) ─────────────── │
     │           ...                      │
     │  ── (close transport) ───────────▶ │   Phase C
     │                                    │   (shutdown)

2. Capability negotiation — why MCP can evolve

During the handshake, each side advertises only what it supports, as a structured capabilities object. Examples:

  • A server might advertise { "tools": { "listChanged": true }, "resources": { "subscribe": true } } — “I offer tools, and I’ll notify you when my tool list changes; I offer resources, and you can subscribe to updates.”
  • A client might advertise { "sampling": {}, "roots": { "listChanged": true }, "elicitation": {} } — “I can run model completions for you, I’ll expose roots and tell you when they change, and I can prompt the user for input.”

Two consequences worth internalizing:

  1. Peers use the intersection. A capability only works if both relevant sides support it. A server that wants sampling can only use it if the client advertised sampling. A client only subscribes to resource updates if the server advertised resources.subscribe. No assumptions — everything is explicit.
  2. Unknown capabilities are simply not negotiated → forward and backward compatibility. When 2025-11-25 introduced new things (the experimental Tasks primitive, sampling-with-tools, URL-mode elicitation), older peers that don’t understand them just… don’t advertise or request them. Nothing breaks. This is the mechanism that lets MCP add features over time without a flag day. It’s the same reason capability negotiation, not a hardcoded feature list, is the right design.

Sub-capabilities like listChanged are a recurring pattern: they let a side say “this set is dynamic and I’ll emit a notifications/.../list_changed when it updates” (e.g. notifications/tools/list_changed). A host that sees this knows to re-fetch rather than cache forever.


3. Protocol version negotiation

MCP versions are date strings: 2024-11-05 (launch), 2025-03-26, 2025-06-18, 2025-11-25 (current baseline), with 2026-07-28 in release-candidate. Negotiation:

  • The client proposes its preferred protocolVersion in initialize.
  • If the server supports it, it echoes that version. If not, the server responds with a version it does support (typically its latest). The client then decides whether it can speak that version; if not, it disconnects.
  • Over HTTP, after initialization clients send a MCP-Protocol-Version header on subsequent requests so the server can enforce/branch on the agreed version per request.

The takeaway: version mismatch is resolved at connect time, explicitly, not discovered halfway through. A client and server that can’t find a common version simply don’t establish a session — far better than silent incompatibility.


4. Transports — the physical pipe

The lifecycle and messages are transport-independent; a transport is just how bytes move. MCP defines two standard transports. Choosing correctly is an architecture decision.

stdio — local subprocess

The host launches the server as a child process and talks over stdin/stdout; each JSON-RPC message is newline-delimited. stderr is free for logging.

  • Use when: the server is local — a filesystem tool, a git tool, a desktop integration, anything running on the same machine as the host.
  • Strengths: dead simple, no network, lowest latency, and trust is process-local — no network authentication needed because the host spawned the process and controls its environment (env vars, working dir, OS permissions). This is why local servers in Phase 3 don’t do OAuth: the boundary is the OS, not a token.
  • Constraints: one host process per server instance; not reachable across a network; lifecycle tied to the subprocess.

Streamable HTTP — the modern remote transport

A single HTTP endpoint that the client POSTs JSON-RPC requests to. The server can reply with either a normal JSON response or an SSE (Server-Sent Events) stream when it needs to push multiple messages (progress notifications, streamed results, server-initiated requests) before the final result. The server may also use a session id (via an Mcp-Session-Id header) to correlate a logical session across requests.

  • Use when: the server is remote — a hosted SaaS tool, a shared internal service, anything reached over a network or by multiple clients.
  • Strengths: firewall/proxy-friendly (it’s ordinary HTTP), supports streaming when needed without requiring a permanently open socket, scales behind standard web infrastructure (load balancers, gateways), and — critically — this is the transport MCP authorization (OAuth 2.1) applies to. Every token, every 401, every Protected-Resource-Metadata discovery flow in Phase 3 rides on Streamable HTTP.
  • Constraints: now you have a network trust boundary → you must think about authn/authz, TLS, and the threats in Module 8. Remote power, remote responsibility.

HTTP+SSE — the deprecated predecessor (know this cold)

The original remote transport (from the 2024-11-05 launch) used two endpoints: a long-lived SSE connection on a GET endpoint for server→client messages, plus a separate POST endpoint for client→server messages. It worked but had real problems: it required a persistent open SSE connection (costly, awkward behind some proxies, bad for serverless/horizontally-scaled deployments where the next request might hit a different instance), and the two-endpoint split complicated session correlation and resumption.

Streamable HTTP (introduced in 2025-03-26) replaced it: collapse to one endpoint, make streaming optional and on-demand (only stream when there’s something to stream), and add session ids for correlation. This makes it stateless-friendly and scalable. You may still encounter old servers on HTTP+SSE, but for anything new, Streamable HTTP is the answer.

The design-review soundbite: “Local? stdio — trust is the OS, no network auth. Remote? Streamable HTTP — one endpoint, stream only when needed, and it’s the surface OAuth 2.1 secures. It superseded the old two-endpoint HTTP+SSE transport, which required an always-open SSE connection and didn’t scale horizontally.”

stdioStreamable HTTPHTTP+SSE (deprecated)
LocationLocal subprocessRemote / networkedRemote / networked
Endpointsstdin/stdout pipeOne HTTP endpointTwo (GET SSE + POST)
Streamingn/a (pipe)On-demand via SSEAlways-open SSE required
Trust boundaryOS / processNetwork → OAuth 2.1Network
Scales horizontallyn/aYes (session id)Poorly (sticky SSE)
StatusCurrentCurrent, preferredSuperseded (2025-03-26)

5. Putting Phase 1 together

You can now narrate a full connection without hand-waving:

A host spawns or connects a client to a server over a transport (stdio if local, Streamable HTTP if remote). They exchange initialize to negotiate a protocol version and the intersection of their capabilities, then notifications/initialized opens the operation phase. From there it’s the JSON-RPC messages of Module 2 — tools/list, tools/call, results with the protocol-vs-tool-error split — until the transport closes and the session ends. Capability negotiation is what lets new primitives (Tasks, sampling-with-tools) land without breaking older peers.

That sentence is the whole of Phase 1. If it flows naturally, the foundation is solid.


6. Milestone exercise

  1. Draw the lifecycle as a sequence diagram from memory: the three handshake steps, a couple of operation messages, and shutdown. Mark which message is a notification.
  2. Annotate the transport: redraw it twice — once assuming stdio, once assuming Streamable HTTP. For each, label where shutdown happens and where a network trust boundary exists (trick: stdio has none).
  3. Version mismatch: write the initialize request/response pair for the case where the client asks for 2025-11-25 but the server only supports 2025-06-18. What does the server put in its response, and what are the client’s two options?
  4. Capability reasoning: a server advertises { "tools": {} } (no listChanged). The client caches the tool list. Later the server adds a tool. What goes wrong, and which sub-capability would have prevented it?

Self-check:

  • #1: three handshake steps in order; notifications/initialized marked as a notification (no id); operation before shutdown.
  • #2: stdio shutdown = closing the subprocess’s stdin / process exit, no network trust boundary; HTTP shutdown = closing the HTTP session/stream, trust boundary at the network → OAuth.
  • #3: server responds with "protocolVersion": "2025-06-18" (a version it supports); client either proceeds in that version or disconnects.
  • #4: the client serves a stale tool list because the server never promised to emit list_changed; advertising tools: { listChanged: true } (and emitting notifications/tools/list_changed) fixes it.

Clear all four and Phase 1 is complete — you understand MCP’s anatomy end to end. Next is Phase 2 · Module 4 — Server primitives (Tools, Resources, Prompts), where we start designing real capabilities and meet the model/app/user control triad.


Quick reference — Module 3 glossary

  • Lifecycle — Initialization (initialize ↔ result → notifications/initialized) → Operation → Shutdown.
  • Capability negotiation — each side advertises supported features; peers use the intersection; unknown features are ignored → forward/backward compatible.
  • listChanged — sub-capability promising notifications/.../list_changed when a dynamic list updates.
  • Protocol version — date string (e.g. 2025-11-25); negotiated at connect; mismatch → server offers a supported version or the client disconnects; carried per-request over HTTP via MCP-Protocol-Version.
  • stdio — local subprocess transport over stdin/stdout; trust is OS-level; no network auth.
  • Streamable HTTP — modern remote transport; single endpoint, optional SSE streaming, session ids; the surface OAuth 2.1 secures.
  • HTTP+SSE — deprecated two-endpoint remote transport (always-open SSE); replaced by Streamable HTTP in 2025-03-26.