Ir al contenido

Modo servidor HTTP

Por defecto, GitLab MCP Server se ejecuta en modo stdio — cada cliente de IA inicia su propio proceso de servidor. El modo HTTP es una alternativa donde un único proceso de servidor atiende a múltiples clientes a través de la red, cada uno autenticándose con su propio token de GitLab.

EscenarioModo Recomendado
Desarrollador individual, cliente de IA localstdio
Equipo compartiendo una instancia de servidorHTTP
Despliegue en servidor remoto/sin pantallaHTTP
Integración CI/CD con MCPHTTP
Pruebas con curl o clientes HTTPHTTP

La instancia no es opcional: --gitlab-url nombra el GitLab al que sirve este despliegue, y un arranque sin ella se rechaza salvo que se pase --allow-any-gitlab-url.

Ventana de terminal
# Instancia única de GitLab.com (URL fija para todos los clientes; reemplázala para GitLab autogestionado)
gitlab-mcp-server --http --gitlab-url=https://gitlab.com
# Varias instancias publicadas (cada cliente elige una con la cabecera GITLAB-URL, que pasa a ser obligatoria)
gitlab-mcp-server --http --gitlab-url=https://gitlab.com,https://gitlab.internal.example.com
# Ninguna instancia publicada: GITLAB-URL nombra cualquier host. Solo para despliegues locales de un único usuario
gitlab-mcp-server --http --allow-any-gitlab-url --http-addr=127.0.0.1:8080

El servidor comienza a escuchar en el puerto 8080 por defecto. El endpoint MCP está disponible en /mcp.

FlagPor DefectoDescripción
--http(desactivado)Habilitar modo de transporte HTTP
--http-addr:8080Dirección de escucha: host:puerto, o una ruta del sistema de ficheros para escuchar en un socket unix. El socket es local a la máquina, así que un proxy o cliente del mismo host conecta a través de él y el tramo TCP entre ambos desaparece en vez de cifrarse; los clientes remotos siguen llegando al servidor a través de ese proxy
--http-socket-mode0660Modo de permisos en octal para un socket unix indicado en --http-addr; por defecto pueden conectar el propietario y el grupo, nadie más
--tls-cert / --tls-key(vacío)Certificado y clave PEM. Sirve HTTPS en el propio listener, para un proxy que no comparte máquina. Ambos o ninguno
--gitlab-url(obligatoria)URL de la instancia de GitLab, salvo que se pase --allow-any-gitlab-url. Repetible (o separada por comas) para publicar varias instancias: todas se listan en el campo authorization_servers de RFC 9728 y GITLAB-URL pasa a ser obligatoria para elegir entre ellas
--allow-any-gitlab-urlfalseArranca sin publicar ninguna instancia, dejando que GITLAB-URL nombre cualquier host. Para un despliegue local de un único usuario; avisa al arrancar y no debe usarse en un puerto que otros puedan alcanzar
--skip-tls-verifyfalseOmite la verificación del certificado TLS al llamar a GitLab (saliente; sin relación con --tls-cert). El modo OAuth lo rechaza para una instancia que no sea loopback, porque los tokens bearer se reenvían allí en cada llamada
--tool-surfacedynamicSelector canónico del catálogo; consulta Opciones de superficie de herramientas y capacidades
--capability-surfacefullSelector de recursos y prompts; consulta Opciones de superficie de herramientas y capacidades
--meta-param-schemaopaqueModo de esquema de entrada de meta-herramientas: opaque, compact o full; solo afecta schemas de meta-herramientas
--tier(detectado)Forzar el nivel de licencia (free, ce, premium, ultimate); omítelo para detectarlo por entrada token+URL desde la licencia de la instancia (por defecto free)
--read-onlyfalseModo solo lectura: elimina las operaciones que modifican, acción por acción; las lecturas siguen funcionando
--safe-modefalseIntercepta herramientas modificantes y devuelve una vista previa JSON en lugar de ejecutarlas
--embedded-resourcestrueIncrustar URIs canónicas de recursos MCP en resultados de herramientas get_*
--exclude-tools(vacío)Nombres de herramientas, separados por comas, que se excluyen del registro
--ignore-scopesfalseOmite la detección de scopes del PAT y registra todas las herramientas que permita el catálogo configurado
--max-http-clients100Máximo de entradas únicas token+URL en el pool del servidor; acota las entradas del pool, no las sesiones ni las peticiones concurrentes (límite: 10000)
--session-timeout30mTimeout de sesión MCP inactiva; solo se aplica con --stateless=false, porque con el transporte sin estado por defecto la sesión de cada POST termina con su respuesta (límite: 24h)
--http-idle-timeout0 (desactivado)Timeout de conexión inactiva del servidor HTTP. 0 (por defecto) desactiva el cierre por inactividad, de modo que --session-timeout es la vida efectiva; usa una duración positiva para reciclar conexiones inactivas antes
--auth-modelegacyModo de autenticación: legacy u oauth (RFC 9728)
--public-url(vacío)Origen https accesible desde fuera de este despliegue. Obligatoria con --auth-mode=oauth: es el identificador de recurso protegido de RFC 9728, y la URL de metadatos se deriva de él; consulta Dónde viven los metadatos
--resource-documentation(vacío)URL https publicada como resource_documentation de RFC 9728; apúntala a una página que describa tu propia aplicación OAuth (su client ID y sus redirect URIs). Vacío publica la página del modo servidor HTTP de este proyecto
--resource-policy-uri(vacío)URL https publicada como resource_policy_uri de RFC 9728; vacío omite el campo
--resource-tos-uri(vacío)URL https publicada como resource_tos_uri de RFC 9728; vacío omite el campo
--oauth-cache-ttl15mTTL de caché de identidad de token OAuth (rango: 1m–2h)
--oauth-client-uid(vacío)uids de aplicaciones OAuth de GitLab, separados por comas, cuyos tokens se admiten. Vacío admite cualquier credencial que acepte la instancia; ponerlo rechaza también los tokens de acceso personal, que no pertenecen a ninguna aplicación
--trusted-origins(vacío)Orígenes absolutos (esquema://host[:puerto]), separados por comas, autorizados a hacer peticiones cross-origin desde un navegador; * acepta cualquier origen y desactiva la protección; vacío no añade ninguno. El origen de --public-url es de confianza automáticamente
--action-timeout65mCancela una acción que siga en marcha tras este tiempo; 0 lo desactiva (límite: 24h). Toma su valor de GITLAB_MCP_ACTION_TIMEOUT si no se pasa
--drain-delay0Tras SIGTERM, mantiene el listener abierto y responde /health con 503 draining durante este tiempo antes de cerrarlo, para que un balanceador que sondea /health retire la instancia antes del cierre (límite: 5m); 0 cierra al instante. Toma su valor de GITLAB_MCP_DRAIN_DELAY si no se pasa
--pool-idle-timeout1hRecupera una entrada de credencial del pool (token + URL de GitLab) tras este tiempo sin usarse; 0 mantiene las entradas hasta que el límite de tamaño del pool las expulse (límite: 24h). Una entrada con una suscripción viva nunca está inactiva según esta medida
--revalidate-interval15mIntervalo de revalidación del token; 0 detiene la comprobación periódica, pero una entrada cuya credencial tenga más de 1h se reconstruye igualmente (límite: 24h)
--rate-limit-rps10Límite de tasa por credencial, en req/s, sobre toda llamada que llega a GitLab: tools/call, resources/read, resources/subscribe, subscriptions/listen, prompts/get (0 lo desactiva), más tools/list en un bucket propio que se rellena diez veces más despacio y conserva el mismo burst, cobrado por gastar el procesador compartido y no por llegar a GitLab; activo por defecto porque un despliegue HTTP es compartido
--rate-limit-burst40Tamaño máximo del token bucket cuando --rate-limit-rps > 0
--trusted-proxies(vacío)Direcciones o rangos CIDR de los proxies inversos cuya --trusted-proxy-header se cree (ej. 127.0.0.1,10.0.0.0/8); desde cualquier otro origen la cabecera se ignora. Obligatoria junto a --trusted-proxy-header
--trusted-proxy-header(vacío)Cabecera HTTP con la IP real del cliente (ej. CF-Connecting-IP, X-Forwarded-For), para que el limitador de fallos de autenticación cargue a quien llama y no al proxy; solo se cree desde --trusted-proxies, obligatoria junto a esta
--statelesstrueHTTP streamable sin sesiones (SEP-2567 / protocolo 2026-07-28): sin seguimiento de Mcp-Session-Id, cada POST es autónomo, GET/DELETE devuelven 405. Usa --stateless=false para sesiones con estado heredadas
--json-responsefalseDevuelve cuerpos application/json en lugar de text/event-stream (SSE)
--max-request-body-bytes0Tamaño máximo del cuerpo de las peticiones HTTP streamable en bytes; 0 usa el valor por defecto del SDK (4 MiB); los valores negativos se rechazan al arrancar. Los cuerpos que lo superan se rechazan con 413
--log-levelinfoVerbosidad del registro: debug, info, warn o error. Fija GITLAB_MCP_LOG_LEVEL
--client-compatautoCompatibilidad de respuesta por cliente, auto u off. Fija GITLAB_MCP_CLIENT_COMPAT; consulta Compatibilidad de clientes
--upload-max-file-size2GBTamaño máximo para las herramientas de subida y de lectura de ficheros. Fija GITLAB_MCP_UPLOAD_MAX_FILE_SIZE
--yolo-modefalseOmite la confirmación en las acciones destructivas. Fija GITLAB_MCP_YOLO_MODE
--description-substitutions(vacío)Reescribe las descripciones y títulos listados para validadores de gateway estrictos: pares viejo=nuevo separados por comas. Fija GITLAB_MCP_DESCRIPTION_SUBSTITUTIONS
--pprof-addr(vacío)Sirve los manejadores de perfilado de Go (net/http/pprof) en esta dirección de loopback, en un listener propio; un host que no sea loopback se rechaza al arrancar. Fija GITLAB_MCP_PPROF_ADDR
--telemetryfalseExporta trazas, métricas y logs de OpenTelemetry por OTLP al colector que nombren las variables estándar OTEL_EXPORTER_OTLP_*; consulta OpenTelemetry
--telemetry-identitynoneQué registra la telemetría sobre quien llama: none, pseudonymous o full
--telemetry-identity-rotation(vacío)Cuánto vive una clave de seudonimización generada, p. ej. 24h; vacío la mantiene lo que dure el proceso. Se ignora cuando GITLAB_MCP_TELEMETRY_IDENTITY_KEY está definida
--telemetry-tool-nameautoSi gen_ai.tool.name es una dimensión de métrica: auto (activa en dynamic y meta, inactiva en individual), on u off

Las opciones compartidas con el modo stdio (--transport, --env-file, --probe, --shutdown, --tool-search, --version) están documentadas en la referencia de la CLI y no se repiten aquí.

Opciones de superficie de herramientas y capacidades

Sección titulada «Opciones de superficie de herramientas y capacidades»

--tool-surface selecciona el catálogo de herramientas MCP visible que se sirve a cada cliente HTTP:

  • dynamic (predeterminada cuando se omite): la superficie de bajo consumo con dos herramientas, gitlab_find_action y gitlab_execute_action.
  • meta: meta-herramientas por dominio, un catálogo consolidado que enruta por el parámetro action.
  • individual: cada operación de GitLab se expone como una herramienta independiente.

--capability-surface controla recursos y prompts de forma independiente a las herramientas: full registra todos los recursos, guías de flujo, prompts y el manifiesto gitlab://tools, mientras que minimal conserva el manifiesto gitlab://tools, y omite prompts, guías y recursos opcionales de GitLab. El descubrimiento de schemas dinámicos sigue funcionando con minimal porque find devuelve schemas inline.

--meta-param-schema solo afecta los schemas visibles de meta-herramientas de dominio. Mantén opaque salvo que un cliente necesite compact o full en tools/list; las formas de llamada exactas siguen disponibles en gitlab://tools/{id}.

Los clientes HTTP solo controlan su token de GitLab y, en modo multi-instancia, el selector GITLAB-URL. Las opciones de política del servidor como --tool-surface, --capability-surface, --meta-param-schema, --rate-limit-rps, --read-only, --safe-mode, --auth-mode, --trusted-proxy-header y --trusted-proxies quedan fijadas por el proceso MCP y no pueden cambiarse por usuario, sesión ni petición JSON-RPC.

Si un cliente envía cabeceras con aspecto de configuración, como TOOL-SURFACE, META-TOOLS, CAPABILITY-SURFACE, META-PARAM-SCHEMA, RATE-LIMIT-RPS, POOL-IDLE-TIMEOUT o GITLAB-SAFE-MODE, el servidor las ignora y registra sus nombres en ignored_options sin registrar sus valores. Las cabeceras deprecadas META-TOOLS también se identifican en deprecated_options.

La configuración del servidor se resuelve en tres capas, de mayor a menor prioridad: un flag de CLI pasado explícitamente, después la variable de entorno correspondiente y por último el valor por defecto. Las tres son del lado del proceso; ningún cliente puede alcanzarlas, así que un usuario nunca puede cambiar el comportamiento ni para sí mismo ni para los demás.

Los clientes deben proporcionar su Token de Acceso Personal de GitLab en cada solicitud HTTP usando una de dos cabeceras.

Cuando el servidor arranca con --allow-any-gitlab-url y no publica ninguna instancia, el cliente elige a qué instancia de GitLab dirigirse con la cabecera GITLAB-URL. Ahí también es obligatoria: una petición sin ella se rechaza, porque responderla enviaría el token de quien llama a una instancia que nunca nombró. Cuando el servidor publica varias instancias la cabecera elige entre ellas, y una petición sin ella se rechaza igual.

GITLAB-URL: https://gitlab.ejemplo.com

Si el despliegue fija exactamente una instancia, esta cabecera se ignora y se registra. Si publica varias, la cabecera elige una de ellas y cualquier otro valor se rechaza. Si no fija ninguna y la cabecera se omite, la petición se rechaza con 400 en lugar de resolverse a https://gitlab.com.

PRIVATE-TOKEN: glpat-xxxxxxxxxxxxxxxxxxxx
Authorization: Bearer glpat-xxxxxxxxxxxxxxxxxxxx

Si ambas cabeceras están presentes, PRIVATE-TOKEN tiene precedencia. Las solicitudes sin un token válido son rechazadas.

El modo OAuth (--auth-mode=oauth) habilita autenticación OAuth 2.1 compatible con RFC 9728. En lugar de gestionar tokens manualmente, los clientes MCP descubren el servidor de autorización automáticamente y manejan el flujo OAuth:

Ventana de terminal
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --auth-mode=oauth --public-url=https://mcp.example.com

Cómo funciona:

  1. El servidor expone /.well-known/oauth-protected-resource con metadatos que apuntan a tu instancia de GitLab como servidor de autorización
  2. Los clientes MCP (VS Code, Claude Code) descubren este endpoint e inician el flujo OAuth 2.1 PKCE
  3. Los usuarios autorizan en el navegador — no se requiere copiar tokens
  4. El servidor valida los tokens Bearer contra la API de GitLab y cachea la identidad durante --oauth-cache-ttl (por defecto: 15 minutos)

Dónde viven los metadatos: raíz del host o sub-ruta

Sección titulada «Dónde viven los metadatos: raíz del host o sub-ruta»

El segmento well-known se sitúa siempre en la raíz del host, y la ruta propia del recurso pasa por detrás. Eso da dos formas de despliegue:

Despliegue--public-urlURL de metadatos
Un servidor dueño del nombrehttps://mcp.example.comhttps://mcp.example.com/.well-known/oauth-protected-resource
Un servidor bajo prefijo de rutahttps://mcp.example.com/gitlabhttps://mcp.example.com/.well-known/oauth-protected-resource/gitlab

La segunda fila es la que sorprende: los metadatos de un servidor en /gitlab no están en /gitlab/.well-known/.... Un proxy que reenvíe solo /gitlab/* deja el descubrimiento tirado mientras todas las llamadas MCP siguen funcionando, que es una forma confusa de fallar.

Un servidor único que es dueño de su nombre de host no tiene ninguno de los dos problemas: la forma derivada es la escueta, y no hay vecino por quien hablar.

Configuración del cliente en modo OAuth:

{
"servers": {
"gitlab": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"oauth": {
"clientId": "TU_APPLICATION_ID_DE_GITLAB",
"scopes": ["api"]
}
}
}
}
  • clientId: El Application ID de tu Aplicación OAuth de GitLab (ver docs/guides/oauth-app-setup.md)
  • scopes: Debe incluir api para funcionalidad completa de herramientas

VS Code maneja el descubrimiento OAuth y la autorización automáticamente.

El modo sin estado es el predeterminado y sigue el diseño sin sesiones introducido por SEP-2567 (protocolo MCP 2026-07-28):

  • El servidor no lee ni establece la cabecera Mcp-Session-Id. Cada POST es un intercambio JSON-RPC autónomo — no se necesita la ronda initialize.
  • GET y DELETE sobre el endpoint MCP devuelven 405 Method Not Allowed (Allow: POST), sin necesidad de credencial: no pueden dirigirse a nada, así que exigirla sustituiría la respuesta especificada por un 401. Con --stateless=false sí se autentican y se comprueba su propiedad, igual que un POST, porque ahí un GET abre el stream SSE de una sesión y un DELETE la termina. Los endpoints /health y /.well-known/* no se ven afectados.
  • Las peticiones síncronas iniciadas por el servidor no están disponibles. Los clientes con protocolo 2026-07-28 conservan la elicitación completa mediante peticiones multi-ronda (MRTR), que viajan dentro del resultado de la herramienta; solo los clientes con protocolo heredado recurren a las alternativas no interactivas (p. ej. el parámetro confirm para acciones destructivas).
  • --session-timeout no tiene efecto: ninguna sesión sobrevive a su petición.
  • El pool de credenciales por token y URL sigue aplicando, así que las peticiones repetidas reutilizan una entrada del pool cacheada.

El modo sin estado encaja en despliegues con balanceo de carga donde las peticiones de un cliente pueden aterrizar en réplicas distintas. Combínalo con --json-response para clientes o gateways que prefieren cuerpos JSON planos en lugar de SSE:

Ventana de terminal
gitlab-mcp-server --http --gitlab-url=https://gitlab.example.com \
--json-response

En modo sin estado la petición legada resources/subscribe se rechaza con un error explicativo — cada POST recibe su propia sesión que se cierra con la respuesta, así que una suscripción aceptada nunca podría notificarse; los clientes con protocolo 2026-07-28 conservan las suscripciones a recursos mediante subscriptions/listen, que mantiene la petición abierta.

--stateless=false restaura el transporte basado en sesiones: se emite Mcp-Session-Id en initialize, GET abre el flujo SSE independiente, DELETE termina la sesión y --session-timeout gobierna el tiempo de vida en reposo. Es un modo de compatibilidad para clientes que aún no negocian el protocolo 2026-07-28 (negocian 2025-11-25 o anterior y usan elicitación síncrona), el servidor registra un aviso al arrancar cuando está activo, y la intención es retirarlo cuando los ecosistemas de clientes hayan migrado.

Todos los resultados cacheables llevan sugerencias SEP-2549. Casi todo es private, porque los catálogos y el contenido de los recursos se filtran según los ámbitos del token y el nivel de licencia de quien llama, y nunca deben servirse desde una caché compartida. El catálogo de prompts es la excepción: sus prompts están compilados en el binario y ningún nivel, token ni superficie los altera, así que marcarlo private costaría a cada cliente un viaje de ida y vuelta por un cuerpo que podría haberse compartido.

La ventana de frescura depende de cómo se resolvió el nivel de licencia. Un nivel detectado puede cambiar con el servidor en marcha, así que el catálogo de herramientas recibe una ventana más corta; --tier o GITLAB_MCP_TIER lo fijan y la elevan a una hora.

ResultadocacheScopettlMs, nivel detectadottlMs, nivel fijado
prompts/listpublic3600000 (1 hora)3600000 (1 hora)
resources/list, resources/templates/listprivate3600000 (1 hora)3600000 (1 hora)
tools/list, server/discoverprivate300000 (5 minutos)3600000 (1 hora)
resources/read de gitlab://toolsprivate300000 (5 minutos)3600000 (1 hora)
resources/read de una guía de trabajoprivate3600000 (1 hora)3600000 (1 hora)
resources/read de datos en vivo de GitLabprivate0 (siempre fresco)0 (siempre fresco)

Las cancelaciones del cliente siempre se propagan a los contextos de los manejadores, de modo que un POST abandonado cancela sus llamadas en curso a la API de GitLab, incluidos los reintentos de client-go. Esto vale para cualquier versión del protocolo y tanto en modo stateless como stateful: el POST es toda la vida de las llamadas que transporta, porque no hay ningún almacén de eventos configurado y una respuesta cuyo POST ha terminado ya no tiene dónde escribirse. La propagación propia del SDK solo cubre el protocolo 2026-07-28, y un cliente anterior no tiene ningún notifications/cancelled que enviar, así que el servidor ata cada llamada al POST que la transportó.

En la superficie dinámica de herramientas, la propiedad action de gitlab_execute_action lleva la anotación x-mcp-header de SEP-2243 con el valor Action —el SDK le antepone el prefijo para formar la cabecera real Mcp-Param-Action—, de modo que los gateways compatibles con MCP pueden enrutar, limitar y observar las llamadas por ID canónico de acción sin analizar el cuerpo JSON-RPC.

El núcleo del modo HTTP es un pool LRU limitado de entradas por credencial, indexado por el hash SHA-256 del token y la URL de GitLab de cada cliente. Una entrada guarda lo que decide la credencial: su cliente de GitLab, su cubo del rate limit, sus vigilantes de recursos y sus sesiones. El servidor MCP y su catálogo de herramientas se construyen una vez por configuración y los comparten todas las credenciales cuya configuración coincide, porque nada de lo que guarda un servidor depende de la credencial; la credencial viaja con cada petición.

Arquitectura del Modo HTTP

Cliente A
Token: glpat-aaa
URL: gitlab.com

StreamableHTTPHandler

Cliente B
Token: glpat-bbb
URL: gitlab.com

Cliente C
Token: glpat-aaa
URL: self-hosted.ejemplo.com

Pool de Credenciales

hash(glpat-aaa + gitlab.com)
Cliente GitLab + estado de credencial

hash(glpat-bbb + gitlab.com)
Cliente GitLab + estado de credencial

hash(glpat-aaa + self-hosted)
Cliente GitLab + estado de credencial

Servidor MCP
uno por configuración

API de GitLab
como usuario A @ gitlab.com

API de GitLab
como usuario B @ gitlab.com

API de GitLab
como usuario A @ self-hosted

Propiedades clave:

  • Los clientes con el mismo token y la misma URL de GitLab comparten la misma entrada del pool
  • Los clientes con diferentes tokens o diferentes URLs de GitLab obtienen entradas completamente aisladas: ninguna credencial puede observar los datos de otra, su estado de vigilancia ni la existencia de su tráfico
  • El servidor MCP se comparte por configuración, y cada petición se ejecuta con el cliente de GitLab de su propia entrada. Una petición que no se puede atribuir a una credencial se rechaza en lugar de atenderse con un valor por defecto
  • Las búsquedas en el pool usan solo hashes SHA-256 de token+URL; el cliente de GitLab de cada entrada conserva la credencial con la que se autentica mientras la entrada vive, porque no puede llamar a GitLab sin ella, y nada se escribe en disco
  • Cuando el pool alcanza --max-http-clients, se desaloja la entrada menos usada recientemente que no esté atendiendo una suscripción, y solo cuando todas las entradas atienden una se va la más antigua de ellas
  1. Primera solicitud: El token y la URL de GitLab se extraen, se combinan y hashean, y se crea una entrada del pool con su propio cliente de GitLab. También se construye el servidor MCP si esta es la primera credencial de su configuración; cualquier credencial posterior lo encuentra listo
  2. Solicitudes posteriores: La entrada existente se encuentra y se promueve en la lista LRU
  3. Timeout de inactividad: Después de --session-timeout de inactividad, la sesión MCP se cierra (pero la entrada del pool permanece)
  4. Desalojo del pool: Cuando se alcanza la capacidad, la entrada desalojable más antigua se elimina completamente, y se avisa a su cliente en lugar de dejarlo mudo: sus vigilantes se detienen, sus peticiones subscriptions/listen abiertas se completan con un resultado que nombra el final (credential_evicted por capacidad, credential_revoked por un token que GitLab rechazó, y cinco más) y, con --stateless=false, las sesiones que ningún stream terminó se cierran, de modo que su siguiente petición vuelve a inicializar

--max-http-clients decide con qué frecuencia ocurre todo eso. Ponlo por encima de la población simultánea de clientes y una credencial que llega no desaloja a nadie; que haya desalojos frecuentes significa que el número está por debajo de la población.

Una entrada está ocupada mientras su credencial mantiene un stream subscriptions/listen abierto o al menos un vigilante, y ambas cosas están acotadas en todo el proceso: 512 streams abiertos y 512 vigilantes simultáneos, ninguno configurable. Como mucho puede haber 1024 entradas ocupadas a la vez, así que un pool de 1025 o más no puede ponerse entero en estado ocupado y el recurso que se lleva la entrada de un suscriptor nunca se dispara. Eso cuesta unos 50 MiB de ocupación con los 50 KiB por entrada medidos, frente a un tope de 10000 para el flag.

El transporte sin estado por defecto necesita menos: allí un resources/subscribe de la era de sesiones se rechaza salvo que llegue por un listen, así que un vigilante no puede existir sin un stream y 513 entradas ya hacen inalcanzable ese recurso.

Modelo de timeouts: capa HTTP vs sesión MCP

Sección titulada «Modelo de timeouts: capa HTTP vs sesión MCP»

Dos capas independientes gobiernan la vida de la conexión y de la sesión:

  • Sesión MCP (--session-timeout, por defecto 30m): vida útil de inactividad de la sesión MCP a nivel del transporte del SDK.
  • Conexión HTTP inactiva (--http-idle-timeout, por defecto 0 = desactivado): el tiempo máximo que el http.Server espera la siguiente petición en una conexión keep-alive antes de cerrarla.
  • Escritura de respuesta HTTP (fijo 60s, desactivado para SSE): acota cuánto puede tardar en escribirse una respuesta.

El servidor habla el transporte moderno Streamable HTTP, que usa Server-Sent Events (text/event-stream) para las respuestas en streaming y para el stream independiente que transporta las notificaciones iniciadas por el servidor. Ambos permanecen en silencio durante largos periodos por diseño. Una respuesta SSE activa está acotada por WriteTimeout (no por IdleTimeout, que solo limita la espera entre peticiones en una conexión inactiva), y el escritor SSE del go-sdk nunca reinicia el deadline de escritura.

Para no cortar esos streams sin debilitar la protección del resto, el WriteTimeout global se mantiene en unos seguros 60s (protegiendo endpoints estándar como /health de ataques de escritura lenta) y cualquier respuesta que el servidor conteste realmente como text/event-stream — tanto el stream GET independiente como las respuestas POST en streaming — desactiva dinámicamente su propio deadline de escritura y lleva X-Accel-Buffering: no. La decisión se toma a partir de la respuesta, no de la cabecera Accept de la petición: a un cliente que envía */* o text/* también se le responde con un stream. Como --http-idle-timeout vale 0 (desactivado) por defecto, la capa HTTP tampoco cierra las conexiones inactivas de fábrica. Con --stateless=false eso hace que --session-timeout sea la vida efectiva de inactividad; con el transporte stateless por defecto no hay sesión MCP que expirar —la sesión de cada POST termina con su respuesta—, así que nada por encima del transporte acota una conexión. Pon un --http-idle-timeout bajo solo si quieres reciclar antes las conexiones inactivas.

Una respuesta SSE que se queda en silencio emite un keep-alive cada 25 segundos: un frame de comentario (una línea que empieza por :), que un lector SSE conforme descarta sin producir ningún evento. Limpiar el deadline de escritura resuelve este extremo de la conexión y ninguno de los saltos intermedios, y nginx cierra una respuesta upstream inactiva en proxy_read_timeout — 60 segundos por defecto — así que el latido es lo que mantiene el stream abierto. Un stream que ha escrito hace poco se omite en lugar de rellenarse. Es comportamiento propio del servidor: el go-sdk no emite ningún ping periódico.

El modo HTTP incluye un rate limiter por credencial con token bucket que regula todas las llamadas que llegan a GitLab: tools/call, resources/read, resources/subscribe, subscriptions/listen y prompts/get comparten un mismo bucket. En modo HTTP está activado por defecto (--rate-limit-rps=10; pon 0 para desactivarlo) porque un despliegue HTTP es compartido: cada llamada que reenvía se carga a su propia dirección de salida, así que el volumen de un cliente en bucle recae sobre todos los demás inquilinos. Stdio lo deja apagado, al no tener con quién compartir. El bucket pertenece a la entrada del pool, indexada por (token + URL de GitLab), y no al servidor MCP, que comparten las credenciales de la misma configuración: dos inquilinos en un mismo servidor se limitan por separado, y los dos clientes de un mismo inquilino se limitan juntos.

tools/list también se regula, en un bucket propio que se rellena diez veces más despacio que el anterior, y es la excepción que confirma la regla: no llega a GitLab, y lo que gasta es el procesador que esperan todos los inquilinos del proceso. Un listado en la superficie individual serializa unos 3,2 MB, la mayor parte del tiempo de procesador de esa superficie, así que un cliente que lista en bucle deja sin procesador a sus compañeros mientras un bucket que cuenta llamadas a GitLab no ve nada. Su burst es --rate-limit-burst, sin dividir: el ritmo de relleno es lo que acota un bucle, mientras que el burst es lo que gasta una flota de clientes que comparten una credencial cuando se reconectan a la vez, y dividirlo también convertiría la exploración en el presupuesto más estrecho del despliegue. Tener buckets separados es además lo que mantiene viva la exploración: un cliente que ha agotado su presupuesto de llamadas a herramientas todavía puede preguntar qué herramientas existen.

FlagPor defectoSignificado
--rate-limit-rps10Tasa sostenida de relleno, en solicitudes por segundo. 0 desactiva el limitador
--rate-limit-burst40Capacidad máxima del bucket (pico de ráfaga durante 1s)

Cuando --rate-limit-rps > 0, cada entrada del pool obtiene su propio token bucket dimensionado en --rate-limit-burst tokens, rellenado a --rate-limit-rps por segundo. Toda llamada que llega a GitLab consume un token: tools/call, resources/read, resources/subscribe, subscriptions/listen y prompts/get. tools/list consume un token de su propio bucket, que guarda los mismos --rate-limit-burst tokens que el primero pero se rellena a una décima parte de --rate-limit-rps, así que un listado se responde incluso con --rate-limit-burst=1 y una flota sobre una sola credencial puede explorar entera a la vez. Un token es una petición, así que un catálogo repartido en varias páginas costaría uno por página; este servidor mantiene todo su catálogo en una sola. resources/list, prompts/list, initialize y el resto de RPCs que el servidor responde desde su propio catálogo no están limitadas, ni tampoco los listados que el servidor se hace a sí mismo al arrancar. Una llamada a herramienta rechazada es un resultado de herramienta con isError: true; una petición de recurso, prompt o listado rechazada es un error JSON-RPC con código -42900, el código que refleja el HTTP 429.

Una llamada a herramienta rechazada vuelve como un CallToolResult con IsError: true y un mensaje de texto como rate limit exceeded for <tool>; retry after a short backoff. Esa forma es deliberada: hay un modelo en el bucle, lee el mensaje y el agente puede aplicar backoff (exponencial o detectando ese mensaje) y reintentar.

El resto de métodos regulados devuelven un error JSON-RPC con código -42900 y la misma frase, porque sus resultados no tienen bandera de error: resources/read, resources/subscribe, subscriptions/listen, prompts/get y tools/list. Ahí no hay ningún modelo mirando. Le toca sobrevivirlos a la fontanería del propio cliente, y el tools/list rechazado es el que conviene prever, porque un cliente que trata un listado fallido como fatal no reintentará por su cuenta. Da a un despliegue compartido burst suficiente para que toda su población conecte a la vez, y mantén la exploración fuera de cualquier bucle de reintentos.

El limitador nunca devuelve HTTP 429, en ninguna de las dos formas, porque el límite se aplica después del enrutado JSON-RPC, dentro de la capa MCP.

  • Despliegue de un solo usuario (dev local típico): --rate-limit-rps=0 es una desactivación razonable; stdio ya viene así por defecto
  • Instancia compartida tras un proxy (Kubernetes, nginx, Cloudflare): empieza con --rate-limit-rps=10 --rate-limit-burst=40. Cada par token+URL obtiene su propia cuota, lo que protege frente a un único cliente ruidoso sin afectar a los demás. Con esas cifras la exploración va a un listado por segundo con cuarenta en la mano
  • Una pasarela o una flota sobre una credencial compartida: toda la población tira del mismo juego de buckets, exploración incluida, así que dimensiona --rate-limit-burst por el número de clientes que se reconectan a la vez tras un reinicio o un despliegue. Cada uno lista una vez al conectar
  • Despliegue multi-tenant grande: combina con rate limiting a nivel de infraestructura (Cloudflare, Caddy, nginx). El limitador a nivel MCP es una red de seguridad, no un sustituto del control en el edge

Añadir a .vscode/mcp.json:

{
"servers": {
"gitlab": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": {
"PRIVATE-TOKEN": "glpat-tu-token"
}
}
}
}

El proyecto publica una imagen Docker multi-arquitectura en ghcr.io/jmrplens/gitlab-mcp-server para linux/amd64 y linux/arm64. La imagen se ejecuta como usuario no-root (UID 10001), expone el puerto 8080, incluye un endpoint /health para orquestadores y sirve HTTP siempre que se arranca sin una entrada estándar por la que hablar, que es como se ejecuta en cualquier despliegue como servicio.

Ventana de terminal
docker run -d \
--name gitlab-mcp \
--read-only \
--tmpfs /tmp:rw,size=64m \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
-p 8080:8080 \
ghcr.io/jmrplens/gitlab-mcp-server:latest \
--http \
--http-addr=0.0.0.0:8080 \
--gitlab-url=https://gitlab.com
services:
gitlab-mcp:
image: ghcr.io/jmrplens/gitlab-mcp-server:latest
ports:
- "8080:8080"
command:
# Modo instancia única (URL fija de GitLab.com para todos los clientes; reemplázala para GitLab autogestionado):
- "--http"
- "--gitlab-url=https://gitlab.com"
- "--http-addr=:8080"
- "--max-http-clients=200"
- "--session-timeout=1h"
# O varias instancias: sepáralas por comas en --gitlab-url; los clientes eligen entonces una con la cabecera GITLAB-URL
# Endurecimiento de seguridad (mínimo privilegio, OWASP Docker security)
read_only: true
tmpfs:
- /tmp:rw,size=64m,mode=1777
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "gitlab-mcp-server", "--probe"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
restart: unless-stopped

Iniciar el servicio:

Ventana de terminal
docker compose up -d

La imagen sigue las guías OWASP Docker Top 10:

PropiedadValor
Imagen basealpine:3.24 (mínima, parcheada regularmente)
Usuarioappuser (UID 10001, no-root)
Sistema de archivosSolo lectura con tmpfs escribible para /tmp
Capacidades de LinuxTodas eliminadas (--cap-drop=ALL)
Escalada de privilegiosDeshabilitada (no-new-privileges:true)
Flags de compilación-trimpath -buildmode=pie (binario PIE, sin rutas de fuente en stack traces)
Etiquetas OCIorg.opencontainers.image.* con versión, commit, URL de origen

Descarga una imagen con etiqueta nueva y reinicia el contenedor. No hay actualización in situ que plantearse: el servidor no reemplaza nunca su propio binario, así que la etiqueta de la imagen es la versión, y un contenedor en marcha sigue sirviendo aquello con lo que arrancó hasta que lo reinicies sobre una imagen más nueva.

Hay una instancia lista para usar de este servidor en https://mcp.jmrp.io/gitlab — nada que instalar, sin más cuenta que tu propio token de GitLab. Es la forma más rápida de probar el servidor; ejecutarlo en local (stdio, Docker) sigue siendo la forma correcta de usarlo a diario, porque en un endpoint alojado tu token y todas tus peticiones pasan por la máquina de otra persona.

{
"mcpServers": {
"gitlab": {
"type": "http",
"url": "https://mcp.jmrp.io/gitlab",
"headers": { "Authorization": "Bearer glpat-xxxxxxxxxxxx" }
}
}
}
  • Authorization (opcional) — Bearer <token>. El endpoint funciona en modo OAuth, así que un cliente que hable el flujo OAuth no necesita cabecera alguna y descubre el servidor de autorización desde el desafío del 401. Un token de acceso personal de GitLab también sirve, enviado como Bearer glpat-..., verificado igual que uno de OAuth. Viaja en cada petición y nunca se guarda en el servidor. Un token read_api se acepta y recibe una superficie de solo lectura.
  • PRIVATE-TOKEN — la cabecera del modo legacy; aquí no se acepta.
  • GITLAB-URL — se ignora: este despliegue fija la instancia a https://gitlab.com.

El endpoint funciona en modo streamable HTTP sin estado: POST es el transporte y un GET autenticado responde 405 por diseño; sin credencial, cualquier método responde 401 con el desafío RFC 6750 que sigue un cliente OAuth — un curl a secas que recibe 401 es el endpoint funcionando, no fallando. https://mcp.jmrp.io/gitlab/health no necesita credencial y responde 200 con {"status":"ok",…}. La superficie de herramientas es la dynamic por defecto — dos herramientas, gitlab_find_action y gitlab_execute_action.

Es uno de los servidores listados en mcp.jmrp.io, un directorio de los servidores MCP que mantiene el autor, cada uno accesible en su propio endpoint; https://mcp.jmrp.io/servers.json es esa misma lista para clientes automáticos.

Dimensiona el host por el conjunto residente del servidor HTTP, no por el tamaño del binario, y hazlo por el trabajo en vuelo. Estas cifras vienen del consumo de recursos, que mide el binario real en ambos transportes e indica la máquina en la que se ejecutó: un Intel i5-14400 con 16 CPU lógicas y 62 GiB de RAM, kernel 6.12, Go 1.27.1. Se vuelven a medir en esa máquina en cada versión.

ModoConjunto residenteNotas
HTTP, en reposo, sin credenciales35 a 38 MiBEl proceso no tiene ningún catálogo hasta que una credencial lo pide
HTTP, veinte credenciales, todas llamando257 a 1073 MiBMedido por la serie de concurrencia, un proceso por superficie
HTTP, cien credenciales, todas llamando0,4 a 1,7 GiBLa misma serie, en su paso de cien credenciales
HTTP, por credencial adicional en el pool30 a 53 KiBHeap vivo en reposo, la credencial mantenida y no llamando
HTTP, por credencial adicional que llama0,81 a 4,27 MiBPico de conjunto residente, con entre dos y cuatro peticiones en vuelo
stdio, un proceso por cliente106 a 277 MiBEmpieza a construir su catálogo enseguida, así que no tiene reposo
Binario en disco~55 MBBinario estático único, sin dependencias de runtime

De aquí se derivan tres cosas.

La memoria sigue al trabajo concurrente, no al número de tokens. El pool mantiene una entrada por cada par token+URL distinto, pero una entrada es un cliente de GitLab y su contabilidad, no un catálogo: el servidor MCP y su superficie registrada se construyen una vez por configuración y los comparten todas las credenciales cuya configuración coincide. La serie de concurrencia del benchmark mide lo que cuesta mantenerlas en cada paso hasta mil credenciales y lo sitúa en 50,6 KiB cada una en dynamic, 52,6 en meta y 29,8 en individual, de modo que mil credenciales admitidas mantienen menos de 120 MiB de heap vivo en cualquier superficie. Los escenarios puntuales dicen lo mismo desde el otro extremo, admitiendo 64 credenciales de una en una: 0,15, 0,09 y -0,53 MiB por credencial adicional, es decir, nada por encima del ruido de una lectura de conjunto residente. Lo que crece son las peticiones en vuelo.

--max-http-clients no es un ajuste de memoria. A unos 50 KiB por entrada, su valor por defecto de 100 acota cinco mebibytes; dimensionar una instancia con él es erróneo en ambos sentidos, porque ni reserva esa memoria ni limita lo que reservan los llamantes que hay detrás de esas credenciales mientras se atienden sus peticiones. Lo que sí acota es cuántos clientes de GitLab y vigilantes vivos mantiene el proceso, y --pool-idle-timeout (1h por defecto) decide cuánto tiempo se conserva una entrada sin usar, salvo que esté atendiendo una suscripción, en cuyo caso no se considera inactiva. --session-timeout es otra cosa distinta, aunque lo parezca: acota una sesión MCP inactiva, solo se aplica con --stateless=false (con el transporte sin estado por defecto, una sesión termina con la respuesta a su propio POST) y terminar una sesión no libera la entrada del pool que hay detrás.

La superficie de herramientas cambia las respuestas más que la memoria. Las tres construyen el mismo catálogo canónico de acciones, y como el servidor se comparte por configuración, la superficie casi no dice nada sobre lo que cuesta una credencial más: 51, 53 y 30 KiB en dynamic, meta e individual, en superficies cuyo número de herramientas registradas difiere en un factor de quinientos. Bajo carga las superficies sí se separan, y tampoco por número de herramientas: meta es la más barata por credencial que llama, 0,81 MiB frente a 1,92 y 4,27, porque lo que queda una vez compartido el catálogo es lo que cada superficie reserva mientras responde a una llamada. Lo que la superficie decide de verdad es el tamaño de un tools/list: 12 KB, 599 KB y 3,2 MB para dynamic, meta e individual. Elige la superficie por coste de tokens y tiempo de respuesta, y el tamaño de instancia por concurrencia.

Las cifras absolutas varían según plataforma y versión del runtime de Go: tómalas como punto de partida y ejecuta make bench-resources para medir las tuyas.

GET /health no necesita credenciales y responde 200 con un cuerpo JSON:

Ventana de terminal
curl -s http://localhost:8080/health
{
"status": "ok",
"version": "2.8.0",
"commit": "a6561ff7",
"build": "2.8.0+a6561ff",
"config_digest": "9f2a7c41e0b3",
"started_at": "2026-08-22T09:14:03Z",
"uptime_seconds": 1209600
}

started_at es el instante de arranque del proceso (RFC 3339, UTC) y uptime_seconds son los segundos enteros transcurridos desde entonces. Se publican ambos porque hacen cosas distintas: started_at es idéntico byte a byte entre sondas, así que un monitor puede cachearlo y detectar un reinicio al ver que cambió, mientras que uptime_seconds es el valor que se lee de un vistazo.

build es la etiqueta que conviene mostrar: la release más cercana a la compilación más el commit corto del que se construyó, con .dirty cuando el árbol tenía cambios sin confirmar, de modo que un binario de release y una compilación desde main publican la misma forma, mientras que version a secas da a uno un número liso y al otro una pseudo-versión de Go. config_digest son doce caracteres hexadecimales sobre los ajustes que deciden lo que ve un cliente (superficie de herramientas, superficie de capacidades, esquema de parámetros de las meta-herramientas, tier y si se fijó o se detecta por credencial, detección de ámbitos del token, solo lectura, modo seguro y herramientas excluidas): todas las instancias detrás de un balanceador deben publicar el mismo, o una de ellas sirve un catálogo distinto y nada más lo detecta. Es una huella para comparar, no un secreto: los ajustes que cubre son pocos y públicos, así que quien pueda leerla puede deducir qué combinación la produjo.

Una vez solicitada la parada, el mismo endpoint responde 503 con "status": "draining" y Cache-Control: no-store. Por defecto el listener se cierra justo después, así que fija --drain-delay (GITLAB_MCP_DRAIN_DELAY) en al menos un intervalo de sondeo cuando un balanceador consulte /health: el listener se mantiene abierto ese tiempo respondiendo 503, el balanceador retira la instancia y solo entonces las peticiones en curso reciben su drenaje.

/health solo informa de que el proceso está en marcha — no hace ninguna llamada a GitLab. Para verificar la conectividad de extremo a extremo de un token concreto, llama a un método MCP autenticado:

Ventana de terminal
curl -s -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "PRIVATE-TOKEN: glpat-tu-token" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' | head -c 200

Una respuesta exitosa devuelve un resultado JSON-RPC con la lista de herramientas disponibles.

Son dos documentos, en dos rutas, y ninguno necesita credenciales. No son el mismo documento.

GET /server-card responde con la ficha SEP-2127: quién es este servidor y cómo conectarse. Identidad (name, version, description, title, websiteUrl, repository) y, cuando el despliegue declara --public-url, una entrada remotes con la cabecera de credencial que debe enviar el cliente. No lleva herramientas, recursos, prompts ni capacidades, y es una decisión de esa extensión: lo que un servidor expone varía según el usuario autenticado, la sesión, la configuración y los feature flags, así que un documento estático no puede responder a eso.

Ventana de terminal
curl -s http://localhost:8080/server-card

La respuesta lleva Content-Type: application/mcp-server-card+json.

GET /.well-known/mcp/server-card.json responde con el documento SEP-1649 anterior, que sí enumera: cada herramienta, recurso, plantilla de recurso y prompt que registra este despliegue, con sus esquemas, más las capacidades que anuncia y los bloques de autenticación, suscripciones y telemetría. Se sirve como application/json, y es lo que hay que pedir cuando quieres el catálogo sin credencial.

Ventana de terminal
curl -s http://localhost:8080/.well-known/mcp/server-card.json

Ambas se montan también bajo el prefijo de ruta de --public-url, para un proxy que reenvía su prefijo en lugar de quitarlo. Antes de la 3.1.0 las dos rutas respondían el documento enumerante y solo cambiaba el Content-Type, lo que dejaba la forma antigua en la ubicación que SEP-2127 reserva.

Es la vía sancionada para publicar el catálogo a algo que no tiene credencial — un directorio, un escáner, una compilación de documentación. tools/list sigue autenticado, porque la especificación de autorización de MCP exige que un servidor que requiere autorización valide el token antes de procesar la petición.

Ambas fichas y el documento RFC 9728 en /.well-known/oauth-protected-resource/<ruta de --public-url> responden los mismos bytes hasta que el proceso se reinicia, y las tres lo dicen dos veces: Cache-Control: public, max-age=3600 es cuánto tiempo puede reutilizar una copia el cliente sin preguntar, y el ETag es lo que envía después para preguntar si esa copia sigue vigente. Las fichas se construyen una sola vez al arrancar; el documento RFC 9728 se serializa en cada petición, a partir de un valor fijado al arrancar, y su tag se calcula sobre los bytes que produjo esa petición.

Ventana de terminal
curl -sI -H 'If-None-Match: "<el etag>"' \
http://localhost:8080/.well-known/mcp/server-card.json
# HTTP/1.1 304 Not Modified

Importa sobre todo en el documento enumerante, que ronda los 137 KB en la superficie por defecto: un escáner que lo consulte cada hora se lo descarga entero cada vez si no hay validador, y una vez por réplica.

Una CDN puede cachear las tres rutas apoyándose en esas cabeceras. Lo que no debe cachear es /mcp, que es un POST con credencial y lleva Cache-Control: no-store, ni /health, cuyo cuerpo cambia en cada sonda.

Preguntas frecuentes

¿Cuándo debería usar el modo HTTP en lugar de stdio?

Usa el modo stdio para un único desarrollador con un cliente de IA local, donde cada cliente inicia su propio proceso de servidor. Usa el modo HTTP cuando un equipo comparte una instancia de servidor, para despliegues remotos o sin pantalla, para integración CI/CD con MCP y para pruebas con curl o clientes HTTP. En modo HTTP un único proceso de servidor atiende a varios clientes por la red, cada uno autenticándose con su propio token de GitLab.

¿Cómo se autentican los clientes en el modo HTTP?

Los clientes envían su Token de Acceso Personal de GitLab en cada petición usando la cabecera PRIVATE-TOKEN (recomendada) o una cabecera Authorization: Bearer; si ambas están presentes en modo legacy, PRIVATE-TOKEN tiene precedencia (el modo OAuth solo lee el token Bearer). Un servidor arrancado con --allow-any-gitlab-url y sin instancia toma la instancia destino de la cabecera GITLAB-URL, y uno que publica varias exige esa cabecera para elegir entre ellas. Para producción, --auth-mode=oauth habilita OAuth 2.1 con PKCE compatible con RFC 9728, de modo que los clientes descubren el servidor de autorización y autorizan en el navegador en lugar de copiar tokens.

¿Los clientes del modo HTTP comparten estado o contexto?

No. El modo HTTP usa un pool LRU limitado de entradas por credencial indexado por el hash SHA-256 del token y la URL de GitLab de cada cliente. Los clientes con el mismo token y la misma URL comparten una entrada, mientras que tokens distintos o URLs distintas obtienen entradas completamente aisladas: el cliente de GitLab, el cubo del rate limit, los vigilantes de recursos y la propiedad de las sesiones son todos por entrada. El servidor MCP en sí lo comparten todas las credenciales cuya configuración coincide, porque lo que guarda lo decide la configuración y no la credencial, y cada petición se ejecuta con el cliente que lleva su propia entrada. Las búsquedas en el pool usan solo los hashes SHA-256, el cliente de GitLab de cada entrada conserva la credencial con la que se autentica mientras la entrada vive, y cuando el pool alcanza --max-http-clients se desaloja la entrada menos usada recientemente que no esté atendiendo una suscripción.

¿Por qué se caen mis sesiones MCP o se cortan los streams?

Dos capas independientes gobiernan la vida útil: el timeout de inactividad de la sesión MCP (--session-timeout, por defecto 30m) y el timeout de conexión HTTP inactiva (--http-idle-timeout, por defecto 0 = desactivado). Como --http-idle-timeout vale 0 por defecto, la capa HTTP no cierra conexiones inactivas, así que --session-timeout es la vida efectiva de inactividad. Si las sesiones caen pronto, un --http-idle-timeout bajo o un timeout de lectura/inactividad del proxy inverso suele cerrar los streams SSE de larga duración; aumenta el timeout del proxy para streams MCP largos.