6 Min. Lesezeit

Warum schützt sauberes MCP-stdout die Protokollgrenze?

Eine saubere MCP-stdout-Verarbeitung verhindert, dass Protokollausgaben, Banner und fehlerhafte Frames die Grenze eines Agenten-Gateways überschreiten. Baue ein Harness, das beweist, dass Aufrufe gestoppt werden.

Warum schützt sauberes MCP-stdout die Protokollgrenze?

Eine vertrauenswürdige stdio-MCP-Grenze ist nur dann möglich, wenn jedes Byte auf stdout zum Protokoll gehört und jede fehlerhafte oder mehrdeutige Anfrage stirbt, bevor sie einen Aktions-Executor erreicht. Ein Parser, der Datenmüll ablehnt, aber eine teilweise verstandene Anfrage bis zu HTTP oder SSH durchlässt, versagt genau an der gefährlichen Stelle.

Teams behandeln störende Ausgaben auf stdout oft als lästigen Interoperabilitätsfehler. Das ist zu nachsichtig. Wenn ein Agent externe Aktionen auslösen kann, kann ein zusätzliches Banner die Unterhaltung aus dem Takt bringen, eine Fehlerantwort verbergen oder einen großzügigen Client dazu bringen, die falsche Antwort der falschen Anfrage zuzuordnen. Ein guter Test prüft mehr als einen sauberen Prozessabschluss. Er beweist, dass fehlerhafte Eingaben über die Leitung keine externen Auswirkungen haben.

Der stdout-Vertrag akzeptiert eine JSON-RPC-Nachricht pro Zeile

Der stdio-Transport des Model Context Protocol verlangt JSON-RPC-Nachrichten auf stdout, die durch Zeilenumbrüche getrennt sind. Andere Ausgaben dürfen nicht in diesen Strom gelangen. Die entsprechende Regel gilt auch in die andere Richtung: Ein Client sendet Protokollnachrichten über stdin und verwendet diesen Kanal nicht als Protokollleitung.

Das klingt selbstverständlich, bis ein Paketinstallationsprogramm einen Hinweis ausgibt, eine Abhängigkeit eine Warnung schreibt oder jemand vorübergehend ein print() in den Startpfad einfügt. In einem menschlich bedienten Terminal sind solche Zeilen harmlos. In einem Protokollstrom sind sie Bytes, die der Kommunikationspartner interpretieren muss. Er kann nicht sicher erkennen, ob loading credentials ein Banner, ein fehlerhaftes Ergebnis, ein Fragment einer Antwort oder der Beginn eines manipulierten Austauschs ist.

Der Transportvertrag besteht aus drei Teilen, die Tests ausdrücklich festhalten sollten:

  • Jede vollständige Zeile muss als UTF-8 decodiert und als genau ein JSON-Wert geparst werden können.
  • Dieser Wert muss in der jeweiligen Übertragungsrichtung die Form einer zulässigen JSON-RPC-Anfrage, -Antwort oder -Benachrichtigung haben.
  • Keine Aktion darf beginnen, bevor die vollständige Anfrage die Prüfungen für Framing, JSON, Protokoll, Schema und Autorisierung bestanden hat.

Die erste Bedingung erkennt störende Ausgaben. Die zweite erkennt ein Objekt, das zwar gültiges JSON ist, aber keine MCP-Nachricht darstellt. Die dritte schützt vor dem entscheidenden Fehler: frühes Parsen mit einer Ausführungserlaubnis zu verwechseln.

Die JSON-RPC-2.0-Spezifikation unterscheidet zwischen einem Parsefehler und einer ungültigen Anfrage. Ungültiges JSON kann den Fehlercode -32700 erhalten, wenn der Kommunikationspartner noch eine korrekt gerahmte Antwort schreiben kann. Ein JSON-Wert mit der falschen Anfragestruktur ist eine ungültige Anfrage, normalerweise mit dem Code -32600. Diese Codes helfen einem konformen Kommunikationspartner, den Fehler zu diagnostizieren. Sie sagen jedoch nicht, ob dein eigener Executor unberührt geblieben ist. Deine Tests müssen diese Frage direkt beantworten.

Ein Banner kann eine autorisierte Antwort beschädigen

Ein Startbanner kann eine völlig legitime Aktion stören, nachdem die Autorisierung bereits erfolgreich war. Deshalb ist die Sauberkeit von stdout nicht nur ein Thema der Eingangsvalidierung.

Stell dir einen Server vor, der eine Initialize-Anfrage angenommen hat und gerade ein Tool-Ergebnis zurückgeben will. Eine Abhängigkeit schreibt warning: configuration missing zwischen Beginn und Ende der Antwort auf stdout. Ein strikter Client weist die Zeile zurück und trennt die Verbindung. Ein großzügiger Client überspringt sie und macht weiter. Der strikte Client verliert Verfügbarkeit. Der großzügige Client hat nun eine Parserregel, die nicht zugeordneten Bytes innerhalb eines sicherheitsrelevanten Austauschs akzeptiert.

Belohne den großzügigen Client nicht dafür, bequem zu wirken. Sobald ein Client beliebige Zeilen verwirft, steht er vor einer langen Liste von Fragen, die er nicht zuverlässig beantworten kann. Hat er eine Diagnose verworfen? Die Antwort auf eine andere Anfrage? Hat ein Wrapper eine Zeile dupliziert? Hat ein Angreifer, der den Kindprozess beeinflussen kann, Text eingeschleust, der den Clientzustand verändert? Ein Client kann aus einer zufälligen Bytefolge keine Absicht ableiten.

Halte Diagnosen auf stderr. Gib stderr eigene Regeln für Erfassung, Aufbewahrung und Maskierung, und sorge dafür, dass die Prozessüberwachung diese Trennung bewahrt. Ein erstaunlich häufiger Fehler, der erst im Release auftritt, kommt von einem Wrapper, der beide Datenströme zusammenführt, weil die Terminalausgabe bei der Entwicklung übersichtlicher aussah. Dieser Wrapper zerstört unbemerkt die Protokollgrenze.

Teste ausgehenden Datenverkehr als rohe Bytes, bevor eine Clientbibliothek ihn normalisiert. Wenn eine Bibliothek eine ungültige Sequenz in eine Ausnahme umwandelt und das ursprüngliche Transkript verbirgt, bewahre das Transkript in der Ausgabe des fehlschlagenden Tests auf. Die genaue erste fehlerhafte Zeile zu sehen, erspart stundenlanges Rätselraten.

Die Ablehnung muss den Aktions-Executor erreichen

Ein zurückgewiesener Frame ist nur dann sicher, wenn der Code, der eine externe Aktion ausführt, ihn niemals zu sehen bekommt. Eine Fehlerantwort zurückzugeben ist nützlich, aber das ist nicht die eigentliche Sicherheitseigenschaft.

Setze direkt hinter die letzte Validierungs- und Autorisierungsgrenze einen Aktionsrekorder. In einer echten Implementierung kann diese Nahtstelle die Funktion umschließen, die eine HTTP-Verbindung öffnet oder einen SSH-Helfer aufruft. Verwende im Test einen In-Memory-Rekorder oder einen lokalen Fake-Dienst. Richte Tests für fehlerhafte Eingaben niemals an einen echten Endpunkt und vertraue darauf, dass der Fehlerpfad dich schützt.

Der Unterschied verschwimmt leicht, weil eine normale Anfrage einen langen Weg zurücklegt. Sie kommt als Bytes an, wird zu JSON, zu einem JSON-RPC-Objekt, zu einem MCP-Methodenaufruf, wird einem Tool-Schema zugeordnet, erhält eine Autorisierungsentscheidung und wird schließlich zu einer Aktion. Ein Entwickler kann bereits vor Abschluss aller Prüfungen einen Audit-Eintrag schreiben oder ein Anfrageobjekt erstellen. Das ist nur dann akzeptabel, wenn keine dieser Operationen die Außenwelt kontaktiert oder eine Berechtigung verbraucht.

Eine nützliche Invariante lautet: Der Executor akzeptiert ein vollständig typisiertes, autorisiertes Aktionsobjekt, niemals rohes JSON und niemals eine teilweise validierte Anfrage. Wenn der Executor ein allgemeines Dictionary akzeptiert, wird ihn irgendwann jemand zu früh aufrufen. Der Code kann monatelang erfolgreiche Tests für den Normalfall bestehen, weil fehlerhafte Frames in der alltäglichen Entwicklung selten sind.

Halte die Erfassung von Ablehnungen getrennt von der Erfassung von Ausführungen. Ein Test sollte sagen können, dass ein Parser einen Frame abgelehnt hat, die Sitzung geschlossen wurde und der Executor null Aufrufe erhalten hat. Wenn diese Ereignisse in einem allgemeinen Erfolgs- oder Fehlerzähler landen, kannst du eine saubere Ablehnung nicht von einer Aktion unterscheiden, die begonnen und erst später fehlgeschlagen ist.

Setze die Testnaht unter den Parser und über die Ausführung

Das kleinste brauchbare Harness besteht aus einem strikten Validator für die Leitung und einem Fake-Executor. Der Validator ist für Bytes und die Protokollstruktur zuständig. Der Fake-Executor zeichnet jeden Versuch auf, Arbeit auszuführen. Der Produktionsadapter darf anders aussehen, aber der Vertrag zwischen beiden sollte eng bleiben.

Dieses Python-Beispiel ist absichtlich klein. Speichere es als test_stdio_boundary.py, installiere pytest und führe pytest -q test_stdio_boundary.py aus. Ersetze Gateway durch einen Adapter für dein eigenes Gateway, aber behalte die Prüfungen rund um Recorder bei.

import io
import json
import pytest

class Recorder:
    def __init__(self):
        self.calls = []

    def execute(self, action):
        self.calls.append(action)
        return {'ok': True}

class Gateway:
    def __init__(self, executor):
        self.executor = executor
        self.closed = False

    def reject(self, code, reason):
        self.closed = True
        return {'jsonrpc': '2.0', 'id': None,
                'error': {'code': code, 'message': reason}}

    def receive_line(self, raw_line):
        if self.closed:
            return None
        try:
            message = json.loads(raw_line)
        except json.JSONDecodeError:
            return self.reject(-32700, 'parse error')

        if not isinstance(message, dict):
            return self.reject(-32600, 'invalid request')
        if message.get('jsonrpc') != '2.0':
            return self.reject(-32600, 'invalid request')
        if message.get('method') != 'tools/call':
            return self.reject(-32601, 'method not found')
        if not isinstance(message.get('id'), (str, int)) or isinstance(message.get('id'), bool):
            return self.reject(-32600, 'invalid request')

        params = message.get('params')
        if not isinstance(params, dict):
            return self.reject(-32602, 'invalid params')
        if not isinstance(params.get('name'), str):
            return self.reject(-32602, 'invalid params')
        if not isinstance(params.get('arguments', {}), dict):
            return self.reject(-32602, 'invalid params')

        action = {'name': params['name'], 'arguments': params.get('arguments', {})}
        result = self.executor.execute(action)
        return {'jsonrpc': '2.0', 'id': message['id'], 'result': result}

def frame(value):
    return json.dumps(value, separators=(',', ':')) + '\n'


def test_bad_lines_never_execute():
    bad_lines = [
        'debug: entering tool handler\n',
        '\u003chtml\u003egateway unavailable\u003c/html\u003e\n',
        '{not json}\n',
        'null\n',
        frame({'jsonrpc': '1.0', 'id': 4, 'method': 'tools/call', 'params': {}}),
        frame({'jsonrpc': '2.0', 'id': 4, 'method': 'tools/call', 'params': 'run'}),
    ]

    for raw_line in bad_lines:
        recorder = Recorder()
        gateway = Gateway(recorder)
        response = gateway.receive_line(raw_line)
        assert response['jsonrpc'] == '2.0'
        assert 'error' in response
        assert gateway.closed
        assert recorder.calls == []

def test_complete_valid_request_executes_once():
    recorder = Recorder()
    gateway = Gateway(recorder)
    request = frame({
        'jsonrpc': '2.0',
        'id': 9,
        'method': 'tools/call',
        'params': {'name': 'safe-test', 'arguments': {'value': 'green'}},
    })

    response = gateway.receive_line(request)
    assert response['result'] == {'ok': True}
    assert recorder.calls == [{'name': 'safe-test', 'arguments': {'value': 'green'}}]

Das ist keine vollständige MCP-Implementierung und sollte auch keine werden. Ziel ist, die Eigenschaft, dass keine Aktion stattfindet, ausführbar zu machen. Dein Adapter kann echte stdin-Bytes in dieselbe Art von Testnaht einspeisen und den echten Anfragevalidator verwenden. Übernimm die verkürzte Methodenbehandlung jedoch nicht in einen Produktionsserver.

Beachte die Entscheidung, eine Sitzung nach einer fehlerhaften oder ungültigen Eingangszeile zu schließen. Das Protokoll zwingt nicht jede Implementierung zu dieser Sitzungspolitik. Für ein Gateway, das externe Nebenwirkungen kontrolliert, ist sie eine sinnvolle Voreinstellung, weil eine beschädigte Zeile bedeuten kann, dass der Kommunikationspartner die Rahmenstruktur verloren hat. Wenn du die Sitzung nach einer sauber gerahmten, ungültigen Anfrage offenhältst, teste diesen Pfad separat und beweise, dass die nächste gültige Anfrage keinen Zustand von der abgelehnten Anfrage übernimmt.

Ein ausführbares Harness sollte Bytes und Auswirkungen beobachten

Die Aktionsgrenze absichern
Solange der Tresor gesperrt ist, weist Sallyport jede Aktion an der Tresorgrenze zurück.

Der Unit-Test oben prüft die Ablehnung eingehender Anfragen. Ergänze einen Test auf Prozessebene, um Ausgaben zu erkennen, die außerhalb des Codepfads entstehen, den du normalerweise ausführst. Der folgende Helfer prüft ein aufgezeichnetes stdout-Transkript, ohne darauf zu vertrauen, dass eine Clientbibliothek es für dich parst.

import json
import subprocess

def assert_protocol_stdout(data):
    assert data.endswith(b'\n'), 'stdout ended without a complete frame'
    for number, raw_line in enumerate(data.splitlines(), start=1):
        try:
            line = raw_line.decode('utf-8')
            message = json.loads(line)
        except (UnicodeDecodeError, json.JSONDecodeError) as error:
            raise AssertionError(
                f'non-protocol stdout on line {number}: {raw_line!r}'
            ) from error

        assert isinstance(message, dict), f'line {number} is not an object'
        assert message.get('jsonrpc') == '2.0', f'line {number} lacks JSON-RPC 2.0'
        is_notification = isinstance(message.get('method'), str) and 'id' not in message
        is_response = 'id' in message and ('result' in message or 'error' in message)
        assert is_notification or is_response, f'line {number} has no permitted shape'

def run_server(command, stdin_bytes):
    completed = subprocess.run(
        command,
        input=stdin_bytes,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )
    assert_protocol_stdout(completed.stdout)
    return completed

def test_release_command_has_clean_stdout():
    initialize = (
        b'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",'
        b'\"params\":{\"protocolVersion\":\"2025-03-26\",'
        b'\"capabilities\":{},\"clientInfo\":{\"name\":\"boundary-test\",\"version\":\"1\"}}}\n'
    )
    completed = run_server(['./gateway-under-test'], initialize)
    assert completed.returncode == 0

Verwende anstelle von ./gateway-under-test deinen tatsächlichen Startbefehl. Wenn eine Laufzeitumgebung, ein Paket-Wrapper oder Umgebungsvariablen erforderlich sind, verwende genau die Bedingungen aus der Produktion. Der Test muss stdout auch dann prüfen, wenn der Prozess mit einem Fehlerstatus endet. Ein fehlerhafter Prozess kann noch vor seinem Abbruch ein unerlaubtes Banner ausgeben.

Ein Fehler sollte die rohe Bytezeile sichtbar machen. Eine nützliche Ausgabe sieht so aus:

AssertionError: non-protocol stdout on line 1: b'loading optional extension\n'

Dieses Ergebnis zeigt dem Maintainer, wo er suchen muss. Eine allgemeine Meldung wie «Server wurde nicht initialisiert» schickt Menschen auf die Suche im Protokollcode, obwohl der Fehler auch in einem Shell-Skript, einer Protokollkonfiguration oder einem Abhängigkeitsimport liegen kann.

Führe das Harness in der paketierten Form aus und nicht nur aus einem ausgecheckten Quellcodeverzeichnis. Die Paketierung verändert die Modulauflösung, Umgebungsvariablen, Ausführungsrechte und Fehlerpfade. Genau dort tauchen versehentliche Ausgaben auf stdout auf.

Teste hässliche Eingaben statt höflicher Fehler

Eine Grenztestsuite braucht Eingaben, die den tatsächlichen Fehlerarten von Prozessströmen ähneln, und nicht nur handgeschriebenen ungültigen Objekten. Schicke jeden Fall durch denselben Einstiegspunkt, der stdin verarbeitet, und prüfe, dass der Executor leer bleibt.

Verwende mindestens diese Fälle:

  • Eine einfache Debugzeile vor einer gültigen Anfrage, gefolgt von der gültigen Anfrage in der nächsten Zeile.
  • Eine gültige Anfrage, auf die in derselben Zeile ohne Zeilentrenner ein Banner folgt.
  • Ein abgeschnittenes JSON-Objekt, gefolgt vom Ende der Datei.
  • Zwei vollständige JSON-Objekte, die ohne Trennzeichen aneinanderhängen.
  • Ein gültiges JSON-Objekt mit einer plausiblen Methode, aber fehlerhaften Parametern.

Der erste Fall erkennt eine Implementierung, die fehlerhafte Zeilen stillschweigend überspringt und weitermacht. Der zweite erkennt Framingfehler, die ein Zeilenleser für eine einzige beschädigte Anfrage halten kann. Der abgeschnittene Fall erkennt Code, der nach dem Wegfall des Kommunikationspartners versucht, Eingaben mithilfe eines Puffers zu reparieren. Der zusammengefügte Fall erkennt Parser, die so konfiguriert sind, dass sie mehrere JSON-Werte auf oberster Ebene akzeptieren, obwohl der Transport genau einen Frame pro Zeile erlaubt.

Reduziere diese Suite nicht auf eine Sammlung von Parserfällen. Kombiniere jede fehlerhafte Eingabe mit einem eindeutigen Namen für eine Fake-Aktion und prüfe, dass keiner davon im Rekorder auftaucht. Füge eine gültige Anfrage nach einer abgelehnten Anfrage nur dann hinzu, wenn deine dokumentierte Richtlinie eine offene Sitzung erlaubt. Wenn deine Richtlinie die Sitzung schließt, prüfe, dass die zweite Anfrage keine Aktion auslöst, weil die Sitzung bereits geschlossen ist.

Teste außerdem gültige Daten, die im Kontext gefährlich sind. Eine Anfrage mit method gleich tools/call und params gleich einem String ist gültiges JSON, aber kein Aufruf. Eine Anfrage mit einer numerischen ID, die deine Sprache als Boolean behandelt, überschreitet eine Typgrenze, die du nicht beabsichtigt hast. Eine Anfrage mit unbekannten Argumenten kann vom Schema abgelehnt werden, aber ein großzügiges Zusammenführen von Objekten kann sie versehentlich an einen nachgelagerten Befehls-Builder weiterreichen.

Startausgaben haben gewöhnliche Ursachen

Einen laufenden Agenten widerrufen
Das Sitzungsjournal erfasst Agentenläufe und ermöglicht es, einen Lauf sofort zu widerrufen.

Die meisten Verunreinigungen von stdout entstehen durch normale Wartungsarbeiten und nicht durch jemanden, der ein Protokoll überwinden will. Das ändert nichts daran, dass du sie erkennen musst.

Ein Befehls-Wrapper kann einen Versionshinweis ausgeben. Eine Laufzeitumgebung kann nach einer Änderung der Umgebung eine Veraltungswarnung schreiben. Ein Entwickler kann eine Debugausgabe in einem Paketimport hinterlassen, den die Tests nicht laden. Ein Absturz-Handler kann eine freundliche Meldung an stdout schreiben, weil er für eine Terminalanwendung gebaut wurde. Ein Supervisor kann stderr als Teil der Protokollsammlung mit stdout zusammenführen.

Behandle jede Quelle als eigenen Release-Testfall. Setze Umgebungsvariablen, die ausführliche Ausgaben von Abhängigkeiten aktivieren. Starte die ausführbare Datei aus einem Verzeichnis ohne ihre optionale Konfiguration. Erzwinge einen behebbaren Startfehler. Führe die erste Anfrage nach einem Start im Leerlauf aus. Prüfe anschließend jedes Mal das rohe Transkript.

Schalte nicht alle Protokolle stumm, damit der Test besteht. Damit verschiebst du das Betriebsproblem nur an eine andere Stelle. Leite Diagnosen an stderr, gib Betreibern eine ausdrückliche Möglichkeit, sie zu erfassen, und maskiere sensible Werte, bevor sie den Prozess verlassen. Protokolltreue und brauchbare Diagnosen lassen sich miteinander verbinden, solange die Datenströme getrennt bleiben.

Gültiges JSON beweist keine sichere Anfrage

Ein strikter JSON-Parser schützt die Rahmenstruktur. Er entscheidet nicht, ob eine Anfrage Zugangsdaten verwenden oder einen Host erreichen darf.

Halte diese Entscheidungen in der richtigen Reihenfolge. Zuerst liest der Transport einen Frame. Danach erzeugt der Parser einen JSON-Wert. Anschließend stellt der Protokollvalidator die Form der JSON-RPC- und MCP-Methode fest. Der Schemavalidator prüft die Tool-Argumente. Erst danach sollte die Autorisierung entscheiden, ob die angeforderte Aktion ausgeführt werden darf. Der Executor sollte eine typisierte Aktion zusammen mit dieser Entscheidung erhalten, keinen rohen Methodennamen und kein Parameter-Dictionary.

Diese Reihenfolge verhindert einen subtilen Fehler: eine HTTP-Anfrage aufzubauen, während die Validierung noch läuft. Wenn eine spätere Prüfung den Aufruf ablehnt, eine Bibliothek aber bereits einen Host aufgelöst, eine Verbindung geöffnet oder eine Befehlsschablone erweitert hat, kann dein Test eine Ablehnung melden, obwohl die Grenze bereits Arbeit nach außen gegeben hat. Der Test mit dem Fake-Executor erkennt die offensichtliche Form. Ein lokaler Fake-HTTP-Dienst oder ein SSH-Testhelfer kann in Integrationstests versehentliche Netzwerkaktivität erkennen.

Eine abgelehnte Anfrage kann weiterhin in einen Audit-Eintrag gehören. Verwandle die Audit-Aufzeichnung jedoch nicht in einen Seitenkanal, der die Aktionsschicht aufruft. Zeichne die Ablehnung als Ablehnung mit einem Grund und einem Anfrage-Fingerabdruck auf, der keine Geheimnisse preisgibt. Halte das Journal externer Aktionen getrennt von einem versuchten Aufruf, der die Autorisierung nie überschritten hat.

Ein versehentliches print kann einen gefährlichen Fehler verbergen

Aktionen hinter Sallyport ausführen
Die integrierte sp-mcp-Schnittstelle leitet HTTP- und SSH-Aktionen über Sallyport statt über den Agenten.

Stell dir ein Gateway vor, das ein Tool namens deploy-preview unterstützt. Sein Handler validiert einige Argumente, beginnt mit dem Aufbau einer ausgehenden Anfrage und prüft anschließend, ob der Aufrufer die ausgewählten Zugangsdaten verwenden darf. Während einer Überarbeitung fügt ein Entwickler ein stdout-print ein, um das ausgewählte Ziel zu prüfen.

Ein strikter MCP-Client sieht die Ausgabe, kann sie nicht als JSON-RPC parsen und trennt die Verbindung. Der Entwickler sieht einen Protokollfehler und entfernt das print. Das ist ärgerlich, aber sicher.

Ein großzügiger Client überspringt die Zeile, empfängt eine Fehlerantwort und teilt dem Betreiber mit, dass die Autorisierung die Anfrage abgelehnt hat. Inzwischen hat der Handler die vorbereitete Anfrage bereits an einen HTTP-Helfer mit Wiederholungslogik übergeben, bevor die Autorisierungsprüfung abgeschlossen war. Der entfernte Dienst erhält die Anfrage ohne brauchbare Zugangsdaten, gibt möglicherweise einen Fehler zurück und hinterlässt dem Team eine irreführende Audit-Spur: Es sieht so aus, als sei der Agent abgelehnt worden, aber der Dienst hat trotzdem Aktivität gesehen.

Das Banner hat den Fehler in der Reihenfolge der Autorisierungsprüfungen nicht verursacht. Es hat gezeigt, warum eine großzügige Transportwiederherstellung und der frühe Aufbau einer Aktion ein schlechtes Paar sind. Die Lösung ist keine intelligentere Regel zum Überspringen von Zeilen. Verschiebe die Autorisierungsentscheidung vor den Aufbau der Anfrage, beweise das mit dem Rekorder und halte stdout so strikt, dass eine versehentliche Ausgabe den Test sofort scheitern lässt.

Mache den Vertrag für die Leitung zur Release-Schranke

Platziere neben jedem paketierten Gateway-Befehl einen Test für sauberes stdout und führe ihn in der kontinuierlichen Integration aus. Die Fehlermeldung sollte die ersten fehlerhaften Bytes, den Startbefehl und stderr als separate Anlage enthalten. Ein Maintainer sollte den Fehler reproduzieren können, ohne eine Agentenunterhaltung rekonstruieren zu müssen.

Halte eine kleine Sammlung fehlerhafter eingehender Frames unter Versionskontrolle. Füge jedes Mal einen Fall hinzu, wenn ein echter Fehler auftritt. Widerstehe dem Drang, eine neue Besonderheit zu akzeptieren, nur weil ein Client sie ausgegeben hat. Wenn eine Implementierung fehlerhaften Datenverkehr sendet, korrigiere die Implementierung oder dokumentiere eine versionierte Kompatibilitätsgrenze, die keine Aktionen erlaubt.

Sallyport führt HTTP- und SSH-Ausführungen über seine integrierte sp mcp-stdio-Schnittstelle aus. Deshalb gehört dieses Harness an die Grenze dieser Schnittstelle und sollte prüfen, dass ein zurückgewiesener Frame keinen externen Aufruf auslösen kann. Dieselbe Disziplin gilt für jeden MCP-Server, der mehr tun kann, als Text zurückzugeben.

Das Release-Kriterium ist klar: stdout enthält ausschließlich vollständige Protokollnachrichten, und eine abgelehnte Eingabe hinterlässt im Aktionsrekorder keine Spur. Wenn eine der beiden Prüfungen fehlschlägt, ist der Build noch nicht bereit, Agentenverkehr zu verarbeiten.

FAQ

Darf ein MCP-Server eine Startmeldung an stdout ausgeben?

Nein. Beim stdio-Transport überträgt stdout Protokoll-Frames. Ein Banner hat deshalb denselben Status wie beschädigtes JSON. Sende Diagnosen an stderr oder an eine separate strukturierte Protokollsenke.

Wie sollte ein MCP-Gateway mit fehlerhaftem JSON umgehen?

Behandle sie als Fehler an der Transportgrenze und stelle sicher, dass keine Aktion den Executor erreicht hat. Ob der Prozess vor dem Schließen noch einen JSON-RPC-Parsefehler zurückgibt, ist eine Produktentscheidung. Er darf jedoch niemals raten, was die Zeile bedeutet hat.

Welcher JSON-RPC-Fehler gilt für ungültiges JSON?

JSON-RPC 2.0 definiert den Fehlercode -32700 für ungültiges JSON. Dieser Fehler beschreibt das Nachrichtenproblem. Er beweist nicht, dass deine Implementierung den nachgelagerten HTTP- oder SSH-Vorgang gestoppt hat. Deshalb gehören Wirkungsprüfungen in den Test.

Reicht gültiges JSON aus, um einen MCP-Tool-Aufruf zu erlauben?

Nein. Eine Nachricht kann sich korrekt parsen lassen und trotzdem die falsche Protokollversion, eine nicht unterstützte Methode, eine ungültige Anfrage-ID oder Argumente enthalten, die nicht zum Aktionsschema passen. Parserprüfung und Aktionsautorisierung sind getrennte Prüfungen.

Soll ich MCP stdio mit Unit-Tests oder Integrationstests testen?

Prüfe den vollständigen Byte-Pfad in Integrationstests und verwende Unit-Tests, um Parser und Aktionsgrenze kostengünstig abzudecken. Unit-Tests allein übersehen oft eine Bibliothek, einen Wrapper oder ein Shell-Profil, das vor dem Start deines Codes ein Banner ausgibt.

Wie erkenne ich stdout-Ausgaben, die nur in der Produktion erscheinen?

Führe den Server mit derselben Umgebung, demselben Shell-Wrapper, derselben Paketierung und demselben Startbefehl wie in der Produktion aus. Ein sauberer lokaler Terminaltest beweist wenig, wenn der installierte Build eine Meldung, Warnung oder Fortschrittszeile ergänzt.

Ist stderr für MCP-Serverprotokolle sicher?

stderr ist vom stdio-Protokollstrom getrennt und daher das übliche Ziel für Protokolle und Diagnosen. Halte stderr für Betreiber verfügbar, aber verhindere, dass Prozessüberwacher oder Test-Runner stderr mit stdout zusammenführen.

Was sollte ein MCP-stdio-Test-Harness prüfen?

Er sollte zwei Dinge beweisen: Jede stdout-Zeile hat eine zulässige JSON-RPC-Form, und jede zurückgewiesene Eingabe lässt den Aktionsrekorder leer. Eine reine Transkriptprüfung ohne Wirkungsprüfung kann den gefährlichen Teil übersehen.

Sollte ein MCP-Client versuchen, sich von einer unerwarteten stdout-Zeile zu erholen?

Lass Mehrdeutigkeit fehlschlagen. Parsefehler, zusätzliche Nicht-Leerraum-Bytes, doppelte Frames, wenn nur einer erwartet wird, und unvollständige Eingaben sollten die Sitzung oder die Anfrage beenden, bevor der Executor einen Aufruf erhält.

Wie kann ich abgelehnte MCP-Aufrufe testen, ohne echte Zugangsdaten zu riskieren?

Verwende einen lokalen Fake-Endpunkt oder einen Rekorder, der keine echten Dienste kontaktieren kann, und prüfe dann, dass seine Aufrufliste leer bleibt. Teste fehlerhafte Eingaben nicht mit echten Zugangsdaten, nur weil die Aktion eigentlich zurückgewiesen werden sollte.

Sallyport

Sallyport führt API-Aufrufe und SSH-Befehle für Ihren KI-Agenten aus. Die Schlüssel bleiben in einem lokalen Tresor auf Ihrem Mac; Sie geben jeden Lauf frei, und jede Aktion landet in einem versiegelten Journal.

© 2026 Sallyport · Open Source unter Apache-2.0 · Oleg Sotnikov