JFlow Labs / WIEDZA

Własny serwer MCP: kiedy go zbudować i w czym

MCP (Model Context Protocol) to otwarty standard opisujący, jak aplikacja AI podłącza się do twoich danych i funkcji. Obowiązująca wersja specyfikacji to 2026-07-28 — adres /specification/latest przekierowuje właśnie na nią (sprawdzone 21 września 2026 r.). Własny serwer buduj wtedy, gdy masz system bez gotowego serwera pierwszej strony i chcesz wystawić wąski, kontrolowany zbiór operacji; jeśli serwer producenta istnieje, użyj jego. Język wybieraj według tego, w czym stoi reszta twojego systemu, a nie według mody: oficjalnych SDK jest dziesięć, w tierze 1 są TypeScript, Python, C#, Go i Rust. Zacznij od transportu stdio i trzech narzędzi wyłącznie do odczytu, z nazwanymi operacjami, limitami wywołań i dziennikiem. Autoryzacja jest w MCP opcjonalna i dotyczy transportów HTTP — ale jeśli serwer ma być zdalny i publiczny, obowiązującą ścieżką jest OAuth 2.1 z walidacją odbiorcy tokena. Zapis, HTTP i uwierzytelnianie dokładaj jako trzy osobne decyzje, każdą z własnym kosztem.

Czym jest MCP i co dokładnie standaryzuje

Zanim zdecydujesz, w czym pisać, warto wiedzieć, co ten standard obejmuje, a czego nie.

MCP to otwarty protokół, który — jak ujmuje to specyfikacja — umożliwia płynną integrację między aplikacjami opartymi na modelach językowych a zewnętrznymi źródłami danych i narzędziami. Standaryzuje trzy rzeczy: format wiadomości (JSON-RPC 2.0), zestaw metod, którymi klient odkrywa i wywołuje możliwości serwera, oraz sposób, w jaki te możliwości się opisuje. Dokumentacja dopowiada wprost, czego MCP nie robi: nie dyktuje, jak aplikacja używa modelu ani jak zarządza kontekstem. To ważne rozróżnienie, bo połowa nieporozumień wokół MCP bierze się z oczekiwania, że protokół załatwi też warstwę produktową.

Role są trzy i warto je nazywać precyzyjnie, bo w dokumentacji pojawiają się na każdym kroku. Host to aplikacja AI, która koordynuje połączenia. Klient to komponent wewnątrz hosta, utrzymujący dedykowane połączenie — jeden na każdy serwer. Serwer to program dostarczający kontekst i możliwości, niezależnie od tego, gdzie działa. Ten sam program uruchomiony lokalnie przez stdio nazywamy serwerem lokalnym, a wystawiony pod adresem HTTP — zdalnym.

Jedna rzecz zmieniła się w wersji 2026-07-28 na tyle mocno, że trzeba o niej wiedzieć, zanim sięgniesz po starszy poradnik. Protokół jest teraz bezstanowy: każde żądanie niesie w polu `_meta` wersję protokołu i możliwości klienta, więc serwer nie wnioskuje niczego z wcześniejszych żądań. Miejsce obowiązkowego uścisku dłoni `initialize` zajęło żądanie `server/discover`, które w jednej odpowiedzi zwraca obsługiwane wersje, możliwości i tożsamość serwera. Dla klienta wywołanie go jest opcjonalne — może wysłać dowolne żądanie od razu i obsłużyć ewentualny błąd wersji — ale dokumentacja nazywa `server/discover` żądaniem obowiązkowym po stronie serwera. Jeśli piszesz serwer dziś, musisz je obsłużyć.

Czym MCP różni się od zwykłego REST API

Różnica nie leży w formacie wiadomości. Leży w tym, kto decyduje, które wywołanie wykonać.

W zwykłym REST API to twój kod decyduje, które wywołanie wykonać. Piszesz warunki, piszesz kolejność kroków, znasz z góry ścieżkę. W MCP tę decyzję podejmuje model, na podstawie opisów, które serwer sam o sobie publikuje. Specyfikacja nazywa narzędzia wprost sterowanymi przez model: model odkrywa je i wywołuje automatycznie na podstawie kontekstu rozmowy i tego, o co poprosił użytkownik.

Konsekwencje są praktyczne i nieprzyjemne, jeśli się ich nie przewidzi. Opis narzędzia przestaje być dokumentacją dla programisty, a staje się częścią wejścia modelu — źle napisany opis to błędne wywołania. Schemat wejścia (`inputSchema` w formacie JSON Schema, domyślnie w wersji 2020-12) nie jest ozdobą, tylko jedyną barierą między zdaniem po polsku a wywołaniem na twoich danych. I nie ma stałej kolejności: model może wywołać narzędzie numer trzy, nie wywoławszy wcześniej pierwszego i drugiego. Każde narzędzie musi być bezpieczne samo w sobie, bez założeń o tym, co zdarzyło się przed nim.

Do tego dochodzi drugi brak stanu — protokolarny. Specyfikacja mówi wprost, że MCP nie ma sesji na poziomie protokołu, więc serwer nie może opierać się na domyślnym stanie połączenia, żeby powiązać jedno wywołanie z następnym. Koszyk, otwarty kontekst przeglądarki, transakcja bazodanowa — wszystko to trzeba prowadzić przez jawny uchwyt zwracany przez narzędzie tworzące i przyjmowany z powrotem jako zwykły argument.

Dlatego MCP nie zastępuje REST API. Jest warstwą nad nim — świadomie zawężoną, opisaną językiem naturalnym i pomyślaną tak, żeby decyzję o wywołaniu mógł podjąć ktoś, kto nie czytał twojej dokumentacji.

Trzy prymitywy: narzędzia, zasoby, prompty

Różnią się nie tyle techniką, co tym, kto nimi steruje. To rozróżnienie jest wprost w dokumentacji, nie jest umowne.

Narzędzia (`tools/list`, `tools/call`) to funkcje wykonujące działanie: zapytanie do bazy, wywołanie API, zapis pliku. Steruje nimi model. Definicja zawiera nazwę, opcjonalny `title` do wyświetlenia, opis, `inputSchema` i opcjonalnie `outputSchema`. Przy podanym `outputSchema` serwer musi zwracać dane zgodne ze schematem, a klient powinien je walidować — to jeden z niewielu miejsc, gdzie specyfikacja daje ci twardy kontrakt na wyjściu, więc warto z niego korzystać.

Zasoby (`resources/list`, `resources/templates/list`, `resources/read`) to pasywne dane tylko do odczytu, identyfikowane przez URI. Bywają stałe (`calendar://events/2024`) albo parametryzowane szablonem (`travel://activities/{city}/{category}`). Steruje nimi aplikacja: to host decyduje, co wciągnąć do kontekstu i w jakiej postaci — czy przepuścić przez wyszukiwanie, czy podać modelowi surowo.

Prompty (`prompts/list`, `prompts/get`) to parametryzowane szablony pracy. Steruje nimi użytkownik — dokumentacja mówi o jawnym wywołaniu i wymienia typowe interfejsy: komendy ze slashem, palety poleceń, dedykowane przyciski.

Po stronie klienta w wersji 2026-07-28 został w praktyce jeden prymityw: elicitation, czyli możliwość dopytania użytkownika w trakcie obsługi żądania. Sampling (proszenie klienta o uzupełnienie modelem) i logging zostały oznaczone jako przestarzałe — dokumentacja kieruje nowe wdrożenia odpowiednio do bezpośredniej integracji z API dostawcy modelu i do pisania logów na `stderr` albo do OpenTelemetry. Jeśli projektujesz serwer wokół samplingu, projektujesz go wokół funkcji na wylocie.

Praktyczna rada na start: zacznij od narzędzi. Zasoby i prompty bywają obsługiwane nierówno przez różne hosty; oficjalna macierz wsparcia klientów już nie istnieje, więc obsługę konkretnego hosta trzeba sprawdzić w jego własnej dokumentacji.

Transport: stdio kontra Streamable HTTP

Specyfikacja definiuje dwa transporty i to naprawdę jest wybór binarny.

Stdio: klient uruchamia serwer jako proces potomny i rozmawia z nim przez standardowe strumienie. Jedna wiadomość JSON-RPC na linię, bez znaków nowej linii w środku. Obowiązuje tu twarda reguła: serwer nie może wypisać na `stdout` niczego, co nie jest poprawną wiadomością MCP. Każde `print()`, `console.log()`, `System.out.println()`, `println()` czy `Console.WriteLine()` psuje strumień JSON-RPC i — jak pisze dokumentacja — zabija serwer. Logi idą na `stderr`: specyfikacja wprost pozwala serwerowi pisać tam ciągi UTF-8 w celach informacyjnych, debugowych i błędowych, a klient nie powinien traktować wyjścia na `stderr` jako sygnału awarii.

Streamable HTTP: serwer wystawia jedną ścieżkę HTTP przyjmującą POST. Każde żądanie to osobny POST, a odpowiedź to albo pojedynczy obiekt JSON, albo strumień SSE związany z tym jednym żądaniem. W wersji 2026-07-28 usunięto samodzielny strumień GET i sesje na poziomie protokołu, a strumienie nie są wznawialne przez `Last-Event-ID`. Długo żyjące powiadomienia o zmianach dostaje się przez osobne żądanie `subscriptions/listen`, którego odpowiedź jest strumieniem SSE pozostającym otwartym. Serwer obsługujący wyłącznie tę wersję ma na GET i DELETE odpowiadać kodem 405, ignorować `Mcp-Session-Id` i ignorować `Last-Event-ID`.

Kiedy co: stdio, gdy serwer ma działać na maszynie użytkownika i obsługiwać jednego klienta — to domyślny wybór dla narzędzi deweloperskich i dla wszystkiego, co sięga do lokalnych plików. Streamable HTTP, gdy serwer ma stać u ciebie i obsługiwać wielu klientów; dokumentacja architektury opisuje ten podział dokładnie w ten sposób.

Ten wybór pociąga za sobą model uwierzytelniania — ale nie tak ostro, jak się to zwykle powtarza. Specyfikacja autoryzacji zaczyna się od zdania, że autoryzacja jest opcjonalna: implementacje na transporcie HTTP powinny się do niej stosować, implementacje na stdio nie powinny i mają pobierać poświadczenia ze środowiska. Dokumentacja architektury wymienia dla Streamable HTTP standardowe metody uwierzytelniania HTTP — tokeny bearer, klucze API i własne nagłówki — i dopiero rekomenduje OAuth jako sposób zdobycia tokena. Praktyczny wniosek jest więc dwuczłonowy: przy stdio klucz do bazy siedzi w zmiennej środowiskowej procesu, a przy HTTP wybierasz świadomie — jeśli serwer jest zdalny i dostępny publicznie, obowiązującą ścieżką jest OAuth 2.1 z walidacją odbiorcy tokena, opisana w sekcji o uwierzytelnianiu niżej.

Serwer lokalny kontra zdalny

Ta sama różnica co przy transporcie, ale patrzona od strony ryzyka i utrzymania.

Serwer lokalny to serwer uruchomiony na maszynie użytkownika, zwykle przez stdio i zwykle obsługujący jednego klienta. Zdalny to serwer działający pod twoim adresem, przez Streamable HTTP, typowo dla wielu klientów. Lokalny jest prostszy, nie wymaga hostingu i nie wystawia twojej bazy do internetu, ale biegnie z uprawnieniami użytkownika i musi być u niego zainstalowany oraz aktualizowany.

Dokumentacja bezpieczeństwa traktuje serwery lokalne jako osobny rozdział i wymienia trzy scenariusze ataku: złośliwa komenda startowa wstrzyknięta w konfigurację klienta, złośliwy ładunek w samym serwerze oraz dostanie się przez DNS rebinding do niezabezpieczonego serwera zostawionego na localhost. Lista zaleceń dla klientów jest konkretna: pokazać pełną, nieskróconą komendę przed wykonaniem, oznaczyć ją jako operację potencjalnie niebezpieczną, wymagać jawnej zgody, ostrzec, że serwer MCP działa z tymi samymi uprawnieniami co klient, i uruchamiać serwery w piaskownicy o minimalnych uprawnieniach.

Autorom serwerów przeznaczonych do uruchamiania lokalnie ta sama dokumentacja radzi coś, co warto zapamiętać: używaj transportu stdio, żeby ograniczyć dostęp wyłącznie do klienta MCP. Jeśli mimo wszystko wystawiasz serwer lokalny po HTTP, ogranicz dostęp — wymagaj tokena autoryzacyjnego albo użyj gniazd domeny uniksowej lub innego mechanizmu IPC z ograniczonym dostępem.

Zasady dla samego transportu HTTP są w specyfikacji sformułowane wprost: serwer musi walidować nagłówek `Origin` na wszystkich przychodzących połączeniach, a przy obecnym i nieprawidłowym `Origin` odpowiedzieć kodem 403; uruchamiany lokalnie powinien nasłuchiwać tylko na 127.0.0.1, a nie na 0.0.0.0; i powinien mieć poprawne uwierzytelnianie wszystkich połączeń. Bez tego — cytując specyfikację — atakujący mogą użyć DNS rebindingu, żeby ze zdalnej strony rozmawiać z lokalnym serwerem MCP.

Serwer zdalny aktualizujesz w jednym miejscu i kontrolujesz, co widzi, ale musisz go utrzymać, uwierzytelnić i pilnować, bo stoi w sieci. To jest realny koszt stały, a nie jednorazowy — wróć do niego, zanim obiecasz komuś zdalny serwer.

W czym to napisać: dziesięć oficjalnych SDK

To jest sedno pytania „TypeScript, .NET czy coś innego”. Odpowiedź: oficjalnych SDK jest dziesięć i żadne nie jest „tym właściwym”.

Wybór idzie za tym, w czym stoi reszta twojego systemu, bo serwer MCP to cienka warstwa nad kodem, który już masz — ma sięgać do twojej bazy, twojego systemu magazynowego, twoich uprawnień. Przepisywanie tego w innym języku, żeby dopasować się do modnego SDK, to najdroższy możliwy sposób na uzyskanie tych samych trzech narzędzi.

Projekt dzieli SDK na trzy tiery i opisuje je zobowiązaniami, a nie wrażeniami. Tier 1: sto procent testów zgodności, nowe funkcje protokołu przed wydaniem nowej wersji specyfikacji, triaż zgłoszeń w dwa dni robocze, krytyczne błędy w siedem dni, wymagane stabilne wydanie, dokumentacja z przykładami do wszystkich funkcji, opublikowana polityka zależności i mapa drogowa. Tier 2: osiemdziesiąt procent testów, nowe funkcje w ciągu sześciu miesięcy, triaż w miesiąc, krytyczne błędy w dwa tygodnie. Tier 3: brak minimum testów, brak zobowiązania czasowego co do nowych funkcji, brak wymogu triażu i brak wymogu stabilnego wydania.

Tiery nie są nadane raz na zawsze i to jest argument, który warto znać, wybierając na dłużej. Dokumentacja opisuje degradację: SDK spada o tier, jeśli testy zgodności na najnowszym stabilnym wydaniu padają nieprzerwanie przez cztery tygodnie (dla tieru 1 wystarczy jeden test, dla tieru 2 — ponad dwadzieścia procent), albo jeśli zgłoszenia zostają bez reakcji przez dwa miesiące. Innymi słowy: etykieta „tier 1” jest mierzona, a nie deklarowana.

Kilka rzeczy widocznych dziś w repozytoriach, które warto wziąć pod uwagę przy planowaniu. SDK dla TypeScriptu i Pythona wydały linię v2 obsługującą specyfikację 2026-07-28; w TypeScripcie pakiety rozdzielono na `@modelcontextprotocol/server` i `@modelcontextprotocol/client`, a linia v1.x ma dostawać poprawki błędów i bezpieczeństwa przez co najmniej sześć miesięcy od wydania v2. SDK dla Go jest utrzymywane we współpracy z Google i publikuje w README tabelę zgodności: specyfikację 2026-07-28 obsługują wersje od v1.7.0. SDK dla C# jest utrzymywane we współpracy z Microsoftem i rozbite na kilka pakietów NuGet — `ModelContextProtocol.Core`, główny `ModelContextProtocol`, `ModelContextProtocol.AspNetCore` dla serwerów HTTP oraz osobne pakiety rozszerzeń Apps i Tasks. SDK dla PHP powstaje we współpracy Fundacji PHP i projektu Symfony i samo określa się jako eksperymentalne do pierwszego wydania głównego.

Oficjalne SDK MCP — tier, typowy kontekst użycia i na co uważać (stan na 21 września 2026 r.)
JęzykTierKiedy ma sensNa co uważać
TypeScript1Backend w Node, zespół frontendowy, serwer uruchamiany przez npx u użytkownikaLinia v2 rozbita na dwa pakiety (`server` i `client`); v1.x ma dostawać poprawki błędów i bezpieczeństwa przez co najmniej sześć miesięcy od wydania v2
Python1Analityka, integracje z danymi, zespół, który i tak pisze skrypty w Pythoniev2 to obecna stabilna linia — bez ograniczenia wersji w zależnościach migracja zaskoczy cię przy najbliższym buildzie; repozytorium ma osobny przewodnik migracji
C#1Firmy stojące na .NET, integracja z istniejącym backendem lub systemem ERPUtrzymywany we współpracy z Microsoftem; oficjalny przewodnik instaluje pakiet z flagą `--prerelease`; serwer HTTP to osobny pakiet `ModelContextProtocol.AspNetCore`
Go1Usługi sieciowe, jeden binarny plik do wdrożenia, serwery zdalneUtrzymywany we współpracy z Google; tabela zgodności w README: specyfikację 2026-07-28 obsługują wersje od v1.7.0
Rust1Systemy o wysokich wymaganiach wydajnościowych, narzędzia natywneCrate `rmcp`, implementacja oparta na środowisku tokio; obsługuje 2026-07-28 i zachowuje zgodność z 2025-11-25 oraz wcześniejszymi; przejście na 3.x ma osobny przewodnik migracji
Java2Duże wdrożenia korporacyjne, zespoły na SpringuTier 2 oznacza nowe funkcje protokołu w ciągu sześciu miesięcy, nie od razu; repozytorium deklaruje Javę 17+, dystrybucja przez Maven Central, integracja przez Spring AI
Ruby2Istniejąca aplikacja w Rails, do której serwer ma sięgaćGem `mcp`, obsługa stdio i Streamable HTTP oraz integracja z Rails; jak w całym tierze 2 — opóźnienie względem nowych wersji specyfikacji
Kotlin3Wieloplatformowe narzędzia JVM/Native/JS/Wasm, ekosystem AndroidaTier 3 — brak zobowiązań czasowych co do nowych funkcji protokołu; dystrybucja przez Maven Central
PHP3Serwer przy istniejącej aplikacji w Symfony lub LaraveluTier 3; projekt prowadzony wspólnie przez Fundację PHP i Symfony, sam określa się jako eksperymentalny do pierwszego wydania głównego
Swift3Narzędzia natywne dla macOS i iOSTier 3, bez minimalnego progu testów zgodności; README deklaruje zgodność z wersją specyfikacji 2025-11-25, nie 2026-07-28 — sprawdź stan repozytorium, zanim oprzesz na nim serwer

Rozszerzenia: co jest poza rdzeniem protokołu

Część rzeczy, których ludzie szukają w MCP, nie jest w rdzeniu — i to jest dobra wiadomość, bo są opcjonalne.

Poza rdzeniem specyfikacja definiuje rozszerzenia: dodatki modularne, wyspecjalizowane albo eksperymentalne. Dokumentacja stawia przy nich trzy warunki, które ułatwiają planowanie. Są zawsze domyślnie wyłączone i wymagają jawnego włączenia przez programistę. Są negocjowane — klient deklaruje obsługę w polu `extensions` wewnątrz `io.modelcontextprotocol/clientCapabilities`, serwer w odpowiedzi `server/discover`. I ewoluują niezależnie od rdzenia.

Oficjalne rozszerzenia mieszczą się w kilku rodzinach. Tasks to asynchroniczne wykonywanie długich operacji, z odpytywaniem o status, dopytywaniem w trakcie i trwałymi uchwytami — jeśli twoje narzędzie liczy się minutami, to jest miejsce, w które warto zajrzeć, zamiast przeciągać jedno wywołanie. MCP Apps pozwala serwerowi wyświetlić interaktywne elementy interfejsu (wykresy, formularze, odtwarzacze wideo) wewnątrz rozmowy. Skills over MCP pozwala odkrywać instrukcje do przepływów pracy i czytać pliki pomocnicze przez zasoby MCP. Osobna rodzina dotyczy autoryzacji: przepływ OAuth client credentials dla komunikacji maszyna–maszyna oraz autoryzacja zarządzana przez przedsiębiorstwo.

Praktyczna konsekwencja przy wyborze SDK: dokumentacja tierów mówi tylko tyle, że funkcje eksperymentalne i rozszerzenia protokołu nie są wymagane w żadnym tierze, a zdanie o pełnej autonomii maintainerów SDK co do obsługiwanych rozszerzeń stoi na osobnej stronie o rozszerzeniach. Jeśli budujesz na Tasks albo Apps, sprawdź to w dokumentacji konkretnego SDK, bo sam tier nic tu nie obiecuje.

Dobra praktyka, którą dokumentacja nazywa łagodną degradacją: jeśli jedna strona obsługuje rozszerzenie, a druga nie, obsługująca ma albo wrócić do zachowania rdzenia, albo odrzucić żądanie z czytelnym błędem, gdy rozszerzenie jest obowiązkowe. Serwer z narzędziami wzbogaconymi o interfejs powinien nadal zwracać sensowną treść tekstową klientom bez tego rozszerzenia.

Opisy narzędzi: to jest wasz produkt, nie kod

Kod narzędzia napisze każdy. O trafności wywołań decyduje to, co model przeczyta.

Skoro to model wybiera narzędzie, opis narzędzia jest interfejsem użytkownika tego serwera. Dokumentacja architektury podaje wprost wzorzec nazewnictwa: nazwa ma jednoznacznie identyfikować narzędzie w przestrzeni serwera i trzymać się czytelnego schematu — `calculator_arithmetic` zamiast samego `calculate`. Specyfikacja dokłada ograniczenia: od jednego do stu dwudziestu ośmiu znaków, wielkość liter ma znaczenie, dozwolone są wyłącznie litery ASCII, cyfry, podkreślenie, myślnik i kropka, bez spacji i przecinków, nazwy unikalne w obrębie serwera.

Pole `description` opisuje, co narzędzie robi i kiedy go użyć — tak formułuje to specyfikacja. Pisz je jak instrukcję dla nowego pracownika, a nie jak komentarz w kodzie: co robi, kiedy sięgnąć, kiedy nie sięgać, co znaczą parametry i w jakim formacie. Osobne pole `title` służy do wyświetlania człowiekowi, więc możesz mieć jednocześnie techniczną nazwę i ludzką etykietę.

Trzy rzeczy, które podnoszą jakość działania serwera, a które łatwo przeoczyć. Po pierwsze, kolejność: specyfikacja zaleca zwracanie narzędzi w kolejności deterministycznej, bo to pozwala klientom niezawodnie buforować listę i poprawia trafienia w pamięć podręczną promptów modelu. Po drugie, buforowanie: odpowiedzi `tools/list` niosą pola `ttlMs` i `cacheScope`, czyli podpowiedź świeżości i zakres ponownego użycia — warto je ustawiać świadomie, zamiast zostawiać domyślne. Po trzecie, stronicowanie: `tools/list` obsługuje kursor, więc długa lista nie musi lecieć w jednym kawałku.

Ważny niuans dotyczący uprawnień. Specyfikacja mówi, że zbiór narzędzi nie może zmieniać się „per połączenie” ani jako efekt uboczny innych żądań, ale może zależeć od autoryzacji przedstawionej w danym żądaniu — bo poświadczenia są wejściem żądania, a nie stanem połączenia. To jest sankcjonowany sposób na to, żeby operacje zapisu w ogóle nie pojawiały się na liście komuś, kto nie ma do nich prawa.

Wreszcie błędy. Specyfikacja rozdziela dwie ścieżki: błędy protokołu (nieznane narzędzie, źle zbudowane żądanie) wracają jako zwykłe błędy JSON-RPC, a błędy wykonania narzędzia — awaria API, zła data, wartość poza zakresem, reguła biznesowa — wracają w wyniku z `isError: true` i treścią, na podstawie której model może się poprawić i spróbować jeszcze raz. Klienty powinny podawać modelowi tę drugą kategorię. Jeśli wszystkie twoje błędy lecą jako błędy protokołu, odbierasz modelowi możliwość naprawienia własnej pomyłki.

Bezpieczeństwo: lista dozwolonych operacji, limity, dziennik

To jest najważniejsza sekcja tego przewodnika i jedyna, przy której nie warto oszczędzać czasu.

Serwer MCP to wykonywanie kodu na czyjeś żądanie, gdzie „ktoś” to model interpretujący zdanie po polsku. Specyfikacja nakłada na serwery cztery twarde obowiązki przy narzędziach: walidować wszystkie wejścia, wdrożyć właściwą kontrolę dostępu, ograniczać częstotliwość wywołań i sanityzować wyjścia. Po stronie klienta zaleca potwierdzanie operacji wrażliwych przez użytkownika, pokazywanie mu wejść przed wywołaniem — wprost po to, by uniknąć złośliwego lub przypadkowego wycieku danych — walidowanie wyników przed podaniem ich modelowi, limity czasu i logowanie wywołań do celów audytowych.

Do tego dochodzi zasada powtarzana w kilku miejscach: przy narzędziach powinien zawsze istnieć człowiek w pętli, mający możliwość odmówić wywołania. Adnotacje opisujące zachowanie narzędzia klient ma traktować jako niezaufane, jeśli nie pochodzą z zaufanego serwera. To nie jest ozdobnik — adnotacja „to narzędzie tylko czyta” jest deklaracją serwera, a nie gwarancją protokołu.

Osobny wektor ataku wynika wprost z bezstanowości i ma w dokumentacji własną nazwę: przejęcie uchwytu stanu. Skoro serwery pamiętające coś między wywołaniami zwracają jawny uchwyt (identyfikator koszyka, przepływu pracy, sesji roboczej), atakujący, który go zgadnie lub zdobędzie, może operować na cudzym stanie. Wymogi są jednoznaczne: serwer implementujący autoryzację musi weryfikować wszystkie przychodzące żądania i nie może traktować posiadania uchwytu jako uwierzytelnienia. Zalecenia: uchwyty losowe i nieprzewidywalne, wiązane po stronie serwera z uwierzytelnionym użytkownikiem — na przykład przez klucz `<user_id>:<uchwyt>`, gdzie identyfikator użytkownika pochodzi ze zweryfikowanego tokena, a nie od klienta — oraz odrzucanie uchwytu przedstawionego przez kogokolwiek innego.

Uwierzytelnianie: OAuth 2.1 i zakaz przekazywania tokenów

Autoryzacja w MCP jest opcjonalna, ale przy serwerze zdalnym trudno się bez niej obejść — a jak już ją włączysz, jedna reguła jest bezwzględna.

Specyfikacja mówi to wprost: autoryzacja jest opcjonalna, implementacje na transporcie HTTP powinny się do niej stosować, implementacje na stdio nie powinny i mają brać poświadczenia ze środowiska, a implementacje na innych transportach muszą trzymać się uznanych praktyk bezpieczeństwa swojego protokołu. Gdy już ją włączasz, standard opiera się na OAuth 2.1 i dokłada wymogi, które w praktyce decydują o tym, czy serwer jest bezpieczny.

Serwer MCP występuje jako OAuth-owy resource server i musi zaimplementować OAuth 2.0 Protected Resource Metadata (RFC 9728), żeby klient wiedział, gdzie szukać serwera autoryzacji. Klient musi implementować Resource Indicators (RFC 8707) i wysyłać parametr `resource` wskazujący konkretny serwer MCP — zarówno w żądaniu autoryzacji, jak i przy wymianie na token — i to niezależnie od tego, czy serwer autoryzacji ten parametr wspiera. Warto też zapamiętać dwie rzeczy z listy wymogów: token idzie w nagłówku `Authorization: Bearer`, a nie w query stringu, i musi być dołączany do każdego żądania HTTP, bo połączenie nie niesie stanu.

Najważniejsze zdanie całej tej sekcji dotyczy przekazywania tokenów dalej. Dokumentacja bezpieczeństwa nazywa to antywzorcem i formułuje wymóg bez miejsca na interpretację: serwer MCP nie może przyjmować żadnych tokenów, które nie zostały wydane wprost dla niego. Specyfikacja dopowiada, że serwer musi zweryfikować, iż to on jest zamierzonym odbiorcą tokena, może akceptować wyłącznie tokeny ważne dla własnych zasobów i nie może przyjmować ani przekazywać dalej żadnych innych. Powody są wyliczone: przyjmowanie cudzych tokenów rozbraja limity i kontrolę ruchu, psuje ślad audytowy (serwer przestaje odróżniać klientów, a logi API producenta pokazują cudzą tożsamość) i pozwala komuś z wykradzionym tokenem użyć twojego serwera jako serwera proxy do wyciągania danych.

Jeśli twój serwer pośredniczy do API trzeciej strony, przeczytaj rozdział o problemie zdezorientowanego zastępcy. Wymogi są konkretne: własny ekran zgody wyświetlany przed przekierowaniem do zewnętrznego serwera autoryzacji, rejestr zatwierdzonych `client_id` per użytkownik sprawdzany przed rozpoczęciem przepływu, dokładne (a nie wzorcowe) dopasowanie `redirect_uri`, ochrona przed CSRF i przed osadzeniem ekranu zgody w ramce, oraz jednorazowy, krótko żyjący parametr `state` — którego ciasteczko lub sesję ustawiasz dopiero po zatwierdzeniu zgody przez użytkownika. Ostatni punkt jest kluczowy: ustawienie go wcześniej sprawia, że ekran zgody przestaje cokolwiek chronić.

Kiedy NIE budować własnego serwera

Najlepszy serwer MCP to ten, którego nie musiałeś napisać.

Jak zacząć: minimalny serwer z trzema narzędziami do odczytu

Ta wersja jest bezpieczna z definicji — nie ma czego zepsuć — a odpowiada na jedyne pytanie, które ma na tym etapie znaczenie: czy model trafnie wybiera narzędzie na podstawie opisu, który mu dałeś.

  1. Wybierz SDK zgodne z resztą systemuNie z listą życzeń, tylko z tym, w czym już utrzymujesz kod. Masz backend w .NET — bierz SDK dla C#; w Node — TypeScript. Tier 1 daje ci zobowiązanie, że nowe funkcje protokołu pojawią się w bibliotece przed wydaniem nowej wersji specyfikacji, a zgłoszenia trafią do triażu w dwa dni robocze.
  2. Zacznij od transportu stdioSerwer uruchamiany lokalnie jako proces potomny, bez hostingu, bez OAuth-a, z poświadczeniami w zmiennych środowiskowych — dokładnie tak, jak przewiduje specyfikacja autoryzacji dla stdio. Na tym etapie nie masz powierzchni ataku po stronie sieci.
  3. Zdefiniuj trzy narzędzia wyłącznie do odczytuTypowy komplet: wyszukanie rekordu po identyfikatorze, lista rekordów z ostatnich N dni i podsumowanie jednego zbioru. Każde z nazwanym, wąskim `inputSchema`. Zero zapisu, zero usuwania, zero dowolnego SQL-a.
  4. Napisz opisy jak dla nowego pracownikaOpis narzędzia to wejście modelu: napisz, co robi, kiedy go użyć, a kiedy nie, i co znaczą parametry. Nazwy trzymaj w granicach zalecanych przez specyfikację — od 1 do 128 znaków, litery ASCII, cyfry, podkreślenie, myślnik i kropka, bez spacji — i pilnuj unikalności w obrębie serwera.
  5. Loguj wyłącznie na stderrPrzy stdio każdy zapis na stdout, który nie jest wiadomością MCP, psuje strumień JSON-RPC. Uwaga na częste nieporozumienie: ten zakaz dotyczy stdio. Przy serwerze HTTP dokumentacja mówi wprost, że logowanie na standardowe wyjście jest w porządku, bo nie koliduje z odpowiedziami HTTP.
  6. Przetestuj w MCP Inspectorze, zanim podłączysz do hostaInspector to oficjalne narzędzie projektu, opisane jako wizualne narzędzie do testowania serwerów MCP. Pozwala zobaczyć listę narzędzi i wywołać je ręcznie, bez pośrednictwa modelu — czyli oddzielić błąd serwera od błędu opisu.
  7. Sprawdź zachowanie przy niepowodzeniuZanim pokażesz serwer komukolwiek, wywołaj narzędzie ze złym argumentem i zobacz, co wraca. Błąd wykonania ma wrócić w wyniku z `isError: true` i treścią, z której model zrozumie, co poprawić. Sprawdź też, co się stanie, gdy proces padnie — specyfikacja zakłada, że klient go zrestartuje, a żądania w locie po prostu przepadną i zostaną powtórzone.
  8. Dopiero potem: zapis, HTTP, uwierzytelnianieKażdy z tych kroków to osobna decyzja z własnym kosztem. Zapis wymaga potwierdzenia użytkownika i osobnych poświadczeń. HTTP wymaga walidacji nagłówka `Origin` z odpowiedzią 403, nasłuchiwania na 127.0.0.1 przy pracy lokalnej, limitów i — jeśli serwer jest publiczny — OAuth-a z weryfikacją odbiorcy tokena.

Wersje protokołu i co to znaczy dla utrzymania

Zanim napiszesz drugą wersję serwera, sprawdź, jak zachowa się przy zmianie wersji standardu.

Wersje protokołu są datami w formacie RRRR-MM-DD i — jak zaznacza dokumentacja — numer nie rośnie przy każdej aktualizacji, a wyłącznie wtedy, gdy zmiana łamie zgodność wstecz. Obowiązująca wersja to 2026-07-28; adres /specification/latest przekierowuje właśnie na nią (sprawdzone 21 września 2026 r.). Negocjacja jest per żądanie: klient deklaruje wersję w polu `_meta`, a na Streamable HTTP dodatkowo w nagłówku `MCP-Protocol-Version`, którego wartość musi zgadzać się z ciałem żądania — w przeciwnym razie serwer odrzuca żądanie kodem 400 i błędem `HeaderMismatch`. Przy nieobsługiwanej wersji serwer odpowiada błędem `UnsupportedProtocolVersionError` z listą wersji, które obsługuje. Klient i serwer mogą obsługiwać wiele wersji naraz.

Funkcje usuwane ze standardu przechodzą najpierw przez status „przestarzała”. Strona o wersjonowaniu podaje termin z wyjątkiem, którego łatwo nie zauważyć: taka funkcja zostaje w specyfikacji co najmniej dwanaście miesięcy — albo co najmniej dziewięćdziesiąt dni, jeśli zadziała przewidziany w polityce cyklu życia wyjątek przyspieszonego usunięcia. Planuj według krótszego terminu, nie dłuższego. Aktualnie przestarzałe funkcje są wypisane w osobnym rejestrze, do którego ta strona linkuje — warto go raz na kwartał przejrzeć.

Praktyczny wniosek: nie wpisuj wersji protokołu na sztywno w logikę biznesową. Trzymaj ją w SDK, a SDK aktualizuj świadomie — repozytoria publikują tabele zgodności wersji biblioteki z wersjami specyfikacji, jak robi to SDK dla Go. Do tego dochodzi drugi sygnał, który warto obserwować: tier SDK jest mierzony testami zgodności i może zostać obniżony, jeśli testy padają nieprzerwanie przez cztery tygodnie albo zgłoszenia leżą dwa miesiące bez reakcji.

Jeśli twój serwer ma współpracować z klientami sprzed wersji 2026-07-28, weź pod uwagę, że tamte wersje wymagały uścisku dłoni `initialize`, pozwalały serwerowi wysyłać własne żądania JSON-RPC na strumieniach SSE i znały sesje na poziomie protokołu z nagłówkiem `Mcp-Session-Id` oraz wznawianie strumieni przez `Last-Event-ID`. Specyfikacja opisuje, jak wykryć, z którą epoką rozmawiasz — klient sonduje żądaniem `server/discover` i dopiero przy nierozpoznanym błędzie schodzi do `initialize` — ale to jest kod, który trzeba napisać i przetestować. Decyzja o wstecznej zgodności nie jest darmowa i warto ją podjąć świadomie, a nie przy okazji.

Pytania i odpowiedzi

Serwer MCP — co to właściwie jest, w jednym zdaniu?

To program, który wystawia aplikacji AI zestaw nazwanych narzędzi, danych i szablonów, opisanych na tyle dokładnie, żeby model mógł sam zdecydować, którego użyć. Komunikacja idzie po JSON-RPC 2.0, przez standardowe strumienie procesu (stdio) albo przez jedną ścieżkę HTTP przyjmującą POST.

TypeScript, .NET czy coś innego — w czym pisać?

W tym, w czym stoi reszta twojego systemu. Oficjalnych SDK jest dziesięć; w tierze 1, czyli ze stuprocentową zgodnością testową i nowymi funkcjami protokołu przed wydaniem nowej wersji specyfikacji, są TypeScript, Python, C#, Go i Rust. Java i Ruby są w tierze 2, a Swift, PHP i Kotlin w tierze 3. Serwer MCP to cienka warstwa nad twoim istniejącym kodem — zmiana języka po to, żeby dopasować się do modnej biblioteki, jest najdroższym sposobem na to samo.

Czy mogę uruchomić serwer MCP lokalnie, bez wystawiania go do internetu?

Tak i to jest zalecany start. Serwer lokalny działa na transporcie stdio: host uruchamia go jako proces potomny i rozmawia przez standardowe strumienie. Nie potrzebujesz hostingu ani OAuth-a — specyfikacja autoryzacji mówi, że implementacje na stdio nie powinny jej stosować i mają pobierać poświadczenia ze środowiska. Jeśli mimo to wystawiasz serwer lokalny po HTTP, ma walidować nagłówek Origin (403 przy nieprawidłowym) i nasłuchiwać tylko na 127.0.0.1.

Dlaczego mój serwer na stdio się nie uruchamia?

Pierwsze, co sprawdź: czy cokolwiek w twoim procesie pisze na stdout. Specyfikacja zabrania wypisywania tam czegokolwiek, co nie jest poprawną wiadomością MCP, a dokumentacja ostrzega przy każdym języku osobno: `print()` w Pythonie, `console.log()` w Node, `System.out.println()` w Javie, `println()` w Kotlinie i `Console.WriteLine()` w C# domyślnie piszą właśnie tam i psują strumień JSON-RPC. Przenieś logi na stderr — specyfikacja wprost na to pozwala. Przy serwerze HTTP ten problem nie występuje: tam logowanie na standardowe wyjście jest w porządku.

Czy muszę robić OAuth, jeśli wystawiam serwer po HTTP?

Nie z automatu. Specyfikacja mówi, że autoryzacja jest w MCP opcjonalna, a dokumentacja architektury wymienia dla Streamable HTTP standardowe metody uwierzytelniania HTTP — tokeny bearer, klucze API i własne nagłówki — rekomendując OAuth jako sposób zdobycia tokena. Jeśli jednak serwer jest zdalny i dostępny publicznie, obowiązującą ścieżką jest OAuth 2.1: serwer implementuje RFC 9728, klient wysyła parametr `resource` z RFC 8707, a serwer musi zweryfikować, że token został wydany właśnie dla niego.

Czym to się różni od zwykłego API, które już mam?

Tym, kto decyduje o wywołaniu. W twoim API decyduje kod, który sam napisałeś, i zna kolejność kroków. W MCP decyduje model, na podstawie opisów publikowanych przez serwer. Dlatego opis narzędzia i schemat wejścia przestają być dokumentacją, a stają się mechanizmem kontroli — a każde narzędzie musi być bezpieczne niezależnie od tego, czy model wywołał je jako pierwsze, czy jako trzecie.

Ile narzędzi powinien mieć pierwszy serwer?

Trzy i wszystkie wyłącznie do odczytu. Trzy wystarczą, żeby sprawdzić, czy model trafnie wybiera narzędzie na podstawie opisu, a brak zapisu oznacza, że najgorszy możliwy błąd to zła odpowiedź, a nie zmiana w twoich danych. Zapis, transport HTTP i uwierzytelnianie dokładaj jako osobne, świadome decyzje.

Jak serwer ma pamiętać cokolwiek między wywołaniami, skoro protokół jest bezstanowy?

Przez jawny uchwyt. Narzędzie tworzące zwraca identyfikator (koszyka, transakcji, sesji roboczej), a kolejne wywołania przyjmują go jako zwykły argument — protokół nie zna pojęcia uchwytu, to po prostu ciąg znaków w wyniku i w argumentach. Warunki są jednak twarde: serwer nie może traktować posiadania uchwytu jako uwierzytelnienia, uchwyty mają być losowe i nieprzewidywalne, a stan wiązany po stronie serwera z uwierzytelnionym użytkownikiem. Dokumentacja radzi też podać czas życia uchwytu w opisie narzędzia i zwracać czytelny błąd, gdy uchwyt wygasł.

Źródła

Liczby i terminy w tym przewodniku sprawdziłem w oficjalnych źródłach. Ceny i przepisy się zmieniają, więc przy decyzji warto zajrzeć do nich ponownie.

Masz pytanie do swojego projektu?

Opisz, co chcesz osiągnąć. Odpowiem konkretnie, także wtedy, gdy najlepszą odpowiedzią jest „nie rób tego”.

Jonasz Jankowski · Bezpośrednia współpraca · Mikołów (Śląsk), zdalnie w całej Polsce

Pola oznaczone jako opcjonalne możesz pominąć. Odpowiadam na każdą wiadomość.

Minimum 20 znaków. Wystarczy zalążek pomysłu.0/5000

Na podany e-mail otrzymasz potwierdzenie i kopię swojej wiadomości. Odpowiadam w ciągu jednego dnia roboczego. Dalszą rozmowę możemy prowadzić w tym samym wątku.