MCP 2026protokół od zera · wydanie 2026-07-28
część I · model 2025-11-25 · fundamenty

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.

bez standardu — siatka połączeń
Bez wspólnego standardu każda z 4 aplikacji łączy się osobno z każdym z 4 narzędzi — 16 bezpośrednich, krzyżujących się połączeń. IDE Desktop Agent CLI Slack Baza Pliki CRM 16 osobnych integracji

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.

z MCP — jeden wspólny punkt
Z MCP każda aplikacja i każde narzędzie łączy się tylko z jednym wspólnym protokołem — 8 połączeń zamiast 16. IDE Desktop Agent CLI MCP Slack Baza Pliki CRM 8 implementacji jednego protokołu

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.

💡 Skąd to się wzięło

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:

Request

Ma id. Oczekuje odpowiedzi — albo result, albo error.

Response

Ten sam id co request. Nigdy oba pola result/error naraz.

Notification

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:

host
Aplikacja LLM

Claude Desktop, Twoje IDE, własny agent. To ONA inicjuje połączenia i pokazuje użytkownikowi UI zgody.

client
Konektor wewnątrz hosta

Jeden na jeden serwer. Host może trzymać wiele klientów naraz — po jednym na każde połączenie MCP.

server
Dostawca kontekstu

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" }
  }
}
🕯 Zapamiętaj to — w Części II znika

Cała ta trójwiadomościowa ceremonia (initializeresultnotifications/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.

dlaczego to bolało przy skalowaniu

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ę":

Tools

Funkcje, które model MOŻE wywołać. Ma nazwę, opis, schemat wejścia. Model decyduje, kiedy ich użyć.

Resources

Dane do odczytu — pliki, wpisy w bazie, wynik zapytania. Użytkownik albo model mogą je dołączyć do kontekstu.

Prompts

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}
💡 Progressive Disclosure

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ł?"

⚠ Dlaczego to boli w praktyce

Ż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ą

stdio

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.

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

standalone GET SSE

Osobny endpoint GET, który klient otwierał, żeby dostawać wiadomości zainicjowane przez serwer poza kontekstem konkretnego żądania.

🕯 Zapamiętaj to — w Części II znika

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.

Sprawdź się: model 2025-11-25

Kto w MCP inicjuje połączenie?
Jaka jest poprawna kolejność handshake'u?
Dlaczego Mcp-Session-Id utrudniał skalowanie?
Co robi sampling/createMessage w modelu 2025-11-25?