No sessions, no handshake: how 2026-07-28 works
- Spec
2026-07-28- Evidence
- E2Reproducedarm64
- TypeScript SDK (server)
@modelcontextprotocol/server@2.4.0- TypeScript SDK (client)
@modelcontextprotocol/client@2.4.0- Python SDK
mcp@2.3.0- Verified
Until 2025-11-25, an MCP connection worked like a phone call: the client said hello with initialize, both sides remembered what was agreed, and the server could call back with its own questions. In 2026-07-28 every request stands alone. This page explains what replaced each piece.
Every request carries its own context
Section titled “Every request carries its own context”There is no initialize and no notifications/initialized. Instead, every request puts three things in params._meta:
| Key | Required | Meaning |
|---|---|---|
io.modelcontextprotocol/protocolVersion |
Yes | The protocol version for this request |
io.modelcontextprotocol/clientCapabilities |
Yes | What the client supports, for this request only. {} means nothing optional |
io.modelcontextprotocol/clientInfo |
Should | Client name and version, for logs and display |
The server answers with io.modelcontextprotocol/serverInfo in the result’s _meta, and every result has a resultType: "complete", or "input_required" (see below).
A server must not remember capabilities from an earlier request. Because nothing depends on an earlier message, any request can go to any server instance behind a load balancer.
server/discover
Section titled “server/discover”Servers must implement server/discover. It returns the supported versions, capabilities and server info in one call:
{"supportedVersions":["2026-07-28"],"capabilities":{"tools":{"listChanged":true},…},"resultType":"complete","ttlMs":0,"cacheScope":"private"}Calling it is optional. A client can just send tools/list; if the server does not speak that version it gets -32022 Unsupported protocol version with the list of supported versions. On stdio, a client that supports both generations should send server/discover first: a modern server answers it, an older one fails, and the client falls back to initialize.
Handles instead of sessions
Section titled “Handles instead of sessions”Streamable HTTP no longer has Mcp-Session-Id, and lists such as tools/list must not vary per connection. If a server needs state across calls, for example an open shopping cart or a database transaction, it creates an ID itself and returns it from a tool. The model passes it back as an ordinary argument:
open_cart() → {"cart":"c_81f2"}add_item(cart="c_81f2", sku="A1")checkout(cart="c_81f2")Treat a handle like any user input: check that the caller may use it.
Asking the user: multi round-trip requests
Section titled “Asking the user: multi round-trip requests”A server sometimes needs more input in the middle of a call: a confirmation, a missing field, or an LLM completion. Before, it sent its own request (elicitation/create, sampling/createMessage, roots/list) to the client while the call was open. In 2026-07-28 it returns instead, and the client calls again:
- Client calls
tools/call. - Server returns
resultType: "input_required"withinputRequests, and optionally an opaquerequestState. - Client asks the user, then sends the same call again with a new id, adding
inputResponsesand therequestStateunchanged. - Server finishes and returns
resultType: "complete".
Only tools/call, prompts/get and resources/read may return input_required. Here is the exchange with a TypeScript SDK 2.4.0 tool that asks before deleting:
// 1st response{"resultType":"input_required","inputRequests":{"confirm":{"method":"elicitation/create", "params":{"mode":"form","message":"Delete note 7?","requestedSchema":{"type":"object", "properties":{"confirm":{"type":"boolean"}},"required":["confirm"]}}}}}
// retry, new id, same arguments, plus:"inputResponses":{"confirm":{"action":"accept","content":{"confirm":true}}}
// 2nd response{"content":[{"type":"text","text":"deleted 7"}],"resultType":"complete"}The server code:
import { McpServer, inputRequired, acceptedContent } from '@modelcontextprotocol/server';
server.registerTool('delete_note', { inputSchema: z.object({ id: z.string() }) }, async ({ id }, ctx) => { const schema = z.object({ confirm: z.boolean() }); const answer = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', schema); if (answer?.confirm !== true) { return inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message: `Delete note ${id}?`, requestedSchema: schema }) } }); } return { content: [{ type: 'text', text: `deleted ${id}` }] };});What we saw when testing it:
- The client must declare the capability. Without
"elicitation":{"form":{}}inclientCapabilities, the server answered HTTP 400 with-32021andCannot request input 'confirm' (elicitation/create): the request's client capabilities do not declare the required capability. - One handler serves both generations. A TypeScript client on the 2025-11-25 handshake and one on 2026-07-28 both got the confirmation question once and the result
deleted 7. On the old protocol the SDK sends a realelicitation/createrequest instead. The TypeScript client also retries automatically insidecallTool(), using the handler you registered withsetRequestHandler('elicitation/create', …). - Python: use resolvers, not
ctx.elicit(). In the Python SDK 2.3.0,await ctx.elicit(...)worked on 2025-11-25 but failed on 2026-07-28 withCannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.A parameter annotatedAnnotated[Confirm, Resolve(ask_confirm)], whereask_confirmreturnsElicit(...), worked on both. The Python server also signed itsrequestState.
Subscriptions replace the GET stream
Section titled “Subscriptions replace the GET stream”Change notifications (tool, prompt or resource lists changed, a resource updated) now flow on the response stream of one long-lived request, subscriptions/listen. The client says which kinds it wants; the server’s first message confirms which it will send:
{"method":"notifications/subscriptions/acknowledged","params":{"notifications":{"toolsListChanged":true,"promptsListChanged":true},"_meta":{"io.modelcontextprotocol/subscriptionId":9}}}We asked for toolsListChanged, promptsListChanged and resourceSubscriptions: ["config://app"]. The acknowledgment left out the resource subscription, because that resource did not support subscriptions. The subscription ID is the id of the subscriptions/listen request. Progress and log notifications for a specific call still arrive on that call’s own response.
Deprecated is not removed
Section titled “Deprecated is not removed”The 2026-07-28 changelog uses two different words. Keep them apart:
| Still works in 2026-07-28? | Examples | |
|---|---|---|
| Removed | No. Our TypeScript server answered ping and logging/setLevel with 404 and -32601 Method not found, and GET /mcp with 405 |
initialize, ping, logging/setLevel, resources/subscribe, notifications/roots/list_changed, the HTTP GET stream, Mcp-Session-Id |
| Deprecated | Yes, but new code should not use it | Roots, Sampling, Logging, HTTP+SSE transport, OAuth Dynamic Client Registration |
For Roots, Sampling and Logging the earliest removal is the first revision released on or after 2027-07-28. The suggested replacements: pass directories through tool parameters or configuration instead of Roots, call an LLM provider directly instead of Sampling, and log to stderr or OpenTelemetry instead of Logging. Tasks moved out of the core protocol into the extension io.modelcontextprotocol/tasks.
Sources
MCPDev is community documentation. It is not affiliated with the Model Context Protocol project or with the vendors of the clients it covers.