Suscripciones a Recursos
Las suscripciones a recursos permiten a un cliente decir «avísame cuando esto cambie» en lugar de releer un recurso por intuición. El cliente se suscribe a una URI como gitlab://project/42/pipelines/latest, y GitLab MCP Server envía notifications/resources/updated cada vez que el contenido de ese recurso cambia.
El servidor atiende las suscripciones mediante sondeo: un vigilante relee la URI suscrita a través del mismo handler al que despacha resources/read, compara un SHA-256 del contenido y notifica solo ante un cambio real. «El contenido cambió» significa por tanto «lo que tú leerías cambió», nunca una suposición, con una excepción: la duration y la queued_duration de un job, que GitLab recalcula en cada lectura mientras el job se ejecuta, quedan fuera de la comparación. La decisión y sus límites están registrados en ADR-0015.
Las suscripciones se anuncian con GITLAB_MCP_CAPABILITY_SURFACE=full (el valor por defecto). La superficie minimal no registra los recursos de datos de GitLab a los que aplica una suscripción, así que tampoco anuncia la capacidad.
¿Qué método usa un cliente?
Sección titulada «¿Qué método usa un cliente?»| Protocolo del cliente | Cómo se suscribe un cliente | Qué recibe |
|---|---|---|
2025-11-25 y anteriores | resources/subscribe y resources/unsubscribe | notifications/resources/updated |
2026-07-28 | subscriptions/listen, con las URI en resourceSubscriptions (SEP-2575) | La misma notificación, más un resultado en la petición abierta cuando el servidor termina la suscripción |
El servidor atiende ambos. Cuál envía un cliente depende de la versión del protocolo que negoció su sesión, y cuáles puede atender un despliegue depende de su transporte (Notas de transporte).
En 2026-07-28 la suscripción es la petición de listen: el cliente la deja abierta, y termina cuando el cliente la cancela o el servidor la responde. Un mismo listen puede llevar varias URI, y además suscripciones a cambios de lista. El servidor solo lo responde cuando todas las URI que lleva se han detenido, y nunca mientras lleve también una suscripción a cambios de lista, para que el resto de lo que un cliente agrupó en una petición no se pierda con un solo recurso.
¿Qué se puede vigilar?
Sección titulada «¿Qué se puede vigilar?»26 tipos de recurso: objetos únicos con un ciclo de vida que merece seguirse, más tres listas de un solo padre (los jobs de un pipeline, las discusiones de un merge request y sus notas):
| Grupo | Recursos suscribibles |
|---|---|
| CI/CD | Pipeline, lista de jobs del pipeline, último pipeline, job concreto, deployment, environment |
| Colaboración | Merge request (con sus discusiones y notas), issue |
| Releases y refs | Release, tag, rama, feature flag |
| Planificación | Milestone y label (de proyecto o grupo), board |
| Contenido | Página wiki, archivo del repositorio, snippet (de proyecto o personal) |
| Contenedores | Proyecto, grupo, deploy key |
Las colecciones están excluidas deliberadamente. Suscribirse a gitlab://project/42/issues se dispararía con cada cambio de cualquier issue del proyecto, la mayoría nada que el suscriptor pidiera, y costaría una página completa por sondeo, así que una suscripción a una colección de primer nivel se rechaza.
¿Por qué se admiten tres listas?
Sección titulada «¿Por qué se admiten tres listas?»La lista de jobs de un pipeline y las discusiones y notas de un merge request son listas, y se pueden suscribir a propósito: cada una está acotada por un único objeto padre cuyo ciclo de vida el suscriptor ya sigue, así que «¿ha cambiado esto?» es exactamente la pregunta que responde vigilar ese padre.
Un vigilante sobre una de ellas solo ve la primera página, 100 elementos, que abarca cualquier pipeline o merge request corriente; un cambio más allá no genera ninguna notificación. Cada lectura de una lista dice lo completa que está bajo la clave io.github.jmrplens/pageInfo de su _meta (returned, total cuando GitLab lo envía, y complete), para que un suscriptor sepa cuándo lo que vigila es parcial.
¿Dónde se publica la lista?
Sección titulada «¿Dónde se publica la lista?»En cuatro sitios, todos construidos a partir de la lista blanca que la aplica, así que ninguno puede desviarse de los demás:
- La descripción de cada plantilla de recurso suscribible termina con la frase
Subscribable: subscriptions/listen (protocol 2026-07-28). Resources/subscribe on stateful sessions., añadida al registrarla y nunca escrita a mano. Son dos frases y no una con punto y coma porque la superficie servida no lleva ninguno de los caracteres que rechazan los validadores estrictos de los gateways. - Esas mismas plantillas llevan la clave
_metaio.github.jmrplens/subscribable: true, para un cliente genérico que filtra sin conocer el manifiesto de este servidor. MCP no tiene un campo de suscribible por recurso, solo la capacidadresources.subscribede todo el servidor. - El manifiesto
gitlab://toolsenumera todas las plantillas bajosubscriptions.subscribable_uri_templates. - En modo HTTP, la ficha del servidor enumerativa en
/.well-known/mcp/server-card.jsonlleva un bloquesubscriptionsde primer nivel, junto a un objetocapabilitiesque refleja el handshake, para que un directorio pueda conocerlo todo sin conectarse. La ficha en/server-cardes otro documento y no declara capacidades (Ficha del servidor).
El bloque de la ficha dice, por método, si este despliegue lo atiende:
"subscriptions": { "supported": true, "methods": { "subscriptions/listen": { "available": true, "requires": "stateless sessions (--stateless, the default)", "since_protocol": "2026-07-28" }, "resources/subscribe": { "available": false, "requires": "stateful sessions (--stateless=false)" } }, "subscribable_uri_templates": ["gitlab://group/{group_id}", "..."], "notification": "notifications/resources/updated, sent when the watched content changes (server polls GitLab)", "end_reasons": [ "credential_evicted", "credential_reset", "credential_revoked", "credential_insufficient", "resource_gone", "lifetime_reached", "watcher_evicted", "shutdown" ], "end_reason_meta_key": "io.github.jmrplens/watch-end"}Ese es un despliegue sin estado. Con --stateless=false los dos valores de available se intercambian, y en la superficie de capacidades minimal el bloque no aparece.
¿Qué cuesta una suscripción rechazada?
Sección titulada «¿Qué cuesta una suscripción rechazada?»Una suscripción rechazada no arranca ningún vigilante. Una rechazada por su tipo, como una colección de primer nivel, no cuesta ninguna llamada a GitLab; una rechazada porque el recurso no se pudo leer ha gastado esa única lectura. En 2026-07-28 el rechazo es el error JSON-RPC que responde al propio subscriptions/listen, enviado antes de cualquier confirmación, así que un cliente que lee la respuesta del listen lo ve. El cliente del SDK de Go (v1.8.0) nunca lo ve, porque envía el listen sin esperar su respuesta (entrada aguas arriba); el rechazo surte efecto igualmente.
¿Cómo se detecta un cambio?
Sección titulada «¿Cómo se detecta un cambio?»Un vigilante relee el recurso a través del mismo handler que usa resources/read y compara un SHA-256 de lo que devuelve. Para un job, y para cada job de la lista de jobs de un pipeline, el hash deja fuera duration y queued_duration: GitLab recalcula ambas a partir del reloj en cada lectura mientras un job se ejecuta, y un job en ejecución se sondea cada cinco segundos, así que incluirlas notificaba en cada sondeo sin que nada se hubiera movido. Un cambio de estado, de motivo de fallo o de cualquier otro campo sigue notificando, y una lectura sigue devolviendo ambas duraciones. La primera lectura ocurre de forma síncrona al aceptar la suscripción, antes de decirle al cliente que está suscrito, así que a un suscriptor nunca se le notifica un cambio anterior a su suscripción.
Esa primera lectura es también la comprobación de autorización: se hace con el propio token del suscriptor, así que una URI que el token no puede leer se rechaza de entrada en lugar de aceptarse y no dispararse nunca en silencio. Un vigilante es por URI y por credencial, así que una segunda sesión que vigila la misma URI con el mismo token se une al vigilante existente, espera a esa misma primera lectura y no cuesta nada más. La comprobación tiene también su otra cara: un token que pierde el acceso se sigue usando hasta que el siguiente sondeo responde 401, 403 o 404, lo que es como mucho un intervalo, y hasta diez minutos para un vigilante ralentizado; esa respuesta es lo que detiene al vigilante.
¿Con qué rapidez, y a qué coste?
Sección titulada «¿Con qué rapidez, y a qué coste?»La cadencia de sondeo se adapta al recurso:
| Estado del recurso | Intervalo | Ejemplo |
|---|---|---|
| Trabajo en curso | 5s (mínimo) | un pipeline running, un merge request opened |
| Asentado | 60s | un pipeline success, un issue closed |
| Sin campo de ciclo | 15s | una página wiki, un archivo, un label |
| Concesión degradada | 10min | 30 minutos sin ninguna petición en la sesión |
Trabajo en curso significa un pipeline o job en created, waiting_for_resource, preparing, pending o running (una lista de jobs está activa mientras lo esté cualquiera de sus jobs), o un merge request o issue en opened o locked. Cualquier otro estado cuenta como asentado, manual y scheduled incluidos: ambos esperan algo ajeno a CI, una persona o un reloj, así que sondearlos al mínimo gastaría presupuesto en un recurso que no puede cambiar hasta que eso ocurra. Las wikis, los archivos, las ramas, las etiquetas, los tableros y los demás tipos no tienen estado que leer, así que se sondean a la base de 15 segundos.
Nada se retira por estar terminado. GitLab no tiene estado terminal: un pipeline reintentado reutiliza su ID y vuelve a ejecutarse, un issue cerrado se reabre, un merge request fusionado aún puede editarse, así que un vigilante que se detuviera en success quedaría ciego ante un cambio real que su suscriptor pidió conocer.
La latencia está acotada por la cadencia por diseño. Los canales de eventos que GitLab ofrece hoy y que se pueden consultar sin aceptar conexiones entrantes (la Events API, las suscripciones GraphQL sobre ActionCable y las peticiones condicionales) se probaron en vivo uno a uno y se descartaron, como recoge ADR-0017.
¿Cuáles son los límites?
Sección titulada «¿Cuáles son los límites?»| Límite | Valor | Por qué |
|---|---|---|
| Vigilantes por credencial | 10 | Un vigilante sondea con el propio token del suscriptor, así que este es el techo del sondeo de una credencial |
| Vigilantes por proceso | 512 | Crear una credencial es una llamada a la API, así que un número por credencial se multiplica por cuantas tenga quien llama; solo este acota el proceso |
| Intervalo mínimo | 5s | Diez vigilantes al mínimo ya hacen 120 peticiones por minuto |
| Pausa por límite de tasa | 30s, doblando hasta 5 minutos | GitLab aplica sus límites por usuario, así que un 429 pausa todos los vigilantes de esa credencial |
Una credencial es una entrada del pool, un par de token y URL de GitLab, en modo HTTP. En stdio el proceso atiende a una sola credencial, así que sus diez vigilantes son el límite y el techo del proceso nunca llega a aplicarse.
La aritmética de peticiones. Diez vigilantes hacen 120 peticiones por minuto al mínimo, 40 a la base de 15 segundos, 10 una vez asentados y una una vez ralentizados. En todo un proceso, 512 vigilantes son unas 102 peticiones por segundo al mínimo y unas 34 a la base. En una instancia autogestionada, el límite de peticiones a la API autenticada de GitLab es por defecto de 7200 peticiones por hora y usuario, es decir 120 por minuto, y está desactivado hasta que un administrador lo activa (documentación de GitLab). Diez vigilantes al mínimo lo gastarían entero mientras ese mismo usuario hace llamadas a herramientas, y por eso el mínimo no es más bajo. GitLab.com publica sus límites por plan en su página de límites de peticiones.
En el tope por credencial, el vigilante ralentizado desde hace más tiempo cede su sitio. Una suscripción nueva desaloja el vigilante de esa credencial que lleva más tiempo ralentizado en lugar de rechazarse: un suscriptor que espera ahora gana el hueco a uno al que su propia inactividad ya ralentizó, y el listen del vigilante desalojado recibe como respuesta watcher_evicted. Si ninguno de los diez está ralentizado, la suscripción nueva se rechaza.
El techo del proceso solo rechaza. Hacer sitio ahí supondría detener el vigilante de otra credencial para arrancar este, y nada de la suscripción que llega la hace valer más que la que ya está en marcha. Rechaza antes de desalojar nada, así que una credencial en su propio tope conserva su vigilante ralentizado cuando el proceso está lleno y se le dice que se alcanzó el techo de todo el servidor: un rechazo nunca te cuesta una suscripción que ya tenías.
Ninguno de los dos techos es configurable, y ambos rechazos dicen cuál se alcanzó: el del proceso dice server-wide con todas las letras, y el de la credencial lleva su propio recuento y su límite, para que un operador que lea el error sepa si mirar al que llama o al despliegue. La palanca que sí tiene un operador es --max-http-clients, que decide cuántas credenciales atiende el pool a la vez (La palanca y lo que cuesta).
Conviene saber lo que cuesta el techo del proceso antes de toparse con él. Cincuenta y una credenciales en su tope ocupan 510 de los 512 huecos, y los conservan mientras vivan sus vigilantes, hasta 24 horas cada uno, mientras cualquier otra suscripción se rechaza. Se acepta a propósito: la alternativa es un sondeo sin límite desde un solo proceso, y un reparto por credencial no acotaría nada, porque acuñar una credencial es una llamada a la API. Un despliegue saturado lo dice en su propio log, con una línea WARN por rechazo.
Ciclo de vida y renovación
Sección titulada «Ciclo de vida y renovación»Un vigilante sin renovar no muere en silencio: se ralentiza. Tras 30 minutos sin ninguna petición en la sesión suscriptora, baja a un sondeo de 10 minutos, y cualquier petición en esa sesión restaura la velocidad completa. Un vigilante termina, o se ralentiza, por estos motivos y no por otros:
| Evento | Efecto | ¿Se le dice al cliente? |
|---|---|---|
| El cliente se desuscribe o cancela su listen | Se detiene cuando se va el último suscriptor | Lo pidió él |
| La sesión se desconecta | Se detiene | Ya no está |
| 30 minutos sin peticiones en la sesión | Se ralentiza a un sondeo de 10 minutos | _meta en la siguiente notificación |
| Cualquier petición en la sesión | Restaura la velocidad completa | No hay nada que decir |
| El recurso responde 401, 403 o 404 | Se detiene | El listen se responde con resource_gone |
| 24 horas, renovaciones incluidas | Se detiene | El listen se responde con lifetime_reached |
| Desalojado para hacer sitio en el tope por credencial | Se detiene | El listen se responde con watcher_evicted |
| El pool suelta la entrada de esta credencial | Se detiene | El listen se responde con credential_evicted, credential_reset, credential_revoked o credential_insufficient; con --stateless=false la sesión se termina |
| El servidor se apaga | Se detiene | El listen se responde con shutdown (HTTP) |
Un resources/subscribe heredado no recibe ninguna de esas respuestas, porque no mantiene ninguna petición abierta; consulta Por qué terminó una suscripción.
El plazo de renovación ralentiza un vigilante; no lo termina. MCP no define plazos de renovación, ni caducidad, ni mensaje de caducidad, y su única notificación significa «esto cambió, vuelve a leerlo». Un vigilante que se retirara en un plazo se quedaría callado de una forma que ningún cliente podría distinguir de «aún no ha pasado nada», así que ralentizarse es lo único que un servidor puede afirmar con honestidad.
Qué lo renueva. Cualquier petición que llegue al servidor en la sesión: una llamada a herramienta, una lectura de recurso, un prompt, un autocompletado, un listado. Un ping y el handshake de initialize no, deliberadamente, porque un ping demuestra que un socket está abierto y no que alguien espere al otro lado; las notificaciones tampoco. Desde 2026-07-28 un cliente puede responder a tools/list, prompts/list o a un resources/read repetido desde su propia caché, y entonces aquí no llega nada que renueve nada, lo cual es correcto: un cliente que se sirve de su caché no es prueba de que nadie esté esperando.
Un subscriptions/listen abierto renueva sus propios vigilantes, cada diez minutos mientras siga abierto. En el transporte sin estado predeterminado la sesión de un listen no ve más petición que el propio listen, así que nada más lo renovaría, y un listen abierto es mejor prueba de un suscriptor que espera que el tráfico. Por eso el plazo de renovación gobierna el resources/subscribe heredado, y el tope de 24 horas se aplica a todo vigilante.
Un vigilante también puede terminar porque el pool soltó su credencial. En modo HTTP los vigilantes pertenecen a una entrada del pool, y --max-http-clients acota cuántas entradas mantiene un proceso. La presión de tamaño prefiere una entrada que no esté atendiendo una suscripción, y se lleva una que sí solo cuando todas las entradas del pool atienden alguna; entonces los vigilantes de la credencial se detienen, sus peticiones subscriptions/listen abiertas se responden, y con --stateless=false se terminan las sesiones que ningún listen terminó. La credencial no tiene nada malo: reconecta y vuelve a suscribirte.
¿Qué lleva una notificación?
Sección titulada «¿Qué lleva una notificación?»Cada notificación lleva en _meta, bajo io.github.jmrplens/watch, el estado del vigilante que la produjo. MCP no registra ninguna clave para la vida de una suscripción, así que esta lleva el espacio de nombres de este proyecto:
{ "method": "notifications/resources/updated", "params": { "uri": "gitlab://project/42/pipeline/99", "_meta": { "io.github.jmrplens/watch": { "state": "active", "renewBy": "2026-08-25T19:30:00Z", "pollIntervalMs": 5000, "renewedByActivity": true } } }}| Campo | Significado |
|---|---|
state | active a velocidad completa, slow una vez ralentizado |
renewBy | Cuándo vence el plazo de renovación y el vigilante se ralentiza (en uno ralentizado, cuándo venció) |
pollIntervalMs | La cadencia a la que sondea ahora el vigilante, en milisegundos |
renewedByActivity | Siempre true: cualquier petición en la sesión aplaza de nuevo renewBy |
Ningún cliente está obligado a leer nada de esto, y ninguno conocido hoy lo hace. Está ahí porque un suscriptor no tiene otra forma de saber que su vigilante se ralentizó, y todo lo que dice es cierto lo mire alguien o no.
Por qué terminó una suscripción
Sección titulada «Por qué terminó una suscripción»En el protocolo 2026-07-28 una suscripción es una petición subscriptions/listen que el cliente deja abierta, y el servidor la termina respondiéndola. Todo final que inicia el servidor dice cuál fue, en el _meta del resultado, junto al identificador de suscripción que el SDK estampa ahí:
{ "jsonrpc": "2.0", "id": 1, "result": { "_meta": { "io.modelcontextprotocol/subscriptionId": 1, "io.github.jmrplens/watch-end": { "reason": "credential_evicted", "detail": "the server released this credential's pooled entry under capacity pressure; reconnect and subscribe again, the credential itself is still valid" } } }}reason | Qué ocurrió | Qué hacer |
|---|---|---|
credential_evicted | La presión de tamaño se llevó la entrada del pool de esta credencial | Reconecta y vuelve a suscribirte ya; la credencial sigue siendo válida |
credential_reset | La entrada se reclamó por inactividad, antigüedad o una reconstrucción | Reconecta y vuelve a suscribirte; la credencial no tiene nada malo |
credential_revoked | GitLab rechazó la credencial | Reautentícate primero; el mismo token se rechazará de nuevo |
credential_insufficient | GitLab acepta la credencial, que no lleva ni read_api ni api | Sustituye el token por uno que los lleve; sus scopes no pueden cambiar |
resource_gone | El recurso vigilado respondió 401, 403 o 404 | Revisa el acceso; vuelve a suscribirte solo si el recurso reaparece |
lifetime_reached | Se agotó el tope absoluto de vida de 24 horas | Vuelve a suscribirte; es lo esperado en un cliente de larga duración |
watcher_evicted | Un vigilante tuyo, degradado, se detuvo en el tope de vigilantes | Vuelve a suscribirte y mantén la sesión activa |
shutdown | El servidor se está deteniendo | Reconecta; tras un balanceador te atenderá otra instancia |
detail es una frase de consejo, para que un cliente que no reconozca la palabra del motivo pueda actuar igualmente. El campo status aparece solo en resource_gone, y solo cuando la lectura fallida trajo un estado HTTP; se retransmite sin interpretarlo. GitLab responde 404 a un recurso que quizá no puedas ver, precisamente para que no se distinga de uno que no existe, así que un 404 aquí no significa «borrado». Un 401 tampoco significa que el token esté muerto: GitLab responde con 401 a algunos permisos que faltan. Solo credential_revoked dice que GitLab rechazó la propia credencial, y en modo HTTP se envía únicamente cuando GitLab lo ha dicho o la sonda de credenciales lo ha confirmado (Llamadas rechazadas).
El vocabulario es cerrado y se publica como subscriptions.end_reasons en la ficha del servidor enumerativa en /.well-known/mcp/server-card.json, que solo se sirve en modo HTTP (la ficha en /server-card no lleva capacidades). En stdio no hay endpoint del que leerla, y allí pueden llegar a un cliente tres de los motivos: resource_gone, lifetime_reached y watcher_evicted. Un proceso stdio que se detiene no responde nada a un listen abierto, como recoge ADR-0015. Trata un valor desconocido como «un final que este cliente no conoce», nunca como «cualquier final».
El servidor no envía notifications/cancelled para un listen que termina, aunque 2026-07-28 lo pide: el SDK de Go no ofrece forma de enviarlo para una petición que hizo el cliente (entrada aguas arriba). Lo que le dice a un cliente que el stream ha terminado es el resultado del listen.
Un final que causas tú no lleva motivo, precisamente porque lo causaste tú, y el resultado quizá ni siquiera te llegue. Tampoco lo lleva un resources/subscribe de la era de sesiones con --stateless=false: no mantiene ninguna petición abierta, así que no hay nada que responder, y terminar la sesión es el único final que el protocolo le ofrece. Un cliente así ve terminar su stream SSE independiente y recibe su siguiente petición respondida como una sesión nueva. La capacidad logging sería el único sitio que queda para llevar una frase de aviso, y este servidor no la declara a propósito, porque SEP-2577 la dejó obsoleta. Usa subscriptions/listen si quieres el motivo.
Notas de transporte
Sección titulada «Notas de transporte»- stdio: funcionan ambos métodos, en ambas generaciones del protocolo.
- HTTP, modo sin estado (el predeterminado): solo funciona
subscriptions/listen. Elresources/subscribeheredado se rechaza con un error explicativo, porque cada POST sin estado recibe su propia sesión, que se cierra con la respuesta, así que una suscripción aceptada nunca podría notificarse.subscriptions/listenmantiene su petición abierta, y eso el modo sin estado sí lo admite. - HTTP,
--stateless=false: solo funcionaresources/subscribe. El despliegue no ofrece el protocolo2026-07-28, así que una petición que lo nombra recibe un400con las revisiones que sí negocia (Revisiones del protocolo que negocia este servidor), y la ficha del servidor marcasubscriptions/listencomo no disponible.
¿Cómo se comportan los clientes?
Sección titulada «¿Cómo se comportan los clientes?»| Cliente | Protocolo | Notas |
|---|---|---|
| VS Code | 2025-11-25 | Se suscribe a cada recurso que lee y encamina las actualizaciones a su pipeline de cambios de archivo, no al chat |
| Cursor | 2025-11-25 o anterior | Envía resources/subscribe incluso a servidores que anuncian subscribe: false |
| Cliente del SDK de Go (v1.8.0) | 2026-07-28 | Envía subscriptions/listen sin esperar la respuesta, así que nunca ve un rechazo ni el resultado que termina una suscripción, y un segundo Subscribe a una URI rechazada no envía nada hasta Unsubscribe |
| Tu propia integración | cualquiera | El _meta descrito arriba está ahí para ti; sin él, la notificación sola basta |
Códigos de error
Sección titulada «Códigos de error»Un resources/subscribe heredado rechazado lleva un código JSON-RPC deliberado, nunca el code: 0 accidental con el que se serializaría un error plano:
| Rechazo | Código | ¿Reintentar más tarde? |
|---|---|---|
| URI deliberadamente no suscribible | -32602 (parámetros inválidos) | No: elige una URI suscribible |
| Recurso ilegible en la lectura de autorización (401/403/404) | -32602, el mismo código con el que el SDK responde un resources/read desconocido | Solo si el recurso aparece o se concede el acceso |
| GitLab limitó la primera lectura por tasa | -42900 (demasiadas peticiones) | Sí: tras la espera de GitLab |
| Los vigilantes de tu credencial en su cupo (10) sin ninguno desalojable | -42900, con unsubscribe from one and retry en el mensaje | Sí: cuando termine una de tus vigilancias |
| El límite de tasa propio de este servidor, antes de consultar a ningún vigilante | -42900 | Sí: tras una breve espera |
| Vigilantes por proceso en 512, o servidor apagándose | -50300 (no puede atenderla ahora), con server-wide en el mensaje del tope | Sí: la condición es transitoria |
| Fallo transitorio de GitLab en la primera lectura | -32603 (error interno) | Sí |
resources/subscribe en HTTP sin estado | -32600 (petición no válida para este estado de sesión) | No: usa subscriptions/listen |
El mensaje junto al código lleva el detalle del fallo de GitLab tal cual. El código dice de qué clase de rechazo se trata y refleja el estado HTTP que recibiría lo mismo en una pasarela: -42900 (429) cuando quien llama hizo demasiadas peticiones o tiene demasiado de algo, -50300 (503) cuando el servidor no puede atender la petición ahora, la haga quien la haga. Los dos son transitorios. Sustituyeron a -32000, que la especificación reserva para códigos asignados antes de que dividiera el rango y al que un cliente no debe atribuir ningún significado. En el protocolo 2026-07-28 un subscriptions/listen rechazado lleva los mismos códigos, como el error JSON-RPC que responde al propio listen, enviado antes de cualquier confirmación; la última fila nunca se le aplica, porque HTTP sin estado sí sirve un listen. Un listen tiene además rechazos propios: los topes de streams de listen abiertos, GITLAB_MCP_MAX_LISTEN_STREAMS por credencial (64 por defecto), respondido con -42900 y close one and retry, y 512 por proceso, respondido con -50300 y retry later. El límite de peticiones por credencial de este propio servidor (--rate-limit-rps en modo HTTP, GITLAB_MCP_RATE_LIMIT_RPS en stdio), que no es el de GitLab, rechaza ambos métodos antes aún, con el -42900 de la tabla, antes de consultar a ningún vigilante. El cliente del SDK de Go (v1.8.0) no ve ninguno de ellos en un listen, y un segundo Subscribe suyo a una URI rechazada no envía nada hasta Unsubscribe.
Preguntas frecuentes
¿Qué son las suscripciones a recursos MCP?
Un cliente pide al servidor vigilar un recurso, con resources/subscribe en el protocolo 2025-11-25 y anteriores o con subscriptions/listen desde 2026-07-28, y el servidor envía notifications/resources/updated cuando su contenido cambia. GitLab MCP Server lo atiende mediante sondeo: un vigilante relee la URI suscrita a través del mismo handler que usa resources/read y notifica solo cuando el contenido cambió de verdad. GitLab no ofrece ningún canal push que un servidor local pueda consumir, así que el sondeo con límites explícitos es la implementación honesta.
¿A qué recursos se puede suscribir?
26 tipos de recurso, objetos únicos más tres listas de un solo padre: un proyecto o grupo; un pipeline, su lista de jobs, el último pipeline, un job concreto; un merge request con sus discusiones y notas; un issue; un deployment, environment o feature flag; una release, tag o rama; un milestone o label (de proyecto o grupo); un board; una deploy key; un snippet; una página wiki; y un archivo del repositorio. Las colecciones se rechazan — suscribirse a una lista de issues notificaría cambios que el suscriptor nunca pidió.
¿Con qué rapidez llegan las notificaciones?
La latencia es la cadencia de sondeo: 5 segundos mientras el recurso está activo (un pipeline en ejecución, un merge request abierto), 15 segundos para un recurso sin estado que leer, 60 segundos una vez asentado y 10 minutos después de que un vigilante se ralentice por 30 minutos sin peticiones en su sesión. Cualquier petición en la sesión restaura la velocidad completa, y un subscriptions/listen abierto mantiene sus propios vigilantes a velocidad completa mientras siga abierto. El _meta de cada notificación informa del estado del vigilante y su cadencia actual.
¿Cuánto cuesta vigilar contra mi límite de peticiones de GitLab?
Los vigilantes sondean con el token del propio suscriptor, con un tope de 10 vigilantes por credencial. Los diez al mínimo de 5 segundos hacen 120 peticiones por minuto, que es todo el presupuesto de una instancia autogestionada cuyo administrador activó el límite de peticiones a la API autenticada de GitLab con su valor por defecto de 7200 peticiones por hora. A la base de 15 segundos esos mismos diez hacen 40 por minuto, y diez vigilantes ralentizados hacen una. Un 429 de GitLab pausa todos los vigilantes de esa credencial con retroceso exponencial.