Zurück zum Blog

Sechs MCP-Server tot, vier gesund: wenn die Projektdatei die Anmeldung überstimmt

Nach einem Umbau meldeten sechs von zehn MCP-Servern nur noch „Connection closed“. Kaputt war keiner davon. Die Ursache: derselbe Name in zwei Konfigurationen und eine Umgebungsvariable, die mitvererbt wurde.

5 Min. Lesezeitvon Daniel AlischKI-Agenten, MCP, Engineering

In AI Employee meldeten im September sechs von zehn eingebauten MCP-Servern beim Start nur noch „Connection closed“. Die Server waren in Ordnung, die Konfiguration auch, jedenfalls jede für sich. Kaputt war das Zusammenspiel zweier Einträge mit demselben Namen, und der Fall taugt als Prüfliste für alle, die MCP-Server nicht bloß von Hand anmelden.

Der Umbau, der es auslöste

Bis dahin startete jeder Agentenlauf seine MCP-Server als eigene stdio-Prozesse. Gemessen in einem laufenden Container: 81 Threads allein für die MCP-Server eines einzigen Laufs, hier elf inklusive des optionalen Microsoft-Graph-Servers. Das Limit (pids.max) liegt bei 512, die Grundlast bei rund 40. Viel Luft für parallele Läufe bleibt da nicht.

Also wanderten die eingebauten Server in einen gemeinsamen Prozess, der je Container einmal läuft und auf 127.0.0.1:8790 lauscht. Angemeldet werden sie als HTTP-Adressen per claude mcp add --scope user. Ein ordentlicher Umbau, der die Ursache beseitigt, statt sie zu deckeln. Danach fehlten dem Agenten die Werkzeuge für Gedächtnis, Benachrichtigungen, Aufgaben und Skills.

Das Tückische daran: Er arbeitete trotzdem weiter. Er wich auf REST und die GitHub-CLI aus, mindestens einen Tag lang, wahrscheinlich länger, bevor die Ursache gefunden war. Ein Agent, der sich selbst zu helfen weiß, ist auch ein Agent, dessen Ausfall nicht auffällt.

Die Gesunden sind die beste Spur

Die Fehlermeldung half nicht. „Connection closed“ sagt nur, dass die andere Seite weg ist. Aufschlussreich war die Aufteilung: bash-approval, memory, notifications, orchestrator, skills und desktop fielen aus, brain, read-logs, email und hyperframes liefen. Gleicher Prozess, gleiche Adresse.

Das entlastete den Sammelprozess, und auch ein direkter Aufruf zeigte es: Gegen http://127.0.0.1:8790/mcp/memory liefen initialize, tools/list und tools/call in unter einer halben Sekunde. Der Server war da. Nur die Sitzung kam nicht bei ihm an.

Die nützliche Frage lautete deshalb nicht „Was ist der Fehler?“, sondern „Was unterscheidet die beiden Mengen?“. Die Antwort kam aus einem Vergleich der Servernamen in .mcp.json und ~/.claude.json: Genau die sechs kranken Namen standen in der Projektdatei, die vier gesunden waren nur per CLI angemeldet.

Zwei Einträge, ein Name

Neben der CLI-Anmeldung schrieb der Agent beim Start eine .mcp.json, gedacht als Rückfall für den interaktiven Betrieb und als Dokumentation. Der Umbau hatte sie nicht angepasst, sie trug weiter die alten Kommandos:

json
// .mcp.json (Projekt): gewinnt
"memory": { "command": "node", "args": ["/opt/mcp/memory-server.mjs"] }

// Anmeldung (User): verliert
"memory": { "type": "http", "url": "http://127.0.0.1:8790/mcp/memory" }

Die Doku von Claude Code ist da eindeutig: Ist ein Server mehrfach definiert, verbindet es sich einmal, und zwar über die Quelle mit dem höchsten Rang, in der Reihenfolge Local, Project, User. Der ganze Eintrag gilt, Felder werden nicht gemischt. Die Projektdatei schlug also die frische HTTP-Anmeldung.

Der Rest ist eine Kette. Claude Code startet den stdio-Prozess. Der erbt die Umgebung samt MCP_HTTP_PORT. Das Transportmodul sieht den Port, schließt daraus, es solle HTTP sprechen, und will lauschen. Den Port hält aber der Sammelprozess. Wer das Kommando von Hand startet, bekommt die ehrliche Fassung:

bash
$ node /opt/mcp/memory-server.mjs
Error: listen EADDRINUSE: address already in use 127.0.0.1:8790

Der Prozess stirbt, der Client sieht eine geschlossene Verbindung. Jede Komponente tat exakt, was ihre Konfiguration verlangte. Die Warnung gab es sogar: claude mcp list meldet Namen, die in mehreren Scopes mit unterschiedlichen Endpunkten stehen. Nur liest das in einem automatischen Lauf niemand.

Der Fix und sein Beifang

Die Korrektur ist klein: Im gemeinsamen Modus schreibt die Datei dieselben Adressen wie die Anmeldung. Der Port ist dabei ein Pflichtparameter ohne Standardwert, weil ein vergessenes Argument sonst still in den alten Fehler zurückfiele.

python
def _write_mcp_json_fallback(http_port: int | None) -> None:
    if http_port:
        for name in _COMBINED_MCP_NAMES:
            mcp_config["mcpServers"][name] = {
                "type": "http",
                "url": f"http://127.0.0.1:{http_port}/mcp/{name}",
            }

Ein verwandter Pfad wurde parallel repariert und sogar zuerst zusammengeführt: Scheitert der Sammelprozess, fallen die Server auf stdio zurück und erbten auch dort den Port. Die Variable in der Konfiguration wegzulassen reicht nicht, geerbt ist geerbt. Sie muss ausdrücklich leer gesetzt werden. Das nahm dem Ausfall bereits das Symptom, ließ aber je Lauf sechs Einzelprozesse stehen. Erst die Korrektur oben führte alles in den einen Prozess zurück.

Der Beifang war lehrreicher. Auch die eigenen Server des Betreibers standen in der Datei, allerdings ohne type-Feld, und Claude Code übersprang sie still („has a url but no type“). Das war Zufall, kein Schutz. Mit einem gültigen Eintrag hätte die Datei jene Anmeldung geschlagen, die bei jeder Token-Rotation neu geschrieben wird, und nach der ersten OAuth-Rotation hätte es 401 gegeben. Ein Fehler verdeckte also einen zweiten. Solche Stellen räumt man besser auf, bevor jemand den harmlosen der beiden behebt. Die Einträge sind nun draußen.

Was ich seither zuerst prüfe

  1. Mengen vergleichen. Fällt nur ein Teil aus, zuerst fragen, was Kranke und Gesunde unterscheidet. Das hat hier mehr eingegrenzt als die Fehlermeldung.
  2. Abweichende Endpunkte suchen. claude mcp list gehört in die regelmäßige Gesundheitsprüfung, nicht erst in die Fehlersuche. Ein Name, der in zwei Scopes auf verschiedene Endpunkte zeigt, ist auch dann ein Befund, wenn heute alles läuft.
  3. Das Kommando einzeln starten. Die Sitzung sagt „Connection closed“, der Prozess selbst sagt, was los ist.
  4. Geerbte Umgebung mitdenken. Ein Schalter per Umgebungsvariable gilt für jedes Kind, auch für die, die man nicht gemeint hat.
  5. Handkorrekturen nicht überschätzen. Die geänderte .mcp.json wirkte erst in der nächsten Sitzung, und ein Containerstart schrieb sie bis zum ausgelieferten Fix wieder neu.

Fazit

Nichts an diesem Ausfall war exotisch: eine mitgeschleppte Datei, eine dokumentierte Rangfolge und eine vererbte Variable. Im Code hieß die Datei Rückfall und Dokumentation, zur Laufzeit stach sie die Anmeldung aus. Ein Rückfall, der Vorrang hat, ist kein Rückfall. Wer Konfiguration an zwei Stellen schreibt, sollte sie aus einer Quelle erzeugen oder ihren Gleichlauf testen. Beim nächsten Umbau vergisst man die zweite Stelle sonst wieder, und sie meldet sich nicht von selbst.

Weiterlesen