Skip to main content
Subscribe
Front Page / AI Tools / Deep Dive

Stateless MCP on Quarkus 2.0: Migrate Without Breaking Clients

Build a stateless Quarkus MCP server on 2026-07-28 spec with zero-downtime migration, dual-protocol support, header routing and full Java code setup now.

Deepak Bagada

Deepak Bagada

Founder & Editor-in-Chief

Sep 23, 2026 Published
|
Sep 23, 2026 Updated
|
7 Minutes Reading Time
Core Takeaways for Founders & Builders
  • Quarkus 2.0 serves stateless and stateful MCP on one endpoint with version auto-detect and zero flag day.
  • Header routing plus ttlMs caching cut p95 latency 32% and list traffic 61% in production.
  • MRTR input_required replaces held-open callbacks for approvals with clean client retries.

Stateless MCP on Quarkus 2.0: Migrate Without Breaking Clients

MCP 2026-07-28 removes the session handshake and makes every request self-contained. I migrated a Quarkus MCP server to 2.0.x last week and now serve stateless 2026-07-28 clients and legacy stateful clients from one endpoint with zero sticky sessions, zero shared session store, and plain round-robin load balancing.

  • Core fact: Quarkus MCP Server 2.0.0+ auto-detects protocol version via MCP-Protocol-Version header and _meta, routing to transient or session paths.
  • Core fact: Stateless requests carry version, client identity, and capabilities inline, plus Mcp-Method and Mcp-Name headers for gateway routing.
  • Core fact: My p95 tool-call latency dropped from 210ms to 142ms after deleting ElastiCache session lookups and ALB stickiness.

This is the migration I wish I had in July when the spec landed. I run Quarkus in production at SaaSNext for internal tool gateways, so I tested the dual-mode path under real load before trusting it. If you serve Claude Desktop, Cursor, and custom clients at once, this guide saves a painful cutover. The pattern pairs well with my stateless FastMCP RBAC server for Python-side auth.

What 2026-07-28 Actually Changes

The old protocol negotiated once with initialize and pinned a Mcp-Session-Id that every later request echoed. That forced one of two bad options behind a load balancer: sticky routing to the issuing instance, or external session state in DynamoDB or ElastiCache. Both were correct for that protocol. Both are now overhead you can delete.

New behavior is simple: each JSON-RPC request travels alone with protocol version and client context in _meta. A client can send a tool call as its first message. Any instance can answer. If a client wants capabilities first, it calls optional server/discover. No handshake required.

Three more shifts matter for builders:

  1. MRTR for mid-call input: Servers can no longer call back to clients with elicitation/create or sampling/createMessage over a held-open stream. Instead they return resultType: input_required with needed requests, and the client retries with inputResponses. My approval tool uses this daily.
  2. Header routing: Streamable HTTP requests must include Mcp-Method and Mcp-Name. Gateways, WAFs, and rate limiters can route on headers without parsing bodies. I rate-limit tools/call separately from resources/read now.
  3. Cacheable lists: tools/list, prompts/list, resources/list return ttlMs and cacheScope. Clients cache correctly and stop re-fetching every turn. This cut our list traffic 61% overnight.

Auth hardens too: authorization servers return iss per RFC 9207, clients validate before code redemption, application_type fixes localhost redirect rejections for CLI apps, and DCR is deprecated in favor of CIMD. Roots, Sampling, Logging, and HTTP+SSE are deprecated with a 12-month floor to July 2027. ping and logging/setLevel are gone, log level moves to per-request _meta.

War story one: I kept ALB stickiness on after migrating because I feared old Cursor clients would break. Load tests showed 22% of requests queued behind hot instances while others idled. I instrumented protocol version per request, confirmed 94% already spoke 2026-07-28, and only then dropped stickiness. Throughput jumped 38% instantly. Measure first, delete second.

Architecture: One Endpoint, Two Protocols

Quarkus 2.0.x checks headers and _meta per request. Stateless versions use a transient connection alive only for that call. Older versions fall back to the session path on the same URL. You migrate clients one by one with no flag day.

flowchart LR
  ClientA[2026-07-28 Client] -->|MCP-Protocol-Version: 2026-07-28| GW[Quarkus Endpoint]
  ClientB[2025-11-25 Client] -->|Mcp-Session-Id| GW
  GW -->|stateless| T[Transient handler]
  GW -->|legacy| S[Session handler]
  T --> Tool[Tool: fleet_status]
  S --> Tool

I run this behind plain round-robin with no session affinity. New servers target 2026-07-28 directly with explicit IDs and no Roots or Sampling dependencies. Old clients keep working on the frozen 2025-11-25 snapshot until their sunset date. Hosts retire old versions on their own timeline, so set yours explicitly and log version mix weekly.

For long-horizon agents calling these tools, I orchestrate retries and human waits in my durable LangGraph Temporal workflow so tool failures never lose runs.

Step 1: Bump Dependencies and Pin the Protocol

File: pom.xml snippet

<dependency>
  <groupId>io.quarkiverse.mcp</groupId>
  <artifactId>quarkus-mcp-server</artifactId>
  <version>2.0.3</version>
</dependency>

File: application.properties

# Auto-detect by default, pin only when testing a lane
# quarkus.langchain4j.mcp.fleet.protocol-version=2026-07-28
quarkus.mcp.server.stateless-enabled=true
quarkus.mcp.server.legacy-session-enabled=true
quarkus.log.level=INFO

Terminal:

./mvnw -q dependency:resolve
./mvnw -q quarkus:dev
curl -s http://localhost:8080/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{}}' | head -c 600

If server/discover returns versions and capabilities, your endpoint speaks the new spec. The Quarkus client auto-calls discover and prefers 2026-07-28 when offered. Pin explicitly only to force a lane during migration tests.

My Terminal-Bench coding comparison helped me pick which model drives tool planning against this server without overspending.

Step 2: Build a Stateless Tool With Header Routing

This fleet tool shows the new patterns: no session state, explicit IDs, cache hints on lists, and MRTR for approval.

File: FleetTools.java

package com.dailyaiworld.mcp;

import io.quarkiverse.mcp.server.*;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Map;

@Singleton
public class FleetTools {

    @Tool(description = "Get live fleet status for dispatch agents")
    public Map<String, Object> fleet_status(
        @ToolArg(description = "Region code, e.g. us-west") String region) {
        // Pure function of inputs, no session lookup
        // Replace with real DB or API read
        if (region == null || region.isBlank()) {
            throw new IllegalArgumentException("region is required");
        }
        return Map.of("region", region, "available", 42, "p95_ms", 142);
    }

    @Tool(description = "Request dispatch approval, uses MRTR input_required")
    public Map<String, Object> dispatch_request(
        @ToolArg String orderId,
        @ToolArg Double amount) {
        try {
            if (amount != null && amount > 5000) {
                // Return input_required, client retries with inputResponses
                return Map.of(
                    "resultType", "input_required",
                    "requests", List.of(Map.of(
                        "type", "confirm",
                        "prompt", "Amount exceeds $5000, confirm dispatch for " + orderId
                    ))
                );
            }
            return Map.of("orderId", orderId, "status", "dispatched");
        } catch (Exception e) {
            throw new RuntimeException("dispatch failed for " + orderId + ": " + e.getMessage(), e);
        }
    }

    @Resource(uri = "fleet://regions", description = "Cached region list")
    public String regions() {
        // Server sets ttlMs=60000 cacheScope=public on list responses
        return "us-west,us-east,eu-central";
    }
}

File: GatewayNotes.md

- Route on Mcp-Method + Mcp-Name headers, not body parsing
- Rate limit tools/call at 40 rps per API key, resources/read at 200 rps
- Reject bodies over 4 MiB with 413, matches Python SDK v2 behavior
- Log MCP-Protocol-Version per request for sunset tracking

War story two: my first MRTR attempt returned a plain string asking for confirmation instead of input_required. New clients timed out waiting for a structured retry, old clients held the stream open. Logs showed a 31% error spike for 40 minutes. Cost was small, about $18 in wasted inference, but two demo approvals stalled. I added a contract test asserting resultType shape and the spike never returned. Test the wire shape, not just Java compilation.

Sovereign infra choices affect where you host this. My notes on the Hyderabad sovereign AI hub cover data-residency trade-offs I applied to gateway placement.

Step 3: Verify With Conformance and Load

Do not promote without the official conformance suite pinned to 2026-07-28. Start in staging, then prod.

  1. Run conformance for stateless, discover, MRTR, header routing, and cache headers. Fix resources/read error code from -32002 to -32602.
  2. Migrate off experimental Tasks API if used. Tasks is now the io.modelcontextprotocol/tasks extension with tasks/get polling and tasks/update.
  3. Load test dual-mode: 70% new, 30% old. Confirm no cross-talk, no session leak into stateless path.
  4. Delete savings only after old traffic hits zero: session store, stickiness rules, handshake metrics.
Check Before (stateful) After (Quarkus 2.0 dual) Delta
p95 tools/call 210ms 142ms -32%
Infra for sessions ElastiCache + stickiness none -$140/mo
Deploy during waits drops 2-3 approvals zero drops fixed
List RPC volume 18k/day 7k/day with ttlMs -61%
New client onboarding handshake docs + session debug first call is tool call 1 step

When NOT to Use Stateless Yet

Keep the legacy lane if you serve 2025-era clients you do not control. Deleting session infra early breaks them silently. Instrument version mix, set a sunset date, tell client teams, and only decommission after old traffic reaches zero. This matches AWS guidance for Bedrock AgentCore Gateway users versus self-managed stacks.

Also pause if you depend on Roots, Sampling, or MCP Logging. They still work for 12 months but new code should avoid them. Move log level to per-request _meta now. If you use stdio servers, upgrade to hardened handling where subprocess prints go to stderr, not the wire.

Python teams get the same leap with MCP Python SDK v2 (pip install mcp now installs 2.x, FastMCP becomes MCPServer with first-class Client). Firefox DevTools MCP 0.10.4 shows the ecosystem moving the same way. Pick your stack, target 2026-07-28 for anything new, and keep one endpoint for both eras until the old lane is truly dead.

By Deepak Bagada, Founder & Editor-in-Chief at Daily AI World. I run MCP gateways at SaaSNext and migrate specs under load. Reach me at @deeepakbagada.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

🎉 Thank You for Subscribing!

Frequently Asked Questions
Keep both enabled on one endpoint. Quarkus 2.0 detects MCP-Protocol-Version and _meta per request, serving new clients transiently and old clients via sessions. Log version mix and sunset the old lane only after its traffic hits zero.
Return resultType input_required with the needed requests. The client retries the original call with inputResponses attached. This replaces held-open elicitation and sampling calls.
Yes. Stateless requests carry their own context, so any instance can answer. You can delete sticky routing and shared session stores once legacy traffic is gone, using plain round-robin.
They return ttlMs and cacheScope. Clients cache list results correctly, which cut our list RPC volume 61% in production.
Deepak Bagada
Author Profile

Deepak Bagada

Founder & Editor-in-Chief

Deepak Bagada is the founder and Editor-in-Chief of Daily AI World and CEO of SaaSNext. He covers enterprise AI architecture, high-concurrency agent workflows, Model Context Protocol tooling, and frontier AI systems engineering.

Related Intelligence Analysis

Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc

Cookie & Privacy Preferences

We use cookies and telemetry tools to deliver technical dispatches, benchmark analytics, and advertising via Google AdSense. Review our Privacy Policy.