Warum sehen Fehler beim Start von MCP-Servern gleich aus?
Fehler beim Start eines MCP-Servers lassen sich diagnostizieren, wenn du Start, Handshake, Tool-Erkennung und Belege für externe Aktionen getrennt betrachtest.

Ein MCP-Client kann melden, ein Server sei «nicht gestartet», obwohl das Betriebssystem den Prozess bereits gestartet hat, der Prozess Eingaben gelesen hat und der Server schon etwas außerhalb des Rechners kontaktiert hatte. Diese Meldung ist keine Diagnose. Sie ist ein Sammelbecken für Startfehler, Protokollfehler, Fehler bei der Erkennung und manchmal auch Tool-Fehler.
Betrachte den Start als eine Folge von Grenzen, an denen Belege entstehen. Wenn du nicht sagen kannst, welche Grenze der Server überschritten hat, kannst du einem Operator nicht mitteilen, ob ein erneuter Versuch sicher ist, ob eine Zugangsdaten verwendet worden sein könnte oder ob der Client lediglich einen gesunden Server nicht darstellen konnte. Die Lösung ist kein längeres Timeout. Die Lösung besteht darin, die Zustände zu trennen und jeden einzelnen beobachtbar zu machen.
Ein einziger roter Status verbirgt vier verschiedene Fehler
Ein Operator braucht nacheinander vier Antworten: Hat der Client den konfigurierten Befehl gestartet? Haben beide Seiten die MCP-Initialisierung abgeschlossen? Hat der Client eine verwendbare Tool-Liste erhalten? Und hat irgendein Code einen externen Kanal erreicht? Jede Antwort belegt etwas anderes.
Ein Prozess kann scheitern, bevor er im üblichen Sinn existiert. Die ausführbare Datei kann fehlen, das Arbeitsverzeichnis kann nicht vorhanden sein, ein Paket-Runner kann scheitern, bevor er deinen Code aufruft, oder der Kindprozess kann sofort beendet werden, weil eine erforderliche Umgebungsvariable fehlt. Das nennen wir einen Startfehler. Es gibt keine MCP-Sitzung, und dein Anwendungscode ist möglicherweise überhaupt nicht gelaufen.
Ein Prozess kann auch existieren und trotzdem beim Protokollaustausch scheitern. Bei stdio sind stdin und stdout des Kindprozesses mit dem Client verbunden. Der Server muss JSON-RPC von stdin lesen und ausschließlich JSON-RPC-Nachrichten nach stdout schreiben. Anschließend muss er auf die initialize-Anfrage des Clients mit einer kompatiblen Protokollversion und den angegebenen Fähigkeiten antworten. Danach sendet der Client notifications/initialized. Wenn diese Abfolge nicht abgeschlossen wird, handelt es sich um einen Handshake-Fehler.
Ein erfolgreicher Handshake beweist noch nicht, dass der Client die Tools kennengelernt hat. Ein Server kann die Tool-Unterstützung angeben, aber beim Registrieren der Tools scheitern, ein ungültiges Eingabeschema erzeugen, ein fehlerhaftes Ergebnis für tools/list zurückgeben oder eine leere Liste liefern, weil seine eigene Konfiguration alle Tools deaktiviert hat. Das ist ein Fehler bei der Tool-Erkennung. Client und Server können gesund genug sein, um Nachrichten auszutauschen, und trotzdem gibt es nichts, was der Agent aufrufen kann.
Schließlich kann ein Server die Erkennung abschließen und erst beim Ausführen eines Tools scheitern. Das ist ein Fehler bei der Tool-Ausführung. Er gehört in einen anderen Vorfall. Wenn du ihn mit dem Start vermischst, wird irgendwann jemand einen Server erneut starten, der bereits eine HTTP-Anfrage gesendet oder eine SSH-Verbindung geöffnet hat.
Die Dokumentation des Model Context Protocol macht diese Trennung sichtbar, auch wenn viele Client-Oberflächen sie nicht zeigen. Die Hinweise zur Fehlersuche unterscheiden Prozess- und Konfigurationsprobleme von der Protokollierung, außerdem wird darauf hingewiesen, dass lokale stdio-Server stdout frei von gewöhnlichen Logs halten müssen. Der Initialisierungsablauf des Protokolls und die Anfrage tools/list sind getrennte Austausche. Übernimm diese Unterscheidung in deine eigene Telemetrie, statt das allgemeine Label des Clients zu akzeptieren.
Ein Startfehler endet, bevor MCP existiert
Ein Startfehler bedeutet, dass der Client keinen verwendbaren Kindprozess mit einem lesbaren Protokollstrom erhalten hat. Er bedeutet nicht, dass der Befehl in einem Terminal «richtig aussah».
Interaktive Shells verbergen vieles. Deine Shell hat einen PATH, ein aktuelles Verzeichnis, Versionsmanager für Programmiersprachen, Zugangsdaten und Dotfiles, die eine Desktop-Anwendung oder ein Agent-Subprozess möglicherweise nicht übernimmt. Ein Client kann auf macOS mit / als Arbeitsverzeichnis starten. Er kann eine eingeschränkte Umgebung verwenden. Vielleicht übergibt er den Befehl als ausführbare Datei mit einem Argument-Array statt über eine Shell. Dann funktionieren Shell-Aliase und Umleitungen nicht.
Erfasse den genauen Startdatensatz, bevor du über MCP nachzudenken versuchst:
run_id=run_01JX...
phase=launch
command=/usr/local/bin/node
argv=["/Users/dev/work/acme-mcp/dist/index.js"]
cwd=/
pid=84217
started_at=2026-07-22T14:03:12.417Z
Erfasse anschließend ein abschließendes Ereignis, sobald der Prozess beendet wird oder die Handshake-Frist abläuft:
run_id=run_01JX...
phase=launch
exit_code=1
signal=null
stderr=Error: ENOENT: no such file or directory, open './config.json'
Dieser Datensatz beendet einen häufigen Streit schnell. Der Server hatte kein «MCP-Problem». Er ging davon aus, dass ein relativer Pfad im Projektverzeichnis aufgelöst wird, aber der Client startete ihn aus /.
Verwende absolute Pfade für die ausführbare Datei, den Einstiegspunkt, Konfigurationsdateien und alle Dateien, die beim Start gelesen werden. Der MCP-Leitfaden zur Fehlersuche weist ausdrücklich auf undefinierte Arbeitsverzeichnisse für von Clients gestartete Server hin und empfiehlt absolute Pfade. Das ist keine stilistische Vorsicht. So entfernst du eine Fehlerquelle, die erst sichtbar wird, wenn jemand dieselbe Konfiguration auf einem anderen Rechner installiert.
Erkläre den Start nicht allein deshalb für erfolgreich, weil du eine PID erhalten hast. Eine PID sagt, dass der Kernel einen Prozess erstellt hat. Sie sagt nichts darüber aus, ob das Programm geladen wurde, ob die stdout-Pipe intakt ist oder ob der Prozess bereits zu einem Zombie geworden ist, der auf das Einsammeln wartet.
Eine nützliche Zustandsmaschine für den Start bleibt klein:
not_requested
-> spawn_requested
-> spawned
-> executable_ready
-> handshake_pending
spawned bedeutet, dass der Elternprozess eine PID des Kindprozesses erhalten hat. executable_ready bedeutet, dass der Kindprozess nach dem Laden seiner Konfiguration und dem Einrichten des Handlers für fatale Fehler ein bewusstes, nicht zum Protokoll gehörendes Bereitschaftsereignis nach stderr geschrieben hat. Sende dieses Ereignis nicht nach stdout. Bei einem stdio-Server ist stdout kein danebenliegender Logging-Kanal. stdout ist die Leitung.
Das Bereitschaftsereignis sollte nicht behaupten, dass der Server mit einer API, einer Datenbank oder einem entfernten Host verbunden ist. Es sollte nur sagen, was es belegt: Der Prozess hat die Einrichtung seines MCP-Transports erreicht. Eine Zeile wie ready=true wird gefährlich, wenn Teams sie stillschweigend als «Tools können sicher aufgerufen werden» verstehen. Benenne stattdessen die Phase.
Der Handshake hat eine enge Definition
Ein MCP-Handshake-Fehler beginnt, nachdem ein verwendbarer Prozess existiert, und endet, bevor der Initialisierungsablauf abgeschlossen ist. Nenne ihn nicht ohne Prüfung der Nachrichten einen Verbindungsfehler.
Bei einem stdio-Transport kommt es auf die ersten Bytes in stdout an. Ein gewöhnliches Startbanner kann den Datenstrom beschädigen, bevor dein Server die Anfrage sieht. Das gilt auch für eine Abhängigkeit, die einen Hinweis auf ein Update ausgibt, für console.log, print in Python, einen Exception-Formatter des Frameworks oder ein Wrapper-Skript, das Statusinformationen nach stdout schreibt. Die offiziellen MCP-Hinweise zu Aufbau und Fehlersuche sagen es klar: Bei stdio-Servern gehören Logs nach stderr, weil stdout die Protokollnachrichten trägt.
Dies ist der kleinste nützliche Trace einer gesunden Initialisierung:
{"direction":"in","id":1,"method":"initialize"}
{"direction":"out","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"acme","version":"1.4.0"}}}
{"direction":"in","method":"notifications/initialized"}
Die genaue Protokollversion hängt von den Versionen ab, die Client und Server unterstützen. Entscheidend ist, dass der Server eine vom Client akzeptierte Version ausgewählt, ein gültiges Ergebnis zurückgegeben und die Abschlussbenachrichtigung erhalten hat. Speichere für jede Nachricht ein geparstes Ereignis, nicht rohe Payloads mit Zugangsdaten.
Wenn dein Trace stattdessen so beginnt, ändert sich die Diagnose:
stdout: Starting Acme MCP server
{"jsonrpc":"2.0","id":1,"method":"initialize",...}
Der Server kann durchaus in der Lage sein zu antworten, aber der JSON-Parser des Clients ist bereits auf ungültige Eingaben gestoßen. Ein anschließendes Timeout bedeutet nicht, dass der Server langsam war. Es bedeutet, dass der Transport beschädigt wurde.
Ein anderer bekannter Fehler sieht gesünder aus:
phase=handshake
initialize_received=true
initialize_response_sent=false
fatal_error=Cannot read properties of undefined (reading 'tools')
Der Kindprozess wurde gestartet. Er hat die Anfrage erhalten. Beim Vorbereiten der Antwort ist er gescheitert. Das ist ein Fehler im Server oder eine nicht behandelte Annahme über die Konfiguration, keine fehlerhafte Client-Konfiguration.
Mach die Grenzen in den Logs sichtbar:
phase=handshake event=initialize_received run_id=run_01JX request_id=1
phase=handshake event=initialize_responded run_id=run_01JX request_id=1 protocol_version=2025-06-18
phase=handshake event=initialized_received run_id=run_01JX
Wenn du nur connected=true schreibst, löschst du genau die Information, die eine gesendete Antwort von einem abgeschlossenen Initialisierungsablauf unterscheidet. Clients können die Verbindung schließen oder neu starten, nachdem sie die Antwort erhalten, aber bevor sie die Benachrichtigung senden. Operativ ist das etwas anderes als ein Fehler beim Parsen der Initialisierung.
Gib dem Handshake eine eigene Frist. Starte sie, wenn der Prozess gestartet wird, oder wenn der Transport bereit ist, falls du diesen Zeitpunkt beobachten kannst. Beende sie, sobald notifications/initialized eintrifft. Wenn sie abläuft, melde das letzte bestätigte Ereignis, etwa spawned_no_initialize, initialize_received_no_response oder response_sent_no_initialized. Diese Bezeichnungen zeigen einem Operator, welche Seite er zuerst prüfen sollte.
Die Tool-Erkennung scheitert, nachdem der Server bereits erreichbar ist
Ein Fehler bei der Tool-Erkennung bedeutet, dass Client und Server über MCP kommunizieren können, der Client aber keine verwendbare Antwort auf tools/list erhalten hat. Häufig wird er als Startfehler gemeldet, weil viele Clients die Tools direkt nach der Initialisierung erkennen.
Gehe nicht davon aus, dass ein leeres Tool-Panel ein leeres Ergebnis von tools/list beweist. Einige Clients verbergen Tools, wenn die Schema-Validierung fehlschlägt. Manche speichern Erkennungsergebnisse im Cache. Andere fragen Tools erst bei Bedarf an, wenn der Agent eine Aufgabe beginnt. Wieder andere verbinden sich mit einem MCP-Server für Ressourcen oder Prompts und fragen niemals nach Tools. Deine Belege sollten sowohl Anfrage als auch Antwort erfassen.
Ein gesunder Trace der Erkennung sieht so aus:
{"direction":"in","id":2,"method":"tools/list"}
{"direction":"out","id":2,"result":{"tools":[{"name":"issue_lookup","description":"Fetch one issue by identifier","inputSchema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}}]}}
Ein Datensatz zur Erkennung sollte die Anzahl der Tools und einen Digest der normalisierten Schemas enthalten. Mit einem Digest kannst du erkennen, ob zwei Läufe dieselbe angekündigte Schnittstelle hatten, ohne vertrauliche Beschreibungen oder Konfigurationen zu speichern. Außerdem werden versehentliche Änderungen sichtbar, bei denen ein Tool zwar noch existiert, aber seine erforderlichen Parameter verschwunden sind.
Erstelle Tool-Definitionen nicht, indem du während tools/list einen externen Dienst kontaktierst. Dadurch wird die Erkennung zu einer Nebenwirkung, eine Aktualisierung beim Client sieht wie eine Ausführung aus, und es entsteht die denkbar ungünstigste Frage im Vorfall: «Hat das Auflisten der Tools etwas verändert?» Die Tool-Registrierung sollte möglichst lokal und deterministisch sein.
Ein Server kann Konfiguration benötigen, um zu entscheiden, ob er ein Tool anbietet. Lies diese Konfiguration beim Start und protokolliere das Ergebnis, aber lass die Tool-Erkennung nicht auf eine Token-Erneuerung oder eine SSH-Prüfung warten. Wenn ein Tool Zugangsdaten benötigt, prüfe nur, ob die lokale Referenz vorhanden ist, ohne sie zu verwenden. Die eigentliche entfernte Aktion wird erst ausgeführt, wenn der Client das Tool aufruft.
Diese Unterscheidung ist für die Kontrolle von Agents wichtig. Wenn ein Agent über sp mcp Sallyport erreicht, zeigt ein erfolgreicher Datensatz der MCP-Erkennung nur, dass der Vermittler aufrufbare Operationen bereitgestellt hat, nicht, dass Sallyport eine HTTP- oder SSH-Aktion ausgeführt hat.
Es gibt einen legitimen Grund für eine leere Liste: Für die aktuelle Konfiguration sind keine Tools aktiviert. Sag das in einer strukturierten Antwort oder einem für den Client bestimmten Log. Stürze bei der Registrierung nicht ab und überlasse es dem Client, zu erraten, ob die Tool-Menge absichtlich leer ist.
phase=discovery event=tools_list_responded run_id=run_01JX tool_count=0 reason=no_enabled_tools
Dieser Datensatz gibt dem Operator ein Konfigurationsproblem, das er beheben kann. Ein allgemeiner Startfehler liefert nur einen Aberglauben, den er wiederholen kann.
Externe Erreichbarkeit braucht einen eigenen Beleg
Die Frage «Hat der Prozess einen externen Kanal erreicht?» lässt sich nicht durch eine PID, einen erfolgreichen Handshake oder eine gefüllte Tool-Liste beantworten. Du brauchst ein Ereignis an der Grenze, an der dein Code die externe Aktion versucht.
Definiere den externen Kanal eng. Dazu gehören ein ausgehender HTTP-Aufruf, der Aufruf eines SSH-Helfers, eine Datenbankverbindung außerhalb des lokalen Prozesses, eine Erneuerung von Cloud-Zugangsdaten, das Veröffentlichen in einer Nachrichtenwarteschlange oder jeder Aufruf, der außerhalb der MCP-Sitzung eine Wirkung erzeugen oder Informationen offenlegen kann. Das Lesen einer lokalen Konfigurationsdatei zählt nicht. Auch das Laden einer Zugangsdaten aus einem lokal geschützten Speicher zählt für sich genommen nicht. Das Senden in einer Anfrage schon.
Protokolliere den Versuch, bevor der Aufruf beginnt, und anschließend sein Ergebnis. Verwende eine undurchsichtige Aktions-ID, die sich mit der MCP-Anfrage-ID und der Lauf-ID des Servers verknüpfen lässt.
run_id=run_01JX phase=execution event=external_attempt action_id=act_8Qf tool=issue_lookup channel=https host=api.example.test
run_id=run_01JX phase=execution event=external_result action_id=act_8Qf status=200 duration_ms=184
Schreibe keine Authorization-Header, Bearer-Tokens, signierten URLs, Befehlsargumente mit Geheimnissen oder vollständigen Antwort-Bodies in diese Datensätze. Ein Vorfallsprotokoll, das die Zugangsdaten preisgibt, die es untersuchen sollte, macht den Vorfall schlimmer.
Die Position von external_attempt ist nicht nebensächlich. Setzt du es zu früh, behauptest du einen externen Aufruf, obwohl der Code nur ein Anfrageobjekt zusammengestellt hat. Setzt du es zu spät, kann ein Timeout oder Prozessabsturz eine Lücke hinterlassen, obwohl bereits Bytes den Rechner verlassen haben. Gib das Ereignis unmittelbar vor dem Bibliotheksaufruf aus, der Netzwerk- oder SSH-Aktivität starten kann. Wenn die Bibliothek einen Hook auf niedrigerer Ebene für Verbindungen oder Anfragen anbietet, erfasse dort nur dann ein zweites Ereignis, wenn du die Bedeutung von «Versuch» nicht verwischst.
Ein Fehlerablauf zeigt, warum das wichtig ist. Ein Operator fügt einen MCP-Server hinzu, der beim Laden eines Moduls ein Token für einen Issue-Tracker liest und einen «Wer bin ich?»-Endpunkt aufruft, um es zu prüfen. Der Kindprozess startet, schreibt eine Debug-Zeile nach stdout und beschädigt die erste MCP-Nachricht. Der Client zeigt «Server konnte nicht gestartet werden». Das Team startet den Client zweimal neu.
Ohne Phasenprotokolle schließt es daraus, dass keine Anfrage den Rechner verlassen hat, weil der Server in der Oberfläche des Clients nicht erschien. Diese Schlussfolgerung ist falsch. Der Aufruf beim Laden des Moduls lief, bevor der Client initialize sendete, und kontaktierte den Issue-Tracker dreimal. Der Zustand der Oberfläche sagte nichts über die externe Erreichbarkeit aus.
Verschiebe die Identitätsprüfung in ein bewusst schreibgeschütztes Tool oder mache sie zum Bestandteil der ersten Aktion, die den entfernten Dienst tatsächlich benötigt. Protokolliere sie dann als Tool-Ausführung. Der Server kann starten, initialisieren und Tools auflisten, ohne das Netzwerk zu berühren. Ein Operator kann nun «Der Server ist verfügbar» von «Zugangsdaten und entfernter Dienst funktionieren» unterscheiden. Das sind getrennte Tatsachen und sollten getrennt bleiben.
Ein Phasenprotokoll macht vage Vorfälle zu prüfbaren Aussagen
Erstelle pro Lauf einen Eintrag und hänge unveränderliche Phasenereignisse an. Du brauchst keine komplizierte Regel-Engine. Du brauchst stabile Namen, Zeitstempel und genügend Korrelationsfelder, um den Ablauf zu rekonstruieren.
Verwende diese Form:
{
"run_id": "run_01JX",
"server_name": "acme",
"pid": 84217,
"phase": "discovery",
"event": "tools_list_responded",
"request_id": 2,
"tool_count": 4,
"at": "2026-07-22T14:03:13.083Z"
}
Das Protokoll sollte Ereignisse aufzeichnen, keine in eine Zeichenkette geklebten Schlussfolgerungen. phase=handshake und event=initialize_received lassen sich zählen, abfragen und testen. message="MCP seems stuck" nicht.
Halte das Zustandsmodell bewusst langweilig:
spawn_requested,spawned,executable_readyundexitedgehören zum Start.initialize_received,initialize_respondedundinitialized_receivedgehören zum Handshake.tools_list_receivedundtools_list_respondedgehören zur Erkennung.tool_call_received,external_attemptundexternal_resultgehören zur Ausführung.revoked,terminatedundclient_disconnectedbeschreiben eine Unterbrechung, keinen Erfolg.
Die Unterscheidung, die Teams oft verwischen, lautet Sitzungsaufbau versus Berechtigung zum Handeln. Ein Server kann eine MCP-Sitzung aufbauen, ohne die Berechtigung zu haben, eine Zugangsdaten zu verwenden oder eine Verbindung zu einem entfernten System herzustellen. Wenn du beides als denselben Zustand behandelst, kann ein Freigabeereignis wie ein Verbindungsereignis aussehen und eine verweigerte Aktion wie ein fehlgeschlagener Start.
Halte Autorisierungsereignisse neben dem Aufruf, den sie steuern. Zeichne zum Beispiel authorization_requested und authorization_granted nach tool_call_received, aber vor external_attempt auf. Dann kann ein Operator mit Belegen sagen, dass die Tool-Anfrage eingetroffen ist, der Mensch sie abgelehnt hat und kein externer Versuch stattgefunden hat. Das ist deutlich stärker als die Aussage, die Anfrage «wurde nicht abgeschlossen».
Verwende eine Lauf-ID, die nur für einen Kindprozess gilt. Nutze nicht den Servernamen als Korrelationskennung. Ein Client kann zwei Kopien desselben Servers starten, eine nach einem Timeout neu starten und alte Tool-Metadaten behalten. Wiederverwendete Kennungen vermischen diese getrennten Versuche zu einer erfundenen Geschichte.
Hashiere oder schwärze Werte, die Nutzerdaten offenlegen. Im Allgemeinen brauchst du einen Toolnamen, den Host des Endpunkts, eine Statusklasse, eine Fehlerklasse und Zeitangaben. Eine Query-Zeichenkette, ein Request-Body oder eine Antwort brauchst du nur selten. Operatoren müssen belegen, dass eine Grenze überschritten wurde, nicht Nutzerdaten aus Logs wiederherstellen.
Teste die Grenzen, ohne dem vollständigen Client zu vertrauen
Ein vollständiger Client ist für Integrationstests nützlich, aber als erster Zeuge ungeeignet. Seine Oberfläche kann Fehler zusammenfassen, Fähigkeiten zwischenspeichern, Kindprozesse neu starten und ein eigenes Timeout anwenden. Teste jede Grenze zuerst über einen schmaleren Pfad, bevor du den Server oder den Client verantwortlich machst.
Beginne mit genau dem Befehl, der Umgebung und dem Arbeitsverzeichnis, die der Client verwendet. Ersetze den konfigurierten Befehl nicht durch npm run dev. Führe ihn nicht aus dem Projektverzeichnis aus, wenn der Client ihn von einem anderen Ort startet. Leite stderr zur Untersuchung in eine Datei um, lass stdout aber unverändert, wenn ein anderer Prozess darüber MCP spricht.
Verwende bei einem stdio-Server den offiziellen MCP Inspector als ersten Protokolltest. Die MCP-Dokumentation empfiehlt Inspector zum Testen von Servern über verschiedene Transporte hinweg, und das Inspector-Projekt kann einen stdio-Befehl direkt starten. Du kannst den Initialisierungsaustausch beobachten und tools/list aufrufen, ohne raten zu müssen, was ein Desktop-Client mit dem Ergebnis gemacht hat.
Reduziere den Test anschließend auf drei Prüfungen:
1. Bleibt der konfigurierte Befehl lange genug am Leben, um initialize zu empfangen?
2. Gibt er eine gültige initialize-Antwort zurück und empfängt er initialized?
3. Liefert tools/list die erwarteten Toolnamen und Schemas?
Erst wenn diese Prüfungen bestanden sind, solltest du ein Tool aufrufen, das ein externes System erreicht. Wähle eine schreibgeschützte Aktion mit einem ungefährlichen Ziel. Prüfe, ob das Ausführungsprotokoll einen external_attempt und ein abschließendes Ergebnis enthält. Wenn ein Aufruf Daten verändern kann, teste ihn in einer wegwerfbaren Umgebung oder stelle eine eigene Dry-Run-Operation bereit, die den Produktionsendpunkt nicht kontaktiert.
Das offizielle MCP-Inspector-Repository ist hier hilfreich, weil es die Transportgrenze sichtbar macht. Es ist kein Proxy zur Überwachung des Netzwerkverkehrs deines Servers. Es agiert als MCP-Client für den ausgewählten Server und stellt eine Browser-Oberfläche für den Test bereit. Dieser Unterschied ist bei der Untersuchung beschädigter Transporte wichtig: Inspector kann die Client-Seite des Protokolls reproduzieren, aber nicht beweisen, was ein anderer Produktionsclient in die Pipe geschrieben hat.
Füge bei HTTP-Transporten HTTP-Belege hinzu, ohne sie mit dem MCP-Zustand zu vermischen. Erfasse Request-Methode, Endpunktpfad, Status, die Sitzungskennung, falls vorhanden, und ob die Antwort JSON enthielt oder mit einem Event-Stream begann. Eine TCP-Verbindung oder HTTP 200 bedeutet nicht automatisch, dass die MCP-Initialisierung abgeschlossen wurde. Verwende dieselben Lebenszyklusdatensätze, sobald die HTTP-Anfrage deinen Server erreicht.
Halte in deiner Testsuite einen Fixture-Server bereit, der absichtlich an jeder Grenze scheitert. Eine Fixture wird beendet, bevor sie Eingaben liest. Eine andere schreibt vor der Antwort hello nach stdout. Eine dritte beantwortet initialize und liefert anschließend ein ungültiges Tool-Schema. Eine vierte listet ein Tool auf, dessen Handler einen externen Versuch protokolliert und einen kontrollierten Fehler zurückgibt. Wenn deine Client-Integration alle vier Fälle in denselben Alarm zusammenfasst, korrigiere die Integration, bevor du wegen eines echten Servers im Dunkeln debuggen musst.
Timeouts und Wiederholungen brauchen eine verantwortliche Phase
Ein einziges Startup-Timeout führt zu den falschen Reparaturen. Es lässt einen langsamen Paketdownload, einen Parserfehler bei der Initialisierung, einen Schemafehler und einen hängenden Remote-API-Aufruf gleich aussehen. Verwende getrennte Fristen, weil jede Frist zu einem anderen Verantwortlichen gehört.
Der Launcher besitzt den Zeitraum von spawn_requested bis spawned. Server und Transport besitzen den Zeitraum vom Start bis initialized_received. Der Registrierungsweg des Servers besitzt die Erkennung. Der Tool-Handler und seine entfernte Abhängigkeit besitzen die Ausführung. Benenne das Timeout nach seinem Verantwortlichen und gib das letzte bestätigte Phasenereignis aus.
error=handshake_timeout last_event=initialize_received run_id=run_01JX
Das ist handlungsorientiert. Es teilt dem Maintainer des Servers mit, dass er die Erstellung der Antwort und stderr prüfen soll, nicht die entfernte API.
Ein automatischer Wiederholungsversuch ist nur dann sicher, wenn die fehlgeschlagene Phase keine externe Wirkung hatte. Einen fehlgeschlagenen Start zu wiederholen, weil die ausführbare Datei vorübergehend nicht verfügbar war, kann akzeptabel sein. Eine Erkennungsanfrage erneut zu senden, ist normalerweise akzeptabel, wenn die Erkennung lokal und nebenwirkungsfrei ist. Einen Tool-Aufruf nach external_attempt zu wiederholen, ist gefährlich, außer die entfernte Operation verfügt über einen dokumentierten Idempotenzmechanismus und du fügst einen Idempotenzwert hinzu.
Verstecke Wiederholungen nicht hinter einem Serverneustart. Wenn der Startcode ein Token erneuert, einen Tunnel erstellt, ein Telemetrieereignis sendet oder eine entfernte Identität prüft, ist ein Neustart bereits eine externe Aktion. Das ist ein weiterer Grund, den Start lokal zu halten und entfernte Arbeit in explizite Tools zu verschieben.
Wenn ein Client einen Kindprozess nach Ablauf einer Frist beendet, gib nach Möglichkeit vor der Beendigung ein Unterbrechungsereignis aus. Der Server kann es möglicherweise nicht mehr leeren. Der Elternprozess sollte die Beendigungsanforderung als eigenes Ereignis aufzeichnen, einschließlich des letzten von ihm beobachteten Kindereignisses. So bleibt der Datensatz ehrlich: Der Prozess hätte kurz vor einer Antwort stehen können, aber du behauptest nicht, dass er geantwortet hat.
Mache den Start langweilig, bevor du ihn schneller machst
Ein guter MCP-Server kann ohne Netzwerk, ohne Verwendung von Zugangsdaten, ohne verändernde Nebenwirkung und ohne Unklarheit über seinen Protokollzustand starten. Er lädt lokale Konfiguration, richtet seinen Transport ein, antwortet auf die Initialisierung und bietet eine deterministische Schnittstelle an. Dieses Verhalten ist leichter zu betreiben und sicherer zu wiederholen.
Die beliebteste schlechte Empfehlung lautet: «Überprüfe beim Start alles.» Sie wirkt verantwortungsvoll, weil Fehler früh erscheinen. In der Praxis vermischt sie lokale Konfiguration, Identität, entfernte Verfügbarkeit und Autorisierung zu einem undurchsichtigen Ritual. Außerdem veranlasst sie Clients, externe Aktionen unter der Bezeichnung eines Startfehlers zu wiederholen.
Prüfe lokal, was du lokal prüfen kannst. Melde die entfernte Erreichbarkeit über ein ausdrückliches Tool oder über die erste Operation, die sie benötigt. Halte die Namen der Phasen stabil. Bei stdio muss stdout ausschließlich dem Protokoll dienen. Teste einen absichtlich fehlerhaften Server für jede Grenze, die du zu beobachten vorgibst.
Wenn der nächste Client meldet, ein MCP-Server sei nicht gestartet, solltest du anhand eines einzigen Laufdatensatzes vier Fragen beantworten können: Wurde der Prozess gestartet? Wurde die Initialisierung abgeschlossen? Waren Tools erkennbar? Und hat irgendetwas die Außenwelt erreicht? Wenn du nicht alle vier Fragen beantworten kannst, bleibt der Status unklar.
FAQ
Warum zeigt mein MCP-Client für mehrere verschiedene Startprobleme denselben Fehler an?
Behandle den Prozessstart, die Protokollinitialisierung, die Erkennung von Fähigkeiten und die erste externe Aktion als getrennte Zustände. Ein Client kann einen einzigen allgemeinen Fehler anzeigen, obwohl nur einer dieser Zustände fehlgeschlagen ist. Deine Protokolle müssen festhalten, welche Grenze in jedem Zustand überschritten wurde.
Bedeutet ein laufender MCP-Prozess, dass der Server erfolgreich verbunden ist?
Nein. Ein laufender Prozess beweist nur, dass das Betriebssystem ihn erstellt hat und er noch nicht beendet wurde. Er kann beim Laden der Konfiguration blockiert sein, auf eine Abhängigkeit warten, ungültige Daten in stdout schreiben oder die Initialize-Anfrage des Clients ignorieren.
Kann eine Ausgabe in stdout einen MCP-stdio-Server beschädigen?
Bei einem stdio-Server ist stdout die Leitung für das Protokoll. Ein Banner, ein Stacktrace, eine Meldung des Paketmanagers oder eine gewöhnliche Ausgabe kann den JSON-RPC-Datenstrom beschädigen, bevor der Server auf Initialize antwortet. Diagnosedaten gehören deshalb nach stderr.
Was ist der MCP-Initialisierungs-Handshake?
Der Client sendet eine initialize-Anfrage, der Server gibt eine kompatible Protokollversion und seine Fähigkeiten zurück, und der Client sendet anschließend die Benachrichtigung notifications/initialized. Ein Server, der startet, diesen Austausch aber nicht abschließt, hat einen Handshake-Fehler und kein Tool-Problem.
Was bedeutet es, wenn die MCP-Tool-Erkennung fehlschlägt?
Das bedeutet, dass die Initialisierung abgeschlossen wurde, der Client aber kein verwendbares Ergebnis von tools/list erhalten hat. Ursache können ein fehlender Handler, ein Fehler beim Erstellen der Schemas, eine nicht unterstützte Fähigkeitsdeklaration oder ein Client sein, der überhaupt keine Tools anfordert.
Sollte ein MCP-Server beim Start eine externe API kontaktieren?
Lege beim Serverstart keine Netzwerkanfrage, SSH-Anmeldung, Token-Erneuerung oder Geheimnissuche an, außer der Server kann ohne sie nicht funktionieren. Starte zuerst den Protokollendpunkt. Führe den externen Aufruf anschließend im Tool-Handler aus und protokolliere ihn als eigene Aktion.
Wie kann ich einen MCP-Startfehler außerhalb meines Clients reproduzieren?
Führe denselben Startbefehl mit demselben Benutzer, Arbeitsverzeichnis und derselben Umgebung aus wie der Client. Teste ihn anschließend mit MCP Inspector oder einem kontrollierten JSON-RPC-Austausch. Ein Terminaltest mit deiner interaktiven Shell kann den tatsächlichen Fehler verdecken.
Was sollte ich für die Fehlersuche beim Start eines MCP-Servers protokollieren?
Verwende beim Start eine eindeutige Lauf-ID und verknüpfe sie mit stderr-Ereignissen, Protokollereignissen, der Tool-Erkennung und jedem ausgehenden Aufruf. Zeichne Prozess-ID, Pfad der ausführbaren Datei, Exit-Status und eine ID für externe Aktionen auf. Zugangsdaten und Request-Bodies sollten standardmäßig nicht protokolliert werden.
Wie lang sollte ein MCP-Startup-Timeout sein?
Ein Zeitlimit ist sinnvoll, muss aber die Phase nennen, die es absichert. Verwende eine kurze Frist für den Start, eine eigene Frist für die Initialisierung und eine weitere für tools/list. Andernfalls wirft eine einzige Timeout-Meldung genau die Diagnose weg, die du brauchst.
Ist es sicher, den Start eines fehlgeschlagenen MCP-Servers automatisch zu wiederholen?
Nein. Ein Wiederholungsversuch kann Nebenwirkungen wiederholen, wenn der Start Dateien erstellt, Tokens erneuert oder Dienste kontaktiert. Wiederhole nur eine Phase, von der du weißt, dass sie keine externe Wirkung hat, und gib jedem Versuch eine eigene Lauf-ID.