News and views from members of the Java team at Oracle
As a framework, library, or protocol gains adoption, edge cases reveal ambiguities, interoperability gaps, and implementation challenges that earlier versions did not anticipate. The Model Context Protocol (MCP) is no exception. The 2026-07-28 MCP specification introduces refinements around stateless protocol core, discovery, cache metadata, and typed tool output to make implementations more predictable and interoperable across clients and servers.
This article covers a compatibility-first approach to adopting newer MCP specifications, showing how a Java MCP server can evolve at the pace of the ecosystem without disrupting the users who depend on it.```
In late July 2026, the MCP protocol got a new set of specification changes designed to tighten the transport boundary around stateless requests, explicit protocol headers, discovery, cache metadata, and typed tool output.
The MCP 2026-07-28 release moves the protocol core to stateless HTTP, while removing the Mcp-Session-Id presence along with initialize / initialized exchange. This change means that a remote server can serve each request through any server instance behind a simple round-robin load balancer, having it routed by MCP headers, and cached according to the response metadata it returns. However, if your existing MCP server needs to carry state (documents, searches, etc.) across calls, consider moving that from the protocol level into your application model. You can then reference state explicitly through identifiers or handles passed between client and server, rather than being implicitly associated with an MCP session.
Java MCP servers are commonly used in production to expose enterprise tools that query databases, call internal APIs, and provide agents with controlled access to documents, database schemas, knowledge bases, and application-specific context through MCP resources.``` For Java developers who already have an MCP-style HTTP service, the changes lie in a new request contract:
MCP-Protocol-Version: 2026-07-28 identifies the protocol version on every request.
Mcp-Method identifies the JSON-RPC method at the HTTP layer, so gateways and load balancers do not have to inspect the body.
Mcp-Name identifies the tool, resource, or prompt name when the method needs one.
server/discover replaces the up-front initialize handshake for clients that need capabilities before making a call.
List responses such as tools/list can include ttlMs and cacheScope, so clients can cache deterministic capability views.
Tool schemas can use JSON Schema 2020-12, and structuredContent is no longer constrained to an object-only shape.
Java developers live with a strong compatibility culture, where every new Java feature does not freeze evolution of related projects, nor forces every user to migrate the same day. For Java developers, adopting a new protocol version is rarely just a matter of implementing the latest specification. Production systems have existing clients, established integrations, and compatibility guarantees that cannot simply be ignored.
So, how can a Java MCP server make the new behavior available without breaking existing integrations?
To demonstrate adoption of a newer protocol version while preserving the behavior people already rely on, the starting point of this article is a previously discussed version of the urgency-mcp server. The workflow that can leverage the urgency-mcp is handling a patient complaint by transforming it in a helpdesk ticket, AI triage asks for an urgency score, and scores at or above the critical threshold will trigger escalation.
The original urgency-mcp server builds on top of Helidon SE and the Helidon MCP extension. If you want to recreate the MCP server, add the Helidon web server and MCP server extension dependencies:
<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.extensions.mcp</groupId>
<artifactId>helidon4-extensions-mcp-server</artifactId>
<version>${mcp.extension.version}</version>
</dependency>
Because urgency-mcp server uses Helidon annotation processing, the compiler needs the corresponding annotation processors:
<annotationProcessorPaths>
<path>
<groupId>io.helidon.bundles</groupId>
<artifactId>helidon-bundles-apt</artifactId>
<version>${helidon.version}</version>
</path>
<path>
<groupId>io.helidon.extensions.mcp</groupId>
<artifactId>helidon4-extensions-mcp-codegen</artifactId>
<version>${mcp.extension.version}</version>
</path>
</annotationProcessorPaths>
The helidon-bundles-apt bundle helps development as it keeps the Helidon dependencies, i.e., service-registry, builder, JSON, and related processors, aligned with the Helidon parent version and avoids hand-curating a fragile list of processor artifacts. The MCP extension code generator is listed separately because it comes from the MCP extension artifact, not from the core Helidon processor bundle.
Helidon represents an MCP server as an HttpFeature, registered as part of web server routing, so McpUrgencyServer got annotated with @Mcp.Server, using an @Mcp.Path.
@Mcp.Path(McpUrgencyServer.MCP_PATH)
@Mcp.Server(McpUrgencyServer.MCP_SERVER_NAME)
public final class McpUrgencyServer {
static final String MCP_PATH = "/urgency";
public static final String MCP_SERVER_NAME = "helidon-mcp-urgency";
// ...
}
McpUrgencyServer delegates to UrgencyInferenceService through a lazy scorer supplier, so health checks and protocol discovery can succeed before model loading or OpenAI API-key validation happens. Furthermore, clients call the operation that the server has declared via @Mcp.Tool annotation:
@Mcp.Tool(value = "Get urgency score (0-10) for a support ticket complaint",
title = "Get urgency score",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true,
openWorldHint = false)
McpToolResult getUrgency(@Mcp.Description("complaint text to score") String phrase) {
readOnlyHint, destructiveHint, idempotentHint and openWorldHint explain that the tool is a repeatable local scoring read, not an operation that mutates patient data or reaches into an unknown system. Next, the MCP tool does not score the complaint directly. Instead, it delegates that decision based on (the?) active scoring configuration in application.yaml:
urgency:
provider: local
providers:
local:
model:
name: model-scorer-local.dnet
location: ../urgency/model
embedding:
name: sentence-transformers/all-MiniLM-L6-v2
location: ../urgency/model
dimensions: 384
openai:
model:
name: model-scorer-openai.dnet
location: ../urgency/model
embedding:
model:
name: text-embedding-3-small
dimensions: 1536
Programmatically, UrgencyInferenceConfiguration creates local or OpenAI configuration based on urgency.provider instruction:
LocalEmbeddingGenerator handles the local embedding path, while OpenAIEmbeddingGenerator handles the OpenAI embedding path.
DeepNettsInferenceResources loads the configured .dnet scorer.
DeepNettsUrgencyInferenceEngine passes embeddings into the scorer.
To have the MCP server up and running locally, first build and run it from the urgency-mcp directory:
mvn clean package
java --enable-preview -jar target/urgency-mcp.jar
By default, the service will become available on port 9090, but you can override its configuration at runtime.
Java applications tend to adopt new technology diligently, by keeping the stable API path working, add the new behavior beside it, test the boundary, and remove the bridge only when the underlying stack can express the new contract cleanly. That can be the right approach for MCP migrations too, regardless of the specification state.
For existing MCP server implementations, the guidance is to start by looking for server logic that assumes initialize, Mcp-Session-Id, sticky routing, or connection-level state. Then check whether client identity, capabilities, or tracing assumptions need to move into _meta on each request. You should track server/discover because that is the proposed replacement for learning server capabilities up front. You should also plan deprecation migrations if your server depends on the following MCP protocol features:
Roots for when your client advertises filesystem roots to the server.
Sampling for when server can request LLM completions from the client.
Logging for when you have protocol-level log messages from server to client.
The earliest removal window for those deprecated features is July 2027, but waiting until then can turn your migration into a release scramble. Finally, you should evaluate whether server-rendered interactive UI (MCP Apps) or durable long-running operations belong in your user experience.
In the urgency-mcp project, model files, embedding clients, and scorer instances are service dependencies, not MCP session data. That makes the existing Java design compatible with the direction of MCP 2026-07-28 specification. Furthermore, the server does not need to maintain per-client MCP state as getUrgency receives a prompt, delegates to the scorer, and returns a number. So, the McpUrgencyServer can be marked with @Mcp.Stateless, which documents and configures the server as a stateless tool facade. Yet, be aware that @Mcp.Stateless is an annotation-level statement about the server shape, and it does not by itself implement every release candidate rule.
Generally speaking, some clients will not be able to move to MCP 2026-07-28 specifications immediately as they may be pinned to an older SDK, deployed on a slower release cycle, or integrated through infrastructure that still expects the initialized 2025 flow. Those clients should not break just because the server starts accepting the newer stateless contract. For example, Helidon MCP clients should still be able to initialize urgency-mcp without the 2026 protocol header:
curl -X POST http://localhost:9090/urgency \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"readiness","version":"1.0.0"}}}'
but a migrated stateless client should leverage server/discover instead:
curl -X POST http://localhost:9090/urgency \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}'
As compatibility is part of the migration goal for the urgency-mcp project, an adapter for the new specification belongs at the routing boundary. A clean way to introduce an adaptation layer is to go through McpRequestLoggingFeature class as that one is already a Helidon HttpFeature, so it runs before the generated MCP route and can make a protocol-routing decision without touching the generated annotation server or the urgency-scoring code. This approach uses the existing webserver interception point, selects only the new protocol traffic, and lets everything else continue through the normal Helidon MCP path.
The selection of the new specification way of working happens only when McpRequestLoggingFeature intercepts a request that has both:
the request path matches the value of McpUrgencyServer.MCP_PATH and
MCP-Protocol-Version is exactly 2026-07-28.
If either condition is missing, the feature calls chain.proceed(), so clients using the 2025 flow still reach the generated Helidon MCP route. When both conditions match, the feature converts the HTTP request into McpProtocolRequest and passes it to StatelessMcpProtocolHandler. From there, the handler owns only the 2026 protocol concerns: validates the headers, rejects Mcp-Session-Id, answers discovery and tool list requests, and delegates tool execution back to McpUrgencyServer.score(String). The StatelessMcpProtocolHandler is a temporary adapter, and when native annotations support this exact contract, the routing branch can shrink or disappear without changing the urgency-scoring domain code.
With that routing boundary established, let's look at the exact 2026 behavior the adapter enforces in code.
When an incoming request has been routed to the 2026 path, the server passes responsibility to the custom StatelessMcpProtocolHandler that validates the 2026 stateless contract:
rejects Mcp-Session-Id;
requires MCP-Protocol-Version: 2026-07-28;
requires Mcp-Method;
requires the JSON-RPC body method to match Mcp-Method;
requires jsonrpc: "2.0";
requires Mcp-Name: getUrgency for tools/call;
requires params.name: getUrgency for tools/call;
requires non-blank arguments.phrase.
The handler exposes the public protocol version constant (public static final String PROTOCOL_VERSION = "2026-07-28"), but also supports the following methods:
server/discover to return server identity, supported protocol version, tool capability, and cache metadata.
ping to check server responsiveness.
tools/list to return one tool, getUrgency, with cache metadata (ttlMs: 300000 and cacheScope: public).
tools/call delegates to McpUrgencyServer.score(text) and returns both output forms: content for text content containing the score and structuredContent with numeric score for typed clients.
Yet, the migration needs evidence that both paths still behave as intended. A few concrete HTTP requests make the two protocol paths visible, while the unit and integration tests, together with MCP conformance checks, can repeatedly validate the changes.
Testing your code is important because it this way you catch bugs early and check whether existing assumptions on business logic are still valid. In this project, the tests cover application behavior behind the tools and the protocol behavior exposed to MCP clients:
The root package (com.example.urgency) holds application tests covering server startup, HTTP health/readiness, MCP endpoint behavior, and serialization configuration, including both legacy and newer MCP paths.
Tests within config package verify runtime configuration parsing, defaults, precedence, optional values, and validation of required settings.
Tests for local and OpenAI embedding implementations sit in embedding package, and cover construction, input validation, configuration, and delegation to the underlying embedding provider.
Tests in mcp package focus on the new stateless MCP protocol behavior, including protocol headers, discovery, tool operations, invalid requests, and rejection of legacy session headers.
The service package holds tests relevant for inference configuration, provider selection, model/resource handling, caching, and the success and failure paths of the urgency inference service.
RequiredTextTest in validation package verifies the common mandatory text validation rules, including accepted, trimmed, blank, and missing values.
To cover compatibility directions of both MCP protocol versions, the existing unit and integration tests from the root package have been enhanced as follows:
MainTest.mcpInitializeAcceptsSupported2025Protocol proves that the Helidon path can still accept initialize from the clients not be able to use the MCP 2026-07-28 specifications.
MainTest.statelessDiscoverDoesNotCreateSession proves that a request using 2026-07-28 protocol version returns server/discover data and does not return Mcp-Session-Id.
MainTest.statelessToolsListContainsCacheHints proves ttlMs and cacheScope are exposed as cache metadata.
MainTest.statelessRejectsLegacySessionHeader proves that a request using 2026-07-28 protocol version and carrying Mcp-Session-Id is rejected.
The easiest way to build confidence in your changes is by running the local test suite with mvn test or from within your IDE.
Furthermore, to demonstrate that the server behaves according to the MCP protocol, you can run the MCP Conformance Framework. This companion project exercises an MCP server against a collection of protocol scenarios and verifies that its requests, responses, and error handling comply with the specification. Typically, a conformance run consists of three steps:
Start the MCP server under test.
Execute one or more protocol scenarios against the server.
Verify that the responses conform to the selected MCP specification.
As the specification evolves, so does the conformance suite. New protocol features are accompanied by new scenarios, allowing implementers to verify that they correctly support the latest behavior while continuing to exercise compatibility paths where appropriate. For this migration, the goal is slightly different as adoption of the 2026-07-28 specification is done while continuing to support clients implementing the 2025-06-18 version. That means the conformance script strategy must validate both protocol paths independently.
During the transition to a new MCP specification, the published conformance framework may not yet test every draft feature. For such a situation, an approach can be to combine the official conformance runner with specific project validation for draft behavior, then retire those custom checks as the framework adds support for them. The conformance script for this project therefore separates draft validation from official runner scenarios. A plain run of the script starts the server, and probes local draft checks for discovery, tool listing, and session-header rejection:
# draft-check defaults include in this run:
# MCP_CONFORMANCE_PROTOCOL_VERSION=2026-07-28
# MCP_DRAFT_CHECKS="server-discover tools-list reject-session-header"
# MCP_CONFORMANCE_SCENARIOS="ping tools-list"
MCP_CONFORMANCE_ENABLED=true mvn -Pconformance verify
If the installed conformance runner later adds draft scenarios, those can be passed explicitly through MCP_CONFORMANCE_SCENARIOS. To check the Helidon annotation-server path with scenarios that exist in the published runner, override both values for MCP_CONFORMANCE_PROTOCOL_VERSION and MCP_CONFORMANCE_SCENARIOS:
MCP_CONFORMANCE_ENABLED=true \
MCP_CONFORMANCE_PROTOCOL_VERSION=2025-06-18 \
MCP_CONFORMANCE_SCENARIOS="server-initialize ping tools-list" \
mvn -Pconformance verify
The example MCP Server keeps the existing Helidon annotation path for clients that cannot migrate immediately, adds a stateless path for clients that expect compliance with MCP 2026-07-28, and makes both paths call the same urgency scoring code. The main lesson learned from this migration is that a healthy protocol adoption requires satisfying both preservation of compatibility and domain behavior, backed up by tests and conformance checks.
Note: This article is a migration snapshot, not a permanent statement about the Helidon MCP extension or the MCP Conformance Framework. The Helidon team is planning to add support for the 2026-07-28 contract in a future Helidon MCP release, and newer conformance suite releases may add or rename scenarios for release candidate behavior. Treat the adapter, script defaults, and draft checks here as example code that should evolve with the MCP specification, the Helidon extension, and the conformance runner versions you actually use.