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. Derinitialize-Handshake, Sitzungen mitMcp-Session-Id, der GET-Stream und das Wiederaufnehmen von Streams perLast-Event-IDsind entfallen. Dafür trägt jede Anfrage ihre Metadaten selbst, Streamable HTTP spiegelt sie in Pflicht-Header, und dauerhafte Benachrichtigungen laufen übersubscriptions/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
stdindes Servers, der Server antwortet überstdout. - Auf
stdoutdarf nichts anderes stehen als gültige MCP-Nachrichten. Ein einzigesfmt.Println("debug")zerstört die Verbindung. Logs gehören nachstderr. - 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 wienotifications/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.

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/listeneinen 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
Originprüfen: Ein Server muss denOrigin-Header prüfen und bei fremder Herkunft mit403antworten. 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 auf0.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: trueschaltet die Sitzungen ab. Erst damit bietet der Handler die Version2026-07-28an; GET und DELETE beantwortet er mit405, und eine mitgeschickteMcp-Session-Idignoriert er.CrossOriginProtection(aus der Go-Standardbibliothek ab Go 1.25) lehnt Anfragen mit fremdemOriginmit403ab.
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:
- Multiplexing: Beliebig viele Streams teilen sich eine TCP-Verbindung, ohne sich gegenseitig zu blockieren.
- 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. - 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: novom Server oderproxy_buffering offin nginx). - Lese-Timeouts für langlebige Streams hoch genug setzen – und sich trotzdem nicht darauf verlassen, sondern Keep-alive-Kommentare senden.
- Die Header
Mcp-MethodundMcp-Namefü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 echoedDer 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
-32020ab; nutze sie im Proxy für Routing und Logs. - Prüfe den
Origin, binde lokale Server an127.0.0.1und 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-checkin 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)