Skip to main content

Class 6: Capability Negotiation

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

Which revision this class describes

This class describes the initialize handshake, which spec revision 2026-07-28 removed. The current protocol does not have a 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, and it is what the Java MCP SDK 2.0.0 implements. Class 8 summarises what replaced it.

What2025-11-25 (this class)2026-07-28
Agreeing a protocol versionsent once, in the initialize requestio.modelcontextprotocol/protocolVersion in every request's _meta
Declaring client capabilitiessent once, in the initialize requestio.modelcontextprotocol/clientCapabilities in every request's _meta
Confirming the handshakenotifications/initializedremoved
Tasksa core tasks capabilityan extension, io.modelcontextprotocol/tasks (SEP-2663)
Client rootsroots carries a listChanged sub-fieldroots is an empty object, and the feature is deprecated
Optional add-onsexperimental onlyextensions, a map of extension name to settings

An extension is an optional feature specified outside the core protocol and identified by a prefixed name such as io.modelcontextprotocol/tasks. Both sides declare the ones they support in the extensions map.


Why Negotiation Matters

Protocols evolve and features are added. Without a negotiation mechanism, clients would have to guess what a server supports, or servers would have to support every feature forever.

MCP's initialize handshake settles it before any tool calls, resource reads or prompt retrievals happen: client and server exchange capability declarations first.


The Three Messages

Capability negotiation always involves exactly three messages:

The third message is a notification, so it does not carry an id, and the client does not wait for a reply before sending its first real request.

1. initialize request (Client → Server)

The client sends the protocol version it implements, the optional capabilities it supports, and its own name and 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 answers with the version it will use, its own capabilities, and 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:

{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}

Capability Anatomy

Capabilities are nested objects. The presence of a key indicates support for a feature, and sub-fields indicate optional behaviours within it.

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. Deprecated in 2026-07-28 (SEP-2577). The same revision removed logging/setLevel (SEP-2575): the level now travels per request in io.modelcontextprotocol/logLevel. The suggested migration is stderr on stdio, or OpenTelemetry, the vendor-neutral standard for traces, metrics and logs
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. A server that cannot tell one caller from another SHOULD NOT declare tasks.list, because the list would show task metadata to anyone who asks. Removed from core in 2026-07-28: tasks are now an extension, declared under extensions as io.modelcontextprotocol/tasks
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. Deprecated in 2026-07-28 (SEP-2577). The same revision removed notifications/roots/list_changed (SEP-2575) and reduced roots to an empty object. The suggested migration is to pass directories or files through tool parameters, resource URIs or server configuration
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. That parameter is soft-deprecated: its "thisServer" and "allServers" values still work, but servers SHOULD avoid them, and 2026-07-28 moved them onto the formal deprecation list. tools signals support for tool calling in sampling (). An empty sampling: {} remains valid and means plain sampling, so a server MUST NOT send a tool-enabled sampling request to a client that left sampling.tools undeclared. Deprecated in 2026-07-28 (SEP-2577); the suggested migration is to integrate directly with an LLM provider API from inside the server
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 2025-06-18. In 2025-11-25 the optional form and url sub-fields say which elicitation modes the client supports. An empty elicitation: {} remains valid and means form mode only. A client MUST support at least one mode, and a server MUST NOT send a mode the client left undeclared. Not deprecated: elicitation survives into 2026-07-28, where it moved to the Multi Round-Trip Requests pattern from Class 5. There the server answers with resultType: "input_required", and the client retries the request carrying the answer
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. Removed from core in 2026-07-28: SEP-2663 also dropped client-hosted sampling and elicitation tasks, so only tool calls remain task-capable, and support is declared under extensions
experimental(free-form)Declares support for non-standard experimental features

A row marked Deprecated in 2026-07-28 still works. Under the deprecation policy the earliest revision that may remove it is the first one released on or after 2027-07-28, and new implementations SHOULD NOT adopt it.

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.

CapabilityWhat the server asks forWho answersWhat comes back
roots (roots/list)which directories and files it should work inthe client, from the user's configurationa list of file:// roots, each with an optional name
sampling (sampling/createMessage)a model completionthe user approves, then the client's model generatesan assistant message, with a stopReason
elicitation (elicitation/create)a value from the person at the keyboardthe user, in the client's own UIaccept with content, or decline, or cancel

roots: a filesystem MCP server does not know which directories the user wants it to work in. The client (Claude Desktop, for example) holds that list, and the server calls roots/list to ask for it. Each root uri MUST be a file:// URI, and it may name a single file as well as a directory:

{
"roots": [
{ "uri": "file:///home/user/Documents/work", "name": "Work" },
{ "uri": "file:///home/user/Projects", "name": "Projects" }
]
}

When the user adds or removes a root, the client emits a notifications/roots/list_changed so the server's view stays current. Roots are advisory: a server SHOULD respect them and validate every path it touches against the list. The specification puts the MUST on the client: expose only roots the user has permitted, validate root URIs to prevent path traversal, and implement access controls. A stdio server runs as a child process with the user's own filesystem permissions, so the protocol itself does not stop it reading elsewhere.

sampling: sometimes a server needs the model to generate something to finish 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, then use the returned text to decide its next step. Four parties take part:

The client keeps the model handle and the API key: only the finished text travels back. The specification asks for a human on that path. There SHOULD always be someone able to deny a sampling request, and the client SHOULD let the user review and edit the prompt, and review the generated response before it goes back. Without sampling, a server must finish 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 put the question to the user. A deployment tool might ask "Which environment should I deploy to?" before completing the action. The answer comes back as one of three actions:

A decline is a normal answer the server has to handle like any other result. Form mode sends a JSON Schema the client renders as a form. URL mode sends the user to an external page. It exists for values that must stay out of the client: servers MUST NOT ask for passwords, API keys, access tokens or payment credentials through a form.


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

Those five rules are one decision with two branches:

The disconnect is the client's decision, because the server has already answered with a version it can serve. Servers and clients therefore support several revisions at once, and how far back each side goes 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, while the declared capabilities themselves stay fixed after initialization. listChanged: true announces it.

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 notification does not carry any tool data. The client re-issues tools/list to get the updated list and re-exposes 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?

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"
}
}
}

A client should show this error to the user, with the supported and requested values from data.


Key Takeaways

  • Negotiation happens before any tool/resource/prompt operations
  • Three messages: initialize request → response → initialized notification
  • Capabilities are declared by presence: the key means support, and sub-fields switch on optional behaviour
  • Version negotiation lets the server answer with a version it supports, and the client disconnects if it cannot accept that
  • listChanged capabilities enable dynamic updates during a live session

In the next class, we tackle the security model, the trust boundaries that make it safe to expose powerful tools to an AI.

Further Reading

Sources