Skip to main content
Glama

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 de prompts/resources/ resources/templates/completion, ping, reenvío de progreso y cancelación real, fan-out de logging/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 de tools/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/createMessage y roots/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ó sesiones Mcp-Session-Id del lado del servidor con terminación DELETE /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ó silenciosamente max_result_bytes — ahora llegan al diario de llamadas (mcp-gate logs) en lugar de a un stderr que 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: una tools/call rechazada por el límite de tasa o la protección de concurrencia ahora devuelve su propio código de error JSON-RPC -32029 con data: {"retryable":true,"reason":...} legible por máquina en lugar de un -32603 indistinguible, y un resultado no textual que omitió max_result_bytes lleva un marcador result._meta (content permanece intacto byte por byte). Finalmente, auth_token que 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 una VAR no 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 elimina auth_token para 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 entrada ERR dispersa 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 /mcp después de initialize (el encabezado se devuelve en la respuesta de initialize), 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.yaml

npx (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.yaml

Polí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:

  1. El binario es /mcp-gate y NO está en $PATH. El Dockerfile hace COPY mcp-gate /mcp-gate y ENTRYPOINT ["/mcp-gate"] — nada lo coloca en una ruta de búsqueda (consulta el Dockerfile si 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 $PATH

    Usa la ruta absoluta en su lugar — esa es la única diferencia.

  2. 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 que sh simplemente no está allí, y no hay ls/cat para explorar. Mantén tuberías, globbing y redirección en el lado del HOST del comando.

  3. docker exec inicia un NUEVO proceso; no consulta el serve en ejecución. doctor, catalog y call construyen 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 una call que hagas de esta manera no aparece en logs.

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

Los 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:

  • logs es 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 que log_file en 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, a docker logs) y mcp-gate logs no tiene nada que leer. -c es lo que le dice dónde está el diario; --file lo 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 …) con transport: http.

  • El modo HTTP necesita un listen_addr no predeterminado. El predeterminado es 127.0.0.1:28080 — bucle local DENTRO del contenedor, inalcanzable desde el host incluso con -p. Establece listen_addr: 0.0.0.0:<puerto> en la configuración; la puerta de enlace entonces se niega a iniciar sin un auth_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 version

Uso

# 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-gate

Todos 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 -32603 JSON-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/call de 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

upstream_start_failed

Un upstream nunca llegó a levantarse; sus herramientas no están en el catálogo.

upstream_gave_up

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.

notification_dropped

El buffer de un suscriptor estaba lleno, así que se descartó una notificación reenviada — el reenvío es no bloqueante por diseño.

server_request_dropped

Un upstream pidió algo que solo el cliente podía responder (elicitation/sampling/roots) y ningún transporte tomó la pregunta, así que la llamada a la herramienta se rechazó en su nombre.

sse_stream_unavailable

Un upstream HTTP no ofrece un stream SSE GET, así que su tools/list_changed no llegará nunca hasta que el gateway se reinicie.

catalog_collision

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.

catalog_bad_template

Una plantilla de URI de recurso no compila: se lista al cliente pero nunca puede coincidir con una lectura.

result_truncation_skipped

Un resultado superó max_result_bytes pero no tenía texto truncable (p. ej. solo imágenes), así que pasó completo.

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í que mcp-gate logs de ≤ 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 space

Fingerprinta 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á tu config.yaml — pasa --config explícitamente cuando uses go 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_token que referencia una variable sin definir hace fallar el arranque, nombrando la variable — un auth_token vací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 clave auth_token por completo.

  • Una variable sin definir en env/headers de 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 evento unresolved_secret_var en el journal (mcp-gate logs) y una línea WARN en mcp-gate doctor.

  • En modo stdio, mcp-gate client-config avisa (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: true

Lo 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 un tools/call porque 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 -32029 y data legible 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 -32029 de 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 — su content[] nunca se altera — pero el _meta del resultado gana la clave del gateway io.github.akomyagin.aimcpgate/result-over-limit con {"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 evento result_truncation_skipped del journal del operador sigue disparándose como antes.

Licencia

MIT — consulta LICENSE.

Install Server
A
license - permissive license
A
quality
A
maintenance

Maintenance

Maintainers
Response time
6dRelease cycle
7Releases (12mo)
Commit activity

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables 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.
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP Gateway that aggregates multiple upstream MCP servers into a single endpoint with persistent connections, tool registry, and authentication.
    45
    2
    MIT
  • F
    license
    A
    quality
    D
    maintenance
    Universal MCP proxy server that discovers, searches, and executes tools across all configured MCP servers from a single entry point.
    7

View all related MCP servers

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.

View all MCP Connectors

Latest Blog Posts

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