Wir haben unseren MCP-Server auditiert: Befunde und Korrekturen

Eine Rechteausweitung im SSE-Transport, ein Ausgabevertrag, der nicht eingehalten wurde, Beträge ohne Währung und sieben neue Tools. Das vollständige Audit des MCP-Servers von Cord.

Der MCP-Server von Cord verbindet Assistenten wie Claude oder Cursor mit den echten Daten eines Unternehmens: Angebote, Rechnungen, überfällige Forderungen. Mit einem Schlüssel mit Schreibrechten sendet er außerdem Angebote und erfasst Zahlungen. Anders gesagt: ein Sprachmodell, das mit den Zugangsdaten eines Unternehmens handelt. Wir haben ihn von Anfang bis Ende auditiert, und das ist dabei herausgekommen.

1. Ein Leseschlüssel konnte schreiben

Cord stellt MCP über zwei Transporte bereit: zustandsloses HTTP (POST /api/mcp) und das ältere SSE (GET /api/mcp/sse, um die Sitzung zu öffnen, und POST /api/mcp/message, um Nachrichten zu senden). Die SSE-Sitzung speichert die Organisation, den Schlüssel und die Berechtigung, mit der sie geöffnet wurde.

Das Problem lag in /api/mcp/message. Der Endpoint prüfte, ob der Schlüssel, der die Nachricht sendet, zu derselben Organisation gehört wie die Sitzung, und führte das Tool anschließend mit der in der Sitzung gespeicherten Berechtigung aus. Ein Schlüssel mit reinem Lesezugriff desselben Unternehmens, der eine sessionId kannte, konnte so Schreib-Tools mit der Sitzung eines anderen Schlüssels ausführen.

Die Korrektur ist eine einzige Zeile, aber die richtige: Eine Sitzung gehört zu einem Schlüssel, nicht zu einer Organisation.

if (session.orgId !== auth.orgId || session.keyId !== auth.keyId) {
  return rpcErrRes(null, -32001, 'Esta API key no corresponde a la sesión.', 403);
}
// und ausgeführt wird mit auth.scope, der Berechtigung des Aufrufers, nicht der der Sitzung

Dafür gibt es einen Test: Ein Leseschlüssel mit der sessionId eines Schreibschlüssels erhält 403, und das Tool wird nicht ausgeführt.

2. Ein Ausgabevertrag, der nicht eingehalten wurde

Alle 19 Tools deklarierten ein outputSchema. In der Protokollversion 2025-06-18 verpflichtet diese Deklaration dazu, structuredContent zurückzugeben, das dem Schema entspricht. Cord gab nur Text zurück. Außerdem hatten die Schemas additionalProperties: false und einen als String typisierten next_cursor, der tatsächlich null sein kann: Ein validierender Client hätte korrekte Antworten abgelehnt.

Jetzt antwortet jedes Tool mit beidem: structuredContent für Clients, die validieren, und demselben kompakten JSON als Text für die, die es nicht tun. Die Ausgabeschemas sind offen: Ein neues Feld bricht bei niemandem etwas. Und der Text ist nicht mehr eingerückt, was bei jedem Aufruf Tokens spart.

3. Beträge ohne Währung

Cord hat eine Regel: Ein Betrag ohne Währung ist eine Zahl, kein Geld. Die Tools für Angebote, Produkte und Kunden gaben total: 12500 zurück, ohne zu sagen, in welcher Währung. Ein Agent, der von Pesos ausgeht, meldet einem Unternehmen, das in Dollar verkauft, falsche Zahlen. Heute wird jeder Betrag zusammen mit seiner moneda übermittelt.

4. Fehler, die das Modell nicht erreichten

Ein Tool für das Forderungsmanagement warf einen generischen Fehler, wenn der Tarif es nicht enthielt. Der Server behandelte ihn als internen Fehler, und das Modell erhielt „Interner Serverfehler“, ohne dem Nutzer erklären zu können, was los war. Obendrein nannte die Meldung den falschen Tarif. Jetzt werden Tariflimits als fachliche Fehler gemeldet, mit dem Namen des Tarifs, der tatsächlich benötigt wird – gelesen aus demselben Vertrag, den auch der Rest von Cord verwendet.

Nebenbei stellten wir fest, dass der Index der Dokumentation bei einem Fehler den vollständigen Stacktrace zurückgab. Jetzt nicht mehr.

5. Sieben neue Tools

Wir sind von 19 auf 26 Tools gewachsen. Die neuen sind dafür gedacht, dass ein Agent Cord integrieren kann, nicht nur bedienen:

Tool Wofür
contexto_cuenta Unternehmen, Land, Währung, Tarif und ob der Schlüssel ein Test- oder Live-Schlüssel ist. Der erste Aufruf.
proponer_configuracion Schlägt Profil, Steuern und Katalog anhand der Website des Unternehmens vor. Gibt einen Link zurück; eine Person gibt frei.
estado_configuracion Ob dieser Vorschlag bereits freigegeben wurde und was übernommen wurde.
validar_datos_fiscales RFC, Steuerregime und CFDI-Verwendung, NIF oder EIN – mit denselben Regeln wie bei der Rechnungsstellung.
validar_apariencia Prüft ein appearance von Cord Elements, bevor Sie es in den Code übernehmen.
simular_evento Löst einen Test-Webhook über die echte Engine aus. Nur mit Testschlüssel.
buscar_documentacion Durchsucht die Dokumentation und gibt die Seiten mit Auszug und URL zurück.

Keines dieser Tools ändert Kontoeinstellungen direkt. proponer_configuracion hinterlässt einen Vorschlag, den eine Person in Cord freigibt – genau wie den aus dem Onboarding.

6. Eine API, die sich selbst beschreibt

Ein Agent, der Code gegen eine API schreibt, liest ihre Spezifikation. Von den 343 Feldern unserer OpenAPI-Spezifikation hatten 279 keine Beschreibung: Der Agent musste raten, was terminos ist, ob total Steuern enthält oder in welcher Einheit rate geliefert wird.

Jetzt gibt es ein Glossar, FIELD_DOCS, das jeden Feldnamen der API beschreibt, und der Generator wendet es auf jede Eigenschaft ohne eigene Beschreibung an. Der CI-Schritt, der die Spezifikation mit dem Code vergleicht, schlägt fehl, wenn eine Eigenschaft oder ein Parameter ohne Beschreibung bleibt. Ein Feld hinzuzufügen, ohne es zu erklären, kommt nicht mehr durch.

Was sich bewusst nicht geändert hat

  • Kein Tool stempelt Rechnungen. Einen Entwurf anlegen: ja. Ihn bei der Steuerbehörde auszustellen, ist unwiderruflich und wird von einer Person ausgelöst.
  • Text, den der Endkunde im öffentlichen Link schreibt, erreicht das Modell als cliente_externo markiert und abgegrenzt, damit es ihn meldet und nicht als Anweisung befolgt.
  • Aktionen, die beim Kunden ankommen (senden, bestätigen, ablehnen, Zahlung erfassen), tragen destructiveHint, sodass ein gut konfigurierter MCP-Client vor der Ausführung um Bestätigung bittet.

Wenn Sie Cord mit Ihrem Assistenten verbinden, lassen Sie ihn zuerst contexto_cuenta aufrufen. Alles Weitere ergibt sich daraus.