aiMCPGate
aiMCPGate
Versión en ruso — README_RU.md.
Una puerta de enlace / proxy para servidores MCP (Model Context Protocol) escrito en Go. Se presenta ante un cliente MCP (Claude Code, Cursor, etc.) como un servidor MCP, mientras que internamente multiplexa las llamadas entre varios servidores MCP ascendentes, agrega sus herramientas, prompts y recursos en un único catálogo, y registra cada llamada.
Estado: MVP completo (Etapas 0–6) + etapas post-MVP 7–18 publicadas, última versión v0.5.0. Fase 1 — multiplexación de upstreams stdio detrás de un endpoint stdio con registro de llamadas; Fase 2 — transporte HTTP/SSE orientado al cliente, upstreams HTTP, un visor de registros CLI (
mcp-gate logs); canal de publicación (goreleaser, compilación cruzada para linux/darwin/windows × amd64/arm64, sin CGO). Post-MVP se añadió reinicio automático de upstreams, recarga de configuración en caliente, filtrado/renombrado de herramientas,doctor, y — en v0.3.0 — agregación completa deprompts/resources/resources/templates/completion,ping, reenvío de progreso y cancelación real, fan-out delogging/setLevel, límites de llamadas por upstream (límite de tasa / concurrencia / truncamiento de resultados / tiempo de espera), un catálogo perezoso y paginación detools/list, y flujos SSE de servidor→cliente tanto en el lado del cliente como en el lado del upstream. v0.4.0 completó la dirección servidor→cliente: los tres métodos iniciados por el servidor —elicitation/create,sampling/createMessageyroots/list— se proxifican en las cuatro combinaciones de transporte (stdio o HTTP en el lado del cliente × stdio o HTTP en el lado del upstream); la puerta de enlace ahora declara a un upstream exactamente las capacidades que su propio cliente declaró en lugar de un{}genérico; el transporte HTTP ganó sesionesMcp-Session-Iddel lado del servidor con terminaciónDELETE /mcp. v0.5.0 añade observabilidad para el operador (Etapa 18): ocho tipos de eventos — fallos de inicio de upstreams y abandonos del supervisor, notificaciones descartadas y solicitudes servidor→cliente, un upstream HTTP sin GET SSE, colisiones de catálogo y plantillas URI incorrectas, y un resultado que omitió silenciosamentemax_result_bytes— ahora llegan al diario de llamadas (mcp-gate logs) en lugar de a unstderrque normalmente pertenece a un cliente MCP; el análisis de configuración se volvió estricto (claves desconocidas/ mal escritas son fatales). También cierra la mitad orientada al cliente de la historia de protección/truncamiento: unatools/callrechazada por el límite de tasa o la protección de concurrencia ahora devuelve su propio código de error JSON-RPC-32029condata: {"retryable":true,"reason":...}legible por máquina en lugar de un-32603indistinguible, y un resultado no textual que omitiómax_result_byteslleva un marcadorresult._meta(contentpermanece intacto byte por byte). Finalmente,auth_tokenque referencia una variable de entorno no definida ahora se niega a iniciar la puerta de enlace en lugar de deshabilitar silenciosamente la autenticación HTTP.Actualización a v0.5.0 — tres cambios de comportamiento, ninguno toca el formato del archivo de configuración en sí:
El análisis de configuración ahora es estricto. Una configuración con una clave desconocida o mal escrita a nivel superior o por upstream, que solía ignorarse silenciosamente, ahora falla al cargarse. Corrige el nombre de la clave (el error la nombra) o elimínala.
auth_token: ${VAR}con unaVARno definida ahora se niega a iniciar, nombrando la variable. Antes, se convertía silenciosamente en un token vacío — lo cual, en una puerta de enlace HTTP, deshabilitaba la verificación de portador por completo sin advertencia. Define la variable (o pasa--env-file), o eliminaauth_tokenpara ejecutar sin autenticación a propósito.El diario de llamadas (
log_file/calls.jsonl) ganó un segundo tipo de registro,"kind":"event", junto a los registros de llamadas existentes. Un binario en v0.4.0 o anterior que lea un diario de v0.5.0 renderiza una línea de evento como una entradaERRdispersa en lugar de fallar — lee un diario con el mismo binario o uno más nuevo que el que lo escribió.Actualización a v0.4.0: sin cambio en el archivo de configuración, pero dos cambios de comportamiento observables en modo HTTP — un id de sesión ahora es obligatorio en
POST /mcpdespués deinitialize(el encabezado se devuelve en la respuesta deinitialize), y el registro de upstreams se inicia perezosamente en la primera solicitud MCP real en lugar de al inicio del proceso.No implementado: una política de acceso por cliente.
Versiones
Los binarios multiplataforma se compilan mediante goreleaser
(.goreleaser.yaml): linux/darwin/windows × amd64/arm64, sin CGO,
la versión se incorpora mediante -ldflags -X main.version=..., las sumas de verificación van en
SHA256SUMS. Prueba local en seco: goreleaser release --snapshot --clean.
Related MCP server: mcpproxy-go
Instalación desde el registro MCP
Además de los binarios de versión sin procesar, la puerta de enlace se distribuye como una imagen OCI en GitHub Container Registry y como un paquete contenedor npm — los dos formatos desde los que los registros MCP instalan.
Docker:
docker run --rm -i -v $(pwd)/config.yaml:/config.yaml ghcr.io/akomyagin/aimcpgate serve-i es obligatorio: la puerta de enlace habla MCP a través de stdio, por lo que el cliente debe mantener
stdin abierto (sin él, el contenedor ve EOF y sale inmediatamente). La
imagen no tiene configuración propia, así que monta la tuya — el ejemplo anterior la monta
en la ruta predeterminada /config.yaml; cualquier otra ruta funciona con serve -c.
Para reproducir una verificación de sandbox de registro (Glama.ai, etc.) sin ningún upstream real, usa la configuración de demostración integrada en la imagen — este comando exacto es lo que un sandbox debería ejecutar:
docker run --rm -i ghcr.io/akomyagin/aimcpgate serve -c /demo.config.yamlnpx (descarga el binario precompilado para tu plataforma en la primera instalación y verifica su suma de verificación SHA256):
npx aimcpgate serve -c ./config.yamlPolítica de imagen: la imagen OCI contiene solo el binario mcp-gate — sin
entornos de ejecución para upstreams stdio (sin node/npx, python, shells). Si tu configuración
lanza servidores upstream stdio, amplía la imagen tú mismo e instala lo que
necesiten; los upstreams HTTP funcionan de serie (los certificados CA están incluidos).
Configuración de demostración: demo.config.yaml y el subcomando oculto
__demo-echo existen solo para que los sandboxes de registro (Glama.ai) puedan
inspeccionar la puerta de enlace sin ningún upstream real — nunca los uses en una
implementación real.
Ejecutar comandos CLI dentro de un contenedor
doctor, catalog, call y logs son cómo un operador inspecciona una
implementación. Tres hechos deciden cómo deben invocarse dentro de un contenedor:
El binario es
/mcp-gatey NO está en$PATH. ElDockerfilehaceCOPY mcp-gate /mcp-gateyENTRYPOINT ["/mcp-gate"]— nada lo coloca en una ruta de búsqueda (consulta elDockerfilesi esto alguna vez parece incorrecto). Así que la forma obvia falla:$ 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 $PATHUsa la ruta absoluta en su lugar — esa es la única diferencia.
La imagen es distroless, por lo que no hay shell en absoluto. La base es
gcr.io/distroless/static-debian12:nonroot, que incluye el binario y los certificados CA y nada más.docker exec mcp-gate sh -c '…'falla de la misma manera queshsimplemente no está allí, y no hayls/catpara explorar. Mantén tuberías, globbing y redirección en el lado del HOST del comando.docker execinicia un NUEVO proceso; no consulta elserveen ejecución.doctor,catalogycallconstruyen su propio registro, abren sus propias conexiones a los upstreams, informan y salen. Su salida es, por lo tanto, la accesibilidad del upstream en este momento, no el estado de la puerta de enlace en vivo: si el proceso en ejecución perdió un upstream y lo eliminó de su catálogo, estos comandos no lo mostrarán. También mantienen limpio el diario de llamadas — se ejecutan con el registro deshabilitado, por lo que unacallque hagas de esta manera no aparece enlogs.
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 50Los comandos asumen un contenedor iniciado en modo separado y con nombre, p. ej.
docker run -d --name mcp-gate … — a diferencia del ejemplo en primer plano docker run --rm -i … anterior, que sale tan pronto como su cliente stdio se desconecta y
no deja nada a lo que docker exec pueda acceder. Se asume que la configuración está montada en
la ruta predeterminada /config.yaml, como en ese mismo ejemplo; demo__echo representa
una herramienta de tu propio catálogo. Algunas advertencias:
logses la excepción al hecho 3: lee el ARCHIVO de diario que escribe la puerta de enlace en ejecución, por lo que sí refleja el proceso en vivo. Eso requiere quelog_fileen la configuración montada apunte a una ruta visible dentro del contenedor, y un volumen montado allí — de lo contrario, el diario va al stderr del contenedor (es decir, adocker logs) ymcp-gate logsno tiene nada que leer.-ces lo que le dice dónde está el diario;--filelo anula.Esto realmente se trata del modo HTTP. En modo stdio, el cliente MCP inicia y posee el contenedor, por lo que generalmente no hay un contenedor de larga duración al que hacer
exec. Una puerta de enlace que puedas inspeccionar es una iniciada por separado (docker run -d --name mcp-gate …) contransport: http.El modo HTTP necesita un
listen_addrno predeterminado. El predeterminado es127.0.0.1:28080— bucle local DENTRO del contenedor, inalcanzable desde el host incluso con-p. Establecelisten_addr: 0.0.0.0:<puerto>en la configuración; la puerta de enlace entonces se niega a iniciar sin unauth_token, a propósito ("el endpoint HTTP sería alcanzable desde la red sin autenticación").
Por qué
Un usuario activo de MCP normalmente tiene varios servidores configurados (sistema de archivos,
GitHub, búsqueda, personalizados), cada uno duplicado en la configuración de cada cliente.
aiMCPGate te ofrece:
Un único punto de entrada — un solo endpoint MCP en lugar de N entradas en la configuración del cliente.
Un único catálogo — las herramientas y prompts de cada servidor upstream fusionados (con espacios de nombres
<upstream>__<tool>para que los nombres nunca colisionen), más sus recursos y plantillas de recursos (direccionados por URI, por lo que nunca se renombran).Un registro de llamadas — qué upstream, qué herramienta, cuándo, éxito/fracaso. Este es el valor añadido además de "solo un proxy".
Proyecto personal en solitario: la prioridad es aprender Go (concurrencia, os/exec,
JSON-RPC 2.0, los transportes stdio y HTTP/SSE). Coste — $0/mes por
defecto (un proceso local), sin telemetría.
Cómo funciona (versión corta)
MCP client ──stdio/HTTP──▶ aiMCPGate ──JSON-RPC──▶ upstream A (stdio)
│ ├─────────▶ upstream B (stdio)
call log └─────────▶ upstream C (http, Phase 2)MVP (dos fases)
Fase 1 — multiplexación de 2+ upstreams stdio detrás de un endpoint stdio (el mismo transporte que habla Claude Code) más registro básico.
Fase 2 — transporte HTTP/SSE, servidores upstream HTTP, un visor de registros (se construyó el CLI; la vista web se descartó deliberadamente), opcionalmente una política de acceso — esa se consideró y se rechazó.
Compilación
export PATH="$HOME/sdk/go/bin:$PATH" # if go isn't already on PATH
go build ./...
go vet ./...
go test -race ./...
go run ./cmd versionUso
# 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-gateTodos los comandos excepto token --generate, completion y skill (que recurre
a una guía integrada) cargan la configuración: pasa --config, o coloca un
config.yaml junto al binario (consulte Configuración a continuación).
serve, doctor, call y catalog también aceptan --env-file ./.env — un
analizador mínimo de KEY=VALUE aplicado antes de que se cargue la configuración, por lo que las referencias ${VAR}
dentro de la configuración se resuelven desde ese archivo. El entorno real del proceso
siempre gana sobre el archivo.
Sesiones HTTP (Mcp-Session-Id)
En modo http, la puerta de enlace ejecuta sesiones HTTP Streamable: la respuesta a
initialize lleva un encabezado Mcp-Session-Id, y cada solicitud posterior
— POST, el flujo GET SSE, DELETE — debe enviar ese encabezado de vuelta. Sin él, la
respuesta es 400; con un id desconocido o caducado, 404, lo que le indica al
cliente que vuelva a hacer initialize. Una sesión se libera mediante DELETE /mcp (204),
o después de 30 minutos sin solicitudes — un flujo SSE abierto cuenta como actividad y lo
mantiene vivo.
Los clientes MCP hacen todo esto por ti. Para llamadas curl hechas a mano, toma el encabezado
de la respuesta de initialize y devuélvelo:
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"La sesión también hace honesto el registro de llamadas: cada llamada se audita bajo el
clientInfo de la sesión que la hizo, por lo que varios clientes HTTP se distinguen
en calls.jsonl en lugar de compartir un único campo client en blanco.
Solicitudes servidor→cliente sobre HTTP (elicitation, sampling, roots)
Cuando un upstream pide algo a mitad de llamada — elicitation/create,
sampling/createMessage, roots/list — la pregunta se entrega como un evento
SSE en el stream GET /mcp de una sesión, y el cliente responde con un
POST ordinario que lleva una respuesta JSON-RPC con el mismo id y el mismo
Mcp-Session-Id. Solo la sesión a la que se dirigió la pregunta puede
responderla; se ignora una respuesta de cualquier otra sesión. Si nadie ha
declarado la capacidad con un stream abierto, el upstream recibe una negativa
de inmediato con la forma que prescribe la especificación
({"action":"decline"} para elicitation, -32601 para las otras dos) en vez
de dejarlo expirar por tiempo — y ocurre lo mismo si la sesión se termina
mientras hay una pregunta pendiente.
Tres consecuencias que conviene saber:
A los upstreams se les comunican las capacidades del PRIMER cliente que inicializa, y ese conjunto queda fijo durante toda la vida del proceso. MCP 2025-06-18 no contempla renegociación, así que un segundo cliente que declare más no puede cambiar handshakes ya realizados — a un upstream nunca se le promete una capacidad en nombre de un cliente del que no se le informó.
Los upstreams arrancan con la primera petición que los necesita, no cuando el gateway vincula su puerto. Eso es lo que hace posible la declaración anterior: el handshake tiene que ocurrir después de que un cliente haya dicho qué soporta. Si los upstreams no pueden arrancar, el cliente recibe un
-32603JSON-RPC y el gateway sale con el error, igual que cuando los arrancaba de forma anticipada.La pregunta va a un cliente que declaró la capacidad — no necesariamente al que la provocó con su llamada. El enrutamiento se hace por capacidad declarada, y entre las sesiones que coinciden gana la más recientemente activa; una petición de upstream no lleva nada que indique a qué llamante pertenece. Con un solo cliente (el caso normal) esto es invisible, pero con dos, un formulario levantado por el
tools/callde un cliente puede aparecer en la interfaz del otro.
El lado upstream del mismo intercambio también funciona por HTTP: un
servidor MCP remoto alcanzado con url: puede plantear su pregunta como un
frame SSE — ya sea en su stream GET de larga duración o intercalado en el
stream que responde a uno de los POST propios del gateway, que es donde los
servidores SDK colocan un elicitation/create levantado dentro de un
tools/call. El gateway lo proxifica por el mismo pipeline y envía la
respuesta del cliente de vuelta como un POST ordinario que lleva una respuesta
JSON-RPC bajo el propio id de petición del servidor. A ese upstream se le
comunican las capacidades de cliente del gateway con la misma política honesta
que a uno stdio — una capacidad solo se ofrece cuando el propio cliente del
gateway la declaró, y doctor/call/catalog, que no tienen cliente alguno,
siguen declarando exactamente {}. El POST de respuesta no se reintenta: un
upstream que no lo recibe recurre a su propio timeout.
Eventos de operador en el diario
El diario en log_file contiene dos tipos de línea: una por llamada de
herramienta y una por evento de operador — un estado del gateway que de otro
modo nunca conocerías. En modo stdio el cliente MCP es dueño del terminal, así
que el stderr del gateway te es invisible, y varias de estas condiciones solo
se registraban a nivel de depuración. Ahora se escriben en el mismo archivo que
lee mcp-gate logs:
Evento | Qué significa |
| Un upstream nunca llegó a levantarse; sus herramientas no están en el catálogo. |
| El supervisor dejó de reintentar un upstream (intentos agotados, reintento deshabilitado por una recarga, o sin canal de liveness) y lo eliminó del catálogo. |
| El buffer de un suscriptor estaba lleno, así que se descartó una notificación reenviada — el reenvío es no bloqueante por diseño. |
| Un upstream pidió algo que solo el cliente podía responder ( |
| Un upstream HTTP no ofrece un stream SSE |
| Dos entradas reclamaron el mismo nombre de herramienta/prompt o URI de recurso visible para el cliente; ganó la primera y el perdedor queda oculto al cliente. |
| Una plantilla de URI de recurso no compila: se lista al cliente pero nunca puede coincidir con una lectura. |
| Un resultado superó |
Los eventos aparecen en línea con las llamadas, marcados EVT; mcp-gate logs --events muestra solo ellos, y --stats gana una tabla por evento. --tool y
--status son filtros solo de llamadas, así que los eventos quedan excluidos
mientras esté activo cualquiera de los dos (--upstream aplica a ambos). Una
consecuencia que vale la pena saber: notification_dropped no nombra ningún
upstream — una caída es una propiedad del suscriptor cuyo buffer se llenó, no
de quien envió la notificación — así que --upstream X nunca lo muestra.
Búscalo sin ese filtro. Los descartes repetidos se fusionan: el primero se
escribe de inmediato, los siguientes dentro de un minuto se cuentan en el
count= de la siguiente línea para esa clave, y el resto se vacía al apagarse.
Una línea que lleva ese acumulado lo dice en su detail=, nombrando el momento
de la ocurrencia más antigua que pliega — la marca de tiempo de la línea
es la más reciente, así que juntas acotan cuándo ocurrió la ráfaga.
Dos notas prácticas:
Establece
log_file. Si está vacío, el journal va a stderr, que en modo stdio pertenece al cliente MCP — los eventos se escribirían donde no puedes verlos.Lee un journal con el mismo binario (o uno más nuevo) que lo escribió. Los eventos llevan un campo
"kind"que las versiones antiguas no conocen, así quemcp-gate logsde ≤ v0.4.0 los muestra como registros dispersos, casi vacíos.
Nada de esto es visible para el cliente MCP: no cambian códigos de error, cuerpos de resultado ni capacidades — los eventos van solo al journal.
Una llamada que el gateway no pudo enrutar no es un evento — es una línea
CALL fallida ordinaria. Un cliente que pide un nombre de herramienta que
ningún upstream ofrece recibe un CallRecord como cualquier otro, con su
columna upstream puesta al centinela (unrouted); mcp-gate logs --upstream '(unrouted)' selecciona exactamente esas líneas y nada más. Un segundo caso,
distinto, se parece mucho pero nombra un upstream real: la ruta existe (la
herramienta está en el catálogo) pero la conexión del upstream se ha perdido
(reiniciando o eliminado) — esa línea lleva el nombre real del upstream, así
que filtra por él con --upstream <nombre> como siempre, no con el centinela.
Recarga de configuración (SIGHUP)
El gateway recarga su configuración en vivo con SIGHUP — sin reinicio, sin
caída de la conexión del cliente. Edita config.yaml y envía la señal:
kill -HUP $(pgrep -f 'mcp-gate serve')Al recargar, el gateway compara la nueva configuración con los upstreams en
ejecución y aplica el cambio mínimo: los upstreams recién añadidos se lanzan,
los eliminados (o con enabled: false) se apagan, los upstreams cuyos campos
de lanzamiento (command/args/url/env/headers) cambiaron se relanzan,
y los upstreams donde solo cambió el filtro de herramientas (allow/deny/
rename, o las reglas de proyección del catálogo strip_annotations/
strip_output_schema/max_description/describe) se reproyectan sin ningún
reinicio. Los límites de llamada (rate_limit, max_concurrent,
max_result_bytes, call_timeout — globales o por upstream) también se
aplican en vivo: nunca requieren un relanzamiento, la siguiente llamada
simplemente usa los nuevos valores. Los upstreams sin cambios siguen
ejecutándose intactos. Una edición incorrecta (YAML inválido, validación
fallida) se registra y se ignora — la configuración en ejecución sigue viva,
así que un error tipográfico nunca tumba el gateway.
Nota de comportamiento: como el gateway instala un manejador de SIGHUP, SIGHUP ya no termina el proceso como haría el valor por defecto del sistema operativo. Para detener el gateway usa Ctrl-C, SIGINT o SIGTERM.
SIGHUP es solo Unix. En Windows — o en cualquier lugar donde prefieras no enviar señales — usa la alternativa de sondeo opt-in:
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 spaceFingerprinta el archivo de configuración en ese intervalo y aplica la misma ruta de recarga que SIGHUP. Ejecutarlo junto al manejador de SIGHUP es seguro.
El observador compara la mtime y el tamaño del archivo, y espera a que ese fingerprint se repita en el siguiente tick antes de leer el archivo. Eso es lo que hace que un guardado en dos pasos (truncar, luego llenar) sea seguro en la práctica: un escritor tendría que mantener el archivo en estado medio escrito durante más de un intervalo de sondeo completo para engañar a la comprobación. El precio es latencia: una recarga aterriza en hasta dos intervalos de sondeo (hasta 4s con el valor por defecto de 2s).
En stdio, los upstreams aparecen con la primera petición del cliente, así que
una edición hecha antes de que ningún cliente se haya conectado aún no puede
aplicarse. El observador conserva esa edición y la reintenta en cada sondeo
hasta que el gateway esté arriba, y luego la aplica — nunca tienes que guardar
el archivo una segunda vez para que surta efecto. Una edición rechazada de
forma definitiva (YAML no parseable, o la protección de no-upstreams de abajo)
se informa una vez y no se reintenta.
Como red de seguridad en ambos disparadores, una recarga cuya nueva
configuración no declara ningún upstreams se rechaza y se registra: esa es la
firma de un archivo medio escrito, y aplicarla derribaría todos los upstreams
en ejecución. Para eliminar todos los upstreams deliberadamente, reinicia el
gateway. Un enabled: false explícito no se ve afectado — deshabilitar el
último upstream sigue aplicándose.
Configuración
Sin --config, el gateway busca config.yaml junto a su propio binario
(p. ej., si mcp-gate está instalado en /etc/gate/, busca
/etc/gate/config.yaml — independientemente del directorio de trabajo desde
el que se lanzó). Si ese archivo no existe y tampoco se pasó --config, da un
error explícito en lugar de arrancar un gateway vacío. Las rutas relativas
dentro de la configuración (log_file, skill_file, debug_payload_log) se
resuelven contra el directorio del propio archivo de configuración, no el
directorio de trabajo actual.
Las claves desconocidas son un error de arranque. La configuración se
analiza estrictamente: una clave mal escrita o no reconocida detiene el gateway
con el nombre de la clave y su número de línea, en lugar de ignorarse
silenciosamente como antes. La ventaja concreta: un error tipográfico en
enabled ya no puede dejar un upstream ejecutándose en silencio. Las claves
personalizadas x- también se rechazan — para compartir un bloque, pon un
ancla YAML en el primer upstream real y fúndelo (<<: *ancla) en los demás;
las anclas y las claves de fusión funcionan como siempre.
Un upstream está habilitado por defecto: omite enabled: por completo y se
lanza como cualquier otro. Para mantenerlo fuera del gateway sin borrar su
configuración, deshabilítalo explícitamente con enabled: false — entonces
no aparece ni en tools/list ni en la tabla de mcp-gate doctor. Cuidado: un
enabled: sin valor (o enabled: null) se lee como omitido, así que
comentar el valor deja el upstream ejecutándose — solo el literal false lo
deshabilita.
Nota: la búsqueda "junto al binario" usa la ruta del ejecutable en ejecución. Con
go run ./cmd ...ese ejecutable es una compilación desechable en un directorio temporal, así que la búsqueda por defecto no encontrará tuconfig.yaml— pasa--configexplícitamente cuando usesgo run, o ejecuta un binario compilado.
Ejemplo completo con todos los campos — config.example.yaml.
El conjunto de servidores upstream se declara en YAML; los secretos (tokens) pasan por env/.env (expansión de ${VAR} en el momento de la carga), nunca se incluyen en el config.
Cada upstream define exactamente uno de command (subproceso stdio) o url (servidor HTTP, Streamable HTTP) — el tipo de conexión se infiere automáticamente.
Las referencias a ${VAR} sin definir se comportan de forma distinta según el campo:
auth_tokenque referencia una variable sin definir hace fallar el arranque, nombrando la variable — unauth_tokenvacío deshabilitaría silenciosamente la comprobación HTTP bearer, por lo que esto nunca se permite que ocurra en silencio. Para ejecutar sin autenticación, elimina la claveauth_tokenpor completo.Una variable sin definir en
env/headersde un upstream no es un error: el valor se vuelve vacío y el secreto ausente aparece más tarde como un 401 de ese upstream. El gateway lo informa con antelación — un eventounresolved_secret_varen el journal (mcp-gate logs) y una líneaWARNenmcp-gate doctor.En modo stdio,
mcp-gate client-configavisa (en stderr) de que las variables de entorno del operador no se heredan en el cliente MCP, que lanza el gateway en su propio entorno — defínelas donde el cliente lo ejecute.
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: trueLo que ve el cliente cuando se alcanza un límite de llamadas
Dos de los límites de llamadas anteriores se muestran al cliente MCP (agente), no solo al journal del operador:
Rechazos del guard (
rate_limit/max_concurrent). Cuando el gateway rechaza untools/callporque el limitador de tasa o el tope de concurrencia por upstream no pudo admitirlo, el cliente recibe un error JSON-RPC con el código propio del gateway-32029ydatalegible por máquina:data: {"retryable": true, "reason": "rate_limit" | "concurrency_limit"}. La llamada nunca llegó al upstream, por lo que un agente puede esperar y reintentar sin riesgo de doble ejecución. Los fallos ordinarios de transporte/enrutamiento mantienen el histórico-32603, y un error que devuelve un upstream se reenvía tal cual, código y datos intactos — un-32029de un upstream no es una señal del gateway.Resultados sobredimensionados que no se pueden truncar (
max_result_bytes). Los resultados de texto se reducen con un marcador en el contenido[truncated by mcp-gate: …]. Un resultado no textual / no estándar que supera el límite pero no tiene texto truncable (p. ej. solo imágenes) se pasa entero y byte a byte — sucontent[]nunca se altera — pero el_metadel resultado gana la clave del gatewayio.github.akomyagin.aimcpgate/result-over-limitcon{"limitBytes": N, "resultBytes": M}para que un agente pueda saber que se omitió el límite. Un cliente que no conoce la clave simplemente la ignora. El eventoresult_truncation_skippeddel journal del operador sigue disparándose como antes.
Licencia
MIT — consulta 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