Skip to content

ADR-053: Catálogo MCP convergente — taxonomía de exclusiones, «huecos = 0» y carga del catálogo en el asistente

Estado: Aceptado Fecha: 2026-10-28 Autores: Giampiero (mantenedor principal); Épica MCP (bloques 0 a 8) Relacionados: ADR-019 (el MCP es cliente del gateway), ADR-037 (bandera por entidad), ADR-048 (bloqueo de la plataforma), ADR-045 (anulación con segunda persona), ADR-006 (residencia)

Nota de numeración: ADR-049 queda libre como hueco histórico. No se rellenan huecos para no fechar una decisión fuera de orden.


Contexto

El 2026-10-28 el censo del MCP (487 operaciones del API, once servicios) dio 216 con herramienta, 172 excluidas a propósito y 99 huecos reales (69 % de lo ofrecible). Medirlo destapó además:

  1. Defectos vivos del catálogo existente, no huecos:
  2. radicar_documento no declaraba medio_conservacion, que POST /documents exige a propósito (Decreto 103/2015 art. 40, D-2). Toda radicación hecha por el asistente o por tramitar_peticion_ciudadana devolvía 422, y nadie lo vio porque los tests del MCP mockean el gateway y el mock nunca discute el cuerpo.
  3. rechazar_anulacion anunciaba observacion opcional (el backend exige ≥10), reclasificar_radicado y solicitar_desclasificacion no ofrecían excepcion_reserva, y aprobar_anulacion se ofrecía con una tarjeta aunque aprobar exige ver el documento.
  4. El asistente no distinguía asistente_bloqueado_plataforma (ADR-048): decía «su entidad no lo activó» aunque lo hubiera apagado la plataforma.
  5. Instrumentos defectuosos: scripts/censo-esquemas.sh abortaba a mitad si una imagen no estaba construida (la salida del build iba al fichero JSON) con código de salida 0 tras un | tail; y la cobertura publicada se escribía a mano (470/217/168/85 frente a 487/216/172/99).
  6. Gates de backend débiles que el MCP heredaría: indice/rebuild (mutación a nivel 1), PATCH /tipos-documentales (nivel 1) y el descarte/reintento de eventos fallidos a nivel 1 en workflow, signature y notification (a nivel 3 en knowledge): la misma operación con dos umbrales.
  7. Tamaño del catálogo: las 217 tools ocupaban 132.434 caracteres de name + description + input_schema; con 280, ≈ 170.000 (del orden de 45-50 mil tokens) que el bucle del asistente reenvía en cada iteración, sin caché.

Decisión

1. Taxonomía cerrada de motivos de exclusión

Una operación solo se excluye con uno de estos motivos, citado en EXCLUIDAS_A_PROPOSITO (scripts/censo_mcp.py) y declarado en NO_OFRECIDAS (services/mcp-server/app/services/scope.py) con su vía alternativa:

Código Motivo
VER el acto exige ver el documento y el asistente no lo muestra
2FA consume un segundo factor de la persona (TOTP/OTP) o es firma personal. El sello institucional de un artefacto que produce el SISTEMA (firmar_indice, reintentar_sello_acta_transferencia) no consume factor y sí se ofrece
EGR egreso o ingreso masivo de información (paquetes, ZIP)
PLA administración de plataforma o disparo de sistema
FRO ensancha la frontera de autorización o mueve credenciales
REG es la regla con la que se mide lo demás (calendario, catálogos, reglas de reparto, esquema de metadatos)
ACT acto administrativo con acta y responsable, o constancia de un acto tomado fuera del sistema (retención legal, valoración, adopción del índice, avalar un lote de eliminación, aprobar, convalidar o derogar una versión de la TRD/TVD, aprobar el PINAR, decidir eliminar sobre un radicado)
SES mecánica de sesión, o lo que el servidor ya lee para resolver la identidad
BIN binario o artefacto, no una consulta (de salida), ni un artefacto que el agente tendría que componer (de entrada: sign-xml, verificar con un hash aportado)
MAS acto masivo irreversible que la tarjeta no puede resumir de forma contrastable (lotes de radicación o de expedientes, conciliar fichas en lote) y, desde ADR-072 D6, el acto en masa que no se ofrece por criterio (anular, inactivar, retipificar, retención legal, devolver, finalizar o archivar varios, y reprocesar los fallidos de un lote)
SEN dato sensible (salud, fallecimiento) sin nivel_seguridad, al que no alcanza el recorte de residencia
COR vía corta que deja inalcanzable el circuito con acta (transferir sin acta ni FUID)

Cada entrada de EXCLUIDAS_A_PROPOSITO lleva su código ([VER] …) y cada entrada de NO_OFRECIDAS su codigo; shared/tests/test_las_exclusiones_llevan_su_codigo.py valida ambos contra esta tabla (leída de aquí, no copiada) y que el censo y el marco de alcance digan el mismo código. Un solo criterio ACT: las constancias de actos tomados fuera del sistema se EXCLUYEN todas —avalar un lote, aprobar/convalidar TRD y TVD, aprobar el PINAR—, igual que ya lo estaban la valoración y la adopción. derogar una versión de la TRD o la TVD también es ACT (decisión 2026-10-29): retirar la tabla que rige es un acto administrativo de la entidad, igual que aprobar y convalidar, y por eso derogar_version_trd y derogar_version_tvd salieron del catálogo. La selección dentro de la valoración de la TVD es acto del Comité (ACT) y registrar_seleccion_trd no lo es: ejecuta la disposición S ya definida en una TRD vigente.

Los tres últimos (MAS, SEN, COR) son nuevos en esta épica; los nueve primeros ya estaban en NO_OFRECIDAS o en las exclusiones del lote C. Entra por el mismo criterio que la lista previa: lo que la tarjeta de confirmación no puede garantizar no se ofrece, y la ausencia se declara.

Salen del catálogo (exclusión) en esta épica: aprobar_anulacion (VER; rechazar y retirar, que paran, se quedan), y no se ofrecen aceptar_sugerencia (VER: ver §5), los lotes (MAS), sign-xml (2FA), el fundamento y el sobre de una firma inobtenible y el paquete de entrega (SEN), transfer (COR), la credencial de un operador postal (FRO), las reglas de reparto en escritura (REG), la reasignación en cascada (FRO), la conciliación en lote (MAS), las constancias de actos de autoridad (ACT: avalar_lote_eliminacion, aprobar_version_*, convalidar_version_*, derogar_version_*, aprobar_plan_pinar) y dar el visto bueno de workflow (VER; negar_visto_bueno sí se ofrece, solo con aprobar=false).

2. Invariante «huecos = 0»

shared/tests/test_toda_operacion_tiene_postura_mcp.py: toda operación del API tiene herramienta o exclusión con referencia. Una ruta nueva decide en el mismo cambio que la crea. El censo es estático (lee los decoradores de los routers) para que corra en CI sin pila; coincide con el del OpenAPI de los once servicios (487 = 487, medido).

3. La cobertura publicada sale del censo estático

scripts/censo_mcp.py --estatico --escribir docs/es/mcp-flujos.md reescribe SOLO el bloque entre censo:inicio y censo:fin; --frase es|en imprime la frase «N de M operaciones ofrecibles (P %)» que citan roadmap y api.md. shared/tests/test_la_cobertura_publicada_es_la_del_censo.py las compara byte a byte. El censo por OpenAPI queda como contraste manual (--estatico --openapi DIR falla si difieren en una operación que no sea de SISTEMA) y vigila el universo: un servicio que no responde, rutas registradas fuera de los decoradores. censo-esquemas.sh construye las imágenes antes de volcar, valida el JSON dentro del if y devuelve código distinto de cero si falta un servicio.

4. Los campos obligatorios del endpoint los declara la tool

shared/tests/test_los_obligatorios_del_endpoint_estan_en_la_tool.py lee el router y el schema Pydantic del cuerpo (AST, sin mocks del gateway) y exige que cada campo obligatorio figure en los params de la tool. Al escribirlo destapó dos defectos más del mismo tipo: crear_transferencia sin objeto y retencion_tvd sin base_date (ambas devolvían 422 siempre; preparar_transferencia heredaba el primero).

5. Escalón de confirmación: se teclea un dato que el SERVIDOR lee

La versión inicial confirmaba tecleando la acción (conservar/descartar): dos valores que se teclean de memoria. La ronda de auditoría lo sustituyó por un dato del objeto que el servidor lee al proponer (§9): el código del expediente (registrar_seleccion_trd, archivar_expediente), el número del radicado (fijar_disposicion_radicado), el nombre de la ficha que sobrevive (fusionar_terceros) y el código del tipo (cambiar_difusion_tipo_documental). Sin dato legible no hay propuesta. fijar_disposicion_radicado ya no ofrece eliminar (ACT) ni la marca heredada marcado_eliminacion.

  • aceptar_sugerencia se ofrecería solo si la tarjeta mostrara el contenido de la sugerencia. No puede: el endpoint no la devuelve, no existe una lectura que la traiga y mostrar lo que el modelo diga de ella sería enseñar lo que el modelo quiere, no lo que quedó guardado. Pasa a exclusión (VER).

6. Gates de backend subidos en el mismo cambio que las tools

POST /expedientes/{id}/indice/rebuild 1→3; POST /expedientes/{id}/indice/firmar 1→3 en USUA_PERM_EXPEDIENTE y en PERM_FIRMA (la tool declara el segundo en permisos_adicionales y el guardián test_permisos_declarados.py lee TODOS los require_permission de la ruta, no solo el primero); POST /workflows/reassign/cascade, las escrituras de /workflow/rules, las de /metadata/elements|templates y /expediente-metadata/templates y POST /batch/expedientes 1→3 (aunque las tools estén excluidas: el gate débil se cierra igual, y la UI lo espeja); PATCH /tipos-documentales/{id} 1→2 y 409 trd_version_inmutable si la serie de origen o la de destino no está en borrador cuando cambia serie, obligatoriedad o plantilla (como series y TVD); la tool solo ofrece nombre y baja, y publicable va en cambiar_difusion_tipo_documental, con el código tecleado; discard/replay de eventos fallidos 1→3 en workflow, signature y notification (como knowledge). Cada gate con su espejo en el frontend (colas de errores: escribir con PERM_DLQ_ADMIN ≥ 3; editar tipos con USUA_PERM_TRD ≥ 2) y sin entradas en la lista de declaradas de test_escritura_exige_nivel.py.

7. Bloqueo de la plataforma

Identity.asistente_bloqueado_plataforma (solo un true literal). exigir_asistente responde 403 asistente_bloqueado_plataforma ANTES que asistente_deshabilitado; mi_alcance añade asistente: {habilitado, bloqueado_plataforma} y el motivo de las tools de conocimiento dice que no depende del administrador. El bloqueo NO se pasa como segundo booleano a visible_tools: la bandera ya llega efectiva. El MCP genérico sigue vivo (ADR-037 §3).

8. Carga del catálogo en el asistente: caché ahora, búsqueda de tools después

A. Caché del bloque tools B. Búsqueda de tools (tool_search_tool_bm25_20251119, defer_loading) C. Filtrar por flujo antes de llamar
Cambio cache_control en la última tool ~20 tools núcleo + el resto defer_loading: true un clasificador previo elige 1-3 flujos
Riesgo ninguno funcional el modelo puede no encontrar la tool; una vuelta más un clasificador que se equivoca esconde capacidades en silencio

Se implementa A: con_cache_del_catalogo marca la última tool (orden de render tools → system → messages; visible_tools ya es determinista por identidad y no se reordena) y se registra cache_read_input_tokens en el log. tests/test_el_asistente_cachea_el_catalogo.py fija la marca y el orden estable. B queda como trabajo futuro, sin implementar: se evaluará con conversaciones reales, y solo pasa a hacerse si, con A, la lectura del caché no baja de forma sostenida el costo de los turnos de ROOT/administradores amplios o si la elección entre tools parecidas (cuatro familias de DLQ) degrada de forma medible. C se descarta: es un recorte silencioso de capacidades. Los clientes MCP externos no cambian (tools/list completo). El marco de alcance debe seguir listando TODO aunque algún día se difiera la carga.

9. La tarjeta con contexto resuelto en el servidor (ronda de auditoría)

Una tarjeta que dice «fusionar a1b2… con c3d4…» no deja decidir, y lo que confirma el modelo no es fiable. Tool y Procedimiento declaran tarjeta: una o varias Lectura (una tool de LECTURA del catálogo, sus argumentos tomados del acto, los campos que se muestran y, opcionalmente, la ruta del valor que se teclea). write_gate.preparar_con_contexto las hace con el token de la persona al proponer: el clearance, el tenant y el RBAC del objeto mandan igual que en cualquier consulta, y si no puede leerlo (403/404) o la lectura no trae el valor de confirmación no hay propuesta (fail-closed). Los campos leídos se añaden a la tarjeta y el valor a teclear viaja firmado en el token (frase, reclamo opcional: los tokens sin él conservan su forma); ejecutar lo exige con frase_requerida(tool, args, frase), y el cliente no puede cambiarlo por uno más fácil. Una Lectura puede ser condicional (cuando=("accion", "vincular")). GET /terceros/{id} (nueva) existe para esto: nombre e identidad de la ficha, sin contacto, con el predicado de clearance de la cola para las provisionales.

Un Paso de procedimiento puede llevar exigir=(campo, valor): el paso se hizo (200) pero si la respuesta no es lo que el trámite exige el procedimiento se detiene y no reporta «hecho». archivar_expediente sella antes de verificar y exige valido=true.

10. Respuestas: quien redacta no se aprueba a sí mismo

proyectar_y_radicar_respuesta y despachar_respuesta creaban el borrador y lo APROBABAN en el mismo acto, y el gate del circuito solo ata si hay un circuito abierto, que un borrador recién creado nunca tiene: el visto bueno y la firma se saltaban por construcción (Ac. AGN 060/2001 art. 12). Ahora ningún procedimiento aprueba: proyectar_respuesta termina en el borrador (con radicado_padre_id, que vincula la respuesta con la entrada que contesta) y despachar_respuesta recibe un borrador_id YA aprobado, lo radica y registra el envío. radicar_borrador ofrece tipo_documental y origin_dept_code por si el borrador no los deriva de su plantilla (una Salida los exige, 422 datos_minimos_incompletos).

11. Vocabulario

medio_conservacion pierde hibrido (describe un expediente, no el original de un documento; Decreto 103/2015 arts. 38 y 40; migración document 057, CHECK NOT VALID, sin backfill). El objeto de una transferencia es una lista cerrada del Anexo FUID (transferencia primaria, secundaria, valoración de fondos acumulados, fusión o supresión, inventario individual) y «primaria»/«secundaria» deben coincidir con el tipo. Descartar un evento de reclassified en la DLQ de conocimiento es 409 reclassified_no_descartable: se reintenta, porque descartarlo dejaría la copia derivada con el nivel viejo.

Consecuencias

  • 63 herramientas nuevas y, tras la ronda de auditoría, 272 en total (99 de escritura, 35 irreversibles, 5 con valor tecleado), cobertura 100 % (270 de 270 ofrecibles) y 218 operaciones excluidas con código y motivo (488 en total: GET /terceros/{id} es nueva).
  • derogar_version_trd y derogar_version_tvd salen del catálogo (ACT): un cliente MCP externo que las usara recibirá «tool no encontrada».
  • aprobar_anulacion sale del catálogo: un cliente MCP externo que la usara recibirá «tool no encontrada».
  • Los renombres de las familias de DLQ llevan sufijo de servicio (_tramite, _firma, _conocimiento); las cuatro de notification conservan su nombre para no romper clientes.
  • Datos personales al proveedor del modelo (R5). Los remitentes por conciliar (nombre, documento declarado, contacto) y los prestatarios de una unidad declaran ahora el nivel_seguridad de los radicados a los que están atados (FichaPorConciliar, GrupoPorConciliar, PrestamoResponse), así que el recorte por residencia (ADR-006) los alcanza cuando el radicado es reservado. DeadLetterDetail de los cuatro servicios lleva el nivel del sobre, y 3 (el máximo) si no lo trae. shared/tests/test_contenido_declara_su_nivel.py vigila también observaciones y los campos de identidad (documento_declarado_numero, telefono, direccion, tercero_telefono); lo que sigue en su lista de excepciones es directorio, buzón o contacto público que no cuelga de un radicado. El índice de información clasificada y reservada (Ley 1712 art. 20) es content-free por ley y público: su grado va en grado_clasificacion y no en nivel_seguridad, porque con ese nombre la barrera lo tomaría por contenido reservado y la herramienta saldría vacía.
  • Activar el asistente por entidad (ADR-037) es la autorización de la transferencia internacional de los datos personales de NIVEL 1 (Ley 1581/2012 art. 26): lo público y los datos del directorio (terceros confirmados, contactos, prestatarios de unidades públicas) viajan al proveedor del modelo cuando la entidad enciende la bandera, y es la entidad —responsable del tratamiento— quien lo decide al encenderla. Desde el nivel 2 el contenido no sale (ADR-006). Queda dicho aquí para que nadie lo trate como un efecto lateral de un interruptor.
  • El replay de las colas de eventos fallidos produce el efecto del evento (correo en notification; cambio de estado en los demás, atómico con el reproceso): va con escalón 2 y la descripción lo dice.
  • atender_remitente_anonimo se descartó: el enlace #paso.campo solo lee objetos, y pasar de la lista de grupos a los ids de uno sería elegir por la persona.

Alternativas descartadas

  • Dejar los gates débiles y copiarlos en las tools: el MCP heredaría mutaciones a nivel de lectura.
  • Ofrecer aceptar_sugerencia con la tarjeta mostrando el texto que el modelo diga: no es el contenido guardado.
  • Excluir MAS/SEN con escalón 2 en vez de exclusión: la tarjeta no puede resumir mil números ni proteger un texto de salud que saldría al proveedor.
  • Filtrar el catálogo por flujo (opción C): recorte silencioso.