Das MCP-Handbuch

Kapitel 16: Transporte im Detail – stdio und Streamable HTTP

Ein Transport legt fest, wie MCP-Nachrichten zwischen Client und Server reisen – nicht, was sie bedeuten. Die Bedeutung (Tools, Resources, Rückfragen …) ist auf jedem Transport dieselbe. Die Spezifikation kennt zwei Standard-Transporte: stdio für lokale Server und Streamable HTTP für Server im Netzwerk. Dieses Kapitel erklärt beide, zeigt, worauf es bei Streamable HTTP im Betrieb ankommt, und wie man einen Server dagegen prüft.

Stand der Spezifikation 2026-07-28: MCP ist zustandslos geworden. Der initialize-Handshake, Sitzungen mit Mcp-Session-Id, der GET-Stream und das Wiederaufnehmen von Streams per Last-Event-ID sind entfallen. Dafür trägt jede Anfrage ihre Metadaten selbst, Streamable HTTP spiegelt sie in Pflicht-Header, und dauerhafte Benachrichtigungen laufen über subscriptions/listen. Was ältere Clients und Server erwartet, steht in Abschnitt 4.


1. Zustandslos: Jede Anfrage steht für sich

Bis 2025-11-25 begann jede Verbindung mit einem Handshake: Der Client schickte initialize, beide Seiten handelten Version und Fähigkeiten aus, und bei HTTP gab der Server eine Sitzungs-ID zurück, die der Client fortan mitschicken musste. Der Server merkte sich pro Sitzung, was ausgehandelt war.

Seit 2026-07-28 gibt es das nicht mehr. Stattdessen trägt jede einzelne Anfrage in _meta alles, was der Server wissen muss:

"_meta": {
  "io.modelcontextprotocol/protocolVersion": "2026-07-28",
  "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
  "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } }
}

Wer vorab wissen will, welche Versionen und Fähigkeiten ein Server hat, ruft server/discover auf – das muss jeder Server anbieten.

Warum das ein Gewinn ist: Wenn keine Anfrage von einer vorherigen abhängt, kann jede Server-Instanz jede Anfrage bearbeiten. Load Balancer brauchen keine „Sticky Sessions“ mehr, Server können jederzeit neu starten oder skalieren, und Serverless-Plattformen passen ohne Umwege.

Und wenn ein Server doch Zustand braucht? Zum Beispiel einen Warenkorb, der über mehrere Aufrufe wächst. Dann erzeugt der Server selbst einen Griff und gibt ihn als normales Ergebnis zurück – etwa create_cart liefert {"cart_id": "c_8f3a…"}. Folgeaufrufe übergeben die cart_id als ganz normales Tool-Argument. Der Zustand liegt dann in einer Datenbank, die alle Instanzen erreichen, und ist an den angemeldeten Nutzer gebunden – nicht an eine Verbindung.


2. stdio: Der lokale Transport

Bei stdio startet der Client den Server als Unterprozess. Die Regeln sind kurz:

  • Jede Nachricht ist eine Zeile JSON-RPC (ohne eingebettete Zeilenumbrüche): Der Client schreibt in stdin des Servers, der Server antwortet über stdout.
  • Auf stdout darf nichts anderes stehen als gültige MCP-Nachrichten. Ein einziges fmt.Println("debug") zerstört die Verbindung. Logs gehören nach stderr.
  • Abbrechen kann der Client eine laufende Anfrage mit notifications/cancelled.
  • Beenden: Der Client schließt stdin; der Server soll sich dann zügig selbst beenden. Reagiert er nicht, beendet ihn der Client hart.
  • Stürzt der Server ab, startet der Client ihn neu und stellt offene Anfragen einfach erneut – wegen der Zustandslosigkeit geht dabei nichts verloren, was nicht ohnehin in der Anfrage steht.

Das Format – eine JSON-Zeile pro Nachricht über einen zuverlässigen Bytestrom – funktioniert unverändert auch über Unix-Sockets oder TCP. Eigene Transporte auf solchen Strömen sollen genau dieses Format übernehmen.


3. Streamable HTTP: Der Netzwerk-Transport

3.1 Ein Endpunkt, jede Nachricht ein POST

Der Server bietet einen einzigen HTTP-Endpunkt an, etwa https://mcp.meine-firma.de/mcp. Jede Nachricht des Clients ist ein eigener POST an diesen Endpunkt. Der Server antwortet pro Anfrage auf eine von zwei Arten:

  • application/json – ein einzelnes JSON-Objekt mit dem Ergebnis. Der einfachste Fall.
  • text/event-stream – ein SSE-Stream, der nur zu dieser Anfrage gehört: zuerst Zwischenmeldungen wie notifications/progress, am Ende das Ergebnis. Danach schließt sich der Stream.

Der Client muss beides verarbeiten können und kündigt das im Accept-Header an (application/json, text/event-stream). Schickt der Client eine Benachrichtigung statt einer Anfrage, antwortet der Server nur mit 202 Accepted.

Streamable HTTP: einfache Antwort, gestreamte Antwort, Abonnement

Wichtig ist, was nicht mehr geht: Der Server schickt auf keinem Stream eigene Anfragen an den Client. Braucht er etwas vom Client – eine Rückfrage an den Nutzer, eine LLM-Antwort –, gibt er das als input_required-Ergebnis zurück, und der Client wiederholt die Anfrage (Kapitel 21).

3.2 Die Pflicht-Header

Load Balancer, Gateways und Monitoring sollen Anfragen weiterleiten und auswerten können, ohne den JSON-Body zu lesen. Deshalb spiegelt der Client die wichtigsten Felder in HTTP-Header:

Header Inhalt Pflicht für
MCP-Protocol-Version die Protokollversion aus _meta jede Anfrage
Mcp-Method die JSON-RPC-Methode, z. B. tools/call jede Anfrage
Mcp-Name Tool- oder Prompt-Name bzw. Resource-URI tools/call, prompts/get, resources/read
Mcp-Param-{Name} ein Tool-Argument, das der Server mit x-mcp-header markiert hat nach Bedarf
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather

{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": { "name": "get_weather", "arguments": { "location": "Seattle" }, "_meta": { … } } }

Mit Mcp-Param-… lässt sich zum Beispiel nach Region routen: Markiert der Server das Argument region mit "x-mcp-header": "Region", schickt der Client Mcp-Param-Region: us-west1 mit, und der Load Balancer leitet die Anfrage ins passende Rechenzentrum.

Maßgeblich bleibt immer der Body. Weicht ein Header von ihm ab oder fehlt ein Pflicht-Header, muss der Server mit 400 und dem Fehler -32020 (HeaderMismatch) ablehnen. Sonst könnte ein Angreifer den Load Balancer mit einem harmlosen Header täuschen, während der Server etwas anderes ausführt. Werte, die keine reinen ASCII-Zeichen sind, verpackt der Client als =?base64?…?=; der Server dekodiert sie vor dem Vergleich.

3.3 Benachrichtigungen und lange Streams

Es gibt zwei Arten von Benachrichtigungen vom Server:

  • Zu einer Anfrage gehörende – Fortschritt (notifications/progress), Log-Meldungen: Sie laufen ausschließlich im Antwort-Stream genau dieser Anfrage.
  • Unabhängige Änderungen – „die Tool-Liste hat sich geändert“, „diese Resource wurde aktualisiert“: Dafür öffnet der Client mit einem POST auf subscriptions/listen einen langlebigen Stream und sagt, welche Arten er hören will. Der Server bestätigt und schickt fortan genau diese Meldungen, solange der Stream offen ist.

Für solche langlebigen Streams gibt es zwei praktische Regeln: Der Server soll den Header X-Accel-Buffering: no setzen, damit Reverse-Proxies wie nginx die Ereignisse nicht puffern. Und er soll in ruhigen Phasen regelmäßig eine SSE-Kommentarzeile (:) senden, damit Proxies und Clients die Verbindung nicht wegen Inaktivität schließen.

3.4 Abbrechen und Verbindungsabbrüche

Will der Client eine laufende Anfrage abbrechen, schließt er einfach ihren Antwort-Stream. Weil jede Anfrage ihren eigenen Stream hat, ist das eindeutig; der Server muss das als Abbruch behandeln.

Die Kehrseite: Reißt eine Verbindung ungewollt ab, ist die laufende Anfrage verloren. Ein Wiederaufnehmen per Last-Event-ID gibt es nicht mehr – der Client stellt die Anfrage neu, mit neuer ID. Für Arbeiten, die Minuten oder Stunden dauern, ist das kein Problem, sondern ein Hinweis: Solche Operationen gehören in einen Task (Kapitel 19), dessen taskId jeden Verbindungsabbruch übersteht.

3.5 Sicherheit am Endpunkt

  • Origin prüfen: Ein Server muss den Origin-Header prüfen und bei fremder Herkunft mit 403 antworten. Sonst kann eine bösartige Webseite per DNS-Rebinding aus dem Browser des Nutzers heraus einen lokal laufenden MCP-Server ansprechen.
  • Lokal nur auf localhost: Ein Server für den lokalen Gebrauch lauscht auf 127.0.0.1, nicht auf 0.0.0.0.
  • Authentifizierung: Alle anderen Server sollen ihre Endpunkte schützen – wie, beschreibt Kapitel 17.

3.6 Umsetzung mit dem go-sdk

Das go-sdk (v1.8.0) bringt Streamable HTTP mit. Zwei Optionen entscheiden darüber, ob ein Server die Regeln von 2026-07-28 einhält:

package main

import (
	"context"
	"log"
	"net/http"

	"github.com/modelcontextprotocol/go-sdk/mcp"
)

type GreetIn struct {
	Name string `json:"name" jsonschema:"Wen grüßen"`
}

func greet(ctx context.Context, req *mcp.CallToolRequest, in GreetIn) (*mcp.CallToolResult, any, error) {
	return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "Hallo, " + in.Name + "!"}}}, nil, nil
}

func main() {
	s := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "1.0.0"}, nil)
	mcp.AddTool(s, &mcp.Tool{Name: "greet", Description: "Grüßt jemanden"}, greet)

	handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return s },
		&mcp.StreamableHTTPOptions{
			Stateless:             true,                            // keine Sessions: jede Anfrage steht für sich
			CrossOriginProtection: http.NewCrossOriginProtection(), // fremde Origin-Header → 403
		})
	http.Handle("/mcp", handler)
	// Nur auf localhost lauschen – Schutz vor DNS-Rebinding-Angriffen aus dem Browser
	log.Fatal(http.ListenAndServe("127.0.0.1:8931", nil))
}
  • Stateless: true schaltet die Sitzungen ab. Erst damit bietet der Handler die Version 2026-07-28 an; GET und DELETE beantwortet er mit 405, und eine mitgeschickte Mcp-Session-Id ignoriert er.
  • CrossOriginProtection (aus der Go-Standardbibliothek ab Go 1.25) lehnt Anfragen mit fremdem Origin mit 403 ab.

Ohne diese beiden Optionen läuft der Server zwar – ein Client erhält auf tools/call seine Antwort –, verhält sich aber wie ein Server der Vorversion: Er bietet 2026-07-28 nicht an, akzeptiert fremde Origins und vergibt Sitzungen.


4. Ältere Clients und Server

Viele Clients und Server sprechen noch eine Vorversion. Die Spezifikation regelt, wie beide Welten zusammenfinden:

Ein Server nur für 2026-07-28, der Verkehr eines älteren Clients bekommt, antwortet so: auf GET oder DELETE mit 405 Method Not Allowed; eine Mcp-Session-Id ignoriert er und vergibt selbst keine; einen Last-Event-ID-Header ignoriert er ebenfalls.

Ein Client, der alte und neue Server bedienen will, versucht zuerst eine Anfrage nach neuem Muster. Kommt ein 400 zurück, schaut er in den Body: Steht dort ein bekannter neuer Fehler (etwa Unsupported Protocol Version mit der Liste der unterstützten Versionen), spricht der Server die neue Welt – der Client korrigiert die Anfrage. Nur wenn der Body leer oder unbekannt ist, fällt er auf den alten initialize-Handshake zurück. Bei stdio empfiehlt die Spezifikation, zuerst server/discover zu senden.

Der Transport „HTTP+SSE“ von 2024-11-05 – zwei Endpunkte, ein dauerhafter /sse-Stream für die Richtung Server → Client plus eine separate POST-Adresse – ist seit 2025-03-26 abgelöst und offiziell deprecated. Neue Implementierungen sollen ihn nicht mehr anbieten. Wer sehr alte Clients bedienen muss, kann die alten Endpunkte parallel zum neuen MCP-Endpunkt weiter betreiben.


5. HTTP/2 und Reverse-Proxies

Wenn MCP über das Netzwerk läuft, lohnt ein Blick auf die Schicht darunter.

HTTP/1.1 stößt schnell an Grenzen: Browser und viele Clients öffnen pro Host nur etwa sechs gleichzeitige Verbindungen. Jeder offene subscriptions/listen-Stream und jede gestreamte Antwort belegt eine davon – bei mehreren Servern hinter einem Proxy (siehe Kapitel 7) sind sie schnell erschöpft.

HTTP/2 löst das:

  1. Multiplexing: Beliebig viele Streams teilen sich eine TCP-Verbindung, ohne sich gegenseitig zu blockieren.
  2. Header-Kompression (HPACK): Die Pflicht-Header (MCP-Protocol-Version, Mcp-Method, Mcp-Name) und das Bearer-Token wiederholen sich bei jeder Anfrage – HPACK überträgt sie nach dem ersten Mal fast kostenlos.
  3. Lange Verbindungen: HTTP/2 hält Verbindungen stabiler offen als viele Einzelverbindungen.

Reverse-Proxy-Einstellungen, die in der Praxis immer wieder fehlen:

  • Pufferung für SSE abschalten (X-Accel-Buffering: no vom Server oder proxy_buffering off in nginx).
  • Lese-Timeouts für langlebige Streams hoch genug setzen – und sich trotzdem nicht darauf verlassen, sondern Keep-alive-Kommentare senden.
  • Die Header Mcp-Method und Mcp-Name für Routing, Rate-Limits und Logs nutzen, statt den Body zu parsen.

6. Prüfen mit `mcp-tester`

mcp-tester http-check prüft einen Endpunkt gegen die Transportregeln von 2026-07-28: server/discover, Antwortformate, jeden Pflicht-Header (fehlend, abweichend, Base64), Fehlercodes, Origin-Prüfung, die Reaktion auf GET, DELETE und Mcp-Session-Id. MUST-Verstöße lassen den Befehl scheitern (Exit-Code 1) – geeignet für die CI (Kapitel 15).

Für den Server aus Abschnitt 3.6 sieht das so aus (gekürzt):

$ mcp-tester http-check -u http://127.0.0.1:8931/mcp
=== Streamable HTTP checks (spec 2026-07-28): http://127.0.0.1:8931/mcp ===
[PASS] server/discover                      supportedVersions [2026-07-28 2025-11-25 …]
[PASS] missing MCP-Protocol-Version header  400, -32020
[PASS] Mcp-Method differs from body         400, -32020
[PASS] unsupported protocol version         400, -32022
[PASS] tools/call without Mcp-Name          400, -32020
[FAIL] Base64-encoded Mcp-Name              … the server does not decode =?base64?…?= values
[PASS] foreign Origin header                403
[PASS] GET on the MCP endpoint              405
[PASS] DELETE on the MCP endpoint           405
[PASS] Mcp-Session-Id is ignored            no session id minted or echoed

Der eine verbleibende Fehler liegt im go-sdk selbst: v1.8.0 dekodiert Base64-verpackte Mcp-Name-Werte nicht. Behoben ist das bereits (PR #1242), aber erst in der nächsten Version enthalten. Zum Vergleich: ohne die beiden Optionen aus Abschnitt 3.6 scheitert derselbe Server an drei weiteren Prüfungen und bekommt vier Warnungen (fremder Origin akzeptiert, GET/DELETE nicht mit 405 beantwortet, Sitzungen nicht ignoriert).


Fazit für Entwickler

Wenn du einen MCP-Server für den Remote-Einsatz planst (Stand: Spezifikation 2026-07-28):

  • Nutze Streamable HTTP: ein Endpunkt, jede Nachricht ein POST, Antwort als JSON oder als SSE-Stream pro Anfrage.
  • Baue den Server zustandslos. Braucht ein Ablauf Zustand, gib einen eigenen Griff als Tool-Argument zurück und lege den Zustand in einen gemeinsamen Speicher.
  • Prüfe die Pflicht-Header gegen den Body und lehne Abweichungen mit -32020 ab; nutze sie im Proxy für Routing und Logs.
  • Prüfe den Origin, binde lokale Server an 127.0.0.1 und schütze alle anderen mit OAuth 2.1 (Kapitel 17).
  • Lange Arbeiten gehören in Tasks, nicht in einen Stream, den eine Proxy-Pause abreißen kann.
  • Lass mcp-tester http-check in der CI laufen.

← Kapitel 15: Automatisierung & CI/CD | Inhaltsverzeichnis | Nächstes Kapitel: Sicherheit & Authentifizierung →

Copyright Michael Lechner – 2026-08-19, überarbeitet 2026-10-09 (Spezifikation 2026-07-28: zustandslos, Pflicht-Header, subscriptions/listen, Origin-Prüfung, getestetes go-sdk-Beispiel)

Lizenz: CC BY-NC 4.0