Transport MCP: stdio, SSE i Streamable HTTP — historia ewolucji i wybór dla własnego serwera

przez Łukasz | cze 5, 2026

Poprzedni artykuł pokazał że wiadomości MCP to JSON-RPC — zawsze ta sama struktura, niezależnie od kontekstu. Teraz pytanie: jak te wiadomości fizycznie podróżują między klientem a serwerem?

To jest właśnie transport. Protokół definiuje co wysyłasz. Transport definiuje jak.

MCP przeszło przez trzy transporty: stdio (nadal aktywny), SSE (deprecjonowany), Streamable HTTP (aktualny standard dla połączeń sieciowych). Każdy powstał z konkretnego powodu i każdy rozwiązuje inne problemy. Rozumienie tej historii tłumaczy dlaczego Twój stary serwer przestał działać po aktualizacji klienta — i co z tym zrobić.

Transport 1: stdio — lokalny, prosty, niezastąpiony

Stdio (standard input/output) to najprostszy możliwy transport. Klient uruchamia serwer MCP jako podproces i komunikuje się z nim przez stdin i stdout — standardowe wejście i wyjście procesu.

Jak to działa fizycznie:

Claude Desktop
    │
    ├─ uruchamia: node /path/to/github-mcp/index.js
    │
    │  stdin (klient → serwer):
    │  {"jsonrpc":"2.0","id":1,"method":"initialize",...}\n
    │
    │  stdout (serwer → klient):
    │  {"jsonrpc":"2.0","id":1,"result":{...}}\n
    │
    └─ każda wiadomość to jedna linia JSON zakończona \n

Każda wiadomość JSON-RPC to osobna linia. Linia przychodzi na stdin → serwer przetwarza → odpowiedź wychodzi na stdout. Prosto jak rura.

Konfiguracja w Claude Desktop (claude_desktop_config.json):

json
{
  "mcpServers": {
    "github": {
      "command": "node",
      "args": ["/Users/lukasz/.npm-global/lib/node_modules/@modelcontextprotocol/server-github/dist/index.js"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."
      }
    },
    "webflux-slownik": {
      "command": "python",
      "args": ["/path/to/webflux-mcp-client.py"],
      "env": {}
    }
  }
}

Claude Desktop uruchamia każdy serwer jako osobny podproces. Gdy Claude Desktop się zamyka — podprocesy są kończone.

Zalety stdio:

Zero infrastruktury sieciowej. Nie potrzebujesz portu, nie potrzebujesz serwera HTTP, nie martwisz się o CORS ani TLS. Serwer uruchamiany jest tylko gdy klient go potrzebuje.

Naturalny sandbox. Serwer działa z uprawnieniami użytkownika który uruchomił klienta. Nie możesz przypadkowo wystawić go na sieć.

Proste debugowanie. Możesz uruchomić serwer ręcznie, wklejać JSON na stdin i czytać stdout:

bash
# Uruchom serwer i wyślij ręcznie inicjalizację
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node index.js

Wady stdio:

Działa tylko lokalnie. Serwer musi być zainstalowany na maszynie użytkownika. Nie możesz hostować serwera stdio w chmurze i udostępniać go wielu użytkownikom.

Każdy użytkownik instaluje osobno. Dystrybucja wymaga package managera (npm, pip) i lokalnej instalacji.

Transport MCP: stdio, SSE i Streamable HTTP — historia ewolucji i wybór dla własnego serwera

Transport MCP: stdio, SSE i Streamable HTTP — historia ewolucji i wybór dla własnego serwera

Transport 2: SSE — pierwsze podejście do sieci (i dlaczego nie wystarczyło)

SSE (Server-Sent Events) to standard W3C który pozwala serwerowi wysyłać strumień danych do klienta przez długotrwałe połączenie HTTP. Używany od lat do streamingu odpowiedzi modeli AI — ten efekt „tekst pojawia się słowo po słowie” gdy piszesz do ChatGPT lub Claude to jest właśnie SSE.

MCP w pierwszej wersji (2024-11-05) używał SSE jako transport dla połączeń sieciowych. Pomysł był prosty: serwer wystawia endpoint SSE, klient się podłącza i dostaje stream wiadomości.

Problem: SSE jest jednokierunkowe.

SSE to kanał serwer → klient. Serwer może wysyłać dane do klienta w dowolnym momencie. Ale klient który chce wysłać coś do serwera — musi użyć osobnego żądania HTTP POST.

Pierwsza wersja MCP z SSE wymagała więc dwóch połączeń naraz:

Klient ──────── GET /sse ────────▶ Serwer
        ◀── stream zdarzeń ───────
        
Klient ──────── POST /message ──▶ Serwer
        ◀──── odpowiedź HTTP ─────

Jeden kanał do odbierania (SSE), drugi do wysyłania (POST). Serwer musiał korelować żądania POST z sesją SSE przez identyfikator sesji w URL lub nagłówku.

Dlaczego to było problematyczne w produkcji:

Loadbalancery i CDN nie lubią długotrwałych połączeń. SSE utrzymuje otwarte połączenie HTTP dopóki sesja trwa. Typowe timeouty infrastruktury (60-120 sekund) przerywają połączenie. Serverless (AWS Lambda, Vercel Functions) ma twarde limity czasu wykonania — SSE nie działa.

Skalowanie poziome jest trudne. Gdy masz wiele instancji serwera za loadbalancerem, żądanie POST może trafić na inną instancję niż ta która obsługuje połączenie SSE tej sesji. Potrzebujesz sticky sessions lub zewnętrznego brokera — dodatkowa złożoność.

Transport 3: Streamable HTTP — aktualny standard

Specyfikacja MCP 2025-03-26 (ogłoszona w marcu 2025, obowiązująca od Webflux MCP v2.2.0) deprecjonuje SSE jako transport MCP i wprowadza Streamable HTTP.

Kluczowa zmiana: jeden endpoint POST obsługuje całą komunikację dwukierunkową.

Klient ──── POST /mcp ────▶ Serwer
            (wiadomość JSON-RPC)
       ◀─── odpowiedź ─────
            (JSON lub SSE stream)

Klient wysyła żądanie HTTP POST z wiadomością JSON-RPC w ciele. Serwer odpowiada — i tutaj jest elastyczność Streamable HTTP.

Dwa tryby odpowiedzi:

Tryb JSON — serwer odpowiada normalnym JSON-em w ciele odpowiedzi HTTP. Używany gdy wynik jest gotowy od razu:

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

{"jsonrpc":"2.0","id":1,"method":"tools/list"}

HTTP/1.1 200 OK
Content-Type: application/json

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

Tryb SSE stream — serwer odpowiada strumieniem SSE gdy wynik jest długi lub wieloczęściowy. Używany gdy serwer chce wysyłać progresywne aktualizacje:

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

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{...}}

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Analizuję..."}]}}

data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Analiza zakończona. Wynik: ..."}]}}

data: [DONE]

Klient deklaruje że akceptuje oba formaty przez nagłówek Accept: application/json, text/event-stream. Serwer wybiera który zwrócić.

Opcjonalne sesje:

Streamable HTTP obsługuje opcjonalne sesje przez nagłówek Mcp-Session-Id. Przy pierwszym żądaniu serwer może zwrócić identyfikator sesji:

HTTP/1.1 200 OK
Mcp-Session-Id: sess_abc123
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"result":{...}}

Klient dołącza ten nagłówek do kolejnych żądań. Pozwala serwerowi zachować stan (np. subskrypcje resources) między requestami. Ale sesje są opcjonalne — serwer który nie potrzebuje stanu może je ignorować i obsługiwać każdy request niezależnie.

Dlaczego Streamable HTTP jest lepsze:

Standard request-response HTTP — działa ze wszystkimi loadbalancerami, CDN, reverse proxy bez specjalnej konfiguracji.

Serverless-friendly — każdy request to osobne wywołanie funkcji. AWS Lambda, Vercel, Cloudflare Workers — wszystkie działają.

Łatwiejsze debugowanie — zwykły POST, zwykła odpowiedź. curl działa bez żadnych sztuczek.

Porównanie transportów

                    stdio           SSE (stary)      Streamable HTTP
─────────────────────────────────────────────────────────────────────
Lokalne serwery     ✅ idealne      ❌ niepotrzebne   ✅ możliwe
Zdalne serwery      ❌ niemożliwe   ✅ działało       ✅ idealne
Serverless          ❌ niemożliwe   ❌ nie działa     ✅ działa
Loadbalancer        N/A             ⚠️ sticky sess.  ✅ bezproblemowe
CDN                 N/A             ❌ problem        ✅ działa
Debugowanie curl    ⚠️ ręczny pipe  ⚠️ dwa połącz.  ✅ prosty POST
Wsteczna komp.      ✅ zawsze       ⚠️ deprecjonowane ✅ aktualny std.
WordPress REST API  N/A             ⚠️ trudne         ✅ naturalne

Webflux Słownik jako przykład Streamable HTTP

Webflux MCP Server v2.2.0 (aktualizowany właśnie ze względu na zmianę specyfikacji) to standardowy endpoint REST API WordPressa który obsługuje Streamable HTTP:

POST https://webflux.pl/wp-json/webflux/v1/mcp

Implementacja w PHP — jeden endpoint obsługuje wszystkie metody MCP:

php
// Uproszczony schemat endpointu
add_action('rest_api_init', function() {
    register_rest_route('webflux/v1', '/mcp', [
        'methods'  => 'POST',
        'callback' => 'handle_mcp_request',
        'permission_callback' => '__return_true',
    ]);
});

function handle_mcp_request(WP_REST_Request $request) {
    $body = $request->get_json_params();
    $method = $body['method'] ?? '';
    $id = $body['id'] ?? null;
    
    // Capability negotiation
    if ($method === 'initialize') {
        return new WP_REST_Response([
            'jsonrpc' => '2.0',
            'id'      => $id,
            'result'  => [
                'protocolVersion' => '2025-03-26',
                'capabilities'    => ['tools' => new stdClass()],
                'serverInfo'      => ['name' => 'webflux-slownik', 'version' => '2.2.0'],
            ]
        ]);
    }
    
    // Routing metod
    if ($method === 'tools/list')  return handle_tools_list($id);
    if ($method === 'tools/call')  return handle_tools_call($id, $body['params'] ?? []);
    
    // Powiadomienia (brak id, brak odpowiedzi)
    if (!$id) return new WP_REST_Response(null, 204);
    
    return mcp_error($id, -32601, 'Method not found');
}

Żaden specjalny framework MCP — zwykły WordPress REST API endpoint który rozumie JSON-RPC.

Migracja: co zrobić jeśli masz serwer SSE

Jeśli zbudowałeś serwer MCP przed marcem 2025 i używał SSE — prawdopodobnie część klientów przestała się z nim łączyć po aktualizacjach.

Trzy opcje:

Opcja 1 — Pełna migracja do Streamable HTTP. Usuń SSE endpoint, zastąp jednym POST endpointem. Dla większości serwerów to kilkanaście linii kodu — logika biznesowa (tools, resources) zostaje bez zmian.

Opcja 2 — Obsługa obu transportów. Utrzymaj oba endpointy: stary SSE dla starych klientów, nowy Streamable HTTP dla nowych. Klienci MCP wykrywają transport przez próbę połączenia.

Opcja 3 — Wrapper. Zbuduj cienką warstwę Streamable HTTP która forward-uje żądania do istniejącego serwera SSE. Najmniej zmian w istniejącym kodzie, ale dodatkowa latency.

Sprawdź wersję specyfikacji klienta który przestał działać — jeśli używa protocolVersion: "2025-03-26" a Twój serwer odpowiada błędem — problem w transportie lub wersji.

Kiedy który transport

Budujesz narzędzie developerskie instalowane lokalnie (jak GitHub Copilot plugin, integracja z IDE): stdio. Zero infrastruktury, zero konfiguracji sieci, działa offline.

Budujesz serwis dostępny dla wielu użytkowników przez internet: Streamable HTTP. Jeden endpoint, standardowy HTTP, łatwy deployment.

Budujesz serwer MCP jako plugin WordPress: Streamable HTTP przez REST API. Naturalny wybór — jeden register_rest_route i gotowe.

Masz istniejący serwer SSE z 2024 roku: zaplanuj migrację do Streamable HTTP. SSE jest deprecjonowane — nie będzie nowych funkcji, kompatybilność z nowymi klientami może się pogorszyć.

 

W następnym artykule: serwer MCP działa przez sieć — kto ma prawo go wywołać i jak agent się autoryzuje w zewnętrznych serwisach? OAuth dla agentów i zarządzanie tożsamością w ekosystemie MCP.


Pojęcia ze słownika: Streamable HTTP · SSE · MCP · JSON-RPC · 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...