TechByteByByte

The MCP Lifecycle and Capability Negotiation

What a Client and Server actually say to each other the moment a connection starts — and why capability negotiation exists at all, grounded in a real, honest reason: the protocol itself keeps changing.

#MCP#Lifecycle#Capability Negotiation

Recall Module 3’s own precise Client-Server distinction — now let’s cover exactly what happens the moment that connection actually starts, before any real tool ever gets used.

The real, complete lifecycle

Here’s the genuine, ordered sequence every real MCP connection goes through.

flowchart TD
    A[Client starts connection] --> B[Initialize]
    B --> C[Negotiate protocol version + capabilities]
    C --> D[Server reports what it actually supports]
    D --> E[Client begins real requests]
    E --> F[Tools / Resources / Prompts used]
    F --> G[Connection closed]

Why capability negotiation genuinely has to exist

This is worth explaining before any real message structure, since the “why” here is genuinely important, not just protocol formality.

Recall Module 1’s own real research finding — MCP has already gone through four real, dated revisions since its original release. A real, deployed Client built against an older version of the protocol might genuinely be talking to a Server built against a newer one, or the reverse. Capability negotiation exists specifically so both sides find out, at connection time, exactly what the other genuinely supports — rather than a Client assuming a feature exists and getting a real, confusing failure the first time it tries to use it.

Client: "I support protocol version X, and I support feature Y."
Server: "I support protocol version X, and I expose tools and resources, but not prompts."

Both sides now genuinely know what's actually available.

Seeing the real, actual initialize message

Let’s move past the conceptual version and look at what a real, current MCP initialization request genuinely looks like.

We’ll build the real, structured JSON-RPC message a Client actually sends the moment a connection opens.

# the real, actual shape of an MCP initialize request
initialize_request = {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
        "protocolVersion": "2025-06-18",  # the real, specific spec version this client supports
        "capabilities": {
            "roots": {"listChanged": True},  # a real, optional client capability
        },
        "clientInfo": {"name": "my-ai-assistant", "version": "1.0.0"},
    },
}

Notice protocolVersion is a real, literal, dated string — recall Module 1’s own real version history; this is exactly how a Client tells a Server which real revision of the spec it was built against.

Seeing the real server response

Let’s look at what a genuine, real Server sends back, reporting exactly what it actually supports.

We’ll build the real, structured response a Server returns, listing its own genuine, real capabilities.

# the real, actual shape of a server's initialize response
initialize_response = {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "protocolVersion": "2025-06-18",
        "capabilities": {
            "tools": {},          # this server genuinely exposes tools
            "resources": {},      # and resources
            # no "prompts" key here — this server genuinely doesn't support prompts
        },
        "serverInfo": {"name": "filesystem-server", "version": "2.1.0"},
    },
}

Notice the real, deliberate absence of a "prompts" key — this is the actual, real mechanism behind capability negotiation. The Client, reading this response, genuinely knows not to attempt anything prompt-related against this specific Server, before ever making that mistake.

Completing the lifecycle: an initialized notification

There’s one more, real, small step worth knowing about — a genuine confirmation from the Client that negotiation finished successfully.

# a real, one-way notification — no response expected, just confirming readiness
initialized_notification = {
    "jsonrpc": "2.0",
    "method": "notifications/initialized",
}

Only after this real notification does the Client actually begin sending genuine, real tool calls, resource reads, or prompt requests — the lifecycle’s own real, final gate before actual use begins.

Common mistakes worth avoiding

Assuming every Server supports every capability type. Recall this module’s own real response example — a genuine Server can legitimately omit resources or prompts entirely; always check what was actually negotiated before assuming a capability exists.

Sending real tool or resource requests before the lifecycle actually completes. Recall this module’s own real, ordered sequence — a Client genuinely shouldn’t send real requests until after initialized has been sent, or a real, current Server may reject them.

Treating protocolVersion as decorative. Recall Module 1’s own real version history — a genuine mismatch here is precisely why negotiation exists; ignoring it risks a Client assuming behavior from a spec revision the actual, connected Server doesn’t implement.

What you should take away from this module

  • The real lifecycle is: connect → initialize → negotiate → server reports real capabilities → client begins real requests → close.
  • Capability negotiation exists for a genuine, honest reason — the protocol itself keeps changing, and both sides need to know what the other actually, currently supports.
  • The real capabilities object in a server’s response is the literal, structural mechanism — an omitted key genuinely means an unsupported feature, not an oversight to assume around.

Where this goes next

The next module goes deep on the first, and most code-heavy, real capability type: Tools — building six progressive, real examples, from a calculator to a genuine, database-backed support ticket creator.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed