Zanim cokolwiek zniknie, zobacz, jak działało.
Ta część opisuje MCP tak, jak działał do 28 lipca 2026 — model, w którym rozmowa klient-serwer jest połączeniem, a nie ciągiem niezależnych żądań. To fundament: w Części II będziemy go rozbierać punkt po punktu.
1. Po co w ogóle MCP
Masz N aplikacji (Claude Desktop, IDE, własny agent) i M narzędzi/źródeł danych (baza, Slack, system plików, CRM). Bez wspólnego standardu każda para aplikacja↔narzędzie to osobna integracja — czyli w najgorszym razie N×M kawałków kodu do utrzymania.
Każda apka pisze własny konektor do Slacka, własny do bazy, własny do CRM… N×M kawałków kodu do utrzymania.
Aplikacje i narzędzia znają tylko MCP — nie znają się nawzajem. Dojdzie 5. narzędzie? To +1 połączenie, nie +4.
To jest dokładnie problem, który w świecie edytorów kodu rozwiązał Language Server Protocol: zamiast każdy edytor pisał integrację z każdym językiem, LSP ustandaryzował jeden protokół między edytorem a serwerem językowym. MCP robi to samo dla LLM-ów i kontekstu — stąd popularne określenie „USB-C dla świata AI": jeden port, wiele urządzeń po obu stronach.
MCP powstał w Anthropic (listopad 2024) jako projekt dogfoodingowy — Zed był pierwszym zewnętrznym partnerem. To nie jest „format API Anthropic" — to otwarty protokół, z własną specyfikacją i społecznością SEP-ów (Specification Enhancement Proposals), niezależny od konkretnego dostawcy modelu.
2. JSON-RPC 2.0 — wspólny język
MCP nie wymyśla własnego formatu wiadomości — bierze gotowy, dwudziestoletni standard: JSON-RPC 2.0. Trzy typy wiadomości, i tyle:
Ma id. Oczekuje odpowiedzi — albo result, albo error.
Ten sam id co request. Nigdy oba pola result/error naraz.
Brak id. Wysyłasz i zapominasz — odbiorca nie odpowiada.
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_weather" } }
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [{"type":"text","text":"14°C, słonecznie"}] } }
{ "jsonrpc": "2.0", "method": "notifications/progress", "params": { "progress": 0.4 } }
Zapamiętaj kształt tych trzech wiadomości — w Części II nie zmieni się ani jedno pole. To, co się zmieni, to kiedy i ile takich wiadomości musisz wymienić, zanim cokolwiek zadziała.
3. Trzy role: Host, Client, Server
To rozróżnienie myli najwięcej osób na starcie, bo w codziennej mowie mówimy „klient MCP" na wszystko. W specyfikacji role są precyzyjne:
Claude Desktop, Twoje IDE, własny agent. To ONA inicjuje połączenia i pokazuje użytkownikowi UI zgody.
Jeden na jeden serwer. Host może trzymać wiele klientów naraz — po jednym na każde połączenie MCP.
To Ty, gdy piszesz serwer MCP. Wystawia narzędzia, zasoby, prompty. Nie wie nic o hoście poza tym, co klient mu powie.
Dla architekta piszącego serwery kluczowa jest jedna rzecz: nigdy nie mówisz bezpośrednio do modelu. Rozmawiasz z klientem, klient rozmawia z hostem, host (czasem) pyta model. To dlatego np. „serwer prosi model o dokończenie zdania" (sampling) musi iść przez cały ten łańcuch — zobaczysz to w sekcji 7.
4. Handshake: initialize
Zanim w modelu 2025-11-25 poleci choć jeden „prawdziwy" request, klient i serwer muszą się przywitać. To trzy wiadomości, zawsze w tej kolejności, zawsze na początku połączenia:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": { "roots": {}, "sampling": {} }, "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } } }
Cała ta trójwiadomościowa ceremonia (initialize → result → notifications/initialized) przestaje istnieć w rewizji 2026-07-28. Zapamiętaj uczucie „najpierw się przywitajmy, potem rozmawiajmy" — to jest dokładnie to założenie, które zostanie obalone.
5. Sesja: Mcp-Session-Id
Skoro handshake ustala „stan" połączenia (wersję, capabilities), to ten stan musi gdzieś mieszkać między requestami. W transportach HTTP robił to nagłówek sesji — serwer nadawał identyfikator po initialize, a klient musiał go dołączać do każdego kolejnego żądania.
Jeśli serwer działa jako 3 kopie za load balancerem, a stan sesji (capabilities, wersja) żyje tylko w pamięci JEDNEJ kopii — to każdy kolejny request z tym samym Mcp-Session-Id musi trafić z powrotem do TEJ SAMEJ kopii. To się nazywa sticky routing i jest dokładnym przeciwieństwem tego, do czego dąży się przy skalowaniu w chmurze (bezstanowe node'y, dowolny pod obsługuje dowolny request).
Zobaczysz symulator tego problemu w Części II.1 — a tam też poznasz rozwiązanie: usunięcie sesji w ogóle.
6. Co wystawia serwer: tools, resources, prompts
To trzy prymitywy, którymi Twój serwer mówi „to potrafię":
Funkcje, które model MOŻE wywołać. Ma nazwę, opis, schemat wejścia. Model decyduje, kiedy ich użyć.
Dane do odczytu — pliki, wpisy w bazie, wynik zapytania. Użytkownik albo model mogą je dołączyć do kontekstu.
Gotowe szablony rozmów — użytkownik świadomie je wybiera (np. z menu „/" w hoście).
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} } ↓ { "jsonrpc": "2.0", "id": 2, "result": { "tools": [{ "name": "crm__find_customer", "description": "Szuka klienta po e-mailu", "inputSchema": { "type":"object","properties":{"email":{"type":"string"}} } }] } }
from mcp.server.fastmcp import FastMCP server = FastMCP("crm-server") @server.tool() def crm__find_customer(email: str) -> dict: # Konwencja nazw "serwer__akcja" — łatwiej rozróżnić # narzędzia z wielu serwerów w jednej rozmowie. return {"id": "cus_123", "email": email}
Zamiast wrzucać do kontekstu modelu 40 pełnych schematów na raz, dobre serwery wystawiają najpierw skróconą listę (tools/list) i dopiero na żądanie pełny schemat pojedynczego narzędzia. To ten sam kompromis, o którym pewnie już pisałeś w swoich notatkach: „spektrum narzędzi — chaos 13 wąskich funkcji vs porządek 4 szerokich z podkomendami".
7. Co oddaje klient: sampling, elicitation, roots
To trzy rzeczy, które w modelu 2025-11-25 serwer mógł sam zażądać od klienta — prawdziwe, osobne żądania JSON-RPC wysyłane w przeciwnym kierunku, w trakcie obsługi innego żądania:
sampling/createMessage„Serwerze, potrzebujesz odpowiedzi modelu? Poproś klienta — to on ma dostęp i płaci za tokeny."
elicitation/create„Brakuje mi danych od użytkownika — poproś klienta, żeby pokazał formularz."
roots/list„Jakie katalogi/pliki użytkownik mi udostępnił?"
Żeby to zadziałało, serwer musi mieć otwarty, dwukierunkowy kanał do klienta w trakcie obsługi żądania — i musi PAMIĘTAĆ, że wysłał sampling/createMessage i czeka na odpowiedź, zanim dokończy oryginalny tools/call. To jest stan w pamięci procesu, dokładnie ten sam problem co sesja z punktu 5 — tylko teraz w mikroskali, wewnątrz pojedynczego żądania. Zapamiętaj to napięcie — Część II.7 (MRTR) rozwiązuje je zupełnie inaczej, niż byś się spodziewał.
8. Transporty: jak te bajty w ogóle podróżują
Serwer jako podproces. JSON-RPC linia po linii przez stdin/stdout. Najprostszy transport — używany głównie lokalnie (desktop), bo nie ma tu żadnej autoryzacji HTTP.
Jeden endpoint POST. Serwer może odpowiedzieć zwykłym JSON-em albo otworzyć strumień SSE, jeśli chce wysłać kilka wiadomości (progress, potem wynik).
Osobny endpoint GET, który klient otwierał, żeby dostawać wiadomości zainicjowane przez serwer poza kontekstem konkretnego żądania.
Strumień SSE w tym modelu potrafił się zerwać (sieć, restart procesu) — więc każde zdarzenie miało swój numer (Last-Event-ID), a klient po ponownym połączeniu mówił „ostatnie co widziałem to zdarzenie #47", a serwer odsyłał wszystko od #48. To się nazywało resumability. Osobny endpoint GET SSE i to odtwarzanie strumienia — oba znikają w 2026-07-28.