Java Cup
Inside Java

News and views from members of the Java team at Oracle

Evolving a Java MCP Server During MCP Specification Upgrades

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.```

What Does the 2026-07-28 MCP Specification Bring ?

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:

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?

A Bridge to the New Contract

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.

A Quick Look at the Java MCP Server Example

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:

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.

Why Adoption of New Technologies Need an Adapter Layer

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:

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:

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.

Compatibility Through Implementation

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:

The handler exposes the public protocol version constant (public static final String PROTOCOL_VERSION = "2026-07-28"), but also supports the following methods:

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 and Assessing MCP Server Conformance

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:

To cover compatibility directions of both MCP protocol versions, the existing unit and integration tests from the root package have been enhanced as follows:

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:

  1. Start the MCP server under test.

  2. Execute one or more protocol scenarios against the server.

  3. 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

Closing Thoughts

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.