Váš MCP Server funguje lokálně. Pak Kubernetes zabije session.

Provozujeme Spring AI MCP server v produkci na GKE. Slouží Claude Code jako klient a vystavuje 60+ nástrojů pro správu lezeckých dat — lokality, cesty, stěny, prostě všechno. Autentizační vrstva je solidní: JWT tokeny perzistované v PostgreSQL, duální validace (HS256 service tokeny, RS256 OAuth uživatelské tokeny), kontrola revokace při každém požadavku.
Pak, občasně, Claude Code začne vyhazovat toto:
Session not found: f4456324-79fb-4d84-ab2b-eebf27c5a475Token je platný. Server běží. Endpoint odpovídá. Ale session je pryč.
Co vlastně MCP “sessions” jsou
Pokud nasazujete MCP servery se Streamable HTTP transportem, musíte pochopit, že ve hře jsou dvě zcela nezávislé autentizační vrstvy:
| Vrstva | Co to je | Kde žije | Přežije restart? |
|---|---|---|---|
| Auth token | JWT Bearer token | PostgreSQL / Redis | Ano |
| Transport session | Stav MCP protokolu | ConcurrentHashMap v JVM heap | Ne |
Auth token je váš. Řídíte jeho životní cyklus, perzistenci a revokaci. Transport session je interní účetnictví MCP SDK — mapuje hlavičku Mcp-Session-Id na objekt McpStreamableServerSession, který drží reaktivní subskripce, SSE sinky, stavový automat protokolu a výsledky vyjednávání schopností.
Jde o to, že ji nemůžete perzistovat. Obsahuje živé JVM objekty — Reactor Sinks, Flux pipeline, stav vázaný na vlákna. Serializace do Redis nebo PostgreSQL není životaschopná (podrobnosti níže).
Co vlastně způsobuje “Session not found”
- Rolling deployment — Kubernetes nahrazuje pody jeden po druhém. Nový pod má prázdnou
ConcurrentHashMap. Klienti se starými session ID dostanou 404. - Restart podu — OOM kill, selhání liveness probe, preempce nodu. Stejný výsledek.
- Více replik — Požadavek dorazí na pod B, ale session byla vytvořena na podu A. Bez sticky sessions to náhodně selhává.
Všechny tři jsou standardní chování Kubernetes. MCP SDK na to prostě ještě není navrženo.
Oprava na jeden řádek
Auto-konfigurace MCP serveru ve Spring AI podporuje tři transportní protokoly:
public enum ServerProtocol {
SSE, // Legacy Server-Sent Events
STREAMABLE, // Stateful Streamable HTTP (výchozí)
STATELESS // Stateless Streamable HTTP
}STATELESS implementuje podmnožinu Streamable HTTP specifikace, kde na straně serveru není udržován žádný stav session mezi požadavky. Každý HTTP POST na /mcp je soběstačný — autentizace přes Bearer token, žádná hlavička Mcp-Session-Id, žádné vyhledávání session, žádná ConcurrentHashMap.
Oprava:
# Před: stateful sessions, umírá při restartu podu
spring:
ai:
mcp:
server:
protocol: STREAMABLE
streamable-http:
mcp-endpoint: /mcp
keep-alive-interval: 30s
# Po: stateless, přežije cokoliv, co Kubernetes hodí
spring:
ai:
mcp:
server:
protocol: STATELESS
streamable-http:
mcp-endpoint: /mcpŽádná session. Žádný keep-alive-interval (žádné persistentní spojení k udržování). Žádná ConcurrentHashMap. Jakýkoliv pod může obsloužit jakýkoliv požadavek. Rolling deploymenty jsou pro klienty neviditelné.
Co ztrácíte (a proč to pravděpodobně nevadí)
Stateless režim má jedno skutečné omezení: žádné server-to-client notifikace. Ve stateful režimu může server posílat log zprávy, aktualizace průběhu a notifikace o změnách zdrojů klientovi přes SSE. Ve stateless režimu je komunikace striktně request-response.
Zeptejte se sami sebe: volají vaše MCP nástroje exchange.loggingNotification() nebo exchange.sendResourceChanged()? Pokud ne — a většina ne — neztrácíte nic.
Našich 60+ nástrojů jsou všechny request-response: klient zavolá nástroj, server vrátí výsledek. Žádné push notifikace, žádné streaming aktualizace, žádné serverem iniciované zprávy. Stateless režim je funkčně identický pro tento typ zátěže.
Auto-konfigurace prostě funguje
Pokud používáte anotační přístup Spring AI (@McpTool, @McpResource, @McpPrompt), přechod je transparentní. Spring AI má paralelní auto-konfigurační třídy:
McpServerStreamableHttpWebFluxAutoConfiguration— vytváříWebFluxStreamableServerTransportProvider(stateful)McpServerStatelessWebFluxAutoConfiguration— vytváříWebFluxStatelessServerTransport(stateless)
Anotační skener má StatelessServerSpecificationFactoryAutoConfiguration, který automaticky konvertuje vaše @McpTool metody na McpStatelessServerFeatures.AsyncToolSpecification beany. Váš kód nástrojů se nemění. Váš bezpečnostní WebFilter se nemění (matchuje na cestu /mcp bez ohledu na typ transportu). Vaše validace auth tokenů se nemění.
Širší pohled: MCP v produkci je neprobádané území
Na začátku roku 2026 se MCP ekosystém rychle vyvíjí. Java SDK (v0.17.x) a integrace se Spring AI (2.0.0-M2) jsou produkčně schopné, ale stále dozrávají. Několik věcí, které jsme se naučili při nasazení na GKE:
Správa sessions je první věc, která se rozbije. In-memory session store SDK funguje perfektně pro lokální vývoj (jeden proces, žádné restarty). Rozbije se okamžitě v jakémkoliv prostředí s rolling deploymenty nebo horizontálním škálováním. Transport STATELESS je odpověď, ale není výchozí a dokumentace na to neupozorňuje.
Autentizace je vaše zodpovědnost. MCP specifikace zahrnuje OAuth flow, ale SDK neimplementuje serverovou autentizaci z krabice. Postavili jsme kompletní auth vrstvu: JWT validaci, revokaci tokenů, rate limiting, duální typy tokenů (service accounts pro CI, OAuth tokeny pro interaktivní použití). Pokud vystavujete MCP přes internet, plánujte s tím od prvního dne.
SDK nemá expiraci sessions. I ve stateful režimu sessions žijí navždy v ConcurrentHashMap — žádný idle timeout, žádné TTL, žádná evikce. Pokud zůstanete na STREAMABLE, uniklé sessions se budou hromadit dokud se pod nerestartuje. Toto je známý nedostatek.
Klienti nezvládají ztrátu session elegantně. Když session zmizí (restart podu, deployment), klient dostane 404 a přestane fungovat. V Claude Code není automatická re-inicializace — musíte MCP spojení restartovat ručně. Issue tracker Java SDK má několik hlášení tohoto problému. Přechod na stateless celý problém obchází.
“Nemůžeme prostě perzistovat sessions do Redis?”
Pokud jste již perzistovali auth tokeny do databáze, přirozený instinkt je udělat totéž s transport sessions. Ponořili jsme se do zdrojového kódu SDK, abychom zjistili, zda je to životaschopné. Není — a zde je důvod.
McpStreamableServerSession drží živé FluxSink<ServerSentEvent<?>> objekty — skutečná SSE spojení ke klientovi, vázaná na konkrétní HTTP response stream na konkrétním podu. Drží také ConcurrentHashMap<Object, MonoSink<JSONRPCResponse>> pro čekající request/response páry. Jsou to Reactor primitivy vázané na běžící JVM — serializace do Redis je jako pokus serializovat otevřený TCP socket.
Session také drží Map<String, McpRequestHandler<?>> — vaše @McpTool handler funkce. Jsou to Java lambdy, které by teoreticky mohly být znovu injektovány při obnově session, ale SDK neodděluje “obnovitelná metadata” od “runtime-only stavu.” Vše je smícháno v jednom objektu bez SessionStore abstrakce.
Prozkoumali jsme extension pointy:
McpStreamableServerSession.Factoryje zapojitelná přessetSessionFactory(), ale zvládá pouze vytváření. Neexistuje hook pro vyhledávání session — polesessionsjeprivate ConcurrentHashMapuvnitřWebFluxStreamableServerTransportProviderbez getteru nebo zapojitelného rozhraní.WebFluxStreamableServerTransportProviderby mohl být celý reimplementován (implementuje čisté rozhraní), ale přepisovali byste ~500 řádků HTTP handlingu, správy SSE a životního cyklu sessions — v podstatě fork transportní vrstvy.- Vlastní TODO v SDK (
// TODO: review in the context of history storage,// TODO: store message in history) potvrzují, že autoři plánují perzistenci sessions, ale zatím tam není. Metodareplay()vracíFlux.empty().
Pokud skutečně potřebujete STREAMABLE v Kubernetes (např. pro server-push logování), realistické možnosti dnes jsou:
- Sticky sessions na úrovni ingress (
sessionAffinity: ClientIPnebo cookie-based) — nejjednodušší, ale ztrácí sessions při restartu podu - Jedna replika — žádné problémy s routováním sessions, ale žádná HA
- Přispějte
SessionStoreabstrakcí do SDK — hranice rozhraní je dostatečně čistá; issue #107 by byl přirozený výchozí bod
Pro request-response zátěž žádná z těchto variant nestojí za tu složitost. Přejděte na STATELESS.
TL;DR
| Pokud… | Udělejte toto |
|---|---|
| Běžíte MCP lokálně nebo single-instance | STREAMABLE je v pořádku |
| Nasazujete na Kubernetes s replikami | Přepněte na STATELESS |
Používáte anotace @McpTool | Žádné změny kódu nejsou potřeba |
Voláte exchange.loggingNotification() | Zůstaňte na STREAMABLE, akceptujte riziko sessions |
| Máte pokušení perzistovat sessions do Redis | Nedělejte to — obsahují neserializovatelný runtime stav |
MCP protokol je jednou z nejzajímavějších věcí, které se aktuálně dějí v AI nástrojích. Ale “funguje na mém stroji” a “funguje v produkci na Kubernetes” jsou velmi odlišné laťky. Pokud jste jedním z prvních týmů tlačících MCP servery do reálných multi-pod nasazení, ušetřete si debugging: jděte stateless od začátku.
Další čtení:
- Specifikace Model Context Protocol
- Dokumentace Spring AI MCP Server
- MCP Java SDK GitHub
- MCP Java SDK — Session Expiration Issue #107
- MCP Java SDK — Session Loss Issue #379
Stack: Spring AI 2.0.0-M2, MCP Java SDK 0.17.1, Spring Boot 4.0.1, GKE s rolling deploymenty. MCP klient: Claude Code (Anthropic CLI).


