Wdrożenie serwera MCP na EC2 po zmianie specyfikacji sesji bezstanowych
Przenoszenie serwera MCP z localhost na EC2: streamable HTTP, nginx oraz autoryzacja typu bearer, a także co oznaczają sesje bezstanowe i OAuth 2.1 w rzeczywistym wdrożeniu.
Źródła: MCP Servers Explained: A Complete Guide to What I Learned Deploying One to AWS EC2
Od localhost do prawdziwego serwera
Poradniki dotyczące MCP zazwyczaj pozostają abstrakcyjne. Pokazują klienta obok serwera, porównują protokół do portu USB-C lub uruchamiają pięciolinijowy przykład na localhost, który nigdy nie opuszcza laptopa programisty. Prawdziwa praca zaczyna się dopiero potem: narzędzie napisane lokalnie musi działać na rzeczywistym serwerze, za prawdziwym proxyem wstecznym, gdzie rzeczywisty klient może do niego uzyskać dostęp przez publiczną sieć internetową. To odrębny problem, który rzadko jest omawiany.
Opisana tutaj implementacja zaczyna się od lokalnego serwera MCP, przenosi go na instancję EC2 i udostępnia przez transport HTTP typu streamable, przy czym nginx wymaga uprzednio tokena nośnego. Wszystko działało poprawnie. Wkrótce potem protokół przeszedł pod jego zarządzanie. 28 lipca 2026 roku największa dotychczasowa aktualizacja specyfikacji MCP przeszła z wersji kandydackiej na wersję ostateczną, co sprawiło, że protokół stał się całkowicie bezstanowy. Zespoły często zaznaczają „wsparcie dla MCP” jako jeden punkt, ale w rzeczywistości obejmuje to trzy aspekty: transport, autoryzację oraz, od tej aktualizacji, skalowalność. Błędna decyzja w którymkolwiek z tych obszarów decyduje o tym, czy narzędzie nadaje się tylko do demonstracji, czy może być używane w produkcji z zaufaniem.
Omówienie obejmuje kilka kwestii: które aspekty problemu definiuje MCP, a które pozostawia otwarte, dlaczego protokół streamable HTTP zastąpił transport oparty na SSE, w jaki sposób nginx i token nośny umożliwiają dostęp do lokalnego serwera na EC2, jakie zmiany wprowadza bezstanowa przepisówka, dlaczego token statyczny uznawany jest za element autoryzacji, choć nie spełnia wymogów określonych w specyfikacji, oraz które elementy implementacji musiały ulec zmianie, aby nadążyć za rozwojem.
Co standaryzuje, a czego nie standaryzuje MCP
Na papierze Model Context Protocol określa jedną rzecz: sposób, w jaki aplikacja AI znajduje i wywołuje narzędzia oraz odczytuje zasoby dostarczane przez oddzielny proces serwera, za pomocą wiadomości JSON-RPC przekazywanych przez określony kanał transmisji. Logika narzędzi, projekt i wdrażanie agentów znajdują się poza jego zakresem. Jest to standard służący do łączenia, a nie framework. Wcześniej każda aplikacja AI chcąca uzyskać dostęp do czegoś takiego jak wewnętrzna API wymagała specjalnie dostosowanej integracji. Dzięki MCP jeden serwer obsługuje wszystkich zgodnych klientów.
Patrząc w ten sposób, staje się jasne, dlaczego wdrażanie sprawia trudności. Protokół opisuje strukturę wiadomości, ale milczy na temat tego, czy do serwera można dotrzeć tylko z jednego laptopa, czy z dowolnego miejsca, czy coś go chroni, oraz czy radzi sobie z kilkoma klientami jednocześnie. Odpowiedzi na wszystkie te pytania leżą po stronie operatora.
Dlaczego publiczne wdrażanie jest inne
- Serwery lokalne niewiele ujawniają, natomiast serwery publiczne dużo. Dzięki publicznemu adresowi URL serwer staje się zalogowaną API taką jak każda inna, więc obsługa tokenów i walidacja danych mają takie samo znaczenie.
- Reguły dotyczące sesji i skalowania były wcześniej niejasne. Błędne założenia mogły skutkować albo serwerem, który cicho zawodził pod obciążeniem, albo serwerem obciążonym niepotrzebnymi sesjami trwałymi.
- Specyfikacja nie stoi w miejscu. Implementacja oparta na założeniach sprzed sześciu miesięcy może przestać być zgodna z wymogami bez pojedynczego błędu, ponieważ klienci po prostu negocjują użycie starszej wersji protokołu.
Architektura wygląda w ten sposób.
AI Client ── HTTPS POST ──▶ nginx (TLS termination, auth check, reverse proxy)
│
▼
MCP Server Process
(Streamable HTTP transport)
│
┌────────────┴────────────┐
Tools Resources
Jeden zasadniczy princip kieruje tym wszystkim. Gdy serwer MCP otrzymuje publiczną adresację URL, należy go traktować jak każdą zautoryzowaną API dostępną z EC2, ponieważ strukturalnie jest taką samą. MCP to jedynie format wiadomości, natomiast kwestie bezpieczeństwa i skalowalności są identyczne jak w przypadku dowolnego backendu.
1. Transport HTTP typu streamable
HTTP typu streamable to jeden punkt końcowy HTTP, który przyjmuje żądania POST zawierające wiadomości w formacie JSON-RPC. Zastąpił on wcześniejszy transport HTTP+SSE, który wymagał trwałego strumienia server-sent-events obok zwykłych żądań HTTP.
Jeden punkt końcowy POST jest dobrze znany każdemu proxyowi odwrotnemu, balanserowi obciążeń lub bramie API, więc nie ma problemów z utrzymaniem długotrwałego strumienia otwartego za pomocą nginx lub ALB. Ruch przypomina zwykłą API HTTP, a blok location w nginx pozostaje krótki.
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
proxy_http_version 1.1;
proxy_read_timeout 300s;
}
Wskazówka dla środowiska produkcyjnego: nawet przy transporcie jednopunktowym, ustaw wystarczająco długi czas oczekiwania na odczyt u proxy. Poszczególne wywołania narzędzi mogą uzasadnione trwać dłużej niż typowe żądanie API, a zbyt krótki czas oczekiwania zakończy również poprawne, długotrwałe wywołania wraz z tymi, które rzeczywiście utknęły.
2. Autoryzacja za pomocą tokena Bearer
W tym schemacie stały token znajduje się w nagłówku Authorization i jest weryfikowany przed tym, jak żądanie dotrze do procesu serwera MCP, czy to w nginx, czy w middleware aplikacji. Dla treści dostępnych pod publicznym adresem URL jest to najmniej bezpieczny, ale akceptowalny rozwiązanie oraz rozsądny pierwszy krok. Jednak token nigdy się nie wygasa, nie jest powiązany z żadnym konkretnym serwerem i nie zapewnia ochrony, jeśli trafi do pliku logów. Informuje jedynie o tym, że żądanie zawiera dane uwierzytelniające.
Przykład middleware w FastAPI zaczyna się od importów.
from fastapi import Request, HTTPException
Następnie przychodzi sama weryfikacja.
VALID_TOKEN = "your-rotated-secret-token"async def verify_bearer(request: Request):
auth = request.headers.get("authorization", "")
if auth != f"Bearer {VALID_TOKEN}":
raise HTTPException(status_code=401, detail="Unauthorized")
Kolejna rada: unikaj umieszczania surowego nagłówka Authorization w logach dostępu. Standardowe formaty logów nginx i aplikacji utrudniają jego zauważenie, a gdy już się tam znajdzie, plik logu staje się źródłem wyciekłych danych uwierzytelniających.
3. Brak stanu, istota przepisów z lipca 2026 roku
Zgodnie ze specyfikacją z 2026-07-28 sesje na poziomie protokołu znikają. Nie ma już nagłówka Mcp-Session-Id, a mechanizm transmisji rezygnuje również z długotrwałego strumienia GET, który wcześniejsze wersje używały do utrzymywania synchronizacji między klientem a serwerem podczas rozmowy. Każda prośba zawiera wszystko, co jest jej potrzebne.
Kluczową kwestią jest skalowalność. Starszy model pozwalał serwerowi przechowywać informacje o kliencie pomiędzy połączeniami, a gdy za balansem obciążeń znajdowało się kilka instancji, takie przechowywanie wymagało użycia „sticky sessions”. Bez stanu na poziomie protokołu każda instancja może obsługiwać dowolną prośbę, co umożliwia prawdziwe skalowanie poziome i uwalnia klientów od zależności od jednej konkretnej maszyny.
W praktyce serwer, który przechowywał stan klienta w pamięci pomiędzy połączeniami, nie będzie działał zgodnie z nowymi specyfikacjami. Wszelki istotny stan powinien zostać przeniesiony do treści żądania lub do zewnętrznego magazynu, takiego jak Redis, który każda instancja może odczytać.
4. Wielokrotne żądania typu round-trip
Nawet protokół bez stanu wymaga sposobu, aby serwer mógł zadać pytanie klientowi w trakcie wywołania narzędzia, na przykład poprosić o potwierdzenie lub informację o brakuującym parametrze. Nowa specyfikacja rozwiązuje ten problem za pomocą Multi Round-Trip Requests. Zamiast utrzymywać otwarty strumień, serwer odpowiada wartością InputRequiredResult, która zawiera listę jego pytań wraz z nieprzejrzystą wartością requestState. Klient zbiera odpowiedzi i wysyła ponownie pierwotne wywołanie, dodając te odpowiedzi i zwracając niezmieniony stan.
To właśnie sprawia, że prawdziwy brak stanu jest wykonalny. Ponieważ wszystko, czego serwer potrzebuje do kontynuacji, znajduje się w treści wysyłanej z powrotem przez klienta, każda instancja, niekoniecznie ta, która rozpoczęła wywołanie, może je ponowić i dokończyć.
Klienci powinni nie ingerować w requestState: ani go nie badać, ani nie edytować. Należy go zachować i zwrócić dokładnie takim, jakim przybył, ponieważ aplikacja korzystająca z jego integralności może zawieść.
5. Wzmocnienie zasad autoryzacji
Zrewidowana specyfikacja skłania serwery MCP zdalne do używania standardu OAuth 2.1 z mechanizmem PKCE oraz wymaga wskaźników zasobów (RFC 8707), które powiązują wydany token z konkretnym serwerem, dla którego został przeznaczony.
Problemem jest możliwość odtworzenia transakcji. Jeśli ujawni się statyczny token nośny, każdy serwer, który go przyjmie, może stać się celem ataku. Token powiązany z konkretnym zasobem zamyka tę lukę, dlatego token wydany dla jednego serwera MCP nie będzie działał na innym, nawet jeśli oba korzystają z tego samego dostawcy tożsamości.
Ustawienie oparte na tokenach nośnych, od którego zaczyna się wielu użytkowników, spełnia wymóg „ten punkt końcowy wymaga autoryzacji”. Nie zapewnia jednak interoperacyjności ani ochrony przed ponownym wykorzystaniem, których oczekuje nowa specyfikacja od zgodnego serwera zdalnego, a tę lukę należy zamknąć, zanim URL trafi do kogokolwiek poza zespołem głównym.
6. Polityka deprecjacji i rozszerzenia
Możliwości opisane w specyfikacji przechodzą teraz przez formalny cykl życia: Aktywne, Deprecjonowane i Usunięte, przy czym między deprecjacją a usunięciem jest co najmniej dwanaście miesięcy. Nowe funkcje, takie jak interfejs użytkownika generowany przez serwer lub zadania długotrwałe, pojawiają się najpierw jako opcjonalne rozszerzenia, zamiast trafić bezpośrednio do części głównej. Niektóre starsze funkcje, w tym metody roots, sampling i logging w ich oryginalnej formie, są przechodzone na ścieżkę deprecjacji na rzecz zamienników.
Dla operatorów oznacza to, że działająca integracja nie zostanie nagle zniszczona przez przyszłą aktualizację, a nowe funkcjonalności mogą być wprowadzane świadomie, a nie pod presją konieczności nadążania za wszystkim, co dodaje specyfikacja.
Przewodnik krok po kroku
Ścieżka od lokalnego prototypu do wdrożenia na EC2 zgodnego ze specyfikacją wygląda następująco:
- Testowanie lokalne. Serwer działa przez stdio z lokalnym klientem, bez połączenia sieciowego i bez autoryzacji, dzięki czemu iteracje są szybkie.
- Przejście na streamowalny HTTP. Serwer jest przekonfigurowany tak, aby używać transportu streamowalnego HTTP, i wdrażany na instancję EC2.
- nginx na froncie. nginx kończy proces TLS, sprawdza token nośny przed tym, jak cokolwiek dotrze do procesu MCP, a następnie przekierowuje żądanie.
- Zwykłe wywołanie narzędzia. Jedna transmisja typu POST zawiera żądanie JSON-RPC, a wynik wraca w tym samym żądaniu i odpowiedzi.
- Negocjacja wersji. Po ukończeniu opracowywania specyfikacji z dnia 2026-07-28 klient i serwer negocjują aktualizację protokołu. Klient, który nie został zaktualizowany, automatycznie przechodzi na wersję z dnia 2025-11-25 i nie powoduje awarii.
- Wymagane dodatkowe dane wejściowe. Zamiast utrzymywać połączenie otwarte, serwer odpowiada kodem
InputRequiredResult, wskazując, jakie dane nadal musi uzyskać. - Przywracanie działania. Klient powtarza wywołanie, dołączając odpowiedzi oraz odzwierciedlając wartość
requestState. Ponowna próba nie zależy od konkretnej instancji, więc może ją przetworzyć każdy komputer za nginx. - Błędy autoryzacji. Prośby bez tokena lub z błędnym tokenem są odrzucane przez nginx. Kod aplikacji nigdy z nimi nie ma do czynienia.
Przypadki specjalne
Powolne operacje. Zadania, które wcześniej polegały na utrzymywaniu otwartego strumienia przez cały czas, powinny zostać przeniesione do rozszerzenia Tasks. Tworzenie trwałego połączenia w transporcie, który nie obsługuje takiego połączenia, jest niewłaściwym rozwiązaniem.
Narzędzia, które nigdy nie będą używane zdalnie. Serwer, który działa wyłącznie lokalnie obok swojego klienta, najlepiej funkcjonuje przy użyciu stdio, a prawie żadne z problemów związanych z wdrażaniem tu nie mają zastosowania.
Klienci o różnych wersjach. Ponieważ starsi klienci automatycznie przechodzą na wersję 2025-11-25, jedno wdrożenie może przez jakiś czas obsługiwać obie wersje protokołu, dzięki czemu nie wszyscy klienci muszą aktualizować się jednocześnie z serwerem.
Skalowanie i wyzwania w produkcji
Wielokrotne instancje. W modelu sesji utrzymywanie klienta przyłączonego do konkretnej instancji oznaczało użycie sesji trwałych lub wspólnego przechowywania sesji. Ten problem zniknął, a dla osób zarządzających więcej niż jednym serwerem jest to główna zaleta tej przebudowy.
TLS w coraz większej liczbie serwerów. Obsługa certyfikatu na każdej instancji EC2 szybko staje się skomplikowana. Zarządzany balanser obciążeń z certyfikatem ACM umieszczony przed serwerami wymaga znacznie mniej wysiłku niż konfiguracja TLS w nginx na każdym serwerze.
Wydawanie i rotacja tokenów. Statyczne tokeny nośnikowe mają ograniczenia pod względem bezpieczeństwa, a poza kilkoma klientami stanowią również problem operacyjny, ponieważ każda rotacja wymaga ręcznej koordynacji. Wydawanie tokenów na podstawie standardu OAuth 2.1 eliminuje zarówno luki w bezpieczeństwie, jak i obciążenie związane z rotacją.
Śledzenie działań pomiędzy połączeniami bez sesji po stronie serwera. Ponieważ żadna sesja nie łączy sekwencji połączeń na serwerze, identyfikatory korelacji przenoszone w treści żądania stają się jedynym niezawodnym sposobem na odtworzenie tego, co dokonał pojedynczy agent w trakcie kilku połączeń.
Obsługa kontynuacji na stronie klienta
Konfiguracja nginx oraz middleware bearer-check zajmują się transportem i podstawową autoryzacją. Oto minimalny wzorzec dla strony klienta w ramach żądania wielokrotnego obiegu.
async def call_tool_with_resume(client, tool_name, params):
result = await client.call_tool(tool_name, params)
if result.get("type") == "InputRequiredResult":
answers = collect_answers(result["questions"])
return await client.call_tool(
tool_name,
{**params, "answers": answers, "requestState": result["requestState"]},
)
return result
Decydująca jest ostatnia linia, w której requestState wraca dokładnie tak, jak dostarczył go serwer.
Powszechne pułapki
- Traktowanie „wdrożonego” jako „gotowego do użycia w produkcji”. nginx w połączeniu z tokenem nośnym spełnia tylko najniższe wymagania, a nie wszystkie. Endpoint MCP powinien spełniać standardy każdej publicznej API, z odpowiednią autoryzacją, dokładnym logowaniem i ograniczeniami szybkości, zanim adres URL zostanie szeroko udostępniony.
- Wykorzystywanie pamięci serwera. W niektórych wczesnych wdrożeniach zakładano bezgłośnie, że serwer pamięta klienta z poprzedniej prośby. Należy zakładać, że każda prośba trafia do innej instancji niż poprzednia, ponieważ obecne specyfikacje na to pozwalają.
- Przytwierdzanie pojedynczej wersji protokołu. Klient zablokowany na jednej wersji przestaje działać, gdy serwer zostanie zaktualizowany. Należy zaimplementować mechanizm negocjacji z rozsądnym rozwiązaniem awaryjnym, aby aktualizacja jednej ze stron nie sparaliżowała drugiej.
- Omijanie wymogów autoryzacji w narzędziu wewnętrznym. Narzędzie obecnie używane wyłącznie wewnętrznie może później trafić do zespołu partnerskiego, a wtedy statyczny token nie będzie wystarczający. Jeśli serwer może znajdować się poza laptopem lub VPC, od samego początku należy stosować OAuth 2.1 oraz wskaźniki zasobów.
Praktyki sprawdzające się w produkcji
- Unikaj sytuacji, w których infrastruktura nie może się skalować. Wszystko, co musi przetrwać między żądaniami, powinno znajdować się w treści żądania lub w zewnętrznym magazynie.
- Przypisz każdy token do określonego zasobu, ponieważ token akceptowany przez wszystkie zaufane serwery prędzej czy później zostanie użyty w niewłaściwym miejscu.
- Pertraktujcie co do wersji protokołu i nie zakładajcie żadnej konkretnej. Klienci i serwery rzadko aktualizują się w tym samym dniu, więc należy uwzględnić tę różnicę.
- Sprawdzajcie autoryzację na poziomie brzegowym. Gdy nginx odrzuca nieprawidłową prośbę, proces MCP pozostaje prostszy i ujawnia mniej informacji.
- Nie ingerujcie w pola niewidoczne dla użytkownika.
requestStatei podobne elementy istnieją po to, by serwer mógł polegać na informacjach, które są zwracane, dlatego klienci nie powinni ich czytać ani zmieniać.
Jak to wpływa na proces wdrażania
MCP w teorii jest prosty. Decyzje istotne dla wdrażania znajdują się w specyfikacji, a aktualizacja z 28 lipca 2026 roku je zmieniła. Streamable HTTP łatwiej jest uruchomić za pomocą zwykłej infrastruktury, brak stanu sprawia, że dodanie instancji EC2 staje się realną opcją zamiast projektu do zarządzania sesjami, a model autoryzacji pasuje teraz do serwera dostępnego publicznie. Nic z tego nie jest trudne, gdy się to zrozumie, a to właśnie ten aspekt jest pomijany w teoretycznych opisach.
Podobne materiały
- Notatki praktyczne: Model Context Protocol (MCP) wyjaśniony: Przewodnik dla początkujących — Szczegółowy przewodnik po Notatkach praktycznych: Model Context Protocol (MCP) wyjaśniony: Przewodnik dla początkujących: umowy, sprawdzenia oraz miejsca na kod do wstawienia dla zespołów wdrażających ten wzorzec.
- Notatki praktyczne: Systemy RAG: Kompletny przewodnik od zera do mistrzostwa (wydanie 2026) — Szczegółowy przewodnik po Notatkach praktycznych: Systemy RAG: Kompletny przewodnik od zera do mistrzostwa (wydanie 2026): umowy, sprawdzenia oraz miejsca na kod do wstawienia dla zespołów wdrażających ten wzorzec.