Produkty Poradenství O nás Blog Kontakt English
arrow_back Zpět na blog

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

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:

Text
Session not found: f4456324-79fb-4d84-ab2b-eebf27c5a475

Token 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:

VrstvaCo to jeKde žijePřežije restart?
Auth tokenJWT Bearer tokenPostgreSQL / RedisAno
Transport sessionStav MCP protokoluConcurrentHashMap v JVM heapNe

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”

  1. Rolling deployment — Kubernetes nahrazuje pody jeden po druhém. Nový pod má prázdnou ConcurrentHashMap. Klienti se starými session ID dostanou 404.
  2. Restart podu — OOM kill, selhání liveness probe, preempce nodu. Stejný výsledek.
  3. 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:

Java
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:

YAML
# 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.Factory je zapojitelná přes setSessionFactory(), ale zvládá pouze vytváření. Neexistuje hook pro vyhledávání session — pole sessions je private ConcurrentHashMap uvnitř WebFluxStreamableServerTransportProvider bez getteru nebo zapojitelného rozhraní.
  • WebFluxStreamableServerTransportProvider by 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í. Metoda replay() vrací Flux.empty().

Pokud skutečně potřebujete STREAMABLE v Kubernetes (např. pro server-push logování), realistické možnosti dnes jsou:

  1. Sticky sessions na úrovni ingress (sessionAffinity: ClientIP nebo cookie-based) — nejjednodušší, ale ztrácí sessions při restartu podu
  2. Jedna replika — žádné problémy s routováním sessions, ale žádná HA
  3. Přispějte SessionStore abstrakcí 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-instanceSTREAMABLE je v pořádku
Nasazujete na Kubernetes s replikamiPř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 RedisNedě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í:

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

Další z blogu