JSON-RPC pod spodem — co faktycznie leci między klientem MCP a serwerem

przez Łukasz | cze 5, 2026

Dwa poprzednie artykuły opisywały role i prymitywy MCP w kategoriach architektury. Ten artykuł schodzi poziom niżej — do bajtów. Do tego co faktycznie leci między procesami gdy agent wywołuje narzędzie.

Rozumienie tej warstwy ma jeden konkretny cel praktyczny: możesz debugować serwer MCP bez specjalistycznych narzędzi, wyłącznie przez curl i czytanie JSON. Gdy coś nie działa — wiesz gdzie szukać.

JSON-RPC 2.0 — specyfikacja w pięciu minutach

JSON-RPC jest protokołem prostym do bólu. Specyfikacja 2.0 ma kilka stron i opisuje trzy typy wiadomości.

Żądanie (Request)

json
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "get_term",
    "arguments": { "slug": "agent-readiness" }
  }
}

Cztery pola: jsonrpc zawsze "2.0" — tak rozpoznajesz wersję protokołu. id — unikalny numer żądania w ramach sesji, dowolna liczba lub string. method — co chcesz zrobić. params — parametry metody, opcjonalne.

Pole id jest kluczowe: odpowiedź będzie miała ten sam id — tak klient wie że ta odpowiedź dotyczy jego konkretnego żądania, a nie innego które wysłał wcześniej.

Odpowiedź sukces (Response)

json
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Agent-readiness — gotowość agentowa..."
      }
    ]
  }
}

Odpowiedź zawiera ten sam id co żądanie i albo result (sukces) albo error (błąd) — nigdy oba naraz.

Odpowiedź błąd (Error Response)

json
{
  "jsonrpc": "2.0",
  "id": 42,
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "detail": "Brakuje wymaganego parametru 'slug'"
    }
  }
}

Kody błędów JSON-RPC:

  • -32700 — Parse error (nieprawidłowy JSON)
  • -32600 — Invalid Request (poprawny JSON, złe żądanie)
  • -32601 — Method not found
  • -32602 — Invalid params
  • -32603 — Internal error
  • -32000 do -32099 — zarezerwowane dla błędów serwera (MCP używa tego zakresu)

Powiadomienie (Notification)

json
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

Powiadomienie nie ma id. Odbiorca nie odpowiada. W MCP używane do informowania o zdarzeniach: klient powiadamia serwer że inicjalizacja zakończona, serwer powiadamia klienta że resource się zmienił.

Pełna sesja MCP — sekwencja wiadomości od startu do zamknięcia

To jest wiedza której nie ma w większości tutoriali. Pełna sesja MCP ma ściśle określoną kolejność wiadomości. Pominięcie któregokolwiek kroku daje błąd.

Krok 1: Initialize

Klient wysyła pierwsze żądanie — zawsze initialize:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "roots": { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": {
      "name": "claude-desktop",
      "version": "1.5.0"
    }
  }
}

Klient mówi serwerowi: kim jestem, jaką wersję protokołu obsługuję, jakie mam capabilities.

Krok 2: Odpowiedź serwera na initialize

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "tools": {},
      "resources": {},
      "prompts": {}
    },
    "serverInfo": {
      "name": "webflux-slownik",
      "version": "2.2.0"
    }
  }
}

Serwer odpowiada: jaką wersję protokołu obsługuje, jakie ma capabilities, kim jest. To jest capability negotiation o którym pisałem w artykule 1.

Ważne: jeśli klient i serwer mają różne protocolVersion — klient powinien użyć niższej wersji (backward compatibility). Jeśli wersje są kompletnie niekompatybilne — połączenie kończy się błędem.

Krok 3: Powiadomienie initialized

Po otrzymaniu odpowiedzi na initialize klient wysyła powiadomienie — bez id, serwer nie odpowiada:

json
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

Dopiero po tym powiadomieniu sesja jest aktywna i można wysyłać właściwe żądania. Serwery które nie respektują tego kroku często dają tajemnicze błędy.

Krok 4: Właściwa praca

Teraz klient może wysyłać tools/list, tools/call, resources/list, resources/read etc. Kolejność żądań roboczych jest dowolna.

json
// Pobierz listę tools
{"jsonrpc":"2.0","id":2,"method":"tools/list"}

// Wywołaj tool
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_term","arguments":{"query":"MCP"}}}

// Odczytaj resource
{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"memo://notes/projekt"}}

Krok 5: Zamknięcie sesji

Nie ma dedykowanej metody disconnect. Klient po prostu zamyka połączenie. Przy transportach trwałych (Streamable HTTP z sesją) serwer może wysyłać ping żeby sprawdzić czy klient nadal żyje.

Debugowanie przez curl — kompletny przykład

Webflux Słownik MCP działa na Streamable HTTP. Pełna sesja przez curl:

bash
# Krok 1 — Initialize
curl -X POST https://webflux.pl/wp-json/webflux/v1/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-03-26",
      "capabilities": {},
      "clientInfo": {"name": "curl-test", "version": "1.0"}
    }
  }'

Odpowiedź powinna zawierać serverInfo z nazwą i wersją serwera. Jeśli dostajesz błąd — sprawdź URL, nagłówki Content-Type i czy serwer w ogóle odpowiada na POST.

bash
# Krok 2 — Lista tools (w prawdziwej sesji po initialized, tu dla testu)
curl -X POST https://webflux.pl/wp-json/webflux/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list"
  }'
bash
# Krok 3 — Wywołanie search_term
curl -X POST https://webflux.pl/wp-json/webflux/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "search_term",
      "arguments": {"query": "context engineering"}
    }
  }'

Co czytać w odpowiedzi:

Gdy result jest w odpowiedzi — tool wykonał się poprawnie. Gdy error — patrz na code i message. Gdy brak odpowiedzi w ogóle — problem z transportem lub serwerem.

Transport-agnostic — co to znaczy w praktyce

JSON-RPC jest transport-agnostic. Te same wiadomości JSON działają przez różne mechanizmy transportu. MCP definiuje dwa:

stdio — wiadomości przez standardowe wejście/wyjście procesu. Klient uruchamia serwer jako podproces i komunikuje się przez stdin/stdout. Każda wiadomość JSON to osobna linia zakończona \n.

klient → serwer: {"jsonrpc":"2.0","id":1,"method":"initialize",...}\n
serwer → klient: {"jsonrpc":"2.0","id":1,"result":{...}}\n

Używany dla lokalnych serwerów MCP uruchomionych na maszynie użytkownika. Claude Desktop z serwerem GitHub — stdio.

Streamable HTTP — wiadomości przez HTTP POST. Jeden endpoint przyjmuje wszystkie wiadomości. Opcjonalnie serwer może streamować odpowiedzi przez SSE (Server-Sent Events) gdy odpowiedź jest długa lub wieloczęściowa.

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize",...}

Używany dla zdalnych serwerów MCP dostępnych przez sieć. Webflux Słownik — Streamable HTTP. Każdy serwis który chce być dostępny przez internet — Streamable HTTP.

Dlaczego to rozróżnienie ważne: ten sam kod serwera który obsługuje JSON-RPC może działać przez oba transporty bez zmiany logiki biznesowej. Transport to tylko „kabel” — wiadomości są identyczne.

Nagłówki HTTP przy Streamable HTTP

Trzy nagłówki które musisz wysyłać przy każdym żądaniu do serwera MCP przez HTTP:

bash
Content-Type: application/json        # ciało żądania to JSON
Accept: application/json, text/event-stream  # akceptuję JSON lub SSE stream

Jeśli serwer zwraca Content-Type: text/event-stream zamiast application/json — streamuje odpowiedź. Każda linia SSE zaczyna się od data: i zawiera JSON:

data: {"jsonrpc":"2.0","id":1,"result":{"content":[...]}}

data: [DONE]

Biblioteki curl obsługują SSE domyślnie — po prostu czytasz output do końca.

Opcjonalny nagłówek sesji — gdy serwer wspiera sesje MCP, pierwsza odpowiedź może zawierać:

Mcp-Session-Id: abc123xyz

Dołączaj ten header do kolejnych żądań w tej sesji. Pozwala serwerowi zachować stan między requestami (np. subskrypcje resources).

Błędy które zobaczysz najczęściej

-32601 Method not found — wywołujesz metodę której serwer nie obsługuje. Sprawdź capabilities z initialize — czy serwer zadeklarował obsługę prymitywu którego używasz.

-32602 Invalid params — złe parametry. Sprawdź inputSchema toola — czy wszystkie required pola są w arguments, czy typy się zgadzają.

-32603 Internal error — błąd po stronie serwera. Sprawdź logi serwera — tam będzie stack trace.

HTTP 401/403 — problem z autoryzacją. Serwer wymaga tokenu lub klucza API który nie jest przekazywany. Sprawdź czy nagłówki auth są poprawne.

HTTP 400 z „Not an MCP request” — wysyłasz pod właściwy URL ale bez wymaganego nagłówka lub z nieprawidłowym JSON. Sprawdź Content-Type: application/json.

Brak odpowiedzi (timeout) — serwer przyjął request ale nie odpowiada. Może być zapętlony, może czekać na zewnętrzny zasób. Sprawdź logi, dodaj timeout do curl: curl --max-time 30 ...

JSON-RPC poza MCP — ten sam protokół, inne zastosowania

Znajomość JSON-RPC jest szersza niż tylko MCP. A2A (Agent-to-Agent Protocol od Google) używa JSON-RPC. Language Server Protocol (LSP) — ten który napędza autocomplete w VS Code — używa JSON-RPC. Debug Adapter Protocol (DAP) — JSON-RPC. Wiele wewnętrznych API mikroserwisów — JSON-RPC.

Inwestycja w rozumienie jednego prostego protokołu opłaca się szerzej niż w jednym kontekście.

 

W następnym artykule: wiemy jak wyglądają wiadomości. Czas zobaczyć jak te wiadomości podróżują — ewolucja transportu MCP od stdio przez SSE do Streamable HTTP i dlaczego ta zmiana miała znaczenie dla każdego kto buduje serwery dostępne przez internet.


Pojęcia ze słownika: JSON-RPC · MCP · SSE · Streamable HTTP · OAuth dla agentów

Agent kończy, ale zadania nie wykonał

Agent kończy, ale zadania nie wykonał

Objawy Agent odpowiada: „Przygotowałem i wysłałem potwierdzenie do klienta." Potwierdzenie nie zostało wysłane. Albo: „Zaktualizowałem wszystkie rekordy" — zaktualizował trzy z dwunastu. Albo: „Nie znalazłem żadnych zamówień" — bo narzędzie zwróciło błąd, którego nikt...

Agent zrobił coś, o co nikt nie prosił

Agent zrobił coś, o co nikt nie prosił

Objawy Operacja, której nikt nie zlecił. Dane wysłane pod adres, którego nie ma w żadnej konfiguracji. Rekord zmieniony poza zakresem zadania. Odpowiedź, z której wynika, że agent dostał instrukcje od kogoś innego niż ty. Cecha wspólna: technicznie wszystko zadziałało...

Agent zrobił to samo dwa razy

Agent zrobił to samo dwa razy

Objawy Ten syndrom różni się od pozostałych jedną rzeczą: objaw widzi klient, nie ty. Trzy identyczne wiadomości w skrzynce. Dwa zamówienia zamiast jednego. Podwójne obciążenie. Duplikaty rekordów, które ktoś zauważa tydzień później. W twoim dzienniku wszystko wygląda...

Agent gubi wątek w długim zadaniu

Agent gubi wątek w długim zadaniu

Objawy Pierwsze kroki idą wzorowo. Po kilkunastu agent zaczyna się rozjeżdżać. Przestaje przestrzegać reguły z promptu systemowego, której trzymał się na początku. Zmienia format odpowiedzi w połowie zadania. Wraca do czegoś, co już ustalił, i ustala to inaczej....

Agent podaje dane, których nie ma

Agent podaje dane, których nie ma

Objawy Numer zamówienia w idealnym formacie, którego nie ma w bazie. Kwota, której w dokumencie nie ma. Nazwa pola API, które nigdy nie istniało. Cytat z regulaminu, brzmiący dokładnie jak reszta regulaminu i w nim nieobecny. Cecha wspólna wszystkich tych przypadków:...

Działał wczoraj, dziś nie działa

Działał wczoraj, dziś nie działa

Objawy Nic się nie wywala. Nie ma wyjątków, nie ma timeoutów, dziennik wygląda tak samo jak zawsze. Po prostu odpowiedzi są gorsze. Postacie, w jakich to się objawia: Jakość. Agent zaczyna pomijać kroki, które wcześniej wykonywał, albo odpowiada ogólniej. Format....

Agent wybiera złe narzędzie

Agent wybiera złe narzędzie

Objawy Agent odpowiada nie na to pytanie. Pobiera listę zamówień, gdy pytano o jedno konkretne. Odpowiada z pamięci, choć miał sprawdzić w bazie. Albo sięga po właściwe narzędzie i wpisuje w argumenty coś, czego nie da się użyć. Sygnał, który odróżnia ten syndrom od...

Agent kręci się w kółko

Agent kręci się w kółko

Objawy Agent wykonuje kolejne obroty pętli, nie zbliżając się do zakończenia zadania. W dzienniku widać jedną z trzech postaci. Powtórzenie. To samo narzędzie, te same argumenty, raz za razem — czasem dziesiątki razy pod rząd. Oscylacja. Agent wywołuje na przemian dwa...