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:
- Defectos vivos del catálogo existente, no huecos:
radicar_documentono declarabamedio_conservacion, quePOST /documentsexige a propósito (Decreto 103/2015 art. 40, D-2). Toda radicación hecha por el asistente o portramitar_peticion_ciudadanadevolvía 422, y nadie lo vio porque los tests del MCP mockean el gateway y el mock nunca discute el cuerpo.rechazar_anulacionanunciabaobservacionopcional (el backend exige ≥10),reclasificar_radicadoysolicitar_desclasificacionno ofrecíanexcepcion_reserva, yaprobar_anulacionse ofrecía con una tarjeta aunque aprobar exige ver el documento.- El asistente no distinguía
asistente_bloqueado_plataforma(ADR-048): decía «su entidad no lo activó» aunque lo hubiera apagado la plataforma. - Instrumentos defectuosos:
scripts/censo-esquemas.shabortaba 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). - 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. - 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_sugerenciase 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_trdyderogar_version_tvdsalen del catálogo (ACT): un cliente MCP externo que las usara recibirá «tool no encontrada».aprobar_anulacionsale 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_seguridadde 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.DeadLetterDetailde los cuatro servicios lleva el nivel del sobre, y 3 (el máximo) si no lo trae.shared/tests/test_contenido_declara_su_nivel.pyvigila tambiénobservacionesy 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 engrado_clasificaciony no ennivel_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
replayde 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_anonimose descartó: el enlace#paso.camposolo lee objetos, y pasar de la lista de grupos a losidsde 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_sugerenciacon 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.