Lifecycle, Capabilities & Transports
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:
- Walk the full connection lifecycle and say what each phase guarantees.
- Explain capability negotiation and why it lets the protocol evolve without breaking peers.
- Explain protocol version negotiation and what happens on mismatch.
- 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:
- Client →
initializerequest. The client states the protocol version it wants, the capabilities it supports (sampling, roots, elicitation…), andclientInfo(name/version). - Server →
initializeresponse. The server returns the protocol version it will use, its capabilities (tools, resources, prompts, and sub-features likelistChanged), andserverInfo. - Client →
notifications/initialized. A one-way signal: “handshake acknowledged, let’s operate.” (It’s a notification — recall Module 2: noid, 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:
- 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 advertisedresources.subscribe. No assumptions — everything is explicit. - Unknown capabilities are simply not negotiated → forward and backward compatibility. When
2025-11-25introduced 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
protocolVersionininitialize. - 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-Versionheader 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.”
| stdio | Streamable HTTP | HTTP+SSE (deprecated) | |
|---|---|---|---|
| Location | Local subprocess | Remote / networked | Remote / networked |
| Endpoints | stdin/stdout pipe | One HTTP endpoint | Two (GET SSE + POST) |
| Streaming | n/a (pipe) | On-demand via SSE | Always-open SSE required |
| Trust boundary | OS / process | Network → OAuth 2.1 | Network |
| Scales horizontally | n/a | Yes (session id) | Poorly (sticky SSE) |
| Status | Current | Current, preferred | Superseded (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
initializeto negotiate a protocol version and the intersection of their capabilities, thennotifications/initializedopens 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
- 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.
- 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).
- Version mismatch: write the
initializerequest/response pair for the case where the client asks for2025-11-25but the server only supports2025-06-18. What does the server put in its response, and what are the client’s two options? - Capability reasoning: a server advertises
{ "tools": {} }(nolistChanged). 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/initializedmarked 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; advertisingtools: { listChanged: true }(and emittingnotifications/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 promisingnotifications/.../list_changedwhen 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 viaMCP-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.