Ir al contenido

Arquitectura

arcane-mcp sigue el diseño basado en catálogo de gitlab-mcp-server, adaptado a una API que publica un contrato legible por máquina.

Cada capacidad de Arcane se declara una sola vez, como un toolutil.ActionSpec con un ID canónico dominio.acción (container.restart, swarm_service.scale). Las superficies son proyecciones de ese catálogo, nunca registros paralelos, así que no pueden divergir:

SuperficiePaqueteHerramientasForma de llamada
dynamic (predeterminada)internal/tools/dynamicarcane_find_action, arcane_execute_action{"action":"container.restart","params":{...},"confirm":false}
metainternal/tools/metaarcane_<dominio>{"action":"restart","params":{...},"confirm":false}
individualinternal/tools/individualarcane_<dominio>_<acción>{...parámetros, "confirm":false}

Las tres llaman a toolutil.Dispatch, que aplica las mismas reglas en el mismo orden: valida los argumentos contra el esquema de entrada de la acción; ante una acción destructiva sin confirm: true, pregunta al usuario mediante elicitación MCP si el cliente lo admite y el modo seguro está desactivado y, si no, la rechaza; responde con una vista previa en modo seguro; ejecuta dentro del plazo por acción (ARCANE_MCP_ACTION_TIMEOUT) y presenta el resultado; escribe un registro de auditoría. Los errores se devuelven como resultados de herramienta con isError y un siguiente paso, nunca como errores de protocolo.

internal/wiring construye el servidor para la superficie configurada; internal/tools/manifest añade los recursos arcane://tools y arcane://tools/{id}, internal/resources los recursos con el estado de Docker y las guías de trabajo (Recursos), internal/prompts los prompts de flujo de trabajo (Prompts), e internal/completions las sugerencias para sus argumentos y para las variables de las plantillas de recurso, sacadas de las acciones de listado del catálogo con el mismo cliente y guardadas en caché unos segundos (Autocompletado). El modo de solo lectura y las exclusiones se aplican una sola vez, cuando actioncatalog.Build filtra el catálogo, de modo que ninguna superficie tiene que acordarse de ellos.

api/arcane.openapi.json ─┐
api/extra-routes.yaml ───┼─► cmd/gen_actions ─► internal/tools/<dominio>/zz_generated_actions.go
api/actions.yaml ────────┘ + capa escrita a mano (actions.go)
│
internal/tools.BuildCatalog
│
┌───────────────────────────────┼───────────────────────────────┐
dynamic meta individual
find / execute (2 herramientas) una herramienta por dominio una herramienta por acción
api/arcane.openapi.json el documento OpenAPI del propio Arcane (generado por Huma), incorporado al repositorio
api/actions.yaml operationId → dominio.acción, más correcciones de seguridad
api/extra-routes.yaml rutas que Arcane sirve fuera de OpenAPI (sacadas de su código)
api/coverage-allowlist.yaml rutas que no se exponen a propósito, con el motivo
│
▼ go run ./cmd/gen_actions
internal/tools/<paquete>/zz_generated_actions.go constantes de ID + declaraciones
internal/tools/<paquete>/actions.go capa propia, escrita a mano
internal/tools/zz_generated_domains.go la lista de dominios
│
▼ internal/tools.BuildCatalog
actioncatalog.Catalog (validado, filtrado por solo lectura / exclusiones)
  • La API REST de Arcane se declara con Huma, así que el documento OpenAPI que sirve se genera a partir del mismo código que enruta las peticiones. Está incorporado en api/, y de él se deriva el esquema de entrada de cada acción.
  • Las declaraciones generadas contienen el ID, el operationId, la ruta, el resumen y la descripción de Arcane, y la clasificación como modificadora, destructiva o idempotente. api/actions.yaml da nombre a cada operación y corrige la clasificación cuando un verbo engaña.
  • Los esquemas de entrada no se escriben a mano: internal/openapi deriva el de cada acción a partir de la operación al construir el catálogo. Los parámetros de ruta, consulta, cabecera y cookie y las propiedades del cuerpo JSON se convierten en un único objeto plano; los componentes referenciados se copian bajo $defs. El parámetro de ruta {id} de /environments/{id}/... pasa a ser un environmentId opcional que toma por defecto ARCANE_ENVIRONMENT_ID.
  • Las rutas fuera del documento OpenAPI —WebSockets, pprof, el disparador de webhooks, el intercambio de tokens federados— se enumeraron a partir del código de Arcane y se declaran en api/extra-routes.yaml. Las pocas que no se exponen (el protocolo de los agentes, el relé de telemetría del navegador, ayudas que solo existen para pruebas) figuran con su motivo en api/coverage-allowlist.yaml.
  • Las capas escritas a mano (actions.go en cada paquete de dominio) añaden lo que una especificación no puede saber: notas de uso que distinguen acciones vecinas (project.down frente a project.destroy), alias (docker ps), acciones relacionadas y manejadores para las acciones con comportamiento propio. Aplicar una capa falla ante un ID que ya no existe, así que un cambio de nombre en Arcane no puede dejar atrás un ajuste obsoleto.

La mayoría de las acciones pasan por el ejecutor genérico toolutil.ExecuteREST: los argumentos se vuelven a asociar a la plantilla de ruta, la cadena de consulta, las cabeceras, las cookies y el cuerpo (JSON, multipart con objetos de fichero o un octet-stream en bruto) mediante las asociaciones con las que se construyó el esquema. Después, toolutil.ReadResponse da forma a la respuesta:

Arcane respondeLa acción devuelve
JSON {success, data, pagination}data como contenido estructurado (las listas bajo items), con pagination al lado
progreso application/x-json-stream (pull, build, up, redeploy)se consume hasta el final; estado, ID de actividad, errores y últimas líneas de progreso
una descarga de texto (logs, YAML, PEM)el texto, limitado por ARCANE_MCP_MAX_DOWNLOAD_BYTES
una descarga binaria (tarballs, copias de seguridad, perfiles)metadatos más un recurso blob incrustado

Los manejadores escritos a mano (internal/tools/livestream y las capas de cada dominio) cubren las rutas que quedan fuera de OpenAPI: logs y estadísticas por WebSocket, el terminal de contenedores tras container.exec, el canal SSE, los flujos de diagnóstico, pprof, el disparador anónimo de webhooks y el intercambio de tokens federados RFC 8693. Flujos y ficheros describe qué devuelve cada uno.

internal/arcane.Client envía X-API-Key cuando hay una clave de API configurada. Con ARCANE_USERNAME/ARCANE_PASSWORD inicia sesión, guarda los tokens de acceso y de renovación y, ante un 401, renueva (o vuelve a iniciar sesión) y repite la petición una vez. Las operaciones cuya seguridad OpenAPI solo admite una sesión Bearer (passkeys, MFA) usan la sesión aunque haya una clave de API configurada.

En modo HTTP el servidor mantiene un servidor MCP por cada clave de Arcane distinta (la X-Arcane-API-Key del cliente o la del propio servidor), todos compartiendo un único catálogo construido.

ComprobaciónQué demuestra
go run ./cmd/gen_actions -checkLas declaraciones generadas coinciden con la especificación incorporada y con los ficheros de inventario
go run ./cmd/audit_1to1Cada operación OpenAPI y cada ruta adicional corresponde a una acción del catálogo (o a la lista de excepciones), y cada acción cubre una ruta
tests de internal/wiringCada acción está registrada en las superficies meta e individual y la resuelve el execute dinámico; la misma llamada da el mismo resultado en las tres
test/e2eLos escenarios se ejecutan contra instancias reales de Arcane, cada uno como un subtest por superficie
go run ./cmd/gen_tool_reference -checkLa referencia de herramientas de este sitio coincide con el catálogo
Ventana de terminal
ARCANE_URL=http://<nuevo arcane> make update-spec # descarga el nuevo documento OpenAPI
make generate # se niega hasta que cada operación nueva tenga nombre en api/actions.yaml
make audit-1to1 # todas las rutas cubiertas
make gen-tool-reference # actualiza la referencia de este sitio

Después, ejecuta la batería de extremo a extremo contra un entorno de la nueva versión.