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:
- Capabilities as opt-in declarations
- Version negotiation as a soft agreement
- The server’s
instructionsfield
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
}
}
}
A server that offers tools could declare:
{
"capabilities": {
"tools": {
"listChanged": true
}
}
}
A server might offer both tools and resources, including notifications when either list changes:
{
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
}
}
}
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
}
}
says more than “this server has tools.” It also says the server can notify the client when its tool list changes.
By comparison:
{
"tools": {}
}
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"
}
}
If the server supports an older version, it can respond with that version:
{
"result": {
"protocolVersion": "2024-11-05"
}
}
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."
}
}
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)