Module 6: Capability Negotiation
Duration: ~25 minutes | Level: Intermediate | Prerequisites: Module 5: Transports
This module describes the initialize handshake, which spec revision 2026-07-28 removed. The current protocol has no negotiation handshake at all: every request carries its own protocol version and client capabilities in _meta, and the server accepts or rejects each request independently.
The handshake is still worth reading, and everything below is accurate for 2025-11-25. It is what most deployed clients and servers speak, it is what the Java MCP SDK 2.0.0 implements, and the reasons it was removed are the clearest illustration in MCP of what connection-scoped state costs. Read what follows as the legacy mechanism; Module 8 summarises what replaced it.
Why Negotiation Matters
Protocols evolve. Features are added. Not every client supports every server feature, and not every server offers every capability. Without a negotiation mechanism, clients would have to guess what servers support, or servers would have to support everything forever.
MCP's initialize handshake solves this cleanly. Before any tool calls, resource reads, or prompt retrievals happen, client and server exchange capability declarations. Both parties then know exactly what's on the table.
The Three Messages
Capability negotiation always involves exactly three messages:
1. initialize request (Client → Server)
The client tells the server:
- Which MCP protocol version it implements
- Which optional client capabilities it supports
- Information about itself (name, version)
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {},
"elicitation": {}
},
"clientInfo": {
"name": "Claude Desktop",
"version": "0.7.5"
}
}
}
2. initialize response (Server → Client)
The server responds with:
- The protocol version it will use (the same version the client requested if supported, otherwise another version the server supports, typically its latest)
- Its own capabilities
- Its name and version
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"prompts": {
"listChanged": true
},
"logging": {}
},
"serverInfo": {
"name": "acme-database-server",
"version": "2.1.0"
}
}
}
3. initialized notification (Client → Server)
The client confirms it has received and processed the initialize response. This is a notification (no id), not a request:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
After this three-way exchange, the session is fully established. Both parties have a shared understanding of what's available.
Capability Anatomy
Capabilities are nested objects. The presence of a key indicates support for a feature; specific sub-fields indicate optional behaviours within that feature.
Server Capabilities
| Capability | Sub-fields | Meaning |
|---|---|---|
tools | listChanged | Server has tools; listChanged: true means it will send notifications/tools/list_changed |
resources | subscribe, listChanged | Server has resources; subscribe enables resources/subscribe requests; listChanged enables list-change notifications |
prompts | listChanged | Server has prompts; listChanged: true enables list-change notifications |
logging | (none) | Server will accept logging/setLevel and send notifications/message log events |
completions | (none) | Server supports argument autocompletion via completion/complete |
tasks | list, cancel, requests | Server supports task-augmented requests (long-running operations the client can poll via tasks/get and cancel); introduced experimentally in 2025-11-25 |
experimental | (free-form) | Declares support for non-standard experimental features |
Client Capabilities
| Capability | Sub-fields | Meaning |
|---|---|---|
roots | listChanged | Client can expose filesystem roots to the server; listChanged means client will notify when roots change |
sampling | context, tools (both optional) | Client supports server-initiated sampling requests (the server can ask the client to make an LLM inference). In 2025-11-25, context signals support for the includeContext parameter (soft-deprecated: servers SHOULD avoid its "thisServer" and "allServers" values, which may be removed in a future revision) and tools signals support for tool calling in sampling (SEP-1577); an empty sampling: {} is still valid |
elicitation | form, url (both optional) | Client supports server-initiated elicitation requests (the server can ask the client to gather structured information from the user). Introduced in protocol revision 2025-06-18; in 2025-11-25 the optional form and url sub-fields indicate which elicitation modes the client supports; an empty elicitation: {} is still valid |
tasks | list, cancel, requests | Client supports task-augmented versions of the requests it can receive, with requests covering sampling and elicitation on the client side; introduced experimentally in 2025-11-25 |
experimental | (free-form) | Declares support for non-standard experimental features |
What "roots", "sampling", and "elicitation" actually mean
All three are about the server asking the client for something, the reverse of the more familiar tool-call direction. They are easier to understand with concrete examples.
roots: a filesystem MCP server doesn't have direct access to the user's machine. Instead, the client (e.g., Claude Desktop) tells the server which directories the user has granted access to: "you can read from ~/Documents/work and ~/Projects, nothing else." Those are the "roots." When the client declares the roots capability, the server can call roots/list on it to ask which folders it's allowed to operate on. If the user adds or removes a root, the client emits a notifications/roots/list_changed so the server's view stays current.
sampling: when a server needs the AI to generate something to complete its work (summarise a fetched page, classify an entry, decide what to do next), it can ask the client to make the LLM call on its behalf. This is what enables recursive agent loops: the server invokes the model through the client, gets a response, and uses it as input to its next operation. The client retains full control (it can refuse, log, redact). Without sampling, a server cannot involve the client's model mid-operation; it must complete its work with its own logic, data, and API calls.
elicitation: when a server needs more information from the user (not the model) mid-operation, it can ask the client to prompt the user with a JSON-Schema-validated form. For example, a deployment tool might call elicitation to ask "Which environment should I deploy to?" before completing the action. The client owns the UI and the user always has the option to decline or cancel. Introduced in protocol revision 2025-06-18; the 2025-11-25 revision added a second, URL-based elicitation mode alongside the original form mode.
These capabilities are negotiated up front so that neither side asks for something the other can't deliver.
Version Negotiation
MCP uses date-based versioning ("2025-11-25" style). Rules:
- The client declares the version it implements (per the spec, this SHOULD be the latest version the client supports)
- If the server supports the requested version, it MUST respond with the same version. Otherwise, it MUST respond with another protocol version it supports (typically its latest)
- If the client does not support the version in the server's response, it SHOULD disconnect
- After initialization, both parties use the negotiated version
- When using the HTTP transport, the client MUST also include the negotiated version in an
MCP-Protocol-Versionheader on every subsequent request to the server
This means servers and clients are encouraged to maintain backwards compatibility across multiple revisions. How far back each side supports is an implementation choice.
Dynamic Capability Changes
Some capabilities support dynamic changes: the server's tool, resource, or prompt lists can change during a session (the declared capabilities themselves stay fixed after initialization). Support for this is indicated by listChanged: true in the capability.
When the tool list changes (e.g., a plugin was installed, a user was granted additional permissions), the server sends:
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
The client should then re-issue a tools/list request to get the updated list and re-expose the new tools to the model.
This enables use cases like:
- Tools that become available after authentication
- Servers that load plugins on demand
- Permission-gated tool access
What Happens If Negotiation Fails?
Normally a server that does not support the requested version simply responds with one it does support (rule 2), and the client decides whether to disconnect. But if initialization cannot proceed at all, for example when the requested version is malformed like "1.0.0", the server returns an error. The spec illustrates this with -32602 (Invalid params), since the protocol version is a parameter of initialize:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Unsupported protocol version",
"data": {
"supported": ["2025-11-25"],
"requested": "1.0.0"
}
}
}
Well-behaved clients should surface this error clearly rather than silently failing.
Key Takeaways
- Negotiation happens before any tool/resource/prompt operations
- Three messages:
initializerequest → response →initializednotification - Capability declarations are declarative: presence = support, sub-fields = optional behaviour
- Protocol version negotiation lets the server respond with a version it supports; clients disconnect if they cannot accept the server's response
listChangedcapabilities enable dynamic updates during a live session
In the next module, we tackle the security model, the trust boundaries that make it safe to expose powerful tools to an AI.