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 initialize request, the server responds with its capabilities, and the client sends an initialized notification to confirm.
  • JSON-RPC 2.0: The message format used by MCP. Every message is a JSON object with a jsonrpc field set to "2.0". Requests have an id and method. Responses have the matching id and a result or error. Notifications have a method but no id.
  • 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:

text
Phase 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:

text
Message 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:

text
tools/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:

text
notifications/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

  1. 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.
  2. Confusing requests and notifications — Requests have an id field and expect a response. Notifications do not have an id and must not be responded to. Sending a response to a notification violates the protocol.
  3. 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

  1. Implement the full lifecycle — Always implement initialization, normal operation, and shutdown. Skipping graceful shutdown leads to resource leaks (open database connections, file handles).
  2. Log the initialization exchange — During development, log the full initialize request and response. This makes it easy to debug capability mismatches and version issues.
  3. 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

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"}}},
  {"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"}
]
✓ Completed