Bezstanowy serwer MCP: co zmieniła aktualizacja protokołu
Wersja MCP z 2026-07-28 usuwa sesje oraz handshake initialize. Sprawdź, jak te zmiany wpływają na konfigurację reverse proxy, mechanizmy health check oraz obsługę autoryzacji.
Czym jest bezstanowy serwer MCP
Bezstanowy serwer MCP nie przechowuje żadnego stanu dla poszczególnych klientów pomiędzy żądaniami. Każde żądanie zawiera wersję protokołu, możliwości klienta oraz poświadczenia niezbędne serwerowi do udzielenia odpowiedzi, dzięki czemu każdy proces na dowolnej maszynie może obsłużyć każde żądanie. MCP (Model Context Protocol, format komunikacji używany przez agentów do uzyskiwania dostępu do narzędzi) wprowadził tę zasadę w wersji 2026-07-28, która usunęła uzgadnianie initialize oraz sesję HTTP działającą w jego warstwie.
To jest główny cel operacyjny tego rozwiązania. Serwer, który nie przechowuje informacji o kliencie, może działać za standardowym load balancerem bez konieczności stosowania sesji typu sticky, być restartowany podczas wdrażania bez przerywania połączeń klientów oraz działać jako cztery identyczne procesy zamiast jednego. Serwer oparty na sesjach nie oferuje żadnej z tych możliwości bez dodatkowej infrastruktury.
Model Context Protocol jest protokołem bezstanowym: wszystkie informacje potrzebne do przetworzenia żądania znajdują się w samym żądaniu. Serwer przetwarza każde żądanie niezależnie; żaden stan nie powinien być wnioskowany z poprzednich żądań, nawet tych przesłanych w ramach tego samego połączenia lub strumienia.
Bezstanowość nie oznacza, że serwer nie przechowuje żadnych danych. Baza danych, kolejka oraz pamięć podręczna nadal istnieją. Oznacza to, że protokół nie przenosi stanu w ramach połączenia, więc serwer nie może traktować połączenia, procesu ani otwartego gniazda jako reprezentacji "tego klienta w trakcie rozmowy".
Co usunięto w wersji 2026-07-28
2026-07-28 to bieżąca wersja specyfikacji na sierpień 2026 roku. W porównaniu z 2025-11-25 usunięto pięć elementów, które istniały w celu obsługi sesji.
- Żądanie
initializeoraz powiadomienienotifications/initialized. Mechanizm handshake nie istnieje (SEP-2575). - Nagłówek
Mcp-Session-Idoraz zakończenie sesji za pomocą HTTPDELETE(SEP-2567). - Samodzielny strumień HTTP
GET, przez który serwery wysyłały powiadomienia. Został zastąpiony przezsubscriptions/listen, czyli zwykłe żądanie POST, którego odpowiedź jest długotrwałym strumieniem. - Możliwość wznawiania strumieni SSE (server-sent events). Nagłówek
Last-Event-IDoraz identyfikatory poszczególnych zdarzeń zostały usunięte, więc zerwany strumień powoduje utratę bieżącego żądania, a klient musi ponowić je jako nowe żądanie z nowym identyfikatorem. ping,logging/setLeveloraznotifications/roots/list_changed. Poziom logowania jest teraz polem wewnątrz żądania,io.modelcontextprotocol/logLevelw_meta.
Dodano jedną metodę, którą musi zaimplementować każdy serwer. server/discover zwraca obsługiwane przez serwer wersje protokołu, możliwości oraz tożsamość w jednym wywołaniu. Jest to najbliższy odpowiednik handshake, jaki pozostał, a jego wywołanie przez klientów jest opcjonalne.
Dlaczego transport sesji był trudny do uruchomienia w środowisku produkcyjnym
W wersji 2025-11-25 i wcześniejszych serwer mógł wygenerować identyfikator sesji podczas inicjalizacji i zwrócić go w nagłówku Mcp-Session-Id w odpowiedzi InitializeResult. Klient musiał następnie przesyłać ten nagłówek w każdym kolejnym żądaniu. Wynegocjowana wersja protokołu oraz możliwości klienta były przechowywane w pamięci serwera, przypisane do tego identyfikatora. Każde z tych rozwiązań wiąże się z kosztami operacyjnymi.
- Restart serwera powodował utratę tabeli sesji. Specyfikacja wymagała, aby serwer odpowiadał na każde żądanie zawierające nieaktywny identyfikator sesji błędem
404 Not Found, a klient musiał rozpoczynać proces od nowa z nowymInitializeRequest. Każde wdrożenie stawało się zdarzeniem wymuszającym ponowne połączenie dla wszystkich klientów. - Druga replika nie posiadała informacji o sesjach pierwszej repliki. Skalowanie poziome wymagało stosowania mechanizmu sticky routing na load balancerze lub współdzielonego magazynu sesji, z którego każda replika korzystała przy każdym żądaniu.
- Tabela sesji zajmowała pamięć, której zużycie rosło wraz z liczbą nieaktywnych klientów. Komunikat
DELETEbył opcjonalny, a klienci, którzy zamykali połączenie bez jego wysłania, pozostawiali w pamięci niepotrzebne wpisy. - Wyniki listowania mogły różnić się w zależności od połączenia, co uniemożliwiało bezpieczne stosowanie cache'owania przed serwerem.
Usunięcie sesji eliminuje wszystkie cztery problemy jednocześnie. Jest to zmiana, którą należy zrozumieć przed przystąpieniem do jakiejkolwiek modyfikacji konfiguracji.
Co zawiera każde żądanie
Każdy POST do punktu końcowego MCP jest niezależny. Wersja protokołu oraz możliwości klienta są przesyłane w treści żądania w ramach _meta, a wybrane pola są odwzorowywane na nagłówki HTTP, aby pośrednik mógł na ich podstawie kierować ruch bez parsowania JSON.
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"location": "Seattle, WA"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "ExampleClient", "version": "1.0.0"},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}io.modelcontextprotocol/protocolVersion oraz io.modelcontextprotocol/clientCapabilities są wymagane w każdym żądaniu. clientInfo nie jest wymagane, choć klienci powinni je wysyłać. Żądanie, w którym brakuje wymaganego pola, jest nieprawidłowe, więc serwer musi je odrzucić z błędem JSON-RPC -32602 oraz kodem HTTP 400 Bad Request.
Nagłówek Mcp-Method jest wymagany w każdym żądaniu. Mcp-Name jest wymagane w tools/call, resources/read oraz prompts/get. Wartość nagłówka musi być zgodna z treścią żądania, a serwer przetwarzający treść musi odrzucić niezgodność za pomocą 400 Bad Request i kodu błędu -32020, HeaderMismatch. Ta zasada istnieje, ponieważ równoważnik obciążenia kierujący ruch na podstawie nagłówka oraz serwer wykonujący operację na podstawie treści to dwa różne źródła prawdy. W przypadku kierowania ruchu lub ograniczania częstotliwości (rate-limiting) na podstawie tych nagłówków, należy najpierw sprawdzić MCP-Protocol-Version: wcześniejsze wersje nigdy nie weryfikowały nagłówka względem treści, więc w tamtych wersjach wartość nagłówka nie jest wiarygodna.
Niezgodność wersji jest teraz zwykłym błędem na poziomie żądania, a nie nieudanym uzgadnianiem (handshake). Serwer, który nie obsługuje żądanej wersji, odpowiada 400 Bad Request z błędem -32022, UnsupportedProtocolVersion i wymienia obsługiwane wersje w data.supported. Klient wybiera jedną z nich z listy i ponawia próbę.
Gdzie podział się stan: tokeny, kursory, subskrypcje
Stan nie zniknął. Przeniósł się w miejsca, które można obserwować i logować.
Poświadczenia są przesyłane w każdym żądaniu. Nie istnieje sesja, do której można przypisać tożsamość, więc token dostępu jest dołączany do każdego wywołania HTTP i weryfikowany za każdym razem. Szczegóły znajdują się w sekcji dotyczącej uwierzytelniania poniżej.
Kursory muszą przenosić własną pozycję. Stronicowanie w tools/list, resources/list, prompts/list oraz resources/templates/list wykorzystuje nieprzejrzysty ciąg znaków kursora, którego klient nie powinien parsować ani modyfikować. Na serwerze jednowątkowym powszechną praktyką było przechowywanie offsetu w pamięci, powiązanego z sesją. Przy braku sesji kursor musi być wystarczający, aby dowolna replika mogła wznowić listowanie, dlatego należy zakodować pozycję wewnątrz kursora i podpisać ją lub przechowywać w pamięci współdzielonej przez wszystkie repliki. Nieprawidłowy kursor powinien zwracać -32602. Podpisuj kursor, ponieważ nieprzejrzysty ciąg jest danymi wejściowymi dostarczanymi przez klienta, które kod dekoduje i uznaje za zaufane.
Subskrypcje należą do żądania, a nie do połączenia. Klient, który chce otrzymywać powiadomienia o zmianach, wysyła subscriptions/listen z filtrem określającym typy, które go interesują: toolsListChanged, promptsListChanged, resourcesListChanged oraz resourceSubscriptions. Serwer odpowiada za pomocą notifications/subscriptions/acknowledged i utrzymuje ten strumień odpowiedzi otwarty. Jeśli strumień zostanie przerwany, serwer nie przechowuje żadnych danych, a klient wysyła ponownie subscriptions/listen, aby przywrócić subskrypcję.
Stan aplikacji między wywołaniami staje się jawnym uchwytem. Gdy serwer musi zapamiętać coś między wywołaniami, specyfikacja przewiduje użycie identyfikatora wygenerowanego przez serwer, przekazywanego jako zwykły argument narzędzia. Pojawia się on w schemacie narzędzia, może być logowany i nigdy nie jest domyślnie powiązany z połączeniem. Serwer z rzeczywistymi danymi użytkownika, taki jak samodzielnie hostowany serwer MCP poczty e-mail, stosuje ten wzorzec zamiast sesji: identyfikator skrzynki odbiorczej lub wersji roboczej jest argumentem narzędzia, dzięki czemu każda replika może obsłużyć kolejne wywołanie. Wiele narzędzi w ogóle nie potrzebuje uchwytu: narzędzie wyszukiwania oparte na własnej instancji SearXNG przyjmuje zapytanie i zwraca wyniki, nie wymagając wznawiania stanu przy kolejnym wywołaniu i nie dbając o to, która replika udzieliła odpowiedzi.
Wdrożenie: reverse proxy, limity czasu, sprawdzanie stanu (health checks)
Punkt końcowy MCP to jedna ścieżka akceptująca metodę POST. Większość ruchu to krótkie żądania i odpowiedzi JSON, które obsługuje każde proxy. Wyjątkiem jest odpowiedź strumieniowa, w przypadku której domyślne ustawienia proxy mogą być niekorzystne. Jest to element, który zmienia się przy przejściu z wersji demonstracyjnej na laptopie na serwer MCP działający na VPS.
location /mcp {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}proxy_buffering off ma znaczenie, ponieważ nginx domyślnie buforuje odpowiedzi proxy, co wstrzymuje zdarzenia SSE do momentu zapełnienia bufora lub zakończenia odpowiedzi. Specyfikacja wymaga również, aby serwery wysyłały X-Accel-Buffering: no w odpowiedziach SSE, a nginx respektuje ten nagłówek, więc poprawnie skonfigurowany serwer przekaże proxy właściwe instrukcje. Dyrektywę tę należy ustawić również po stronie proxy, ponieważ jest to część konfiguracji, na którą masz bezpośredni wpływ.
proxy_read_timeout domyślnie wynosi 60 sekund. Strumień subscriptions/listen, który pozostaje nieaktywny dłużej, jest zamykany przez nginx, a nie przez serwer, co powoduje, że w logach proces wygląda na zdrowy, a klient zgłasza zerwanie strumienia. Wartość tę należy zwiększyć wyłącznie dla lokalizacji MCP, a nie dla całego serwera. Zaleca się również, aby serwery wysyłały linię komentarza SSE (linię zaczynającą się od dwukropka) jako sygnał keep-alive w okresach bezczynności, co zapobiega przerywaniu strumienia przez pośredników.
Caddy wymaga mniej konfiguracji. Domyślnie buforuje dane częściowo w celu zwiększenia wydajności przesyłu i natychmiast opróżnia bufor, gdy odpowiedź zawiera Content-Type: text/event-stream, dzięki czemu strumieniowanie działa bez dodatkowych dyrektyw.
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}Należy zwrócić uwagę na cel sprawdzania stanu (health check). Nie należy kierować aktywnego sprawdzania na punkt końcowy MCP za pomocą GET, ponieważ serwer implementujący tylko tę wersję odpowiada 405 Method Not Allowed na GET oraz DELETE, a domyślną metodą sprawdzania w Caddy jest GET. Proxy oznaczyłoby wówczas w pełni sprawny backend jako niedostępny. Należy udostępnić prostą ścieżkę, taką jak /healthz, na potrzeby proxy, a protokół sprawdzać oddzielnie za pomocą metody POST.
curl -sS https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'200 zawierający listę supportedVersions oznacza, że proces działa i obsługuje protokół. 404 z błędem JSON-RPC -32601 oznacza, że proces działa, ale nie obsługuje server/discover, co musi implementować każdy serwer 2026-07-28. 400 z -32022 oznacza, że moduł sprawdzający zażądał wersji, której ta kompilacja nie obsługuje; jest to sytuacja, którą należy wykryć po aktualizacji zależności. Wersja open source nginx nie posiada aktywnego sprawdzania stanu, dlatego należy stosować pasywne max_fails i fail_timeout w upstreamie, a sprawdzanie protokołu uruchomić z poziomu monitoringu.
Restart typu rolling powoduje utratę jedynie aktualnie przetwarzanych żądań. Należy opróżnić kolejkę, pozwolić na zakończenie otwartych żądań POST, uruchomić nowy proces, a klienci automatycznie ponowią nieudane operacje. Jedynym elementem, który zostanie przerwany, jest otwarty strumień subscriptions/listen, ponieważ jest to połączenie utrzymywane z konkretnym procesem. Bezstanowość wyeliminowała powinowactwo sesji (session affinity), ale nie wyeliminowała powinowactwa połączenia dla strumienia, który jest aktualnie otwarty, i żadna reguła routingu tego nie naprawi. Klient może rozpoznać różnicę: strumień kończący się pustym wynikiem subscriptions/listen został zamknięty poprawnie, natomiast strumień, który kończy się bez niego, został przerwany, co klient może potraktować jako sygnał do ponownego połączenia.
Buforowanie staje się możliwe po raz pierwszy. Wyniki metod listujących zawierają teraz ttlMs i cacheScope, a cacheScope: "public" informuje współdzielonych pośredników, że mogą buforować odpowiedź. Jest to bezpieczne tylko dlatego, że wyniki listowania nie różnią się już w zależności od połączenia, co jest bezpośrednim skutkiem usunięcia sesji.
Dlaczego uwierzytelnianie zmienia się w przypadku braku sesji
W modelu z sesją kuszące było uwierzytelnienie raz w initialize, a następnie traktowanie identyfikatora sesji jako dowodu dla wszystkich kolejnych działań. Identyfikator sesji użyty w ten sposób jest poświadczeniem typu bearer bez określonego odbiorcy, daty wygaśnięcia ani ścieżki unieważnienia, wygenerowanym przez własny serwer. Usunięcie sesji eliminuje to uproszczenie, a rozwiązanie zastępcze jest bardziej rygorystyczne.
Zabezpieczony serwer MCP działa jako serwer zasobów OAuth 2.1. Każde żądanie HTTP od klienta musi zawierać Authorization: Bearer <access token>, a serwer weryfikuje token przy każdym żądaniu. Weryfikacja obejmuje odbiorcę: serwer musi potwierdzić, że token został wystawiony konkretnie dla niego, zgodnie z RFC 8707 (Resource Indicators for OAuth 2.0), i nie może akceptować ani przekazywać tokenów przeznaczonych dla innych celów. Klienci żądają odpowiedniego odbiorcy, wysyłając parametr resource z kanonicznym URI serwera.
Wykrywanie opiera się na wyzwaniu. Gdy żądanie dociera bez użytecznego tokenu, serwer odpowiada 401 Unauthorized.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"Klient odczytuje resource_metadata, pobiera ten dokument (RFC 9728, OAuth 2.0 Protected Resource Metadata, który serwery MCP muszą implementować), znajduje serwer autoryzacji i uruchamia proces. Ważny token z niewystarczającymi uprawnieniami otrzymuje 403 Forbidden wraz z error="insufficient_scope" oraz zakresami wymaganymi dla danej operacji.
Pociąga to za sobą dwie konsekwencje dla sposobu eksploatacji. Weryfikacja tokenu odbywa się teraz przy każdym żądaniu, a nie raz na sesję, więc sieciowy czas odpowiedzi do punktu końcowego introspekcji dla każdego wywołania będzie widoczny w opóźnieniach: należy preferować tokeny, które można zweryfikować lokalnie na podstawie podpisu, odbiorcy i daty wygaśnięcia, lub buforować wynik weryfikacji przez krótki czas, używając tokenu jako klucza. Ponieważ nie ma sesji przechowującej tożsamość, autoryzacja musi być obliczana na podstawie tokenu przy każdym wywołaniu. Jest to podejście bardziej przejrzyste niż model sesyjny i łączy się z szerszą praktyką utrzymywania poświadczeń poza procesem agenta, co opisano w utrzymywanie sekretów poza agentem AI.
Co jest prawdą w odniesieniu do tej wersji, a co nie
Powyższy opis dotyczy wersji 2026-07-28. Nie opisuje on MCP w sposób uniwersalny ani serwera wdrożonego w ubiegłym roku.
Klienci i serwery w wersji 2025-11-25 oraz starszych nadal korzystają z modelu uzgadniania (handshake). Specyfikacja określa te wersje jako starsze (legacy), a wersje z metadanymi dla każdego żądania jako nowoczesne. Serwer obsługujący wyłącznie tę wersję, po otrzymaniu żądania od starszego klienta, powinien odpowiedzieć kodem 405 Method Not Allowed na GET lub DELETE w punkcie końcowym MCP, zignorować każdy nagłówek Mcp-Session-Id bez jego generowania lub odsyłania oraz zignorować Last-Event-ID, ponieważ strumienie nie obsługują wznawiania. Serwer dwusystemowy może obsługiwać oba modele w jednym punkcie końcowym: żądanie zawierające nowoczesny nagłówek _meta jest obsługiwane bezstanowo, natomiast żądanie initialize aktywuje starszą semantykę sesji.
Przed zastosowaniem powyższych informacji należy sprawdzić ciąg wersji. Jeśli używany zestaw SDK nadal wysyła initialize, sesje w danym wdrożeniu pozostają aktywne, a problemy związane z sesjami nadal wymagają obsługi. To samo dotyczy strony klienckiej: proces agenta na własnej maszynie, taki jak konfiguracja opisana w uruchamianie agenta programistycznego na VPS, jest bezstanowy w tym kontekście tylko wtedy, gdy używana biblioteka obsługuje nowoczesną wersję. Należy odczytać wersję negocjowaną przez środowisko uruchomieniowe, sprawdzić odpowiadającą jej wersję specyfikacji i traktować tę stronę jako opis konkretnej wersji, a nie protokołu jako całości.
FAQ
Czy bezstanowy serwer MCP oznacza, że nie mogę niczego przechowywać?
Nie. Bezstanowość odnosi się do protokołu, a nie do samej aplikacji. Bazy danych, kolejki i pamięci podręczne działają dokładnie tak samo jak wcześniej. Zmienia się jedynie to, że stan obejmujący kilka wywołań musi być wskazywany przez jawny identyfikator przekazywany przez klienta w każdym żądaniu, na przykład uchwyt wygenerowany przez serwer w argumencie narzędzia. Nie wolno natomiast wywodzić kontekstu z połączenia: specyfikacja stanowi, że serwer nie może polegać na poprzednich żądaniach w ramach tego samego połączenia w celu ustalenia możliwości, wersji protokołu czy tożsamości klienta, ponieważ każde żądanie dostarcza te informacje w _meta.
Czy nadal potrzebuję sesji typu sticky na moim load balancerze?
Nie w przypadku zwykłych żądań. Zgodnie z wersją 2026-07-28 każde żądanie POST zawiera własną wersję protokołu, możliwości i poświadczenia, więc każda replika może odpowiedzieć na każde żądanie, a algorytm round-robin jest w pełni wystarczający. Jedynym długotrwałym elementem pozostaje strumień odpowiedzi subscriptions/listen, który jest pojedynczym otwartym połączeniem do konkretnego procesu. Kończy się ono wraz z zakończeniem procesu, a klient wysyła ponownie subscriptions/listen, aby je przywrócić. Jest to kwestia czasu życia połączenia, a nie powinowactwa sesji, i żadna reguła routingu nie jest w stanie temu zapobiec.
Co stało się z Mcp-Session-Id oraz strumieniem HTTP GET?
Oba elementy zostały usunięte w wersji 2026-07-28, zgodnie z SEP-2567 oraz SEP-2575. Serwer implementujący wyłącznie tę wersję powinien odpowiadać 405 Method Not Allowed na GET oraz DELETE w punkcie końcowym MCP i powinien ignorować nagłówek Mcp-Session-Id zamiast odsyłać go z powrotem. Powiadomienia o zmianach inicjowane przez serwer są teraz przesyłane w strumieniu odpowiedzi żądania subscriptions/listen zamiast w samodzielnym strumieniu GET. Serwery, które muszą nadal obsługiwać starszych klientów, implementują zachowanie z poprzedniej wersji równolegle z obecną.
Jak przeprowadzić kontrolę stanu serwera MCP bez handshake'u?
Należy zastosować dwa poziomy kontroli. Skieruj aktywną kontrolę proxy na zwykłą ścieżkę HTTP obsługiwaną przez aplikację, ponieważ żądanie GET do punktu końcowego MCP poprawnie zwraca 405 i oznaczyłoby zdrowy backend jako niedostępny. Następnie sprawdź sam protokół, wysyłając POST server/discover, co musi zaimplementować każdy serwer 2026-07-28, i zweryfikuj, czy odpowiedź to HTTP 200 oraz czy zawiera wersję protokołu używaną przez klientów. Odpowiedź 404 z błędem JSON-RPC -32601 oznacza, że proces działa, ale nie obsługuje tej metody, natomiast 400 z -32022 oznacza, że żądana wersja nie jest wspierana przez daną kompilację.