MCP's Biggest Overhaul Kills Sessions and Handshakes—Here's What Breaks in Java
MCP's Biggest Upgrade Yet: Handshakes Gone, Sessions Gone, and Six Pitfalls in the Java Ecosystem
This article's fact-checking is based on the MCP 2026-07-28 official changelog, the official release announcement, MCP Java SDK Releases, and LangChain4j 1.19.0 official documentation
Opening: A Protocol Upgrade That "Flips the Table"
On July 28, 2026, MCP released the 2026-07-28 specification. The official tone is blunt: this is the largest protocol upgrade since MCP's inception.
This upgrade doesn't just add a few fields; it overhauls the protocol's foundation. The initialize handshake is gone. Mcp-Session-Id is gone. ping is gone. Server-initiated requests are gone. The entire protocol has shifted from a "session-based bidirectional stream" to a "self-describing request/response" model. Combined with the governance backdrop of the Linux Foundation's AAIF (Agentic AI Foundation), MCP's ambition to become the "HTTP of the AI world" is written all over its face.
It's been almost a month since the spec was released. Has the Java ecosystem caught up? I've combed through the official changelog, all release notes for the MCP Java SDK, and the documentation for Spring AI and LangChain4j. The conclusion: clients lead the way (LangChain4j 1.19 already supports it), servers lag behind (MCP Java SDK hasn't followed as of 2.0.1), and there's a conceptual trap in the middle that most people will confuse. This article explains the new specification clearly, then lists out the pitfalls on the Java side one by one.
1. Why Stateless: The Horizontal Scaling Dilemma of Stateful MCP Servers
First, let's understand the motivation for the new specification in one minute. How the old protocol (2025-11-25 and earlier) worked:
- The client connects to the server and sends an
initializehandshake; - The server returns a capability list and assigns a
Mcp-Session-Id; - All subsequent requests carry this session ID, and the server remembers "who you are and what capabilities were negotiated" within the session.
This works fine on a single machine, but imagine deploying a remote MCP Server for a product with millions of users:
- What about multiple replicas? The session exists in the memory of a specific instance, so subsequent requests must be routed to that same instance—either by implementing sticky routing or using shared session storage (like Redis);
- What about gateways? The load balancer needs to understand
Mcp-Session-Idto route correctly, potentially even requiring deep packet inspection; - What about elastic scaling? If an instance restarts, all sessions in its memory are lost, and all clients must disconnect and reconnect.
A sentence from the official announcement puts it bluntly: after the upgrade, remote MCP servers that previously required sticky sessions, shared session storage, and deep packet inspection at the gateway can now run behind a standard round-robin load balancer, routing by the Mcp-Method request header, with clients able to cache tools/list responses based on ttlMs. In other words: MCP servers have finally gained the same deployment privileges as ordinary HTTP microservices—scale up, scale down, and rolling release at will.
2. What the 2026-07-28 Spec Changed: Nine Major Changes Broken Down
All of the following is verified against the official changelog (Major changes section), with corresponding SEP numbers noted.
1. Protocol-Level Session Removal (SEP-2567)
The Mcp-Session-Id header is removed from Streamable HTTP transport. List endpoints like tools/list, resources/list, and prompts/list are no longer differentiated by connection. Servers requiring cross-call state should switch to explicit handles (server-minted handles): the server generates an ID, passes it to the client as a regular tool parameter, and the client passes it back in subsequent calls. This is the design core of the entire new specification, elaborated separately later.
2. initialize Handshake Removed, Requests are Self-Describing (SEP-2575)
The initialize / notifications/initialized handshake is completely removed. Each request carries its own identity in _meta:
{
"jsonrpc": "2.0",
"method": "tools/call",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0" },
"io.modelcontextprotocol/clientCapabilities": { ... }
}
}
Clients SHOULD identify themselves (clientInfo) in every request, and servers SHOULD identify themselves (serverInfo) in the _meta of every result. Version mismatches return UnsupportedProtocolVersionError. The protocol's "who I am, what I support" has changed from a one-time declaration during handshake to being carried along with every message.
3. New server/discover Discovery Method (SEP-2575)
Servers MUST implement the server/discover RPC, broadcasting their supported protocol versions, capabilities, and identity. Clients MAY call it before sending any request to perform version selection, and it can also be used for backward compatibility probing over STDIO transport. This is the companion mechanism for a handshake-less protocol—don't know what version the other side is? Just ask.
4. subscriptions/listen Replaces GET SSE Streams (SEP-2575)
In the old protocol, clients opened a long-lived SSE connection via HTTP GET to receive server notifications (list changes, resource updates). The new specification replaces this with subscriptions/listen: a long-lived POST response stream where clients subscribe by type (toolsListChanged / promptsListChanged / resourcesListChanged / resourceSubscriptions), the server confirms the subscription, and marks subsequent notifications with a subscriptionId. Request-level notifications (notifications/progress, notifications/message) continue to travel over their respective request's response stream and are unaffected.
5. ping, logging/setLevel, roots/list_changed Removed (SEP-2575)
Three methods/notifications disappear. Log level becomes request-level: io.modelcontextprotocol/logLevel in _meta is carried with the request, and the server must not send log notifications for requests without this field. The liveness check responsibility of ping is handed over to the transport layer itself (HTTP status codes, TCP connections).
6. Tasks Moved from Core Protocol to Official Extension (SEP-2663)
The experimental Tasks introduced in 2025-11-25 have entered a standalone extension (io.modelcontextprotocol/tasks) and been redesigned around statelessness: the blocking tasks/result is replaced by tasks/get polling, a new tasks/update supports clients sending supplementary input to the server, tasks/list is removed (cannot safely scope without sessions), and servers can return task handles without request-level opt-in. Implementations built on the old Tasks API must migrate—this is a breaking change explicitly named in the official migration recommendations.
7. MRTR Replaces Server-Initiated Requests (SEP-2322)
In the old protocol, servers could actively send requests to clients: roots/list (ask for filesystem root directories), sampling/createMessage (ask for model completions), elicitation/create (ask for user input). The new specification abolishes all of these, replacing them with the Multi Round-Trip Requests pattern:
- When the server discovers missing information while processing a request, it returns
InputRequiredResult(resultType: "input_required"), listing what the client needs to supplement in theinputRequestsfield; - The client collects the answers (possibly asking the user), and re-initiates the original request carrying
inputResponses; - Any server instance can handle this retry—because all the required information is in the payload, no session needed.
Combined with the mandatory rule that "server-initiated requests can only occur during the processing of a client request" (SEP-2260, upgraded from SHOULD to MUST), users will never be interrupted out of the blue.
8. resultType Mandatory (SEP-2322)
All results must carry the resultType field: "complete" (normal result) or "input_required" (MRTR intermediate result). Clients must treat results from old protocol servers that omit this field as "complete".
9. SSE Stream Resumption Capability Removed (SEP-2575)
The Last-Event-ID header and SSE event IDs are removed from Streamable HTTP transport. If the response stream breaks, in-flight requests are lost, and the client must re-initiate with a new request ID. The complexity of checkpoint resumption is cut, traded for the simplicity of "any instance can handle the retry."
A Quick Note: Deprecation List and Minor Changes
This specification also establishes a formal Feature Lifecycle and Deprecation Policy (minimum 12-month deprecation window) and posts a deprecation list:
| Deprecated | Official Migration Recommendation |
|---|---|
| Roots | Pass directories/files via tool parameters, resource URIs, or server configuration |
| Sampling | Integrate LLM provider APIs directly, not via MCP relay |
| Logging | Write to stderr for stdio scenarios, use OpenTelemetry for remote scenarios |
| HTTP+SSE Transport (deprecated since 2025-03-26) | Migrate to Streamable HTTP |
| OAuth 2.0 Dynamic Client Registration (RFC 7591) | Switch to Client ID Metadata Documents |
Minor changes worth noting for Java developers: Resource Not Found error code changed from -32002 to -32602 (aligning with JSON-RPC standard); error code space formally partitioned (-32000~-32019 for implementation custom, -32020~-32099 reserved for specification, new error codes HeaderMismatch / MissingRequiredClientCapability / UnsupportedProtocolVersion fall at -32020 / -32021 / -32022 respectively); Mcp-Method / Mcp-Name become standard headers for Streamable HTTP POST; list results required to carry ttlMs / cacheScope caching hints; OTel's traceparent / tracestate / baggage written into _meta become standard tracing conventions.
3. Java Ecosystem Pitfall Log
With the specification explained, let's get to the main topic: what you'll step on if you start coding on the Java side now.
Pitfall 1: The Two "Stateless" Concepts Are Not the Same Thing at All (The Most Critical Conceptual Trap)
If you've read Spring AI's documentation, you'll find it has long had the spring.ai.mcp.server.protocol=STATELESS configuration (used in the 10th minute of the previous quickstart article). It's easy to conclude: "Spring AI already supports statelessness, the new specification just makes our existing configuration the default."
Wrong. These are two completely different generations of "stateless":
| Spring AI's STATELESS | 2026-07-28 Spec Statelessness | |
|---|---|---|
| Specification Generation | A stateless variant of the 2025-11-25 specification | The core form of the 2026-07-28 specification |
| initialize Handshake | Still exists (client handshakes as usual, server doesn't allocate a session) | Completely removed, requests are self-describing |
| Session | Server doesn't maintain session state, but protocol-level session semantics still exist | The concept of a protocol-level session disappears entirely |
| Server-Initiated Requests | Mechanism retained at protocol level (but unavailable in Spring AI's STATELESS mode, see minute 10 of the previous article) | All abolished, replaced by MRTR |
| Implementation Vehicle | MCP Java SDK 2.0.x's stateless server | No Java SDK implementation yet |
The evidence chain is solid: MCP Java SDK 2.0.0's release notes explicitly state "tracks the latest 2025-11-25 MCP specification"; across all release notes up to 2.0.1 (released 2026-08-19), not a single version mentions 2026-07-28. Spring AI 2.0.x is built on top of this SDK, so naturally it can only speak the 2025-11-25 "dialect."
Practical implication: Your Spring AI MCP Server configured with STATELESS is still a legacy (2025-11-25) server in the eyes of a new protocol client—it has a handshake, it has the old notification mechanism. It only solves the single dimension of "multi-replica deployment" (which is indeed useful), don't mistake it for new specification compatibility.
Pitfall 2: MCP Java SDK Server Side Hasn't Caught Up Yet, Wait or Not?
Here's the timeline (all verified against GitHub Releases):
| Date | Event |
|---|---|
| 2026-05-21 | 2026-07-28 Spec RC locked, entering ten-week validation window |
| 2026-06-11 | MCP Java SDK 2.0.0 GA (corresponding to 2025-11-25 spec) |
| 2026-07-28 | 2026-07-28 Spec officially released |
| 2026-08-14 | LangChain4j 1.19.0 released, MCP client supports 2026-07-28 (#5881) |
| 2026-08-19 | MCP Java SDK 2.0.1 released—11 changes all fixes and dependency upgrades, no new spec support |
The official RC announcement's expectation for Tier 1 SDKs was "to provide support within this window" (expected, phrasing was not mandatory). On the Java SDK side, two weeks after the spec's official release (2026-08-13), design issue #1089 "MCP Spec 28-7-2026 Design" was opened, status in progress, but no timeline commitment was given. Should you wait? My judgment is: server-side doesn't need to wait, and doesn't need to fear:
- The breaking changes of the new specification for the server side are concentrated in the transport layer and session management—precisely the parts the SDK and framework handle for you. When Spring AI upgrades to an SDK version supporting 2026-07-28, your
@McpToolannotated code will likely not need a single line changed (tools themselves are independent of protocol version); - What needs early action is code dependent on protocol semantics: literal error code matching (see Pitfall 5), subscription and notification logic (see Pitfall 4), and existing implementations that used the experimental Tasks API (officially stated as must-migrate).
Pitfall 3: LangChain4j Already Supports It, but Default Auto-Detection Has a 30-Second Pitfall
The only one in the Java ecosystem that has kept up with the new specification is LangChain4j 1.19's MCP client. It supports both generations of the protocol simultaneously: 2025-11-25 (legacy, stateful) and 2026-07-28 (modern, stateless), with auto-detection by default—sends a server/discover request at startup, and if the server errors or times out, falls back to legacy handling.
The pitfall lies in the timeout default: protocolDetectionTimeout defaults to equal initializationTimeout, which is 30 seconds. The official documentation explains the reason for this default: many MCP servers are launched as child processes and need cold start time; a detection window that's too short would misclassify modern servers as legacy ones. But for scenarios connecting to remote servers, a 30-second detection wait makes your application startup (or first tool call chain) feel inexplicably slow, and because the "silent timeout fallback to legacy" only logs a warning, with no error—troubleshooting is very stealthy.
The solution is to explicitly specify the protocol version when known, skipping detection:
// Known new spec server: specify directly, save the server/discover round trip
McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.protocolVersion("2026-07-28")
.build();
// Known legacy server (e.g., all Spring AI 2.0.x MCP Servers)
McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.protocolVersion("2025-11-25")
.build();
The official docs also call out another applicable scenario: some older MCP servers, upon receiving an unrecognized method, don't return an error but terminate directly. Explicitly specifying the protocol version can completely avoid the problem of such servers being killed by the probe request.
A practical corollary: when using a LangChain4j client to connect to a Spring AI 2.0.x MCP Server, regardless of the auto-detection result, the actual communication will inevitably fall into legacy mode—because the server side only speaks the 2025-11-25 "dialect." Rather than letting the client wait in vain for probing, just use protocolVersion("2025-11-25") directly. There's also an interoperability detail worth knowing: MCP Java SDK 2.0.0 receiving a server/discover probe request will directly return HTTP 500 (java-sdk #1072, P1 bug, fixed in 2.0.1)—so when using LangChain4j auto-detection against a java-sdk 2.0.0 based server, that 500 for the probe request in the logs isn't your service failing, it's its "fallback to legacy" mechanism working normally.
Pitfall 4: Notification and Subscription API Shapes Are Incompatible Between the Two Protocol Generations
LangChain4j's documentation specifically lists the functional differences between the two protocol generations, easy to step on when writing cross-protocol client code:
Resource Subscriptions—legacy is single URI subscription with callback:
// legacy (2025-11-25)
McpClient mcpClient = DefaultMcpClient.builder()
.transport(transport)
.onResourceUpdated((client, uri) -> client.readResource(uri))
.build();
mcpClient.subscribeToResource("file:///status");
// modern (2026-07-28): batch subscription, returns subscription ID
long subId = mcpClient.subscribeToResources(List.of("file:///status", "file:///config"));
mcpClient.unsubscribeFromResources(subId);
Server Notification Channel—the auxiliary GET SSE stream of the legacy era (LangChain4j's subsidiaryChannel(true), off by default) no longer exists under the new protocol; all 2026-07-28 notifications go through subscriptions/listen. If your code has reconnection logic for the GET SSE stream, it needs a full rewrite during migration.
Pitfall 5: Literal Error Code Matching Will Silently Fail During Upgrades
The error code change mentioned earlier is singled out because it's the most typical "silent failure":
- Resource Not Found changed from MCP's custom
-32002to the JSON-RPC standard-32602(Invalid Params). Any literal match likeif (errorCode == -32002)will never enter the branch after the server upgrades to the new specification—no error, no alert, just business logic quietly going wrong; - After error code space partitioning, the newly introduced specification error codes (
HeaderMismatch-32020,MissingRequiredClientCapability-32021,UnsupportedProtocolVersion-32022) differ from the old drafts era numbers (which were -32001 / -32003 / -32004 respectively). Error handling code across SDK versions needs attention.
Troubleshooting suggestion: globally search for literals -32002, -32003, -32004; converge error code semantic judgments into constants or enums as much as possible, don't scatter hardcoded values.
Pitfall 6: x-mcp-header—One of the Most Practical New Features in the New Spec
The new specification requires Streamable HTTP POST to carry the standard headers Mcp-Method / Mcp-Name, and supports generating custom HTTP headers from tool parameters: the server marks parameters in the tool's input schema with x-mcp-header, and the client will place them in both the request body and the HTTP header when calling.
LangChain4j client already supports this: parameter values are converted into request headers shaped like Mcp-Param-X-Tenant-Id (non-ASCII values automatically Base64 encoded). The limitation is that parameter types can only be string / integer / boolean; tools violating this constraint are excluded from listTools() and a warning is logged—like Pitfall 3, this is a troubleshooting blind spot of "no error, just a log." In multi-tenant scenarios, remember to check if the tool list is complete after integration.
This mechanism is tailor-made for multi-tenant routing and API-Key authentication on the gateway side: tenant ID goes in the HTTP header, the gateway and auth layer don't need to parse the JSON-RPC message body.
4. Explicit Handles: The Official New Answer for "Stateful Needs"
After going stateless, what about "my tool needs cross-call state" (shopping carts, browser sessions, multi-step workflows)? The official answer is the explicit handle pattern: the server generates an ID (basket_id, browser_id), passes it to the model as a regular tool parameter, and the model passes the handle back in subsequent calls.
There's a paragraph in the official announcement worth quoting in full: this pattern is even more powerful than sessions—the model can combine handles across tools, reason about relationships between handles, and pass handles to other tools, things that session state hidden in transport metadata cannot do. My own extension: since handles are just ordinary data, any server instance can handle requests carrying the same handle, and the problem of "state only living in one instance's memory" naturally disappears; furthermore, signing handles can prevent forgery—this falls under engineering practice beyond the specification, not covered in the official announcement.
For Java developers, the implementation posture is straightforward: change the state previously implicitly associated via Mcp-Session-Id into the first explicit parameter of the tool:
// Old thinking: state hung on session (Spring AI stateful mode)
// New thinking: state hung on handle, any instance can process
@McpTool(name = "cart_add", description = "Add item to cart. Omit cart_id on first call; a new handle will be returned")
public CartAddResult addToCart(
@McpToolParam(description = "Cart handle, can be omitted on first call", required = false) String cartId,
@McpToolParam(description = "Product SKU", required = true) String sku,
@McpToolParam(description = "Quantity", required = true) int quantity) {
Cart cart = (cartId == null) ? cartService.create() : cartService.load(cartId);
cart.add(sku, quantity);
return new CartAddResult(cart.id(), cart.itemCount()); // return handle to model
}
Note the guiding language in description—how the model correctly uses the handle depends on these few words.
5. Migration Advice: What Scenarios to Act On, What to Wait For
After a month of observation plus the verification above, here are my decision recommendations:
| Your Scenario | Recommendation |
|---|---|
| New MCP Server (Spring AI 2.0.x) | Proceed as normal: 2025-11-25 spec's Streamable HTTP is fully production-usable; use the explicit handle pattern to manage state from day one, don't rely on sessions |
| Existing MCP Server, multi-replica deployment pressure | Use protocol: STATELESS to solve deployment issues (note Pitfall 1: this is an old spec variant), simultaneously refactor session dependencies into explicit handles—once these two steps are done, the transport layer code will barely need changes when upgrading to the new spec in the future |
| Java MCP Client (LangChain4j) | Upgrade to 1.19.0; explicitly set protocolVersion when connecting to known servers, avoiding the 30-second detection; check literal error code matching (Pitfall 5) and subscription code (Pitfall 4) |
| Implementations using the 2025-11-25 experimental Tasks API | Must migrate—in the new spec, Tasks has been moved to an extension (io.modelcontextprotocol/tasks) with a redesigned lifecycle; this is a breaking change explicitly named by the official side |
| Waiting for Spring AI to support the new spec | Watch the MCP Java SDK release notes (the version supporting 2026-07-28 is likely a 2.1 or 3.0 level upgrade); @McpTool annotated code is expected to remain compatible, the migration will mainly be at the transport and configuration layer |
One-sentence summary for those in a hurry: The protocol is stateless, the server-side ecosystem is still on the way; when writing code today, make state explicit, converge error codes and subscription logic—that's paving the way for the new specification.
Summary
Looking back at the essence of this upgrade: MCP has entirely swapped out the concept of "connection"—from "first handshake to establish a session, then back-and-forth within the session," to "every request carries its own full context, any instance can take over." This is identical in philosophy to HTTP versus persistent TCP connections, and completely isomorphic to the historical progression of REST versus stateful sessions. The infrastructure for AI tool calling is retracing the old path of the Web, and the Java ecosystem's position this time is: the client side (LangChain4j) is in the first tier, the server side (MCP Java SDK / Spring AI) still needs a version cycle.
For Java developers, I suggest taking away three key points from this article: distinguish the two generations of "stateless" (Spring AI's STATELESS ≠ the new spec), the explicit handle pattern (the correct posture for managing state in the stateless era), and LangChain4j's detection timeout and silent fallback (the easiest performance pitfall for new clients to step into).
References
- MCP 2026-07-28 Specification Changelog (Official GitHub)
- The 2026-07-28 MCP Specification Release Candidate (Official Blog)
- MCP Java SDK Releases
- LangChain4j — MCP Tutorial (Official Documentation)
- LangChain4j 1.19.0 Release (#5881 MCP client according to 2026-07-28)
- MCP Feature Lifecycle and Deprecation Policy
- Spring AI — Stateless Streamable-HTTP MCP Servers