Introduction
Every MCP connection follows a defined lifecycle: initialization, normal operation, and shutdown. The initialization phase is where the client and server introduce themselves, agree on a protocol version, and exchange capabilities. Once initialized, they communicate using JSON-RPC 2.0 messages — requests that expect responses, and notifications that do not. Understanding this lifecycle is essential for building reliable MCP servers and debugging connection issues.
Key Concepts
- Initialization Handshake: A three-step process where the client sends an
initializerequest, the server responds with its capabilities, and the client sends aninitializednotification to confirm. - JSON-RPC 2.0: The message format used by MCP. Every message is a JSON object with a
jsonrpcfield set to "2.0". Requests have anidandmethod. Responses have the matchingidand aresultorerror. Notifications have amethodbut noid. - Protocol Version: The client proposes a protocol version (for example, "2025-11-25"), and the server responds with the version it will use. Both must agree for the connection to proceed.
- Graceful Shutdown: Either side can initiate shutdown. The client sends a close request, and both sides clean up resources.
Real World Context
When you start Claude Desktop and it connects to your configured MCP servers, each connection goes through this exact lifecycle. If a server fails to respond to the initialize request within a timeout, Claude Desktop shows a connection error. If the server responds with an incompatible protocol version, the connection is rejected. This handshake ensures that both sides can communicate reliably before any tool calls are attempted.
Deep Dive
The Three Phases
The MCP connection lifecycle has three distinct phases:
textPhase 1: Initialization ┌────────┐ ┌────────┐ │ Client │──── initialize ───>│ Server │ │ │<─── result ────────│ │ │ │── initialized ────>│ │ └────────┘ (notification) └────────┘ Phase 2: Normal Operation ┌────────┐ ┌────────┐ │ Client │<── requests ──────>│ Server │ │ │<── notifications ─>│ │ └────────┘ └────────┘ Phase 3: Shutdown ┌────────┐ ┌────────┐ │ Client │──── close ────────>│ Server │ │ │ cleanup │ cleanup│ └────────┘ └────────┘
Each phase builds on the previous one. Let us examine the initialization handshake in detail.
The Initialization Handshake in Detail
Here is the actual JSON-RPC exchange during initialization:
The client starts by sending an initialize request with its protocol version and capabilities:
json{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": { "roots": { "listChanged": true }, "sampling": {} }, "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } }
The server responds with its own protocol version and capabilities:
json{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-11-25", "capabilities": { "logging": {}, "prompts": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true }, "tools": { "listChanged": true } }, "serverInfo": { "name": "ExampleServer", "version": "1.0.0" } } }
The server's response can also include an optional instructions field — free-form text describing how to use the server, which helps the LLM understand available tools and features.
Finally, the client sends an initialized notification to signal that it is ready:
json{ "jsonrpc": "2.0", "method": "notifications/initialized" }
Note that the initialized message is a notification (no id field), not a request. The server does not respond to it.
JSON-RPC 2.0 Message Types
MCP uses three types of JSON-RPC messages:
textMessage Type Has id? Has method? Expects response? ────────────────────────────────────────────────────────── Request Yes Yes Yes Response Yes No No (it IS the response) Notification No Yes No
Requests are used for operations that need a result — like calling a tool or listing resources. Notifications are used for one-way messages — like the initialized signal or change notifications.
Normal Operation
During normal operation, the client and server exchange requests and notifications. The client can request tool lists, call tools, read resources, and retrieve prompts. The server can send notifications about capability changes.
Common client-to-server requests:
texttools/list - Discover available tools tools/call - Execute a tool resources/list - Discover available resources resources/read - Read a resource's content prompts/list - Discover available prompts prompts/get - Retrieve a prompt template
Common server-to-client notifications:
textnotifications/tools/list_changed - Tool list has changed notifications/resources/list_changed - Resource list has changed notifications/prompts/list_changed - Prompt list has changed
These notifications allow the client to keep its capability catalog up to date without polling.
Graceful Shutdown
Either side can initiate shutdown. The client typically sends a close request, gives the server time to clean up (close database connections, save state), and then terminates the connection. For stdio transport, this means closing stdin. For HTTP transport, this means closing the session.
Common Pitfalls
- Sending requests before initialization completes — The server must not process any requests until it has received the initialized notification. Sending a tools/list request before the handshake is complete will result in an error.
- Confusing requests and notifications — Requests have an
idfield and expect a response. Notifications do not have anidand must not be responded to. Sending a response to a notification violates the protocol. - Not handling version mismatches — If the client proposes a protocol version the server does not support, the server should respond with an error. Silently accepting and then behaving differently leads to hard-to-debug failures.
Best Practices
- Implement the full lifecycle — Always implement initialization, normal operation, and shutdown. Skipping graceful shutdown leads to resource leaks (open database connections, file handles).
- Log the initialization exchange — During development, log the full initialize request and response. This makes it easy to debug capability mismatches and version issues.
- Set reasonable timeouts — If the server does not respond to the initialize request within a reasonable time (for example, 30 seconds), the client should fail the connection with a clear error message.
Summary
- The MCP lifecycle has three phases: initialization handshake, normal operation, and graceful shutdown.
- Initialization is a three-step process: client sends initialize, server responds with capabilities, client sends initialized notification.
- All communication uses JSON-RPC 2.0 with requests (need responses), responses, and notifications (no response expected).
- The protocol version (for example, "2025-11-25") is negotiated during initialization.
Code Examples
[
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"ExampleClient","version":"1.0.0"}}},
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"ExampleServer","version":"1.0.0"}}},
{"jsonrpc":"2.0","method":"notifications/initialized"}
]