Lesson 10 - Step 3 of 6

Initialization Flow

Implement the MCP handshake from the client side.

The Initialization Sequence

Before you can use any MCP functionality, the client must complete a handshake with the server. This establishes protocol version and capabilities.

diagram
Client                                    Server
   │                                         │
   │──── initialize request ────────────────►│
   │     (client capabilities)               │
   │                                         │
   │◄─── initialize response ────────────────│
   │     (server capabilities)               │
   │                                         │
   │──── notifications/initialized ─────────►│
   │     (handshake complete)                │
   │                                         │
   │         Ready for operation!            │
   │                                         │

Step 1: Send Initialize Request

The client starts by sending an initialize request with its protocol version and capabilities:

python
def initialize(self) -> dict:
    """
    Perform the MCP initialization handshake.

    Returns:
        Server's initialization response with capabilities.
    """
    # Send initialize request
    response = self.send_request("initialize", {
        "protocolVersion": "2024-11-05",
        "capabilities": {
            # Client capabilities - what we support
            "roots": {
                "listChanged": True  # We can handle root change notifications
            },
            "sampling": {}  # We support sampling (LLM requests from server)
        },
        "clientInfo": {
            "name": "my-mcp-client",
            "version": "1.0.0"
        }
    })

    return response

Understanding the Initialize Response

The server responds with its own capabilities:

json
{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "protocolVersion": "2024-11-05",
        "capabilities": {
            "tools": {},
            "resources": {
                "subscribe": true,
                "listChanged": true
            },
            "prompts": {
                "listChanged": true
            }
        },
        "serverInfo": {
            "name": "my-awesome-server",
            "version": "1.0.0"
        }
    }
}
Capability Negotiation

The capabilities tell you what features are available. If tools is present, the server has tools. If resources.subscribe is true, you can subscribe to resource updates.

Step 2: Parse and Store Capabilities

After receiving the response, parse and store the server's capabilities for later use:

python
def initialize(self) -> dict:
    """Perform the MCP initialization handshake."""
    response = self.send_request("initialize", {
        "protocolVersion": "2024-11-05",
        "capabilities": {
            "roots": {"listChanged": True},
            "sampling": {}
        },
        "clientInfo": {
            "name": "my-mcp-client",
            "version": "1.0.0"
        }
    })

    # Check for errors
    if "error" in response:
        raise RuntimeError(f"Initialize failed: {response['error']}")

    result = response["result"]

    # Store server info
    self.server_info = result.get("serverInfo", {})
    self.server_capabilities = result.get("capabilities", {})
    self.protocol_version = result.get("protocolVersion")

    print(f"Connected to: {self.server_info.get('name', 'Unknown')}")
    print(f"Protocol version: {self.protocol_version}")

    return response

Step 3: Send Initialized Notification

After processing the response, the client must send an notifications/initialized message to complete the handshake:

python
def initialize(self) -> dict:
    """Perform the MCP initialization handshake."""
    # Step 1: Send initialize request
    response = self.send_request("initialize", {
        "protocolVersion": "2024-11-05",
        "capabilities": {
            "roots": {"listChanged": True},
            "sampling": {}
        },
        "clientInfo": {
            "name": "my-mcp-client",
            "version": "1.0.0"
        }
    })

    if "error" in response:
        raise RuntimeError(f"Initialize failed: {response['error']}")

    # Step 2: Store server capabilities
    result = response["result"]
    self.server_info = result.get("serverInfo", {})
    self.server_capabilities = result.get("capabilities", {})
    self.protocol_version = result.get("protocolVersion")

    # Step 3: Send initialized notification
    self.send_notification("notifications/initialized")

    # Mark as ready
    self.initialized = True

    return response
Don't Skip the Notification!

The notifications/initialized message is required by the MCP specification. Some servers may not function correctly if you skip it.

Checking Capabilities

Add helper methods to check what the server supports.

Edit: stdio_client.py

Add these methods to the StdioClient class:

python stdio_client.py - Add to StdioClient class
    # Add these instance variables in __init__:
    # self.server_info = {}
    # self.server_capabilities = {}
    # self.protocol_version = None
    # self.initialized = False

    def initialize(self) -> dict:
        """Perform the MCP initialization handshake."""
        # Step 1: Send initialize request
        response = self.send_request("initialize", {
            "protocolVersion": "2024-11-05",
            "capabilities": {
                "roots": {"listChanged": True},
                "sampling": {}
            },
            "clientInfo": {
                "name": "my-mcp-client",
                "version": "1.0.0"
            }
        })

        if "error" in response:
            raise RuntimeError(f"Initialize failed: {response['error']}")

        # Step 2: Store server capabilities
        result = response["result"]
        self.server_info = result.get("serverInfo", {})
        self.server_capabilities = result.get("capabilities", {})
        self.protocol_version = result.get("protocolVersion")

        # Step 3: Send initialized notification
        self.send_notification("notifications/initialized")

        # Mark as ready
        self.initialized = True

        return response

    def has_tools(self) -> bool:
        """Check if the server provides tools."""
        return "tools" in self.server_capabilities

    def has_resources(self) -> bool:
        """Check if the server provides resources."""
        return "resources" in self.server_capabilities

    def has_prompts(self) -> bool:
        """Check if the server provides prompts."""
        return "prompts" in self.server_capabilities

    def supports_resource_subscriptions(self) -> bool:
        """Check if the server supports resource subscriptions."""
        resources = self.server_capabilities.get("resources", {})
        return resources.get("subscribe", False)

Complete Initialization Example

Here's how initialization looks in practice:

python
with StdioClient(["python", "my_server.py"]) as client:
    # Perform initialization
    client.initialize()

    # Check capabilities
    print(f"Has tools: {client.has_tools()}")
    print(f"Has resources: {client.has_resources()}")
    print(f"Has prompts: {client.has_prompts()}")

    # Now we can call other methods
    if client.has_tools():
        tools_response = client.send_request("tools/list")
        print(f"Available tools: {tools_response}")
Best Practice

Always check capabilities before calling methods. If you call tools/list on a server that doesn't support tools, you'll get an error.