MCP's initialize handshake negotiates version and capabilities, and 2026-07-28 replaces it
DrFritzi · Reviewed · Updated 28 Sept 2026 · Markdown
Answer
In MCP revisions up to 2025-11-25, every connection starts with a three-step handshake. The client sends an initialize request with its protocolVersion, capabilities and clientInfo. The server replies with the version it will use, its own capabilities and serverInfo. The client then sends notifications/initialized. The latest revision, 2026-07-28, removes this handshake. Each request now carries its version and capabilities in _meta, and servers must answer server/discover. This page describes the 2025-11-25 handshake and notes what 2026-07-28 changed.
Details
The three messages (2025-11-25)
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-11-25",
"capabilities":{"elicitation":{}},
"clientInfo":{"name":"ExampleClient","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-11-25",
"capabilities":{"tools":{"listChanged":true}},
"serverInfo":{"name":"ExampleServer","version":"1.0.0"},
"instructions":"Optional instructions for the client"}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
clientInfo and serverInfo also accept optional fields such as title, description, icons and websiteUrl.
Version negotiation
- The client MUST send a version it supports, and SHOULD send its latest one.
- If the server supports that version, it MUST return the same one. If not, it returns another version it supports, and SHOULD pick its latest.
- If the client cannot use the version the server returns, it SHOULD disconnect.
- Over HTTP, the client then sends
MCP-Protocol-Version: <version>on every later request.
Capabilities
Capabilities decide which optional features the session can use. Neither side may use a feature that was not negotiated.
| Side | Examples |
|---|---|
| Client | roots, sampling, elicitation, tasks, experimental |
| Server | tools, resources, prompts, logging, completions, tasks, experimental |
Sub-flags refine these. listChanged means the side sends list-change notifications. subscribe (resources only) allows subscribing to single items.
What may be sent before initialization completes
- Before the server has answered
initialize, the client SHOULD NOT send requests other thanping. - Before the server receives
notifications/initialized, it SHOULD NOT send requests other thanpingand logging.
A common pitfall is a hand-written client that sends tools/list right after initialize to save a round trip. That breaks the first rule above. Wait for the initialize response, send notifications/initialized, and then list tools.
What 2026-07-28 changed
| 2025-11-25 (legacy) | 2026-07-28 (modern) |
|---|---|
initialize + notifications/initialized once per connection |
No handshake. Every request carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in _meta |
clientInfo in initialize |
io.modelcontextprotocol/clientInfo on each request (SHOULD) |
serverInfo in InitializeResult |
io.modelcontextprotocol/serverInfo in each result's _meta (SHOULD) |
| Server returns a different version | Server returns UnsupportedProtocolVersionError (-32022) with a supported list |
| — | Servers MUST implement server/discover, which returns supportedVersions, capabilities and optional instructions |
A dual-era server answers initialize with legacy behavior and answers requests that carry modern _meta statelessly. On stdio, a client that supports both eras SHOULD send server/discover first. If it gets any error that is not a recognized modern error, or no reply, it falls back to initialize.