Skip to content

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.

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.

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.

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:

  1. Client calls tools/call.
  2. Server returns resultType: "input_required" with inputRequests, and optionally an opaque requestState.
  3. Client asks the user, then sends the same call again with a new id, adding inputResponses and the requestState unchanged.
  4. 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":{}} in clientCapabilities, the server answered HTTP 400 with -32021 and Cannot 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 real elicitation/create request instead. The TypeScript client also retries automatically inside callTool(), using the handler you registered with setRequestHandler('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 with Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests. A parameter annotated Annotated[Confirm, Resolve(ask_confirm)], where ask_confirm returns Elicit(...), worked on both. The Python server also signed its requestState.

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.

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.