MCP Lifecycle Guide › Foundation
Foundation

The Three Stages

Every MCP connection that has ever existed moves through exactly three stages, in exactly this order. No exceptions.

Stage 1

Initialization

Client and server meet, agree on a version, and declare what they can do.

Stage 2 · this guide

Operation

The normal back-and-forth — discovering what's available, then calling things.

Stage 3

Shutdown

The connection ends, cleanly or otherwise.

🤝
Analogy — Meeting a Stranger

Initialization is the handshake and introductions. Operation is the actual conversation. Shutdown is saying goodbye and leaving the room — and just like real life, you can't skip straight to the conversation before anyone's been introduced.

"Every MCP connection that has ever existed followed exactly these three stages, in exactly this order. No exceptions."

Reference: the three-stage lifecycle is verbatim, current official specification language.
MCP Lifecycle Guide › Foundation
Foundation

Live Demo Setup

Claude Desktop's own native logging — corrected against a real captured log, no jq required.

ℹ️
What changed

Claude Desktop's log only shows a summary line per message (method, id, whether params/result are present) — not the full JSON body. An earlier jq-based version of this command assumed full JSON and failed. This version matches reality.

Add Filesystem to Claude Desktop:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/folder"]
    }
  }
}

Watch every connected server live (macOS):

tail -n0 -f "$HOME/Library/Logs/Claude"/mcp-server-*.log | awk '
/Message from client:/ {
  t = substr($1, 12, 8); srv = $2; gsub(/[\[\]]/, "", srv);
  sub(/^.*Message from client: /, ""); sub(/ \{ metadata:.*$/, "");
  printf "\033[1;33m%s [%s] CLIENT -> SERVER\033[0m  %s\n", t, srv, $0; next
}
/Message from server:/ {
  t = substr($1, 12, 8); srv = $2; gsub(/[\[\]]/, "", srv);
  sub(/^.*Message from server: /, ""); sub(/ \{ metadata:.*$/, "");
  printf "\033[1;36m%s [%s] SERVER -> CLIENT\033[0m  %s\n", t, srv, $0; next
}
'

No jq needed — pure awk, works with macOS's built-in version. Watches every connected server at once and tags each line with which one it came from.

🛠️
If the log file doesn't exist yet

That file is only created once Claude Desktop actually connects to a server with that exact name. Fully quit (Cmd+Q, not just closing the window) and reopen Claude Desktop after saving your config. The watch-mcp-logs.sh script in this course's files diagnoses this and waits automatically.

No log-tailing setup at all? MCP Inspector (npx @modelcontextprotocol/inspector) shows the same traffic in a browser, with full JSON bodies.

MCP Lifecycle Guide › Foundation
Foundation

Real Servers Reference

Servers you can actually connect to today, verified current.

Local (stdio)

ServerWhat it doesRun it
FilesystemRead/write files in allowed directoriesnpx -y @modelcontextprotocol/server-filesystem <dir>
EverythingAll 3 primitives — tools, resources, promptsnpx -y @modelcontextprotocol/server-everything
GitStatus, diff, log on a local repouvx mcp-server-git
MemoryPersistent knowledge-graph across sessionsnpx -y @modelcontextprotocol/server-memory
FetchFetch a URL, convert to markdownuvx mcp-server-fetch
TimeTimezone conversionuvx mcp-server-time

Remote (HTTP)

ServerURLAuth
DeepWikihttps://mcp.deepwiki.com/mcpNone — free, no signup
Semgrephttps://mcp.semgrep.ai/mcpNone for public surface

"DeepWiki publishes both the old SSE URL and the new Streamable HTTP URL for the same service — point at both, and you're watching the transport migration this course describes, live, on a real production system."

MCP Lifecycle Guide › Initialization
Initialization

JSON-RPC Basics

Every message — in either direction — shares one shape.

Client → Server · Request
{ "jsonrpc": "2.0", "id": 1, "method": "search_recipes", "params": { "tag": "quick" } }
Server → Client · Response
{ "jsonrpc": "2.0", "id": 1, "result": ["Weeknight Pasta", "Five-Minute Salsa"] }
Server → Client · Error (instead)
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32601, "message": "Method not found" } }
FieldWhat it does
jsonrpcAlways "2.0" — the shared format version
idMatches a response/error to the exact request
methodWhat's being asked for (requests only)
params / result / errorThe payload — exactly one, never both
  • Lightweight — plain JSON, human-readable at a glance
  • Transport-agnostic — same shape over stdio or HTTP
  • Two-way by design — either side can send a request
  • Notifications built in — no id, fires and expects nothing back
💡
Familiar comparison

Just like HTTP has 404 and 500, JSON-RPC has its own small set of standard codes — MCP inherits them wholesale rather than inventing new ones.

"JSON-RPC isn't MCP's invention — it's MCP borrowing a format that was already simple, already two-way, and already boring in the best possible sense."

Reference: JSON-RPC 2.0 as MCP's message format is current official documentation; the RecipeBox example is original.
MCP Lifecycle Guide › Initialization
Initialization

The Handshake

Three steps, every time — plus the two rules that protect them.

The client speaks first, sending exactly three things.

Client → Server · initialize request
{
  "jsonrpc": "2.0", "id": 1, "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": { "roots": { "listChanged": true } },
    "clientInfo": { "name": "RecipeBoxDesktopClient", "version": "1.0.0" }
  }
}

"This is the very first sentence of the entire relationship. Everything after this assumes both sides heard it correctly."

The server answers, matching the same id, with its own three things.

Server → Client · initialize response
{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "protocolVersion": "2025-11-25",
    "capabilities": { "tools": { "listChanged": true } },
    "serverInfo": { "name": "RecipeBox", "version": "1.0.0" }
  }
}

Same id: 1 — the matched reply.

The client seals the handshake with a notification — no reply expected.

Client → Server · initialized notification
{ "jsonrpc": "2.0", "method": "notifications/initialized" }

No id — an announcement, not a question.

"Three messages. One sentence each: 'here's who I am,' 'here's who I am back,' 'good, let's begin.' That's the entire handshake."

RuleIn plain terms
Client SHOULD NOT send anything but a ping before the server responds to initializeNo jumping ahead
Server SHOULD NOT send anything but a ping/log line before receiving initializedThe server waits for step 3

"Skip either rule and you're not talking to a stranger politely — you're talking over them."

Reference: verbatim, current official specification.
MCP Lifecycle Guide › Initialization
Initialization

Version Negotiation

What actually happens when client and server don't agree the first time.

Client asks for one version
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "...": "..." } }
Server can only offer an older one
{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "...": "..." } }

What the client does next:

SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2024-11-05"]

if server_version in SUPPORTED_PROTOCOL_VERSIONS:
    send_initialized_notification()   # proceed
else:
    disconnect()                      # no further communication

"Version negotiation isn't a retry loop. It's exactly one counter-offer, checked exactly once, with disconnect as the only fallback."

Reference: verbatim, current official specification.
MCP Lifecycle Guide › Initialization
Initialization

Capability Negotiation

Both sides declare exactly what they're each capable of — an honest menu, both ways.

CategoryCapabilityWhat it means
ClientrootsGrants server access to specific directories
ClientsamplingLets the server borrow the client's own AI model
ServertoolsExposes callable tools
ServerresourcesProvides readable resources
ServerpromptsOffers prompt templates
ServerloggingEmits structured log messages back
⚠️
Current note

roots, sampling, and logging are being formally deprecated in the newest protocol revision (SEP-2577).

A real, observed case: a client asks for something the server never declared.

Client asks anyway
{ "jsonrpc": "2.0", "id": 99, "method": "prompts/list" }
The honest answer
{ "jsonrpc": "2.0", "id": 99, "error": { "code": -32601, "message": "Method not found" } }

The server doesn't crash — it just says, correctly, "I never told you I could do that."

"Capability negotiation is both sides handing over an honest menu before anyone orders."

Reference: capability table verbatim, current official specification; the error case is grounded in the source transcript's own capture (paraphrased).
MCP Lifecycle Guide › Operation
Operation

Discovery & Calling

Discovery fires automatically the instant the handshake completes — before the user even asks a question.

Part 1 — Discovery

Client → Server
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
Server → Client
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [
  { "name": "list_recipes" }, { "name": "get_recipe" }, { "name": "search_recipes" }
]}}

Part 2 — Calling

Client → Server
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "search_recipes", "arguments": { "tag": "quick" } } }
Server → Client
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [{ "type": "text", "text": "Weeknight Pasta, Five-Minute Salsa" }] } }

"Discovery isn't something you triggered. It's something that already happened, quietly, the moment the handshake finished."

Reference: discovery/calling and auto-discovery behavior grounded in current official specification and the source transcript's live demonstration; RecipeBox examples original.
MCP Lifecycle Guide › Transport & Shutdown
Transport & Shutdown

Transport Layer

The road every message above actually travelled on.

  • Client launches the server as a subprocess
  • Server reads stdin, writes stdout — newline-delimited
  • stderr is free for logging
  • No separate auth layer — credentials from the environment

Practical proof, live:

python3 -c "
import subprocess, json
proc = subprocess.Popen(['python3','server.py'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
proc.stdin.write(json.dumps({'jsonrpc':'2.0','id':1,'method':'initialize',
    'params':{'protocolVersion':'2025-11-25','capabilities':{},'clientInfo':{'name':'x','version':'1'}}}) + '\n')
proc.stdin.flush()
print(proc.stdout.readline())
"

"There's no hidden magic in stdio. It's a subprocess, a pipe in, a pipe out, and a newline between messages."

  • One endpoint (commonly /mcp), POST + GET
  • Plain JSON or upgrades to SSE for long calls
  • Optional Mcp-Session-Id header
  • Replaces the older, deprecated HTTP+SSE transport
🌐
Live proof, real server

Point at DeepWiki's /mcp and /sse — same service, both transports, side by side.

"Streamable HTTP is the same conversation as stdio, wearing a different coat."

Reference: both transports verbatim, current official specification.
MCP Lifecycle Guide › Transport & Shutdown
Transport & Shutdown

Shutdown

The one phase with no message format of its own.

🔑
Key fact

No JSON-RPC message is exchanged during shutdown at all. The entire responsibility shifts to the transport layer.

TransportClient-initiated (common)Server-initiated (rare)
stdioClose stdin, wait; SIGTERM if it doesn't; SIGKILL as last resortServer closes its output stream and exits
Streamable HTTPClose the HTTP connectionServer closes unexpectedly — client should reconnect gracefully
🚪
SIGTERM vs SIGKILL

SIGTERM is a polite knock — "please finish up and leave." SIGKILL is the door being forced open — no chance to clean up.

"Shutdown has no goodbye message of its own. The transport closing IS the goodbye."

Reference: verbatim, current official specification; the resilience note grounded in the source transcript (paraphrased).
MCP Lifecycle Guide › Special Cases
Special Cases

Pings

Checking whether anyone's still there.

Client → Server
{ "jsonrpc": "2.0", "id": 42, "method": "ping" }
Server → Client
{ "jsonrpc": "2.0", "id": 42, "result": {} }
🔥
Why it matters beyond aliveness

A quiet connection can get silently dropped by a firewall or proxy that assumes "no traffic" means "not needed." Periodic pings keep that from happening.

"A ping carries no information at all. That's the whole point — a knock on the door, not a conversation."

Reference: verbatim, current official specification; firewall rationale grounded in the source transcript (paraphrased).
MCP Lifecycle Guide › Special Cases
Special Cases

Error Handling

When it happens, and what it looks like.

  1. Protocol version mismatch during initialization
  2. Calling a method never negotiated
  3. Invalid arguments to a real tool
  4. An internal failure on the server's side
  5. A timeout being exceeded
  6. A syntactically malformed JSON-RPC message
Example
{ "jsonrpc": "2.0", "id": 4, "error": {
  "code": -32602, "message": "Unsupported protocol version",
  "data": { "supported": ["2025-11-25"], "requested": "1.0.0" }
}}
CodeNameCause
-32700Parse errorUnreadable JSON
-32600Invalid requestWell-formed, not valid
-32601Method not foundNever advertised
-32602Invalid paramsWrong/missing args
-32603Internal errorServer logic failed
-32000+Server-definedCustom to that server

"An error isn't a crash. It's the exact same message envelope as a success, just carrying different news."

Reference: error object and codes verbatim, current JSON-RPC 2.0 / MCP specification; scenarios grounded in the source transcript (paraphrased).
MCP Lifecycle Guide › Special Cases
Special Cases

Timeouts

  • Every sent request should have a timeout
  • Hit with no response → send a cancellation notification, stop waiting
  • A progress notification may reset the clock — a hard maximum should still apply

"A timeout isn't giving up. It's refusing to wait forever for an answer that might genuinely never come."

Reference: verbatim, current official specification.
MCP Lifecycle Guide › Special Cases
Special Cases

Cancellation

Original long-running call
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
  "params": { "name": "search_recipes", "arguments": { "tag": "sunday" },
              "_meta": { "progressToken": "tok-7" } } }
Cancellation notification
{ "jsonrpc": "2.0", "method": "notifications/cancelled",
  "params": { "requestId": "7", "reason": "Timeout exceeded (30s)" } }

No id — a notification referencing the original request by its requestId.

"Cancellation doesn't ask permission. It announces a decision that's already been made."

Reference: verbatim, current official specification pattern.
MCP Lifecycle Guide › Special Cases
Special Cases

Progress Notifications

Let the client know a long-running request is still alive.

Call with a progress token
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
  "params": { "name": "search_recipes", "arguments": { "tag": "sunday" },
              "_meta": { "progressToken": "tok-7" } } }
Progress update
{ "jsonrpc": "2.0", "method": "notifications/progress",
  "params": { "progressToken": "tok-7", "progress": 60, "total": 100,
              "message": "Searching 60 of 100 recipes" } }

"Progress notifications are how a slow answer stays honest instead of just going quiet."

Reference: verbatim, current official specification pattern.