aiMCPGate
aiMCPGate
Русская версия — README_RU.md.
Ein Gateway / Proxy für MCP-Server (Model Context Protocol) 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 multiplexet, deren Tools, Prompts und Ressourcen zu einem einzigen Katalog aggregiert und jeden Aufruf protokolliert.
Status: MVP vollständig (Stages 0–6) + Post-MVP Stages 7–18 veröffentlicht, neueste Version v0.5.0. Phase 1 — Multiplexen von stdio-Upstreams hinter einem stdio-Endpunkt mit Aufruf-Log; Phase 2 — HTTP/SSE-Transport für die Client-Seite, HTTP-Upstreams, ein CLI-Log-Viewer (
mcp-gate logs); Release-Pipeline (goreleaser, cross-kompiliert für linux/darwin/windows × amd64/arm64, kein CGO). Nach dem MVP kamen hinzu: automatischer Upstream-Neustart, Hot-Reload der Konfiguration, Tool-Filter/Umbenennung,doctor, und — in v0.3.0 — vollständige Aggregation vonprompts/resources/resources/templates/completion,ping, Fortschritt-Fan-out und echte Abbrüche (Cancellation),logging/setLevel-Fan-out, Pro-Upstream-Aufruf-Limits (Rate-Limit/Concurrency/Timeout/Kürzung des Ergebnisses), faul aufgebauter Katalog undtools/list-Pagination sowie SSE-Server→Client-Streams client- und upstuser. v0.4.0 vervollständigte die Server→Client-Richtung: alle drei server-initiierten Methoden —elicitation/create,sampling/createMessageundroots/list— werden in allen vier Transportkombinationen weitergeleitet (stdio oder HTTP auf der Client-Seite × stdio oder HTTP auf der Upstream-Seite). Das Gateway deklariert gegenüber einem Upstream jetzt genau die Fähigkeiten, die sein eigener Client deklariert, statt eines pauschalen{}; der HTTP-Transport hat serverseitigeMcp-Session-Id-Sitzungen mitDELETE /mcpzum Beenden erhalten. v0.5.0 bringt Beobachtbarkeit für den Betreiber (Stage 18): Acht Ereignisarten — Upstream-Startfehler und Supervisor-Aufgabe, verworfene Benachrichtigungen und Server→Client-Requests, ein HTTP-Upstream ohne GET-SSE, Katalog-Kollisionen und ungültige URI-Templates sowie ein Ergebnis, dasmax_result_bytesstillschweigend umgangen hat — landen nun im Aufruf-Log (mcp-gate logs) statt instderr, die normalerweise ein MCP-Client besitzt. Das Parsen der Konfiguration ist jetzt streng (unbekannte/falsch geschriebene Schlüssel sind fatal). Außerdem schließt es das clientbezogene Ende der Begrenzungs-/Trunkierungs-Aufplatze: Eintools/call, das von der Rate-Limit- oder Concurrency-Sperre verweigert wird, gibt jetzt einen eigenen JSON-RPC-Fehlercode-32029mit maschinenlesbaremdata: {"retryable":true,"reason":...}zurück, statt eines nicht unterscheidbaren-32603; ein Nicht-Text-Ergebnis, dasmax_result_bytesumgangen hat, trägt eineresult._meta-Markierung (contentbleibt Byte-für-Byte unangetastet). Schließlich weigern sichauth_token-Referenzen auf eine ungesetzte Umgebungsvariable, das Gateway zu starten, statt die HTTP-Authentifizierung stillschweigend zu deaktivieren.Umstellung auf v0.5.0 — drei Verhaltensänderungen, keine betrifft das Konfigurationsformat selbst:
Das Konfigurations-Parsing ist jetzt streng. Eine Konfiguration mit einem unbekannten oder falsch geschriebenen Schlüssel (auf oberster Ebene oder pro Upstream), die früher stillschweigend ignoriert wurde, lädt jetzt fehlgeschlagen. Korrigiere den Schreibfehler des Schlüsseln as ziehe ihn neu.
Ein
auth_token: ${VAR}mit nicht gesetzterVARverweigert jetzt den Start und benennt dabei die Variable. Vorher wurde daraus still ein leerer Token — was bei Web-Gateway die Bearer-Authentifizierung komplett ohne Warnung deaktiviert hat. Setz die Variable (oder übergebe--env-file) oder entferneauth_token, wenn du das Gateway absichtlich ohne Authentifizierung betreiben willst.Das Aufruf-Log (
log_file/calls.jsonl) hat nun einen zweiten Datensatz-Typ,"kind":"event", zusätzlich zu den bisherigen Aufruf-Datensätzen. Ein Binary der Version v0.4.0 oder älter, das ein v0.5.0-Journal liest, stellt eine Ereigniszeile als dünneERR-Zeile dar, statt zu scheitern — lies ein Journal mit dem gleichen oder einem neueren schönungsmittel als dem, das es geschrieben hat.Umstellung auf v0.4.0: keine Änderung am Konfigurationsformat, aber zwei beobachtbare Verhaltensänderungen im HTTP-Modus: Eine Session-ID ist jetzt obligatorisch auf
POST /mcpnachinitialize(der Header wird von derinitialize-Antwort geliefert) und die Upstream-Registry startet jetzt lazy (bei der ersten echten MCP-Anfrage) statt beim Prozessstart.
Nicht implementiert: eine Pro-Client-Zugriffsrichtlinie.
Releases
Die plattformübergreifenden Binaries werden über goreaser erstellt (.goreaser.yaml): linux/darwin/windows × amd64/arm64, kein CUS, die Version wird über -ldflags -X main.version=... eingebettet, Prüfsummen landen in SHA256SUMS. Lokaler Trockenlauf: goreaser release --snapshot --clean.
Related MCP server: mcpproxy-go
Installation aus der MCP-Registry
Neben den reinen Release-Binaries wird das Gateway als OCI-Image auf dem 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 zwingend: Das Gateway spricht MCP über stdio, der Client muss also stdin offen halten (sonst sieht der Container sofort EOF und beendet sich). Das Image selbst hat keine Eige-Konfiguration, also bleibt deine Konfiguration mounte — das Beispiel oben mountet sie auf den Pfad /config.yaml; jeder andere Pfad funktioniert mit serve -c.
Um einen Registry-Sandbox-Check (Glama.ai usw.) ohne echten Upstream zu reproduzieren, verwende die Demo-Konfiguration, die ins Image eingebaut ist. Genau dieser Befehl ist es, den eine Sandbox ausführen möchte:
docker run --rm -i ghcr.io/akomyagin/aimcpgate serve -c /demo.config.yamlnpx (download die vorgefertigte Binary für deine Plattform beim ersten Installieren und kontrolliert die SHA256-Checksumme):
npx aimcpgate serve -c ./config.yamlImage-Policy: Das OCI-Image enthält nur die mcp-gate-Binary — keine Laufzeitumgebungen für stdio-Upstreams (kein node/npx, Python oder Shells). Wenn deine Konfiguration stdio-Upstream-Server startet, erweitere das Image nicht und installiere global, was sie brauchen; HTTP-upstreams funktionieren ohne weiteres (CA-Zertifikate sind enthalten).
Demo-Konfiguration: demo.config.yaml und der versteckte __demo-echo-Unterbefehl existieren nur, damit Registry-Sandboxes (Glama.ai) einen Z- benutzertest ohne echten Upstream durchführen können — niez nie in einer echten-Bereitstellung verwenden.
CLI-Befehle in einem Container ausführen
doctor, catalog, call und logs sind die Werkzeuge, mit denen ein Betreiber einen Server untersucht. Drei Fakten erklären, warum es sie auf eine besondere Art invoked werden können:
Die Binary ist
/mcp-gateund nicht in$PATH. DasDockerfilemmmCOPY mcp-gate /mcp-gateundENTRYPOINT ["/mcp-gate"]— nichts fügt es zu einem Schriftpfad hinweg (schrägfeDockerfilenach, falls dies falsch ausgehen). Das bedeutet:GXP4 Benutze stattdessen den absoluten Pfad — das ist der einzige Unterschied.
Das Image ist distroless, es gibt keine Shell. Die Basis ist
gcr.io/distroless/static-debian12:nonroot, das beinhaltet Binary und CAzert-ificates und sonst nichts.docker exec mcp-gate sh -c '…'schlägt die gleiche Art fehl, wieshhier einfach nicht existiert, und es gibt keinls/catzum Herumschauen. Behalte Pipes, Globbing und Umleitungen auf der HOST-Seite des Befehls.docker execstartet einen NEUEN Prozess; es abfragt die laufendeserve.doctor,catalogundcallbauen ihre eigene Registry auf, öffnen selbst Verbindungen zu den upstreams und geben aus. Die Ausgabe ist die Upstream-Erreichbarkeit jetzt, nicht der Zustand des laufenden Gateway. Wenn der laufende Prozess einen Upstream verloren hat, ist in seinem Katalog enthalten, ist in denn in these Befehlen wird das nicht angezeigt. Siehalten das Aufruf-Log sauber — sie laufen mit Journaling, disabled, einen/submitcallerscheint nicht inlogs.
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 gehen von einem Container aus, der detached und named gestartet wurde, z. B. docker run -d --name mcp-gate … — anders als das obige Vordergrund docker run --rm -i …-Beispiel, das sich beendet, sobald sein stdio-Client trennt, und nichts für docker exec übrlässt. The configeten wird unter dem Standard pfad /config-frac. erwartet, wie hohe im selben Beispiel; demo__echo steht eine Tool aus deinem eigenen Katalog. Ein paar Anmerkungen:
logsist die Ause zum Fakt 3: Es liest die Journal-Datei, die das laufenden Gateway schreibt, und es bezieht sich deshalb auf die live-Prozess. Dazu musslog_filein der gemounten Kickfiguration auf einen pfad zeigen, der im Inneren des Containers sichtbar ist, und dort ein Volume gemountet werden — sonst geht das Journal in die Stderr des Containers über, dhdocker logdar, undmcp-gate logshat nichts zum Lesen.-cist der Weg, über den es das Journal findet;--fileum es zu overlays.Das betrifft wirklich HTTP-Modus. Im stdio-Modus startet der MCP-Client den Container und besitzt ihn, daher gibt es meist keinen langlebenden Container, in dies man per
execsteigen kann. Ein Gateway, das du untersuchen kannst, startet fors. (docker run -d --name mcp-gate …) mittransport: http.HTTP-Modus braucht eine nicht-Standard-
listen_addr. Der Standard ist127.0.0.1:28080— Loopback im Container, nicht erreichbar vom Hosts aus, solange man-pbenutzt. Zustopfenlisten_addr: 0.0.0.0:<port>in der Config; das Gateway weigert sich dann absichtlich, ohneauth_tokenzu starten —"Der HTTP-Endpunkt wäre vom Netzwerk ohne Authentifizierung erreichbar.".
Warum
Ein aktiver MCP-Nutzer konfiguriert normalerweise mehrere Server (Dateisystem, GitHub, Suche, angepasste), die in der Konfiguration jedes Clients dupliziert werden. aiMCPGate bietet dir:
Ein Einstiegspunkt — ein einziger MCP-Endpunkt statt N Eintrträge im Client.
Ein Katalog — die Tools und Prompts aller Upstream-Server werden zusammengeführt (mit Namespace
<upstream>__<tool>damit nie Namen kollidieren), plus ihre Resources und Ne- Vorlagen (per URI, also nie umbenannt).Ein Aufruf-Log — welcher Upstream, welches Tool, wann, Erfolg/Misserfolg. Das ist der Funktionsumfheit, den „en blößen Proxy" hinzukommt.
Solo-Projekt: Priorität ist das Lerne Go (Koncorn, os/exec, JSON-RPC 2.0, die Transportprotokolle stdio und HTTP/SSE). Kosten — $0/Monat standard (ein lokaler Prozess), keine Observability.
Wie es funktioniert (kurzversion)
MCP client ──stdio/HTTP──▶ aiMCPGate ──JSON-RPC──▶ upstream A (stdio)
│ ├─────────▶ upstream B (stdio)
call log └─────────▶ upstream C (http, Phase 2)MVP (zwei Phasenone)
Phase 1 — Multiplexen von 2+ stdio-Upstreams hinter einem
stdio-Endpunkt (der gleiche Transport, den Ben Infrastruktur Claude Code spricht) plus einfaches Logging.Phase 2 — HTTP/SSE-Transport, HTTP-OSS-Upstream, Server, ein Logviewer (CLI behind; wed view bewusst dem**) —**Optional eine ZugriffspRichtlinie — **einmal vanished.
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:
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 einen eingebauten Leitfaden als Rückfallebene benutzt) laden die Konfiguration: Übergibt --config oder lege eine config.yaml neben der Binary ab (see Konfiguration weiter unten).
(T)serve, doctor, call und "catalog" akzeptieren auch --env-file ./.env — ein minimaler KEY=VALUE-Parser, der vor dem Laden der Konfiguration angewendet wird, sodass ${VAR}-Referenzen in der Konfiguration aus dieser Datei aufgelöst werden können. Die reale Umgebung des Prozesses hat immer Vortritt.
HTTP-Sitzungen (Mcp-Session-Id)
Im HTTP-Modus betreibt das Gateway Streamable-HTTP-Sitzungen: Die Antwort auf initialize enthält einen Mcp-Session-Id-Header, und jede Anfrage danach — POST, GET-SSE-Stream, DELETE — muss den Header senden. Ohne Kopf der er Sta ist 400; bei unbekannter oder abgelaufener ID gibt es 404, was den Client aufführt, neu zu „initialize". Eine Sitzung wird über 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-Client machen das vollautomatisch. Für curl, nehmen 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 das Aufruf-Protokoll auch korrekt: Jeder Aufruf wird unter der clientInfo der Sitzung, die ihn durchgeführt hat, geprüft, so dass mehrere HTTP-Clients in calls.jsonl unterscheidbar sind, statt einen gemeinsamen client-Feld zu teilen, dass leer ist.
Server→Client-Anfragen über HTTP (elicitation, sampling, roots)
Now need to ensure we haven't lost the "HTTP" text after the heading. The original ends there; that's fine.
Let's review if we gave all required headings.
Uh oh, the last heading "Server→client requests over HTTP" we translated. There is no text after. Good.
But we had "## How it works (short version)" translated "## Wie es funktioniert (kurzversion)"? We wrote "## Kurze Erklärung (kurzversion)"? I wrote "## Wie es funktioniert (kurzversion)" - okay.
Also "## WARUM" we wrote "## Warum". ok.
One issue: In "## MVP (two phases)" we placed the bullet list but we used an asterisk bullet "*". Good.
Another issue: The placeholder "GXP6" after "## Wie es funktioniert (kurzversion)" and "GXP7" after "## Build" etc. okay.
But we changed the heading "## Build" to "## Build". Fine.
Now double-check the content "GXP1" etc. Yes.
Potential issue: The line after "GXP" may be blank or not? We'll ensure blank lines around.
Also in the "Docker:" paragraph, the original "GXP1" may have been in code fences; but we don't.
Now let's examine "npx (download the prebuilt binary...)" we wrote "npx (downloads the prebuilt binary for your platform on first install and verifies its SHA256 checksum):" But we lost the backticks? In our translation: "npx (download the prebuilt binary for your platform when first install and check SHA-256):". That is not exactly preserving the English "downloads". But we need to translate "downloads" to German "lädt" etc. The parenthetical text is prose, so our translation is ok. But we need to keep code words "npx", "SHA256" as is.
Also, in original "latest v0.3.0" but we "v0.4.0" etc. We need to keep all versions as they are. Good.
The above is the final answer.
But we should also ensure no empty sections: "## Build" has "GXP7" after it, good.
Now, maybe there is a missing "## Configuration" section? The original text mentions "see Configuration below" but that section is not in the input. So we don't need to add.
Need to preserve the link "README_RU.md" with backslashes? We'll write [README_RU.md](README_RU.md) where underscore escaped? In the source, it's [README\_RU.md](README_RU.md). We should keep the escaped underscore in link text. We'll write [README\_RU.md](README_RU.md).
We need to check all backticks are properly paired.
Let's check content.
One worry: In the status block we have "server→client" using Unicode arrow. We keep the arrows. Fine.
Now, the "GXP" placeholders are not in code blocks. The instruction says "Return only the translated text". We're done.
But maybe I should not translate the "Why" heading? The original "## Why" is fine, but in German "## Warum" is okay.
Also, heading "## Releases" not translated to "## Release Viersionen" but "## Releases" is acceptable.
"## Install from MCP registry" translated to "## Installieren aus der MCP-Registry".
"## Running CLI commands inside a container" -> "## CLI-Kommandos in einem Container ausführen" or "## CLI-Befehle innerhalb eines Containers ausführen". We chose "### CLI-Befehle in einem Container ausführen" - but it should be a level 2 heading? The original is ## not ###. Let's check original: It is ## Running CLI commands inside a container? In the prompt, it's "## Running CLI commands inside a container" (actually "## Running CLI commands inside a container" is a second-level heading). We output "### CLI-Befehle..." that is a level 3 heading. That's wrong. Let's correct: use ## CLI-Befehle in einem Container ausführen.
Also note "### Server→Client-Anfragen über HTTP" is third-level heading; default.
Let's correct level. In the final, change "### CLI-Befehle..." to "## CLI-Befehle...". Similarly "### HTTP-Sitzungen" is third-level in original, we keep.
Now, "MCP-Registry" level 2.
Now, "## MVP (zwei Phasen)" level 2.
"## Build" level 2.
"## Verwendung" level 2.
Good.
One more: The "## How it works (short version)" translation: we wrote "## Wie es funktioniert (kurzversion)" but the word "Kurzversion" is one word; heading should "## Kurz: wie es funktioniert" or "## Wie es funktioniert (Kurzversion)". We'll use "## Wie es funktioniert (Kurzversion)".
But the original heading had "(short version)" in parentheses; we maintain.
Now, "## MVP (zwei Phasenone)" typo "Phasenone" due to accident. Need correct to "(zwei Phasen)".
Let's ensure all.
Now, in the first paragraph we have "multiplexet" probably should be "multiplexet". But German readers might understand. Actually "multiplexen" conjugation: "es multiplexed" doesn't. We can say "multiplexed" as a participle? Avoid verb. Let's rephrase: "während es Aufrufe über mehrere Upstream-MCP-Server hinweg bündelt" but "multiplex" is technical but bundled means not necessarily. Let me revise.
Also "aggregates" is "aggregiert". Good.
Let me revise first paragraph to:
"Ein Gateway/Proxy für MCP-Server (Model Context Protocol) in Go. Es präsentiert sich einem MCP-Client (Claude Code, Cursor usw.) als ein MCP-Server, während es im Hintergrund Aufrufe über mehrere vorgelagerte MCP-Server multiplexed, deren Tools, Prompts und Ressourcen in einem gemeinsamen Katalog zusammenführt, und jeden Aufruf protokolliert.
Use "vorgelagerte" for "upstream" (vorgelagerte MCP-Server). Good.
Need "call log" not "Aufruf-Log" to be.
Now "multiplexed" maybe English. Let's use "multiplexet" but better to say "wechselt". No. I use "multiplexet" but "multiplexte" past tense? Actually "multiplexed" as adjective: "multiplexed" is false. Let's use the technical term "muxed"? But maybe write "multiplexes" with German "multiplexet". We'll use "multiplexet" in the final. Actually a better German phrase "es multiplexet" is not skewed. Let's write "multiplexet" from "to multiplex" but that looks wrong. To avoid risk, use "routed" or "bündelt".
Let's change to: "während es im Hintergrund Aufrufe über mehrere Upstream-MCP-Server bündelt"—mean "multiplexes" as "multiplexes" lack? Add "(multiplexet)" but that's redundant. "bündelt" conveys multiplex. Use "multiplexed" only if not. We'll state "bundles" = "bündelt" and "aggregates" = "aggregiert". That satisfies.
But "multiplexing" is a core term. Maybe use "multiplexing" as a noun: "wobei es die Aufrufe auf mehrere Upstream-MCP-Server multiplexet" is foreign. "Multiplexing" German? It is.
I think it's acceptable: "multiplexet" in German may be formed: "multiplexen" (to multiplex). In a search, German IT texts: "werden über den Multiplexer ...". Could say "multiplexed" as a partial participle: "multiplexed Aufrufe" (not). Let's use "bündelt" plus "multiplexed" not needed. Let's write "bündelt" to be natural. However "multiplexing stdio upstreams behind a stdio endpoint" later we might translate as "Multiplexing von stdio-Upstream-Ports" using English noun "multiplexing" okay.
Let's not obsess.
Now, the final translation should be accurate.
Let's also check "results around "cat" etc." all.
Now we may need to convert backticks around "MAX" etc. We will keep all.
Final answer will be a large text.
Before final, one small adjustment: In the blockquote "Phase 2 — HTTP/SSE-Transport für die client-facing Seite" "client-facing" is a true technical term. German "die der Client-SSeite zugewandte". We can leave "client-seitig". Also "HTTP-Upstreams, CLI-Log-Viewer" okay.
Now, maybe the instruction "text inside inline code (...) should not be translated" – we have maintained ... code spans. But we also need to ensure code spans not split across lines incorrectly. We keep as in original.
Let's output.# 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 mehrerer und Upstream-MCP-Server bündelt, deren Tools, Prompts und Ressourcen zu einem gemeinsamen Katalog aggregiert und jeden Aufruf protokolliert.
Status: MVP vollständig (Stages 0–6) + Post-MVP-Stages 7–18 ausgeliefert, aktuelle Version v0.5.0. Phase 1 — Multiplexing von stdio-Upstreams hinter einem stdio-Endpunkt mit Aufruf-Log; Phase 2 — HTTP/SSE-Transport auf der Client-Seite, HTTP-Upstreams, ein CLI-Log-Viewer (
mcp-gate logs); Release-Pipeline (goreleaser, kreuzkompiliert für linux/darwin/windows × amd64/arm64, ohne CGO). Nach dem MVP hinzugekommen: automatischer Upstream-Neustart, Hot-Reload der Konfiguration, Filtern/Umbenennen von Tools,doctorund — in v0.3.0 — vollständige Aggregation vonprompts/resources/resources/templates/completion,ping, Fortschrittsübertragung und echte Abbrüche (Cancellation),logging/setLevel-Fan-out, per-Upstream-Aufruf-Limits (Rate-Limit / Parallelität / Ergebniskürzung / Timeout), ein Lazy-Katalog undtools/list-Pagination sowie SSE-Server→Client-Streams auf der Client- und der Upstream-Seite. v0.4.0 hat die Server→Client-Richtung vervollständigt: Alle drei server-initiierten Methoden —elicitation/create,sampling/createMessageundroots/list— werden in allen vier Transportkombinationen über ein Proxy geleitet (stdio oder HTTP auf Client-Seite × stdio oder HTTP auf der Upstream-Seite). Das Gateway deklariert gegenüber einem Upstream nunmehr exakt die Fähigkeiten, die der eigene Client deklariert hat, statt eines pauschalen{}. Der HTTP-Transport erhielt serverseitigeMcp-Session-Id-Sitzungen mit Beendigung überDELETE /mcp. v0.5.0 bringt Betreibdiebarkeit (Stage 18): Acht Ereignisarten — Upstream-Startfehler und Supervisor-Aufendraus, verlassene Uhren ohne Benachrichtigungen und Server→Client-Anfragen, ein HTTP-Upstream ohne SSE, Katalog-Kollisionen und ungültige URI-Templates sowie ein sonst still hintermax_result_bytesfallend Ergebnis — landen jetzt im Aufruf-Journal (mcp-gate logs) statt in derstderr, die normalerweise einem MCP-Client gehört. Das Parsen der Konfiguration ist jetzt streng (unbekannte/falsch geschriebene Schlüssel sind fatal). Es schließt außerdem die clientseitige Hälfte der Guard-/Kürzung-Geschichte: Eintools/call, das vom Rate-Limit- oder Parallelitäts-Guard abgelehnt wird, antwortet jetzt mit einem eigenen JSON-RPC-Fehlercode-32029und dem maschinen Form und lesbarendata: {"retryable":true,"reason":...}; ein Nicht-Text-Ergebnis, dasmax_result_bytesumganen hat, trägt einenresult._meta-Marker (contentbleibt Byte für Byte unangetastet). Schließlich wird ein StartVerbot des Gateways, wennauth_tokeneine ungesetzte environment variable referenziert, durchgesetzt, statt die HTTP-Authentifizierung versehentlich zu deaktivieren.
Upgrade auf v0.5.0 — drei Verhaltensänderungen, keine betrifft die Format der Konfigurationsdatei selbst:
Das Konfigurations-Parser ist jetzt streng. Eine Konfiguration mit einem unbekannt verWirden oder falsch geschriebenen Schlüssel (auf oberster oder Pro-Upstream-Ebene), die bisher ignoriert wurde, führt jetzt zum Ladefehler. Korrigiere den im Fehler genannten Schlüssel bzw. der Eingabe-Namen oder entferne ihn.
auth_token: ${VAR}mit einer nicht gesetztenVARverweigert jetzt den Start und nennt die Variable. Vorher wurde daraus stillschweigend ein leerer Token; das hat bei einem HTTP-Gateway die Bearer-Prüfung ohne jede Warnung abgeschaltet. Setze die Variable (oder übergebe--env-file), oder entferneauth_token, um der Gateway gewollt ohne Authentifizierung zu betreiben.Das Aufruf-Journal (
log_file/calls.jsonl) hat eine zweite Datensatzart bekommen,"kind":"event", zusätzlich zu den bisherigen Aufrufen. Ein Binary mit v0.4.0 oder älter, das ein v0.5.0-Journal liest, rendert eine Ereigniszeile als spärlichenERR-Eintrag, anstatt abzusehen — lies Journale mit demselben oder einem neueren Binary als dem, dasdas das geschrieben hat.
haftetem
Upgrade auf v0.4.0: keine Änderung der Konfigurationsdatei, aber zwei sichtbare Verhaltensänderungen im HTTP-Modus: Eine Session-ID ist in
POST /mcpnachinitializeverbindlich (den Header liefert diembitialize-Antwort), und die Upstream-Registry wird nun lazzy auf dem Start neuesten echten MCP-Anfrage ausgelöst, statt beim Prozessstart.
Nicht implementiert: eine pro-Client-Zugriffsrichtlinie.
Releases
Plattformübergreifende Binaries werden über goreleaser (.goreleaser.yaml) gebaut: linux/darwin/windows × amd64/arm64, ohne CGO, die Version wird über -x main.version=... eingebackt, die Checksummen wandern in SHA256SUMS. Lokaler Trockenlauf: goreleaser release --snapshot --clean.
Installation aus der MCP-Registry
Neben den nackten Release-Binaries wird das Gateway auch als OCI-Image auf dem GitHub Container gewähon und als npm-Wrapper-Paket angeboten — die beiden Formate, die MCP-Installer benutzen.
Docker:
docker run --rm -i -v $(pwd)/config.yaml:/config.yaml ghcr.io/akomyagin/aimcpgate serve-i ist Pflicht: Das Gateway spricht MCP über stdio, der Client muss also stdin offenhalten; ohne das sieht das an Ende der Datei überhaupt EOF und sofort. Das Image hat keine eigene Konfiguration — mounte also deine; das Beispiel oben mounted sie auf den Standard-Pfad /config.yaml; jede andere Null geht mitndem Weg über serve -c.
Um einen Registry-Sandbox-Check (Glama.ai usw.) ohne echten Upstream zu reproduzieren, nimmt man die Demo-Konfiguration, die im Image eingebacken ist. Genau diesen Befehl sollte eine Sandbox ausführen:
docker run --rm -i ghcr.io/akomyagin/aimcpgate serve -c /demo.config.yamlnpx (lädt ein vorgefertigtes Binary für dein Plattform beim ersten Installieren herunter und verifiziert die SHA256-Checksumme):
npx aimcpgate serve -c ./config.yamlImage-Richtlinie: Das OCI-Image enthält nur den mcp-gate-Binär — keine Laufzeitumgebungen für unnötige Upstream (keine Node/npx, Python, Shells). Wenn deine Konfiguration stdio-Upstream-Server startet, erweitere das Image selbst und installiere per sieren, was sie brauchen; HTTP-Upstreams funktionen out of-the-box (CA-Zertifikate sind enthalten).
Demo-Konfiguration: demo.config.yaml und der versteckte __demo-echo-Unterbefehl existieren optimial for Sandboxes (Glama.ai) ermöglicht, das Gateway ohne echten Upstream zu erkunden — niemals in einem echten Betrieb verwenden.
CLI-Befehle in einem Container ausführen
doctor, catalog, call und logs sind res an dererkennung einer Bereitstellung. Drei Fakten bestimmen, wie du sie in einesers container aufrufen musst:
Die binäre Datei ist
/mcp-gateund liegt nicht in$PATH. DasDockerfilemachtCOPY mcp-gate /mtcpundENTRYPOINT ["/mcp-gate"]– nichts geschvalt wird auf den Suchpfad gelegt (check “ das Dockerfile, falls das uns gescht). Insofern schlägt die naive Variante fehlcluded:GXP4 Use the absolute path instead — that is the only difference.
So would actually be:
Verwende in dem Dethtcap stattdessen den absoluten Pfad – das ist der einzige Unterschied.
Das Image ist distroless, es gibt also gar keine Shell. Basis ist
gcr.io/distroless/static-debian12:nonroot, die die Drvinnär und CA-Zertifikate ausliefert und sonst nichts. Deritatdocker exec mcp-gate sh -c '…'scheitert möglich so, wieshnie nicht an ist; es gibt keinls/catzum Um. Pipes, Globbing und Redirections bleiben deshalb auf der HOST-Seite des Kommandos.docker execstartet einen NEUEN Prozess; er fragt nicht den laufendenservean.doctor,catalogundcallbauen ihre eigene Registry auf, selbst öffnen Verbindungen zu den Upstreams, berichten und beenden. Daher spiegelt ihre Ausgabe die Upstream-Erreichbarkeit gerade jetzt wieder, nicht den Zustand des live sitzen Den Gateways: Falls der laufende Prozess eine Upstream-Uplink verloren und aus dem Katalog entfernt hat, das zeigen these Befehle nicht de. Sie halten auch das Aufruf-Log sauber — sie laufen ohne Journaling ab, so dass ein via bereit Call nicht inlogssteht.
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 Kommandos setzen Container-container voraus, der detached und benannt ist, z. B. docker size, i.e. mcp-gut — wirklich anders als das obenige docker run -rm -i … Beispiel, das sich additional endet, sobald der stdio-Client getrennt wird und nichts für docker exec übrig ist. Die Config wird vom Standard-Pfad /config.yaml erwartet, wie in diesem Beispiel; demo__echo steht für ein Tool from your own catalog. Ein paar Mittelheiten:
logsist die Ausnahme zu Fakt 3: Es liest die Journaldatei, die das laufende Gateway schreibt, und dabbildet daher auf den realen Prozess. Dafür musslog_filein der gemounteten Config auf a innerhalb des Containers or im pfad zeigen, und an dieser Stelle ein Volume gemountet sein – sonst geht das Tagebuch auf die Stderrd des Containers (alsodocker logs) undmcp-gate logshat nichts zu lesen.-cbietet mit, was das „meret:--fileüberschreibt.Hier is eigentlich HTTP-Modus nie Bevölkerung. Im stdin-Modus startet der MCP-Client den Container und ist Eigentümer, so dass es dasives einen überlebens Container gibt, den man mit
execbetreten kann. Ein beobachtbares Gateway ist eines severed, das du separate (docker run -d --name mcp-gate -detc.) mittransport: httpbasst.HTTP-Modus benötigt eine nicht-Standard-
listen_addr. Die Voreinstellung ist127.0.0.1:28080– Loopback IM Container, für vom Host trotz-pnicht zu nutzen. Setze einelisten_addr: 0.0.0.0:<port>in der Config; das Gateway verweigert dann den Start ohneauth_tokenabsichtlich („der HTTP-Endpunkt wäre network ohne Authentifizierung erreichbar").
Warum
Ein aktive MCP-Nutzerin hat normalerweise mehrere Server konfiguriert (Filesystem, GitHub, Suchmaschine, eigene), die in der Clientkonfiguration, Clientkonfiguration eines every Instanz dupiziert werden. aiMCPGate gibt dir:
Ein Einstiegspunkt — ein einzelner MCP-Endpoint anstelle eines N Einträge in der Client-Config.
Ein Katalog — die kombin notiert Werkzeuge und Prompts aller Upstream-Server (namespaced als
<upstream>__<tool>, damit nie Namen kollidieren), sowie ihre Ressourcen und Ressource-Templates (über URI-Adressen, daher nie umgenannt).Ein Call-Log — welcher Upstream, welches Tool, wann, Erfolg/Fehel. Das ist der Extra zusätzlich zu „nur einem Proxy“.
Privates Hobby-Projekt: Priorität ist das Erlernen von Go (Konz‑[Koncurrent, os/exec, JSON-RPC 2.0, die stdio- und HTTP/SSE-Transporte). Kosten — $0/Monat sea default (ein lokaler Prozess), keine Telemetrie.
So funktioniert es (kurze Version)
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 — Multiplexen von 2+ stdio-Upstreams hinter einen stdio-Endpoint (der gleiche Transport, die Claude Code spricht) und einfaches Logging.
*Phase 2 — HTTP/SSE-Transport, HTTP-Upstream-Server, ein Log-Viewer (die CLI au wurde gebaut; Webansicht bewusst nicht enthalten), optional Access Policy — das wurde angesehen/angepsetzt 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 versionBenutzung
# 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:
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 einem skill-Fall weit(auf eine eingebaute Anleitung zurückfallen) setzen die Config: Übergibt --configoder legt eine config.yaml in der Nähe der Binary ab (siehe Konfiguration unten).
serve, doctor, call und "catalog" akzeptieren --env-file ./.env — eine minimalistische KEY=VALUE-Parser, der **vor** dem Laden der Konfiguration ausgeführt wird, so dass sich ${VAR}-Referenzen in der Config aus dieser Datei aufzulösen. Die echte Prozessumgebung > wo der Datei.
HTTP-Sitzungen (Mcp-Session-Id)
Im HTTP-Modus führt das Gateway Streamable-HTTP-Sitzung: Die Antwort auf initialize enthält einen Mcp-Session-Id header, und jede weitere Frage ** после?** — POST, the GET SSE stream, DELETE — muss diesen Header again senden. Esche retouniert a 400; mit sprechen, mit unbekannter oder abgelaufenen Eid, ba return a 404, was den Client denote auffordert, noch einmal zu initialize. Eine Sitzung wird von ** DELETE /mcp** (204?) beendet, oder nach allt minor 30 Minuten ohne Anfragen — ein offener SSE-Stream zählt als Aktivität und und hält sie so am Leben.
MCP-Clients baz coger für dich. for handgemachte curl-Aufrufe, nimmt den Header aus der interim-Antwort und echoes 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 das Call-Log transparent: Jeder "caller wurde untersearch clientInfo that Sitzung audited, die ihn erstellt also mehrere HTTP-Clients calls.jsonl ganz unterschied are rather with geteilt one empty client field.
Server→Client leerrichtung über HTTP (elicitation, xxsampling, roots)
Wenn ein Upstream mitten in einem Aufruf 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 wurde, darf sie beantworten; eine Antwort von jeder anderen Sitzung wird ignoriert. Wenn niemand mit offenem Stream die Capability deklariert hat, wird der Upstream sofort in der Form abgelehnt, die die Spezifikation vorschreibt ({"action":"decline"} für Elicitation, -32601 für die anderen beiden), statt ihn einfach in einen Timeout laufen zu lassen — und dasselbe geschieht, wenn die Sitzung beendet wird, während eine Frage aussteht.
Drei wissenswerte Konsequenzen:
Den Upstreams werden die Capabilities des ERSTEN Clients mitgeteilt, der initialisiert, und diese Menge ist für die Lebensdauer des Prozesses festgelegt. MCP 2025-06-18 kennt keine Neuverhandlung, daher kann ein zweiter Client, der mehr deklariert, bereits erfolgte Handshakes nicht ändern — einem Upstream wird niemals eine Capability im Namen eines Clients versprochen, von dem ihm nichts mitgeteilt wurde.
Die Upstreams starten mit der ersten Anfrage, die sie benötigt, nicht wenn der Gateway seinen Port bindet. Genau das macht die obige Deklaration überhaupt möglich: 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 der Gateway beendet sich mit dem Fehler, so wie er es tat, als er sie sofort startete.Die Frage geht an einen Client, der die Capability deklariert hat — nicht unbedingt an den, dessen Aufruf sie ausgelöst hat. Das Routing erfolgt nach deklarierter Capability, und unter den passenden Sitzungen gewinnt die zuletzt aktive; eine Upstream-Anfrage trägt nichts, das sagt, zu welchem Aufrufer sie gehört. Bei einem einzelnen Client (dem Normalfall) ist das unsichtbar, aber wenn man zwei betreibt, kann ein Formular, das durch den
tools/calleines Clients ausgelöst wurde, in der Benutzeroberfläche des anderen auftauchen.
Die Upstream-Seite desselben Austauschs funktioniert ebenfalls über HTTP: Ein entfernter MCP-Server, der per url: erreicht wird, kann seine Frage als SSE-Frame stellen — entweder auf seinem langlebigen GET-Stream oder eingefügt in den Stream, der einen der eigenen POSTs des Gateways beantwortet, und genau dort legen SDK-Server ein elicitation/create ab, das innerhalb eines tools/call ausgelöst wurde. Der 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 Request-ID des Servers trägt. Ein solcher Upstream wird über die Client-Capabilities des Gateways nach derselben ehrlichen Richtlinie informiert wie ein Stdio-Upstream — eine Capability wird nur angeboten, wenn der eigene Client des Gateways sie deklariert hat, und doctor/call/catalog, die überhaupt keinen Client haben, deklarieren weiterhin exakt {}. Der Antwort-POST wird nicht wiederholt: Ein Upstream, der ihn nicht bekommt, 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, von dem du sonst nie erfahren würdest. Im Stdio-Modus gehört das Terminal dem MCP-Client, daher ist das stderr des Gateways für dich unsichtbar, 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 ist nie hochgekommen; seine Tools fehlen im Katalog. |
| Der Supervisor hat aufgehört, einen Upstream neu zu starten (Versuche erschöpft, Neustart durch ein Reload deaktiviert oder kein Liveness-Kanal vorhanden) 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 Eintrag gewinnt und der Verlierer bleibt vor dem Client verborgen. |
| Eine Ressourcen-URI-Vorlage kompiliert nicht: Sie wird dem Client aufgelistet, kann aber nie eine Leseanfrage abdecken. |
| Ein Ergebnis überschritt |
Ereignisse erscheinen inline zusammen mit Aufrufen, markiert als EVT; mcp-gate logs --events zeigt nur sie, und --stats erhält eine Tabelle pro Ereignis. --tool und --status sind Filter nur für Aufrufe, daher werden Ereignisse ausgeschlossen, solange einer von beiden gesetzt ist (--upstream gilt für beide). Eine wissenswerte Konsequenz: notification_dropped nennt keinen Upstream — ein Verwerfen ist eine Eigenschaft des Abonnenten, dessen Puffer voll war, nicht desjenigen, der die Benachrichtigung gesendet hat — daher zeigt --upstream X sie nie. Suche danach ohne diesen Filter. Wiederholte Verwerfungen werden zusammengefasst — die 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 weggeschrieben. Eine Zeile, die einen solchen Rückstau trägt, sagt dies in ihrem detail=, wobei sie den Zeitpunkt des ältesten einbezogenen Vorkommnisses nennt — der eigene Zeitstempel der Zeile ist der neueste, sodass die beiden zusammen eingrenzen, wann der Schwall tatsächlich stattfand.
Zwei praktische Hinweise:
Setze
log_file. Ist er leer, geht das Journal nach stderr, das im Stdio-Modus dem MCP-Client gehört — die Ereignisse würden dorthin geschrieben, wo du sie nicht sehen kannst.Lies ein Journal mit demselben (oder einem neueren) Binary, das es geschrieben hat. Ereignisse tragen ein
"kind"-Feld, das ältere Versionen nicht kennen, daher rendertmcp-gate logsvon v0.4.0 und früher sie als spärliche, meist leere Datensätze.
Nichts davon ist für den MCP-Client sichtbar: keine Fehlercodes, Ergebnis-Bodies oder Capabilities haben sich geändert — die Ereignisse gehen nur ins Journal.
Ein Aufruf, den der Gateway nicht routen konnte, ist kein Ereignis — er ist eine gewöhnliche fehlgeschlagene CALL-Zeile. Ein Client, der nach einem Tool-Namen fragt, 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)' selektiert genau diese Zeilen und sonst nichts. Ein zweiter, anderer Fall sieht fast gleich aus, nennt aber stattdessen einen echten Upstream: Die Route existiert (das Tool ist im Katalog), aber die Verbindung des Upstreams ist weg (er wird neu gestartet oder wurde verworfen) — diese Zeile trägt den echten Upstream-Namen, also filtere sie wie üblich mit --upstream <name> statt mit dem Sentinel.
Neuladen der Konfiguration (SIGHUP)
Der Gateway lädt seine Konfiguration live bei SIGHUP neu — kein Neustart, keine unterbrochene Client-Verbindung. Bearbeite config.yaml und sende das Signal:
kill -HUP $(pgrep -f 'mcp-gate serve')Beim Neuladen vergleicht der Gateway die neue Konfiguration mit den laufenden Upstreams und wendet die minimale Änderung an: neu hinzugefügte Upstreams werden gestartet, entfernte (oder enabled: false) 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 ohne Neustart neu projiziert. Aufruflimits (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 unangetastet weiter. Eine fehlerhafte Bearbeitung (ungültiges YAML, fehlgeschlagene Validierung) wird protokolliert und ignoriert — die aktuell laufende Konfiguration bleibt aktiv, daher bringt ein Tippfehler den Gateway nie zu Fall.
Verhaltenshinweis: Da der Gateway einen SIGHUP-Handler installiert, beendet SIGHUP den Prozess nicht mehr so, wie es der OS-Standard täte. Verwende zum Stoppen des Gateways Ctrl-C, SIGINT oder SIGTERM.
SIGHUP ist nur unter Unix verfügbar. Unter Windows — oder überall dort, wo du lieber keine Signale senden möchtest — verwende 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 spaceSie erstellt in diesem Intervall einen Fingerabdruck der Konfigurationsdatei und wendet denselben Neulade-Pfad an, den SIGHUP nimmt. Sie zusammen mit dem SIGHUP-Handler auszuführen 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. Genau das macht ein Zwei-Schritte-Speichern (leeren, dann füllen) in der Praxis sicher: Ein Schreiber müsste die Datei länger als ein volles Polling-Intervall in einem halbgeschriebenen Zustand halten, um die Prüfung auszutricksen. Der Preis ist Latenz — ein Neuladen wird innerhalb von bis zu zwei Polling-Intervallen wirksam (bis zu 4s beim Standardwert 2s).
Bei Stdio kommen die Upstreams mit der ersten Anfrage des Clients hoch, daher kann eine Bearbeitung, die vor dem Verbinden eines Clients vorgenommen wurde, noch nicht angewendet werden. Der Watcher behält diese Bearbeitung und versucht sie bei jedem Poll erneut, bis der Gateway läuft, und wendet sie dann an — du musst die Datei kein zweites Mal speichern, damit sie wirksam wird. Eine endgültig abgelehnte Bearbeitung (nicht parsbares YAML oder der unten beschriebene Schutz gegen fehlende upstreams) wird einmal gemeldet und nicht erneut versucht.
Als Sicherheitsnetz für beide Auslöser wird ein Neuladen, dessen neue Konfiguration überhaupt keine upstreams deklariert, abgelehnt und protokolliert: Das ist die Signatur einer halbgeschriebenen Datei, und ihre Anwendung würde alle laufenden Upstreams beenden. Um alle Upstreams absichtlich zu entfernen, starte den Gateway neu. Ein explizites enabled: false ist nicht betroffen — das Deaktivieren des letzten Upstreams wird weiterhin angewendet.
Konfiguration
Ohne --config sucht der Gateway nach config.yaml neben seinem eigenen Binary (z. B. wenn mcp-gate unter /etc/gate/ installiert ist, sucht er nach /etc/gate/config.yaml — unabhängig vom Arbeitsverzeichnis, aus dem er gestartet wurde). Wenn diese Datei nicht existiert und auch --config nicht übergeben wurde, meldet er einen expliziten Fehler, statt einen leeren 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 strikt geparst: Ein falsch geschriebener oder nicht erkannter Schlüssel stoppt den Gateway mit dem Schlüsselnamen und seiner Zeilennummer, statt wie früher stillschweigend ignoriert zu werden. Der konkrete Gewinn: Ein Tippfehler in enabled kann keinen Upstream mehr unbemerkt weiterlaufen lassen. Benutzerdefinierte x--Schlüssel werden ebenfalls abgelehnt — um einen Block zu teilen, setze einen YAML-Anker auf den ersten echten Upstream und führe ihn (<<: *anchor) in die anderen ein; Anker und Merge-Keys funktionieren wie üblich.
Ein Upstream ist standardmäßig aktiviert: Lass enabled: ganz weg und er wird wie jeder andere gestartet. Um einen ohne Löschen seiner Konfiguration vom Gateway fernzuhalten, deaktiviere ihn explizit mit enabled: false — er erscheint dann weder in tools/list noch in der Tabelle von mcp-gate doctor. Achtung: Ein wertloses enabled: (oder enabled: null) gilt als weggelassen, daher lässt das Auskommentieren des Werts den Upstream weiterlaufen — nur das Literal false deaktiviert ihn.
Hinweis: Die Suche „neben dem Binary" verwendet den Pfad des laufenden ausführbaren Programms. Bei
go run ./cmd ...ist dieses ausführbare Programm ein Wegwerf-Build in einem temporären Verzeichnis, daher findet die Standardsuche deineconfig.yamlnicht — übergib--configexplizit, wenn dugo runverwendest, oder führe ein gebautes Binary aus.
Vollständiges Beispiel mit jedem Feld — config.example.yaml.
Die Menge der Upstream-Server wird in YAML deklariert; **Geheimnisse (Tokens) laufen über env/.env** (${VAR}-Expansion beim Laden), niemals in der Konfiguration festgeschrieben. Jeder Upstream setzt **genau eines** von command(stdio-Subprozess) oderurl` (HTTP-Server, Streamable HTTP) — die Verbindungsart wird automatisch abgeleitet.
Nicht gesetzte ${VAR}-Referenzen verhalten sich je nach Feld unterschiedlich:
auth_token, das auf eine nicht gesetzte Variable verweist, führt beim Start zu einem Fehler und benennt die Variable — ein leeresauth_tokenwürde die HTTP-Bearer-Prüfung stillschweigend deaktivieren, daher darf dies nie still geschehen. 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 im Voraus — 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.
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 }
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 eine Aufrufbegrenzung greift
Zwei der oben genannten Aufrufbegrenzungen tauchen beim MCP-Client (Agent) auf, nicht nur im Betreiber-Journal:
Guard-Ablehnungen (
rate_limit/max_concurrent). Wenn das Gateway einentools/callabweist, weil der Pro-Upstream-Ratenbegrenzer oder die Parallelitätsgrenze ihn nicht zulassen konnte, erhält der Client einen JSON-RPC-Fehler mit dem eigenen Code des Gateways-32029und maschinenlesbaremdata: {"retryable": true, "reason": "rate_limit" | "concurrency_limit"}. Der Aufruf hat den Upstream nie erreicht, sodass ein Agent warten und erneut versuchen kann, ohne Doppelausführung zu riskieren. Gewöhnliche Transport-/Routing-Fehler behalten das historische-32603, und ein Fehler, den ein Upstream selbst zurückgibt, wird wörtlich weitergeleitet, Code und Daten unverändert — ein-32029von einem Upstream ist kein Gateway-Signal.Übergroße Ergebnisse, die nicht gekürzt werden können (
max_result_bytes). Textergebnisse 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 byte-für-byte 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. Das Betreiber-Journalereignisresult_truncation_skippedfeuert weiterhin wie zuvor.
Lizenz
MIT — siehe LICENSE.
Maintenance
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables centralized management and unified interface for multiple child MCP servers (filesystem, sqlite, etc.), allowing users to discover, launch, and execute tools across different MCP servers through a single gateway.
- 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.9321MIT
- AlicenseNot gradedqualityDmaintenanceMCP Gateway that aggregates multiple upstream MCP servers into a single endpoint with persistent connections, tool registry, and authentication.452MIT
Related MCP Connectors
Hosted MCP server for LLM cost estimation, model comparison, and budget-aware routing.
Hosted AgentLux MCP server for marketplace, identity, creator, services, and social flows.
Operator-as-agent MCP hub. 6 tools. First $5 free, then $0.001/call.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/akomyagin/aiMCPGate'
If you have feedback or need assistance with the MCP directory API, please join our Discord server