aiMCPGate
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,doctorund — in v0.3.0 — vollständigeprompts/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 undtools/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/createMessageundroots/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 serverseitigeMcp-Session-Id-Sitzungen mitDELETE /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, dasmax_result_bytesstillschweigend umging — landen jetzt im Aufrufjournal (mcp-gate logs) statt aufstderr, 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 abgelehntertools/callgibt jetzt seinen eigenen JSON-RPC-Fehlercode-32029mit maschinenlesbaremdata: {"retryable":true,"reason":...}zurück, statt eines nicht unterscheidbaren-32603, und ein nicht-textuelles Ergebnis, dasmax_result_bytesumging, trägt einenresult._meta-Marker (contentbleibt byte-für-byte unverändert). Schließlich verweigertauth_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 gesetztemVARverweigert 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 Sieauth_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ärlichenERR-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
initializeaufPOST /mcpobligatorisch (der Header wird von derinitialize-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.yamlnpx (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.yamlImage-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:
Die Binärdatei ist
/mcp-gateund NICHT in$PATH. DasDockerfilemachtCOPY mcp-gate /mcp-gateundENTRYPOINT ["/mcp-gate"]— nichts setzt sie auf einen Suchpfad (prüfen Sie dasDockerfile, 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 $PATHVerwenden Sie stattdessen den absoluten Pfad — das ist der einzige Unterschied.
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, weilsheinfach nicht da ist, und es gibt keinls/cat, um sich umzusehen. Halten Sie Pipes, Globbing und Umleitungen auf der HOST-Seite des Befehls.docker execstartet einen NEUEN Prozess; es fragt nicht den laufendenserveab.doctor,catalogundcallbauen 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 eincall, den Sie auf diese Weise machen, nicht inlogserscheint.
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 50Die 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:
logsist die Ausnahme zu Fakt 3: Es liest die Journal-DATEI, die das laufende Gateway schreibt, spiegelt also den Live-Prozess wider. Das erfordert, dasslog_filein 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. andocker logs) undmcp-gate logshat nichts zu lesen.-csagt 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
execkönnte. Ein Gateway, das Sie inspizieren können, ist eines, das separat gestartet wurde (docker run -d --name mcp-gate …) mittransport: http.Der HTTP-Modus benötigt eine nicht standardmäßige
listen_addr. Der Standard ist127.0.0.1:28080— Loopback IM Container, vom Host aus auch mit-pnicht erreichbar. Setzen Sielisten_addr: 0.0.0.0:<port>in der Konfiguration; das Gateway weigert sich dann, ohneauth_tokenzu 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 versionVerwendung
# 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-gateAlle 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
-32603und 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/calleines 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 |
| Ein Upstream kam nie hoch; seine Tools fehlen im Katalog. |
| 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. |
| Der Puffer eines Abonnenten war voll, sodass eine weitergeleitete Benachrichtigung verworfen wurde — Weiterleitung ist von Natur aus nicht blockierend. |
| Ein Upstream fragte etwas, das nur der Client beantworten konnte ( |
| Ein HTTP-Upstream bietet keinen |
| Zwei Einträge beanspruchten denselben clientseitigen Tool-/Prompt-Namen oder dieselbe Ressourcen-URI; der erste gewinnt und der Verlierer bleibt dem Client verborgen. |
| Eine Ressourcen-URI-Vorlage lässt sich nicht kompilieren: Sie wird dem Client aufgelistet, kann aber nie einem Lesevorgang entsprechen. |
| Ein Ergebnis überschritt |
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, sodassmcp-gate logsvon ≤ 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 spaceEs 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 Ihreconfig.yamlnicht findet — übergeben Sie--configexplizit, wenn Siego runverwenden, 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_tokenmit Verweis auf eine nicht gesetzte Variable bricht den Start ab und nennt die Variable — ein leeresauth_tokenwürde die HTTP-Bearer-Prüfung stillschweigend deaktivieren, daher darf das nie still passieren. Um ohne Authentifizierung zu laufen, entfernen Sie denauth_token-Schlüssel vollständig.Eine nicht gesetzte Variable in
env/headerseines 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 — einunresolved_secret_var-Ereignis im Journal (mcp-gate logs) und eineWARN-Zeile inmcp-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: trueWas 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 einentools/callabweist, 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-32029des Gateways und maschinenlesbaremdata: {"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-32029von 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 — seincontent[]wird nie verändert — aber das_metades Ergebnisses erhält den Gateway-Schlüsselio.github.akomyagin.aimcpgate/result-over-limitmit{"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. Dasresult_truncation_skipped-Journalereignis des Betreibers feuert weiterhin wie zuvor.
Lizenz
MIT — siehe LICENSE.
Available Tools
2 toolsdemo-echo__echoB
Echo the given text back verbatim.
| Name | Required | Description | Default |
|---|---|---|---|
| text | Yes | Text to echo back verbatim. |
TDQS
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.
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.
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.
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.
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.
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".
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
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.
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.
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.
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.
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.
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.
2 tool updates
v0.3.0- First observed
demo-echo__echo - First observed
demo-echo__ping
TDQS
Scored across 2 tools
The two tools serve completely different purposes: echo returns input text, ping returns a fixed health response. No ambiguity.
Both tools follow the same pattern: prefix 'demo-echo__' followed by a verb. The naming is uniform and predictable.
With only 2 tools, the set is minimal but appropriate for a simple demonstration server. It covers the core functionality without being overly sparse.
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
Related MCP Connectors
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
MCP Gateway: wrap any MCP server with cold-start retries, uptime SLA, and per-execution MPP billing.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Unified gateway exposing 150+ tools across all NexGenData MCP servers via one endpoint.
Related MCP Servers
- AlicenseAqualityAmaintenanceLocal-first MCP proxy with BM25 tool discovery, quarantine security, Docker isolation, OAuth support, activity logging, and web UI. Routes multiple upstream MCP servers through a single endpoint.9388MIT
- AlicenseNot gradedqualityDmaintenanceMCP Gateway that aggregates multiple upstream MCP servers into a single endpoint with persistent connections, tool registry, and authentication.31 npm2MIT
- FlicenseAqualityDmaintenanceUniversal MCP proxy server that discovers, searches, and executes tools across all configured MCP servers from a single entry point.7-
- AlicenseAqualityBmaintenanceA 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.434 npm38MIT