Skip to main content
Glama

aiMCPGate

Русская версия — README_RU.md.

Ein Gateway / Proxy für MCP-Server (Model Context Protocol), geschrieben in Go. Es präsentiert sich einem MCP-Client (Claude Code, Cursor usw.) als ein MCP-Server, während es unter der Haube Aufrufe über mehrere Upstream-MCP-Server multiplexed, deren Tools, Prompts und Ressourcen in einen Katalog aggregiert und jeden Aufruf protokolliert.

Status: MVP abgeschlossen (Stufen 0–6) + Post-MVP-Stufen 7–18 ausgeliefert, neueste Version v0.5.0. Phase 1 — Multiplexing von stdio-Upstreams hinter einem stdio-Endpunkt mit Aufrufprotokoll; Phase 2 — HTTP/SSE-Client-seitiger Transport, HTTP-Upstreams, ein CLI-Protokollbetrachter (mcp-gate logs); Release-Pipeline (goreleaser, cross-kompiliert für linux/darwin/windows × amd64/arm64, ohne CGO). Post-MVP hinzugefügt: automatischer Neustart von Upstreams, Hot-Config-Reload, Tool-Filterung/-Umbenennung, doctor und — in v0.3.0 — vollständige prompts/resources/resources/templates/completion- Aggregation, ping, Fortschrittsweiterleitung und echte Abbrüche, logging/setLevel-Fan-out, pro-Upstream-Aufruflimits (Rate-Limit / Parallelität / Ergebnisabschneidung / Timeout), ein fauler Katalog und tools/list-Paginierung sowie SSE-Server→Client-Streams sowohl auf der Client- als auch auf der Upstream-Seite. v0.4.0 vervollständigte die Server→Client-Richtung: alle drei serverinitiierten Methoden — elicitation/create, sampling/createMessage und roots/list — werden in allen vier Transportkombinationen (stdio oder HTTP auf der Client-Seite × stdio oder HTTP auf der Upstream-Seite) proxied; das Gateway deklariert nun gegenüber einem Upstream genau die Fähigkeiten, die sein eigener Client deklariert hat, statt eines pauschalen {}; der HTTP-Transport erhielt serverseitige Mcp-Session-Id-Sitzungen mit DELETE /mcp-Beendigung. v0.5.0 fügt Operator-Beobachtbarkeit hinzu (Stufe 18): acht Ereignisarten — Upstream-Startfehler und Supervisor-Aufgaben, verworfene Benachrichtigungen und Server→Client-Anfragen, ein HTTP-Upstream ohne GET-SSE, Katalogkollisionen und ungültige URI-Templates sowie ein Ergebnis, das max_result_bytes stillschweigend umging — landen jetzt im Aufrufjournal (mcp-gate logs) statt auf stderr, das ein MCP-Client normalerweise besitzt; die Konfigurationsanalyse wurde streng (unbekannte/falsch geschriebene Schlüssel sind fatal). Es schließt auch die clientseitige Hälfte der Guard-/Abschneidungs-Geschichte: Ein von der Rate-Limit- oder Parallelitäts-Guard abgelehnter tools/call gibt jetzt seinen eigenen JSON-RPC-Fehlercode -32029 mit maschinenlesbarem data: {"retryable":true,"reason":...} zurück, statt eines nicht unterscheidbaren -32603, und ein nicht-textuelles Ergebnis, das max_result_bytes umging, trägt einen result._meta-Marker (content bleibt byte-für-byte unverändert). Schließlich verweigert auth_token, das auf eine nicht gesetzte Umgebungsvariable verweist, jetzt den Start des Gateways, statt die HTTP-Authentifizierung stillschweigend zu deaktivieren.

Upgrade auf v0.5.0 — drei Verhaltensänderungen, keine betrifft das Konfigurationsdateiformat selbst:

  • Die Konfigurationsanalyse ist jetzt streng. Eine Konfiguration mit einem unbekannten oder falsch geschriebenen Top-Level- oder Pro-Upstream-Schlüssel, die früher stillschweigend ignoriert wurde, schlägt jetzt beim Laden fehl. Korrigieren Sie den Schlüsselnamen (der Fehler nennt ihn) oder entfernen Sie ihn.

  • auth_token: ${VAR} mit nicht gesetztem VAR verweigert jetzt den Start und nennt die Variable. Vorher wurde es stillschweigend zu einem leeren Token — was bei einem HTTP-Gateway die Bearer-Prüfung vollständig deaktivierte, ohne Warnung. Setzen Sie die Variable (oder übergeben Sie --env-file) oder entfernen Sie auth_token, um absichtlich ohne Authentifizierung zu laufen.

  • Das Aufrufjournal (log_file / calls.jsonl) erhielt eine zweite Datensatzart, "kind":"event", neben den vorhandenen Aufrufdatensätzen. Ein Binärprogramm mit v0.4.0 oder älter, das ein v0.5.0-Journal liest, rendert eine Ereigniszeile als spärlichen ERR-Eintrag, statt zu scheitern — lesen Sie ein Journal mit demselben oder einem neueren Binärprogramm als dem, das es geschrieben hat.

Upgrade auf v0.4.0: keine Konfigurationsdateiänderung, aber zwei beobachtbare HTTP-Modus-Verhaltensänderungen — eine Sitzungs-ID ist jetzt nach initialize auf POST /mcp obligatorisch (der Header wird von der initialize-Antwort zurückgegeben), und die Upstream-Registry startet verzögert bei der ersten echten MCP-Anfrage statt beim Prozessstart.

Nicht implementiert: eine pro-Client-Zugriffsrichtlinie.

Releases

Plattformübergreifende Binärdateien werden über goreleaser (.goreleaser.yaml) erstellt: linux/darwin/windows × amd64/arm64, ohne CGO, die Version wird über -ldflags -X main.version=... eingebettet, Prüfsummen landen in SHA256SUMS. Lokaler Trockenlauf: goreleaser release --snapshot --clean.

Related MCP server: mcpstead

Installation aus der MCP-Registry

Neben den rohen Release-Binärdateien wird das Gateway als OCI-Image auf GitHub Container Registry und als npm-Wrapper-Paket ausgeliefert — die beiden Formate, aus denen MCP-Registries installieren.

Docker:

docker run --rm -i -v $(pwd)/config.yaml:/config.yaml ghcr.io/akomyagin/aimcpgate serve

-i ist obligatorisch: Das Gateway spricht MCP über stdio, daher muss der Client stdin offen halten (ohne das sieht der Container EOF und beendet sich sofort). Das Image hat keine eigene Konfiguration, also mounten Sie Ihre — das Beispiel oben mountet sie auf den Standardpfad /config.yaml; jeder andere Pfad funktioniert mit serve -c.

Um eine Registry-Sandbox-Prüfung (Glama.ai usw.) ohne echten Upstream zu reproduzieren, verwenden Sie die im Image eingebettete Demo-Konfiguration — dieser genaue Befehl ist das, was eine Sandbox ausführen sollte:

docker run --rm -i ghcr.io/akomyagin/aimcpgate serve -c /demo.config.yaml

npx (lädt beim ersten Installieren die vorkompilierte Binärdatei für Ihre Plattform herunter und verifiziert ihre SHA256-Prüfsumme):

npx aimcpgate serve -c ./config.yaml

Image-Richtlinie: Das OCI-Image enthält nur die mcp-gate-Binärdatei — keine Laufzeiten für stdio-Upstreams (kein node/npx, python, Shells). Wenn Ihre Konfiguration stdio-Upstream-Server startet, erweitern Sie das Image selbst und installieren Sie, was sie benötigen; HTTP-Upstreams funktionieren sofort (CA- Zertifikate sind enthalten).

Demo-Konfiguration: demo.config.yaml und der versteckte __demo-echo-Unterbefehl existieren nur, damit Registry-Sandboxes (Glama.ai) das Gateway ohne echten Upstream untersuchen können — verwenden Sie sie niemals in einer echten Bereitstellung.

CLI-Befehle in einem Container ausführen

doctor, catalog, call und logs sind die Art, wie ein Operator eine Bereitstellung inspiziert. Drei Fakten bestimmen, wie sie in einem Container aufgerufen werden müssen:

  1. Die Binärdatei ist /mcp-gate und NICHT in $PATH. Das Dockerfile macht COPY mcp-gate /mcp-gate und ENTRYPOINT ["/mcp-gate"] — nichts setzt sie auf einen Suchpfad (prüfen Sie das Dockerfile, falls das jemals falsch aussieht). Also schlägt die offensichtliche Form fehl:

    $ docker exec mcp-gate mcp-gate catalog -c /config.yaml
    OCI runtime exec failed: exec failed: unable to start container process: exec: "mcp-gate": executable file not found in $PATH

    Verwenden Sie stattdessen den absoluten Pfad — das ist der einzige Unterschied.

  2. Das Image ist distroless, es gibt also überhaupt keine Shell. Die Basis ist gcr.io/distroless/static-debian12:nonroot, die die Binärdatei und CA- Zertifikate und sonst nichts enthält. docker exec mcp-gate sh -c '…' schlägt fehl, weil sh einfach nicht da ist, und es gibt kein ls/cat, um sich umzusehen. Halten Sie Pipes, Globbing und Umleitungen auf der HOST-Seite des Befehls.

  3. docker exec startet einen NEUEN Prozess; es fragt nicht den laufenden serve ab. doctor, catalog und call bauen ihre eigene Registry, öffnen ihre eigenen Verbindungen zu den Upstreams, melden und beenden. Ihre Ausgabe ist daher die Upstream-Erreichbarkeit jetzt, nicht der Zustand des Live-Gateways: Wenn der laufende Prozess einen Upstream verloren und aus seinem Katalog entfernt hat, zeigen diese Befehle das nicht. Sie halten auch das Aufrufjournal sauber — sie laufen mit deaktiviertem Journaling, sodass ein call, den Sie auf diese Weise machen, nicht in logs erscheint.

docker exec mcp-gate /mcp-gate version
docker exec mcp-gate /mcp-gate doctor  -c /config.yaml
docker exec mcp-gate /mcp-gate catalog -c /config.yaml
docker exec mcp-gate /mcp-gate call demo__echo '{"text":"hi"}' -c /config.yaml
docker exec mcp-gate /mcp-gate logs    -c /config.yaml --tail 50

Die Befehle setzen einen Container voraus, der abgetrennt und benannt gestartet wurde, z. B. docker run -d --name mcp-gate … — im Gegensatz zum Vordergrund-docker run --rm -i …-Beispiel oben, das sofort endet, sobald sein stdio-Client die Verbindung trennt, und nichts für docker exec hinterlässt. Die Konfiguration wird als auf dem Standardpfad /config.yaml gemountet angenommen, wie in demselben Beispiel; demo__echo steht für ein Tool aus Ihrem eigenen Katalog. Ein paar Einschränkungen:

  • logs ist die Ausnahme zu Fakt 3: Es liest die Journal-DATEI, die das laufende Gateway schreibt, spiegelt also den Live-Prozess wider. Das erfordert, dass log_file in der gemounteten Konfiguration auf einen Pfad zeigt, der innerhalb des Containers sichtbar ist, und dass dort ein Volume gemountet ist — sonst geht das Journal an den stderr des Containers (d. h. an docker logs) und mcp-gate logs hat nichts zu lesen. -c sagt ihm, wo das Journal ist; --file überschreibt es.

  • Das betrifft wirklich den HTTP-Modus. Im stdio-Modus startet und besitzt der MCP-Client den Container, sodass es normalerweise keinen langlebigen Container gibt, in den man exec könnte. Ein Gateway, das Sie inspizieren können, ist eines, das separat gestartet wurde (docker run -d --name mcp-gate …) mit transport: http.

  • Der HTTP-Modus benötigt eine nicht standardmäßige listen_addr. Der Standard ist 127.0.0.1:28080 — Loopback IM Container, vom Host aus auch mit -p nicht erreichbar. Setzen Sie listen_addr: 0.0.0.0:<port> in der Konfiguration; das Gateway weigert sich dann, ohne auth_token zu starten, absichtlich ("der HTTP-Endpunkt wäre ohne Authentifizierung aus dem Netz erreichbar").

Warum

Ein aktiver MCP-Benutzer hat typischerweise mehrere Server konfiguriert (Dateisystem, GitHub, Suche, eigene), jeder in der eigenen Konfiguration jedes Clients dupliziert. aiMCPGate bietet Ihnen:

  • Einen Einstiegspunkt — einen einzigen MCP-Endpunkt statt N Einträgen in der Client-Konfiguration.

  • Einen Katalog — die Tools und Prompts jedes Upstream-Servers zusammengeführt (namensräumlich als <upstream>__<tool>, sodass Namen nie kollidieren), plus deren Ressourcen und Ressourcen-Templates (per URI adressiert, also nie umbenannt).

  • Ein Aufrufprotokoll — welcher Upstream, welches Tool, wann, Erfolg/Fehler. Das ist der Mehrwert über "nur ein Proxy" hinaus.

Solo-Hobbyprojekt: Die Priorität ist das Lernen von Go (Nebenläufigkeit, os/exec, JSON-RPC 2.0, die stdio- und HTTP/SSE-Transports). Kosten — $0/Monat standardmäßig (ein lokaler Prozess), keine Telemetrie.

So funktioniert es (Kurzversion)

MCP client ──stdio/HTTP──▶ aiMCPGate ──JSON-RPC──▶ upstream A (stdio)
                              │        ├─────────▶ upstream B (stdio)
                          call log     └─────────▶ upstream C (http, Phase 2)

MVP (zwei Phasen)

  • Phase 1 — Multiplexing von 2+ stdio-Upstreams hinter einem stdio- Endpunkt (derselbe Transport, den Claude Code spricht) plus grundlegendes Protokollieren.

  • Phase 2 — HTTP/SSE-Transport, HTTP-Upstream-Server, ein Protokollbetrachter (der CLI wurde gebaut; die Web-Ansicht wurde bewusst verworfen), optional eine Zugriffsrichtlinie — die wurde erwogen und abgelehnt.

Build

export PATH="$HOME/sdk/go/bin:$PATH"   # if go isn't already on PATH
go build ./...
go vet ./...
go test -race ./...

go run ./cmd version

Verwendung

# stdio mode (the client launches the gateway as a subprocess):
mcp-gate serve --config ./config.yaml

# http mode (transport: http in the config) — endpoint at http://<listen_addr>/mcp;
# every request after initialize carries the issued Mcp-Session-Id (see below):
mcp-gate serve --config ./config-http.yaml

# check every enabled upstream once (launch → handshake → tools/list) and print
# a per-upstream OK/FAIL table; exit code is non-zero if any upstream failed
# (scriptable for CI/cron), no auto-restart, no call logging — one pass then exit.
# It also WARNs (POSIX only) if config.yaml or --env-file is readable by
# group/others — either may hold a secret literal, and nothing else checks:
mcp-gate doctor --config ./config.yaml

# call one aggregated tool once from the shell (single bring-up, no supervisor —
# the fastest way to debug a config, a filter or a rename without a live client):
mcp-gate call github__search_repositories '{"query":"mcp"}' --config ./config.yaml

# report the aggregated catalog size per upstream (tools / bytes / ~tokens) plus
# the heaviest individual tools — the data behind allow-list / strip decisions:
mcp-gate catalog --config ./config.yaml --top 20

# view the journal — tool calls AND operator events (last 50 lines; filter by
# upstream/tool/status):
mcp-gate logs --file ./logs/calls.jsonl --tail 50
mcp-gate logs --config ./config.yaml --upstream github --status err
# show ONLY the operator events (see "Operator events" below):
mcp-gate logs --config ./config.yaml --events
# keep watching the log as it grows, or aggregate it instead of listing records
# (--follow and --stats are mutually exclusive):
mcp-gate logs --config ./config.yaml --follow
mcp-gate logs --config ./config.yaml --stats

# generate a random auth token (for the HTTP transport) and see how to wire it in:
mcp-gate token --generate
# print the auth token currently set in the config:
mcp-gate token --config ./config-http.yaml

# print ready-to-paste MCP client config snippets (Claude Code / Cursor / Claude
# Desktop) for whichever transport the config uses: a launch command for stdio, or
# the endpoint URL plus the Bearer header (when auth_token is set) for http:
mcp-gate client-config --config ./config.yaml

# print a SKILL.md teaching an agent how to use the aggregated catalog
# (built-in text by default; overridable via skill_file in the config):
mcp-gate skill > .claude/skills/mcp-gate/SKILL.md

# shell completions (cobra's built-in command; the release archives also ship
# pre-generated ones):
mcp-gate completion bash > /etc/bash_completion.d/mcp-gate

Alle Befehle außer token --generate, completion und skill (das auf einen eingebauten Leitfaden zurückfällt) laden die Konfiguration: übergeben Sie --config oder legen Sie eine config.yaml neben der Binärdatei ab (siehe Konfiguration unten).

serve, doctor, call und catalog akzeptieren auch --env-file ./.env — einen minimalen KEY=VALUE-Parser, der vor dem Laden der Konfiguration angewendet wird, sodass ${VAR}-Referenzen in der Konfiguration aus dieser Datei aufgelöst werden. Die echte Prozessumgebung gewinnt immer über die Datei.

HTTP-Sitzungen (Mcp-Session-Id)

Im HTTP-Modus führt das Gateway Streamable-HTTP-Sitzungen: Die Antwort auf initialize trägt einen Mcp-Session-Id-Header, und jede Anfrage danach — POST, der GET-SSE-Stream, DELETE — muss diesen Header zurücksenden. Ohne ihn ist die Antwort 400; mit einer unbekannten oder abgelaufenen ID 404, was dem Client sagt, erneut zu initialize. Eine Sitzung wird durch DELETE /mcp (204) freigegeben oder nach 30 Minuten ohne Anfragen — ein offener SSE-Stream zählt als Aktivität und hält sie am Leben.

MCP-Clients erledigen das alles für Sie. Für handgemachte curl-Aufrufe nehmen Sie den Header aus der initialize-Antwort und senden ihn zurück:

SID=$(curl -sD - -o /dev/null -X POST http://127.0.0.1:28080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
  | tr -d '\r' | awk -F': ' '/^[Mm]cp-[Ss]ession-[Ii]d/{print $2}')

curl -s -X POST http://127.0.0.1:28080/mcp \
  -H 'Content-Type: application/json' -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

curl -s -X DELETE http://127.0.0.1:28080/mcp -H "Mcp-Session-Id: $SID"

Die Sitzung macht auch das Aufrufprotokoll ehrlich: Jeder Aufruf wird unter der clientInfo der Sitzung geprüft, die ihn gemacht hat, sodass mehrere HTTP- Clients in calls.jsonl unterschieden werden, statt sich ein leeres client-Feld zu teilen.

Server→Client-Anfragen über HTTP (elicitation, sampling, roots)

Wenn ein Upstream mitten im Gespräch etwas anfragt — elicitation/create, sampling/createMessage, roots/list — wird die Frage als SSE-Ereignis auf dem GET /mcp-Stream einer Sitzung zugestellt, und der Client antwortet mit einem gewöhnlichen POST, der eine JSON-RPC-Antwort mit derselben ID und derselben Mcp-Session-Id trägt. Nur die Sitzung, an die die Frage gerichtet war, darf sie beantworten; eine Antwort von einer anderen Sitzung wird ignoriert. Wenn niemand die Fähigkeit mit einem offenen Stream deklariert hat, wird der Upstream sofort in der vom Spezifikation vorgeschriebenen Form abgelehnt ({"action":"decline"} für Elicitation, -32601 für die anderen beiden), anstatt auf einen Timeout zu warten — und dasselbe geschieht, wenn die Sitzung beendet wird, während eine Frage aussteht.

Drei Konsequenzen, die man kennen sollte:

  • Den Upstreams werden die Fähigkeiten des ERSTEN Clients mitgeteilt, der initialisiert, und dieser Satz ist für die Lebensdauer des Prozesses festgelegt. MCP 2025-06-18 kennt keine Neuverhandlung, sodass ein zweiter Client, der mehr deklariert, bereits erfolgte Handshakes nicht ändern kann — einem Upstream wird niemals eine Fähigkeit im Namen eines Clients versprochen, von dem er nicht wusste.

  • Die Upstreams starten bei der ersten Anfrage, die sie benötigt, nicht wenn das Gateway seinen Port bindet. Das ist es, was die obige Deklaration überhaupt möglich macht: Der Handshake muss stattfinden, nachdem ein Client gesagt hat, was er unterstützt. Wenn die Upstreams nicht starten können, erhält der Client ein JSON-RPC -32603 und das Gateway beendet sich mit dem Fehler, wie es auch bei früherem eifrigem Start getan hätte.

  • Die Frage geht an einen Client, der die Fähigkeit deklariert hat — nicht unbedingt an den, dessen Aufruf sie ausgelöst hat. Das Routing erfolgt nach deklarierter Fähigkeit, und unter den passenden Sitzungen gewinnt die zuletzt aktive; eine Upstream-Anfrage trägt nichts, das angibt, zu welchem Aufrufer sie gehört. Bei einem einzelnen Client (der Normalfall) ist das unsichtbar, aber wenn man zwei betreibt, kann ein Formular, das durch tools/call eines Clients ausgelöst wurde, in der Benutzeroberfläche des anderen erscheinen.

Die Upstream-Seite desselben Austauschs funktioniert ebenfalls über HTTP: Ein entfernter MCP-Server, der mit url: erreicht wird, kann seine Frage als SSE-Frame stellen — entweder auf seinem langlebigen GET-Stream oder eingefügt in den Stream, der eine der eigenen POSTs des Gateways beantwortet, wo SDK-Server ein elicitation/create platzieren, das innerhalb eines tools/call ausgelöst wurde. Das Gateway leitet es durch dieselbe Pipeline und sendet die Antwort des Clients als einen gewöhnlichen POST zurück, der eine JSON-RPC-Antwort unter der eigenen Anfrage-ID des Servers trägt. Ein solcher Upstream erhält die Client-Fähigkeiten des Gateways nach derselben ehrlichen Richtlinie wie ein stdio-Upstream — eine Fähigkeit wird nur angeboten, wenn der eigene Client des Gateways sie deklariert hat, und doctor/call/catalog, die überhaupt keinen Client haben, deklarieren weiterhin genau {}. Der Antwort-POST wird nicht wiederholt: Ein Upstream, der sie nicht erhält, fällt auf seinen eigenen Timeout zurück.

Operator-Ereignisse im Journal

Das Journal unter log_file enthält zwei Arten von Zeilen: eine pro Tool-Aufruf und eine pro Operator-Ereignis — einen Gateway-Zustand, den man sonst nie erfahren würde. Im stdio-Modus besitzt der MCP-Client das Terminal, sodass der stderr des Gateways für Sie unsichtbar ist, und mehrere dieser Bedingungen wurden bisher nur auf Debug-Ebene protokolliert. Sie werden jetzt in dieselbe Datei geschrieben, die mcp-gate logs liest:

Ereignis

Bedeutung

upstream_start_failed

Ein Upstream kam nie hoch; seine Tools fehlen im Katalog.

upstream_gave_up

Der Supervisor hat aufgehört, einen Upstream neu zu starten (Versuche erschöpft, Neustart durch Reload deaktiviert oder kein Liveness-Kanal) und ihn aus dem Katalog entfernt.

notification_dropped

Der Puffer eines Abonnenten war voll, sodass eine weitergeleitete Benachrichtigung verworfen wurde — Weiterleitung ist von Natur aus nicht blockierend.

server_request_dropped

Ein Upstream fragte etwas, das nur der Client beantworten konnte (elicitation/sampling/roots), und kein Transport nahm die Frage an, sodass der Tool-Aufruf in seinem Namen abgelehnt wurde.

sse_stream_unavailable

Ein HTTP-Upstream bietet keinen GET-SSE-Stream an, sodass tools/list_changed von ihm erst nach einem Neustart des Gateways eintrifft.

catalog_collision

Zwei Einträge beanspruchten denselben clientseitigen Tool-/Prompt-Namen oder dieselbe Ressourcen-URI; der erste gewinnt und der Verlierer bleibt dem Client verborgen.

catalog_bad_template

Eine Ressourcen-URI-Vorlage lässt sich nicht kompilieren: Sie wird dem Client aufgelistet, kann aber nie einem Lesevorgang entsprechen.

result_truncation_skipped

Ein Ergebnis überschritt max_result_bytes, hatte aber keinen kürzbaren Text (z. B. nur Bilder), sodass es vollständig durchging.

Ereignisse erscheinen inline mit Aufrufen, markiert mit EVT; mcp-gate logs --events zeigt nur sie, und --stats erhält eine Tabelle pro Ereignis. --tool und --status sind nur-Aufruf-Filter, sodass Ereignisse ausgeschlossen sind, während einer von beiden gesetzt ist (--upstream gilt für beide). Eine Konsequenz, die man kennen sollte: notification_dropped nennt keinen Upstream — ein Verwerfen ist eine Eigenschaft des Abonnenten, dessen Puffer voll war, nicht des Absenders der Benachrichtigung — sodass --upstream X es nie zeigt. Suchen Sie danach ohne diesen Filter. Wiederholte Verwerfungen werden zusammengefasst — das erste wird sofort geschrieben, weitere innerhalb einer Minute werden in das count= der nächsten Zeile für diesen Schlüssel gezählt, und der Rest wird beim Herunterfahren geleert. Eine Zeile, die einen solchen Rückstand trägt, sagt dies in ihrem detail= aus und nennt die Zeit des ältesten Ereignisses, das sie einbezieht — der eigene Zeitstempel der Zeile ist das neueste, sodass die beiden zusammen die Zeitspanne begrenzen, in der der Ausbruch tatsächlich stattfand.

Zwei praktische Hinweise:

  • Setzen Sie log_file. Wenn es leer ist, geht das Journal an stderr, das im stdio- Modus dem MCP-Client gehört — die Ereignisse würden dort geschrieben, wo Sie sie nicht sehen können.

  • Lesen Sie ein Journal mit derselben (oder einer neueren) Binärdatei, die es geschrieben hat. Ereignisse tragen ein "kind"-Feld, das ältere Versionen nicht kennen, sodass mcp-gate logs von ≤ v0.4.0 sie als spärliche, meist leere Datensätze darstellt.

Nichts davon ist für den MCP-Client sichtbar: keine Fehlercodes, Ergebnistexte oder Fähigkeiten wurden geändert — die Ereignisse gehen nur ins Journal.

Ein Aufruf, den das Gateway nicht routen konnte, ist kein Ereignis — es ist eine gewöhnliche fehlgeschlagene CALL-Zeile. Ein Client, der einen Tool-Namen anfragt, den kein Upstream bereitstellt, erhält einen CallRecord wie jeden anderen, wobei seine upstream-Spalte auf den Sentinel (unrouted) gesetzt ist; mcp-gate logs --upstream '(unrouted)' wählt genau diese Zeilen aus und sonst nichts. Ein zweiter, unterschiedlicher Fall sieht fast gleich aus, nennt aber einen echten Upstream: Die Route existiert (das Tool ist im Katalog), aber die Verbindung des Upstreams ist weg (wird neu gestartet oder verworfen) — diese Zeile trägt den echten Upstream-Namen, also filtern Sie danach mit --upstream <name> wie üblich statt mit dem Sentinel.

Konfiguration neu laden (SIGHUP)

Das Gateway lädt seine Konfiguration live bei SIGHUP neu — kein Neustart, keine unterbrochene Client-Verbindung. Bearbeiten Sie config.yaml und senden Sie das Signal:

kill -HUP $(pgrep -f 'mcp-gate serve')

Beim Neuladen vergleicht das Gateway die neue Konfiguration mit den laufenden Upstreams und wendet die minimale Änderung an: neu hinzugefügte Upstreams werden gestartet, entfernte (oder enabled: false) werden heruntergefahren, Upstreams, deren Startfelder (command/args/url/env/headers) geändert wurden, werden neu gestartet, und Upstreams, bei denen sich nur der Tool-Filter geändert hat (allow/deny/rename oder die Katalog-Projektionsregeln strip_annotations/strip_output_schema/max_description/ describe) werden neu projiziert ohne Neustart. Aufrufbegrenzungen (rate_limit, max_concurrent, max_result_bytes, call_timeout — global oder pro Upstream) werden ebenfalls live angewendet: Sie erfordern nie einen Neustart, der nächste Aufruf verwendet einfach die neuen Werte. Unveränderte Upstreams laufen unberührt weiter. Eine fehlerhafte Bearbeitung (ungültiges YAML, fehlgeschlagene Validierung) wird protokolliert und ignoriert — die aktuell laufende Konfiguration bleibt aktiv, sodass ein Tippfehler das Gateway nie herunterfährt.

Verhaltenshinweis: Da das Gateway einen SIGHUP-Handler installiert, beendet SIGHUP den Prozess nicht mehr wie der OS-Standard. Zum Stoppen des Gateways verwenden Sie Ctrl-C, SIGINT oder SIGTERM.

SIGHUP ist nur unter Unix verfügbar. Unter Windows — oder überall dort, wo Sie lieber keine Signale senden möchten — verwenden Sie stattdessen die optionale Polling-Alternative:

mcp-gate serve --config ./config.yaml --watch-config        # bare flag = poll every 2s
mcp-gate serve --config ./config.yaml --watch-config=10s    # note the "=", not a space

Es erstellt einen Fingerabdruck der Konfigurationsdatei in diesem Intervall und wendet denselben Neuladepfad an, den SIGHUP nimmt. Die Ausführung neben dem SIGHUP-Handler ist sicher.

Der Watcher vergleicht mtime und Größe der Datei und wartet, bis sich dieser Fingerabdruck beim nächsten Tick wiederholt, bevor er die Datei liest. Das ist es, was einen Zwei-Schritt-Speichervorgang (leeren, dann füllen) in der Praxis sicher macht: Ein Schreiber muss die Datei länger als ein volles Polling-Intervall in einem halbgeschriebenen Zustand halten, um die Prüfung zu täuschen. Der Preis ist Latenz — ein Neuladen erfolgt innerhalb von bis zu zwei Polling-Intervallen (bis zu 4s beim Standard von 2s).

Bei stdio kommen die Upstreams bei der ersten Anfrage des Clients hoch, sodass eine Bearbeitung, die vor dem Verbinden eines Clients vorgenommen wurde, noch nicht angewendet werden kann. Der Watcher behält diese Bearbeitung und versucht sie bei jedem Poll erneut, bis das Gateway hochgefahren ist, und wendet sie dann an — Sie müssen die Datei nie ein zweites Mal speichern, um sie wirksam zu machen. Eine Bearbeitung, die endgültig abgelehnt wird (nicht analysierbares YAML oder der unten stehende Schutz ohne upstreams), wird einmal gemeldet und nicht erneut versucht.

Als Sicherheitsnetz bei beiden Auslösern wird ein Neuladen, dessen neue Konfiguration keine upstreams deklariert, abgelehnt und protokolliert: Das ist die Signatur einer halbgeschriebenen Datei, und das Anwenden würde jeden laufenden Upstream herunterfahren. Um alle Upstreams absichtlich zu entfernen, starten Sie das Gateway neu. Ein explizites enabled: false ist davon nicht betroffen — das Deaktivieren des letzten Upstreams wird weiterhin angewendet.

Konfiguration

Ohne --config sucht das Gateway nach config.yaml neben seiner eigenen Binärdatei (z. B. wenn mcp-gate unter /etc/gate/ installiert ist, sucht es nach /etc/gate/config.yaml — unabhängig vom Arbeitsverzeichnis, aus dem es gestartet wurde). Wenn diese Datei nicht existiert und --config auch nicht übergeben wurde, meldet es einen expliziten Fehler, anstatt ein leeres Gateway zu starten. Relative Pfade innerhalb der Konfiguration (log_file, skill_file, debug_payload_log) werden relativ zum eigenen Verzeichnis der Konfigurationsdatei aufgelöst, nicht zum aktuellen Arbeitsverzeichnis.

Unbekannte Schlüssel sind ein Startfehler. Die Konfiguration wird streng geparst: Ein falsch geschriebener oder nicht erkannter Schlüssel stoppt das Gateway mit dem Schlüsselnamen und seiner Zeilennummer, anstatt wie früher stillschweigend ignoriert zu werden. Der konkrete Gewinn: Ein Tippfehler in enabled kann nicht mehr dazu führen, dass ein Upstream still weiterläuft. Benutzerdefinierte x- Notizschlüssel werden ebenfalls abgelehnt — um einen Block zu teilen, setzen Sie einen YAML-Anker auf den ersten echten Upstream und führen Sie ihn (<<: *anchor) in die anderen ein; Anker und Merge-Keys funktionieren wie üblich.

Ein Upstream ist standardmäßig aktiviert: Lassen Sie enabled: ganz weg und er wird wie jeder andere gestartet. Um einen aus dem Gateway herauszuhalten, ohne seine Konfiguration zu löschen, deaktivieren Sie ihn explizit mit enabled: false — er erscheint dann weder in tools/list noch in der Tabelle von mcp-gate doctor. Vorsicht: Ein wertloses enabled: (oder enabled: null) wird als weggelassen gelesen, sodass das Auskommentieren des Werts den Upstream weiterlaufen lässt — nur das wörtliche false deaktiviert ihn.

Hinweis: Die Suche "neben der Binärdatei" verwendet den Pfad der laufenden ausführbaren Datei. Unter go run ./cmd ... ist diese ausführbare Datei ein Wegwerf-Build in einem temporären Verzeichnis, sodass die Standard-Suche Ihre config.yaml nicht findet — übergeben Sie --config explizit, wenn Sie go run verwenden, oder führen Sie eine gebaute Binärdatei aus.

Vollständiges Beispiel mit allen Feldern — config.example.yaml. Die Gruppe der Upstream-Server wird in YAML deklariert; Geheimnisse (Tokens) laufen über env/.env ( ${VAR}-Expansion beim Laden), niemals in der Konfiguration eingecheckt. Jeder Upstream setzt genau eines von command (stdio-Subprozess) oder url (HTTP-Server, Streamable HTTP) — die Verbindungsart wird automatisch abgeleitet.

Nicht gesetzte ${VAR}-Referenzen verhalten sich je nach Feld unterschiedlich:

  • auth_token mit Verweis auf eine nicht gesetzte Variable bricht den Start ab und nennt die Variable — ein leeres auth_token würde die HTTP-Bearer-Prüfung stillschweigend deaktivieren, daher darf das nie still passieren. Um ohne Authentifizierung zu laufen, entfernen Sie den auth_token-Schlüssel vollständig.

  • Eine nicht gesetzte Variable in env/headers eines Upstreams ist kein Fehler: der Wert wird leer und das fehlende Geheimnis taucht später als 401 von diesem Upstream auf. Das Gateway meldet es vorab — ein unresolved_secret_var-Ereignis im Journal (mcp-gate logs) und eine WARN-Zeile in mcp-gate doctor.

  • Im stdio-Modus warnt mcp-gate client-config (auf stderr), dass die Umgebungsvariablen des Betreibers nicht geerbt werden vom MCP-Client, der das Gateway in seiner eigenen Umgebung startet — setzen Sie sie dort, wo der Client es ausführt.

Standardmäßig erbt jeder stdio-Upstream die vollständige Prozessumgebung des Gateways — einschließlich der Geheimnisse und des auth_token, die --env-file für andere Upstreams geladen hat. Setzen Sie env_isolation: strict (global), damit jeder stdio-Upstream stattdessen nur eine minimale Basis (PATH, HOME, TMPDIR und die Windows-Äquivalente) plus seinen eigenen env:-Block erhält, der das Kind immer erreicht. Empfohlen, wenn Sie Upstreams aus offenen Registries ausführen (npx-gestartete Pakete, denen Sie nicht vollständig vertrauen). Eine Gateway-Variable, die ein strikter Upstream sehen soll, listen Sie explizit in dessen env: auf (z. B. PYTHONPATH: ${PYTHONPATH}). Der Wert wird beim Start gelesen: Ein laufender Upstream behält die Umgebung, mit der er gestartet wurde, bis er (neu) gestartet wird.

Separat und immer aktiv: Sobald das Gateway ein ${VAR}-Geheimnis expandiert hat, kennt es den genauen Wert und schwärzt diesen Wert zu *** in betreiberseitigem Freitext — im Journal (mcp-gate logs) und in Absturzprotokollen. Ohne konfigurierte Geheimnisse kostet das nichts.

Ein Geheimnis, das über klaren, unverschlüsselten HTTP zu einem Nicht-Loopback-Host reist, erhält ein WARN beim Start und eine Zeile in mcp-gate doctor (niemals ein harter Fehler — Klartext auf vertrauenswürdiger Infrastruktur, z. B. einem VPN oder einem isolierten LAN, kann eine bewusste Entscheidung sein, die das Gateway nicht allein aus der Konfiguration anzweifeln kann): ein nicht-leeres auth_token, das über Loopback hinaus gebunden ist, oder headers eines Upstreams, die an eine nicht-https://-Nicht-Loopback-url gesendet werden. TLS selbst ist nicht in das Gateway eingebaut — setzen Sie einen TLS-terminierenden Reverse-Proxy davor, wenn Sie es brauchen, dasselbe Muster, das bereits für gegenseitiges TLS empfohlen wird.

transport: stdio            # stdio (Phase 1) | http (Phase 2)
listen_addr: "127.0.0.1:28080"  # only used for transport: http; loopback by default
# auth_token: ${AIMCPGATE_TOKEN}  # required if you widen listen_addr past loopback;
#                                 # the variable must be set or startup fails
log_file: ./logs/calls.jsonl
# debug_payload_log: ./logs/payloads.jsonl  # OPT-IN, off by default: logs raw
#                                   # arguments AND results — can contain secrets
# Optional global call limits (each can be overridden per upstream):
# rate_limit: { rps: 5, burst: 2 }  # token bucket per upstream for tools/call
#                                   # (refusal → client error -32029, retryable)
# max_result_bytes: 65536           # truncate oversized textual results (0 = off;
#                                   # non-text over-limit results get a _meta marker)
# call_timeout: 30s                 # bounds one upstream request
# How the catalog is presented to the client (both hot-reloadable):
# catalog_mode: lazy                # normal (default) | lazy: the client sees only
#                                   # gate_search_tools / gate_describe / gate_call
# page_size: 50                     # paginate tools/list (0/omitted = whole catalog;
#                                   # ignored in lazy mode)
# Auto-restart policy for crashed stdio upstreams (defaults: on, 1s→30s, 5 tries):
# restart: { enabled: true, initial_backoff: 1s, max_backoff: 30s, max_attempts: 5 }
# env_isolation: strict             # opt-in ("" default = inherit the gateway's full
#                                   # env): a stdio upstream then gets only a minimal
#                                   # base (PATH/HOME/...) plus its own env: block —
#                                   # NOT other upstreams' secrets or the auth token
upstreams:
  - name: filesystem        # stdio upstream
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"]
    enabled: true
  - name: github
    command: github-mcp-server
    env:
      GITHUB_TOKEN: ${GITHUB_TOKEN}   # from the environment, not hardcoded
    enabled: true
    # Optional per-upstream tool filter / catalog projection (keys are ORIGINAL
    # tool names; all editable live via SIGHUP with no upstream restart):
    # tools:
    #   allow: ["search_repositories"]  # if non-empty, only these survive
    #   deny: ["delete_repository"]     # always subtracted, even from allow
    #   rename: { search_repositories: "gh_search" }
    #   strip_annotations: true         # drop heavyweight catalog fields
    #   strip_output_schema: true
    #   max_description: 200            # truncate descriptions to N runes
    #   describe: { get_issue: "Fetch one issue." }   # replace wholesale
    # Optional per-upstream call limits (override the globals for this upstream):
    # rate_limit: { rps: 1, burst: 1 }  # rps: 0 disables the global limit here
    #                                   # (refusal → client error -32029, retryable)
    # max_concurrent: 4                 # cap on simultaneous in-flight calls
    #                                   # (refusal → client error -32029, retryable)
    # max_result_bytes: 32768           # 0 disables the global cap here
    # call_timeout: 120s                # this upstream is slow — give it longer
  - name: remote            # http upstream (Phase 2)
    url: https://mcp.example.com/mcp
    headers:
      Authorization: "Bearer ${REMOTE_MCP_TOKEN}"   # secret, never logged
    enabled: true

Was der Client sieht, wenn ein Aufruflimit greift

Zwei der obigen Aufruflimits werden dem MCP-Client (Agent) angezeigt, nicht nur dem Betreiber-Journal:

  • Guard-Ablehnungen (rate_limit / max_concurrent). Wenn das Gateway einen tools/call abweist, weil der Pro-Upstream-Rate-Limiter oder die Nebenläufigkeitsgrenze ihn nicht aufnehmen konnte, erhält der Client einen JSON-RPC-Fehler mit dem eigenen Code -32029 des Gateways und maschinenlesbarem data: {"retryable": true, "reason": "rate_limit" | "concurrency_limit"}. Der Aufruf hat den Upstream nie erreicht, sodass ein Agent warten und erneut versuchen kann, ohne doppelte Ausführung zu riskieren. Gewöhnliche Transport-/Routing-Fehler behalten das historische -32603, und ein Fehler, den ein Upstream selbst zurückgibt, wird unverändert weitergeleitet, Code und Daten unangetastet — ein -32029 von einem Upstream ist kein Gateway-Signal.

  • Übergroße Ergebnisse, die nicht gekürzt werden können (max_result_bytes). Text- ergebnisse werden mit einem In-Content-Marker [truncated by mcp-gate: …] verkleinert. Ein Nicht-Text-/Nicht-Standard-Ergebnis, das das Limit überschreitet, aber keinen kürzbaren Text hat (z. B. nur Bilder), wird vollständig und bytegenau durchgereicht — sein content[] wird nie verändert — aber das _meta des Ergebnisses erhält den Gateway-Schlüssel io.github.akomyagin.aimcpgate/result-over-limit mit {"limitBytes": N, "resultBytes": M}, damit ein Agent erkennen kann, dass das Limit umgangen wurde. Ein Client, der den Schlüssel nicht kennt, ignoriert ihn einfach. Das result_truncation_skipped-Journalereignis des Betreibers feuert weiterhin wie zuvor.

Lizenz

MIT — siehe LICENSE.

Available Tools

2 tools
demo-echo__echoB

Echo the given text back verbatim.

ParametersJSON Schema
NameRequiredDescriptionDefault
textYesText to echo back verbatim.

TDQS

B3.4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations exist, so the description bears full responsibility. It merely repeats the parameter description ('Echo the given text back verbatim') without adding behavioral details such as side effects, error handling, or output format.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with no extraneous information. Every word serves the purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one required parameter, no output schema), the description is largely complete. It could explicitly mention the return format, but the behavior is trivially inferable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds no new meaning beyond the parameter's description, which already states 'Text to echo back verbatim.'

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states the action ('Echo') and the resource ('the given text back verbatim'), making the purpose unmistakable. The sibling tool 'ping' likely serves a different function (connectivity test), so this description distinguishes well.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus the sibling 'ping' or any alternatives. The description simply states what it does without contextual usage advice.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

demo-echo__pingA

Health check: always returns "pong".

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, but the description fully discloses the output ('always returns pong'), leaving no ambiguity about its behavior for a simple ping tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One concise sentence that is front-loaded with purpose and output, no unnecessary words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple health check tool with no parameters and no output schema, the description is complete and sufficient for an agent to use correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters in the schema; description adds no parameter info, but baseline for 0-param tools is 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it is a health check that always returns 'pong', which is specific and distinguishes it from the sibling tool 'echo'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for health checks, and with only one sibling ('echo'), the purpose is clear without explicit alternatives. Slightly lacking explicit when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.3.0
    • First observeddemo-echo__echo
    • First observeddemo-echo__ping

TDQS

A4.1/5.0

Scored across 2 tools

Disambiguation5/5

The two tools serve completely different purposes: echo returns input text, ping returns a fixed health response. No ambiguity.

Naming Consistency5/5

Both tools follow the same pattern: prefix 'demo-echo__' followed by a verb. The naming is uniform and predictable.

Tool Count4/5

With only 2 tools, the set is minimal but appropriate for a simple demonstration server. It covers the core functionality without being overly sparse.

Completeness5/5

For the stated purpose of a demo echo server, the tools provide exactly what is needed: echo text and health check. No obvious gaps.

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP Gateway that aggregates multiple upstream MCP servers into a single endpoint with persistent connections, tool registry, and authentication.
    31 npm
    2
    MIT
  • F
    license
    A
    quality
    D
    maintenance
    Universal MCP proxy server that discovers, searches, and executes tools across all configured MCP servers from a single entry point.
    7
    -
  • A
    license
    A
    quality
    B
    maintenance
    A zero-dependency MCP gateway: host your own tools, forward and curate tools from other MCP servers, expose them leanly to cut agent token cost, and gate every call through your own policy hooks before it runs.
    4
    34 npm
    38
    MIT