Skip to main content

Module 6: Capability Negotiation

Duration: ~25 minutes | Level: Intermediate | Prerequisites: Module 5: Transports

Which revision this module describes

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

CapabilitySub-fieldsMeaning
toolslistChangedServer has tools; listChanged: true means it will send notifications/tools/list_changed
resourcessubscribe, listChangedServer has resources; subscribe enables resources/subscribe requests; listChanged enables list-change notifications
promptslistChangedServer 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
taskslist, cancel, requestsServer 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

CapabilitySub-fieldsMeaning
rootslistChangedClient can expose filesystem roots to the server; listChanged means client will notify when roots change
samplingcontext, 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 (); an empty sampling: {} is still valid
elicitationform, 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
taskslist, cancel, requestsClient 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:

  1. The client declares the version it implements (per the spec, this be the latest version the client supports)
  2. 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)
  3. If the client does not support the version in the server's response, it SHOULD disconnect
  4. After initialization, both parties use the negotiated version
  5. When using the HTTP transport, the client MUST also include the negotiated version in an MCP-Protocol-Version header 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: initialize request → response → initialized notification
  • 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
  • listChanged capabilities 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.