DEV Community

Cover image for MCP’s Initialize Handshake: Capabilities, Version Negotiation, and Instructions
Ignacio Gonzalez Bohorquez
Ignacio Gonzalez Bohorquez

Posted on

MCP’s Initialize Handshake: Capabilities, Version Negotiation, and Instructions

How MCP makes support explicit, handles version differences, and gives models the context they need before the first tool call.

The Model Context Protocol (MCP) begins a connection with an initialize exchange. It can look like a small setup step, but it does important work: the client and server establish which protocol version they’ll use, declare the capabilities they support, and share any server-wide guidance.

The design has a simple theme: make the important details explicit before the client starts using the server.

This article looks at three parts of that exchange:

  1. Capabilities as opt-in declarations
  2. Version negotiation as a soft agreement
  3. The server’s instructions field

1. Capabilities are opt-in declarations

“Opt-in” means something is off by default and becomes available only when it is explicitly enabled. In MCP, capabilities follow that model: a server declares the features it supports in its capabilities object during initialization.

A client should not assume that every MCP server offers every primitive. Tools, resources, prompts, and other capabilities are available only when declared.

For example, a read-only documentation server might declare resources:

{
  "capabilities": {
    "resources": {
      "subscribe": false
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A server that offers tools could declare:

{
  "capabilities": {
    "tools": {
      "listChanged": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A server might offer both tools and resources, including notifications when either list changes:

{
  "capabilities": {
    "tools": {
      "listChanged": true
    },
    "resources": {
      "subscribe": true,
      "listChanged": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

These are different, valid capability declarations. A server that offers only tools is not an incomplete version of one that offers tools, resources, and prompts. It describes a different contract.

Capability details matter

Capabilities can include sub-options. For instance:

{
  "tools": {
    "listChanged": true
  }
}
Enter fullscreen mode Exit fullscreen mode

says more than “this server has tools.” It also says the server can notify the client when its tool list changes.

By comparison:

{
  "tools": {}
}
Enter fullscreen mode Exit fullscreen mode

declares tool support without that same list-change notification. These declarations describe different expectations for the client.

Missing capabilities are useful information

If a server does not declare sampling, the client should not expect that server to send sampling requests. The absence of a capability is not an error; it tells the client what the server has not promised.

This is why “feature flags” is a useful mental model. Each side advertises what it supports, and the other side bases its decisions on what was actually declared.

Are capabilities negotiated again on every connection?

Yes. Each new connection begins with a new initialize exchange. The client and server exchange their protocol and capability information for that connection.

That means the client can learn what the server offers when connecting, rather than relying on an assumption made earlier. A server could, in principle, expose different capabilities in different connection contexts or after being redeployed. The client should use the declarations it receives for its current connection.

The practical rule is straightforward: don’t call a method whose required capability was never declared.

2. Version negotiation is soft, not a handshake failure

During initialization, the client proposes a protocol version. The server responds with the version it supports.

For example, the client may request:

{
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18"
  }
}
Enter fullscreen mode Exit fullscreen mode

If the server supports an older version, it can respond with that version:

{
  "result": {
    "protocolVersion": "2024-11-05"
  }
}
Enter fullscreen mode Exit fullscreen mode

The version mismatch itself does not make the initialization response an error. The server has completed the exchange and told the client which version it can use.

The client then decides what to do. It might continue if the version difference does not affect the features it needs. Or it might disconnect if it depends on behavior the server’s version does not support.

The protocol communicates the mismatch; the client’s implementation applies its own policy.

Why handle it this way?

A hard negotiation would require the server to reject an unsupported version. The client might then need to retry initialization with another version, adding another round trip before the connection can be used.

With a soft response, the server can report its supported version in the normal initialization exchange. The client receives the information and makes its decision without a separate retry loop.

Date-based version names also do not provide a simple compatibility rule by themselves. A client cannot infer from the dates alone whether the difference affects the capabilities it needs. It must judge compatibility based on the protocol changes and its own requirements.

3. The instructions field gives server-wide guidance

An initialization response can also include an optional instructions string:

{
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": true
      }
    },
    "serverInfo": {
      "name": "finance-server",
      "version": "1.0.0"
    },
    "instructions": "Always call validate_account before transfer_funds. Amounts are in cents, not dollars."
  }
}
Enter fullscreen mode Exit fullscreen mode

This is free-form natural language. It is not a structured set of rules for the client to enforce programmatically. A client may pass it to the model as context or surface it in another way.

Its purpose is to explain how to use the server correctly. For example, the instructions might describe a required order for tool calls, clarify units, or explain an authentication detail.

How is this different from a tool description?

A tool description explains one tool: what it does and what arguments it expects.

The instructions field applies to the server as a whole. It can describe rules that span multiple tools or affect how the client should interact with the server generally.

A useful analogy is the difference between a function’s documentation and a library’s README. Both provide guidance, but at different scopes.

Why include it during initialization?

Timing matters. If the model needs to know “call validate_account before transfer_funds,” that guidance should be available before it considers making its first tool call.

Putting the instructions in the initialization response makes them available at the start of the interaction. They remain guidance for the client to use; they are not a guarantee that every client will enforce them.

One exchange, three kinds of clarity

The initialization exchange gives the client several kinds of information up front:

  • Capabilities describe which operations the server has declared.
  • Protocol version tells the client which version the server supports.
  • Instructions provide server-wide guidance before the model starts using its tools.

Together, these fields help the client make informed decisions without assuming a universal feature set, treating every version difference as a failure, or waiting until after a tool call to receive important guidance.

The core idea is simple: declare what is supported, state what version is available, and share relevant usage guidance before work begins.

Top comments (0)