Lesson 4 - Step 1 of 6

MCP Lifecycle Overview

Understand the three phases of an MCP connection: Initialization, Operation, and Shutdown.

What You'll Build

In this lesson, you'll build mcp_core.py — a working MCP server that properly implements the initialization handshake. By the end, your server will:

  • Track connection state (disconnected, initializing, ready)
  • Handle the initialize request with version negotiation
  • Respond with server capabilities
  • Process the notifications/initialized notification
  • Reject requests before initialization is complete

Each step builds on the previous one, and you'll test your server with curl commands.

The Three Phases of MCP

Every MCP connection follows a strict lifecycle with three distinct phases. Understanding this lifecycle is essential before building a working MCP server.

diagram
┌─────────────────────────────────────────────────────────────────┐
│                     MCP CONNECTION LIFECYCLE                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  PHASE 1: INITIALIZATION                                  │   │
│  │  ─────────────────────────────────────────────────────── │   │
│  │  Client ──► initialize request (capabilities, version)   │   │
│  │  Server ◄── initialize response (capabilities, version)  │   │
│  │  Client ──► notifications/initialized                     │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  PHASE 2: OPERATION                                       │   │
│  │  ─────────────────────────────────────────────────────── │   │
│  │  Client ──► tools/list, tools/call                        │   │
│  │  Client ──► resources/list, resources/read                │   │
│  │  Client ──► prompts/list, prompts/get                     │   │
│  │  ... (normal MCP operations)                              │   │
│  └──────────────────────────────────────────────────────────┘   │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  PHASE 3: SHUTDOWN                                        │   │
│  │  ─────────────────────────────────────────────────────── │   │
│  │  Client closes connection or server terminates            │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Phase 1: Initialization (The Handshake)

The initialization phase is the handshake between client and server. No other operations can happen until this completes successfully.

Step Direction Message
1 Client → Server initialize request
2 Server → Client initialize response
3 Client → Server notifications/initialized
Initialization is Required

A server MUST reject any requests (except initialize) until the handshake completes. Attempting to call tools/list before initialization will result in an error.

Phase 2: Operation

Once initialized, the client and server can exchange messages freely. This is where the real work happens:

  • Tool discovery — Client queries available tools
  • Tool execution — Client invokes tools with arguments
  • Resource access — Client reads available resources
  • Prompt retrieval — Client fetches prompt templates
json
// Example: Client lists available tools
{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list"
}

// Server responds with tool definitions
{
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
        "tools": [
            {
                "name": "get_weather",
                "description": "Get current weather for a location",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string"}
                    },
                    "required": ["city"]
                }
            }
        ]
    }
}

Phase 3: Shutdown

The shutdown phase occurs when either party terminates the connection:

  • Client closes the connection gracefully
  • Server process terminates
  • Network connection drops
  • Timeout occurs

For STDIO transport, shutdown typically means the server process exits. For HTTP/SSE transport, the client simply stops sending requests.

Why the Handshake Matters

The initialization handshake serves several critical purposes:

text
1. VERSION NEGOTIATION
   Client: "I support protocol version 2024-11-05"
   Server: "I also support 2024-11-05, let's use that"

2. CAPABILITY DISCOVERY
   Client: "I can do sampling"
   Server: "I provide tools and resources"

3. IDENTITY EXCHANGE
   Client: "I am Claude Desktop v1.0.0"
   Server: "I am weather-server v2.1.0"
Building Trust

The handshake lets both parties know exactly what to expect from each other. A client won't try to call tools on a server that doesn't support them, and a server knows what features the client can handle.

State Machine View

Your MCP server should track its connection state:

python
from enum import Enum

class ConnectionState(Enum):
    """MCP connection states."""
    DISCONNECTED = "disconnected"    # No connection
    INITIALIZING = "initializing"    # Received initialize, not yet confirmed
    READY = "ready"                  # Handshake complete, operational
    SHUTDOWN = "shutdown"            # Closing down

class MCPServer:
    def __init__(self):
        self.state = ConnectionState.DISCONNECTED

    def handle_initialize(self, params):
        """Handle initialize request."""
        if self.state != ConnectionState.DISCONNECTED:
            raise Exception("Already initialized")
        self.state = ConnectionState.INITIALIZING
        # ... process initialize
        return initialize_response

    def handle_initialized_notification(self):
        """Handle initialized notification."""
        if self.state != ConnectionState.INITIALIZING:
            raise Exception("Invalid state for initialized")
        self.state = ConnectionState.READY

    def handle_request(self, method, params):
        """Handle normal requests."""
        if self.state != ConnectionState.READY:
            raise Exception("Server not initialized")
        # ... handle the request

What's Next?

In the following steps, we'll examine each part of the handshake in detail:

  • Step 2: The initialize request from the client
  • Step 3: The server's response with capabilities
  • Step 4: Understanding capability negotiation
  • Step 5: The initialized notification
  • Step 6: Building the complete handshake in Python