MCP (LILA-053/054/055/056)
packages/mcp (@lila-modeler/mcp) es un servidor MCP por stdio sobre
@lila-modeler/engine, sin lógica propia: dieciocho tools, sobre el mismo pipeline de validación y
simulación que la CLI — ver docs/es/CLI.md para ese mismo pipeline manejado desde una
terminal en vez de un cliente MCP.
Para el flujo completo de un agente — entrevista, esquema, escenario, corrida, documento y ver el resultado en la app — lee la guía para agentes.
Además de las tools, el servidor ofrece esa guía, los formatos del esquema y del escenario y el JSON
Schema del escenario como recursos, y el flujo de la guía como el prompt interview-to-process; ver
Recursos y prompt.
validate_bpmn({ path | xml, process?, locale? })— parsea y valida un.bpmny devuelve exactamente el mismo JSON quelila validate --json(el IR,ignoredProcessIds,errorsywarnings). Se pasapathoxml, nunca los dos: pasar ambos es un error de la tool, no una precedencia silenciosa.describe_process({ path | xml, process?, scenario?, locale? })— parsea un.bpmn(por ruta o XML inline, uno de los dos, con la misma regla quevalidate_bpmn) y devuelve su IR (ProcessIR) junto con un resumen legible (la claveresumen, en el idioma que digalocale): conteo de nodos por tipo, gateways con sus salidas, lanes, subprocesos embebidos aplanados, los otrosbpmn:processdel archivo que no se simulan, y una línea de validación (Validation: N errors, M warnings, o su equivalente en español) que avisa cuando el modelo está fuera del perfil y no se puede simular. Conscenario(ruta a un escenario.json, resuelveextends) agrega los recursos referenciados por elemento; si el escenario no se puede leer, el resumen dice por qué y la tool no falla.run_simulation({ model?, process?, scenario, seed?, replications?, saveTo?, saveRun?, detail?, locale? })(LILA-054) — valida modelo y escenario, simula conlog: falsey devuelve exactamente el mismoRunResultquelila run --json(elementos, flujos, recursos, proceso, bottlenecks y avisos).scenarioacepta una ruta.json(resuelveextends, igual que la CLI) o el escenario ya resuelto como objeto inline;modeles opcional y por defecto esscenario.model; se rechaza un modelo distinto. Ningún campo de nivel 2/3 se rechaza (LILA-184):resourcesycalendarslos simula el motor desde LILA-033…036 y LILA-041.saveToescribe el mismo JSON de forma atómica quelila run --json <ruta>. TraeoutputSchema(@lila-modeler/engine/result-schema) y respondestructuredContentademás del texto. Por defecto la respuesta es un resumen;detail: "full"devuelve elRunResultcompleto (ver Resumen o resultado completo). Una corrida larga informa su progreso y se puede cancelar (Progreso y cancelación). Con unmodel.lilay un escenario suyo,saveRun: trueguarda la corrida en el proyecto (ver Guardar una corrida).compare_scenarios({ model?, process?, scenarios, seed?, replications?, saveTo?, detail?, locale? })(LILA-054) — valida y simula dos o más escenarios sobre el mismo modelo (el primero es la base) y devuelve exactamente el mismoCompareResultquelila compare --json, másnotes: los avisos que la CLI imprime aparte de la tabla (semillas distintas,baseTimeUnitdistinto, réplicas insuficientes para IC95).scenariosacepta rutas y objetos inline mezclados. Todo escenario se resuelve y valida contra el modelo antes de simular ninguno. Por defectocomparisonconserva las filas del proceso y de los recursos y las significativas;detail: "full"devuelve todas.patch_scenario({ scenario, project?, process?, patch, saveTo?, extendsFrom?, name?, description?, locale? })(LILA-055) — aplica un JSON Patch ascenario, valida el resultado contra su modelo (mismas reglas quevalidateScenario,docs/SCENARIO_FORMAT.md§ 5) y solo si valida lo escribe a disco de forma atómica; nunca dos veces. Devuelve{ scenario, file, written, notes }: el escenario resultante ya resuelto, la ruta absoluta escrita, si se escribió y los avisos de lint (W-…) comonotes. Un patch que no cambia nada (solotest, o unsaveTocuyo destino ya tiene el resultado) no escribe nada:writtenesfalse,noteslo dice primero y el escenario conserva su revisión, así que sus corridas guardadas siguen vigentes (#627). Dos modos:- Sin
saveTo(modo a) — parcheascenarioen sitio: el patch se aplica sobre el escenario ya resuelto (conextendsfusionado) y se sobrescribe el mismo archivo con el resultado completo, sinextendspropio. Es un "aplanar y parchear": siscenariotenía su propioextends, el archivo escrito ya no lo tiene.modelse escribe relativo al propio escenario, no como la ruta absoluta que tenía ya resuelta: un escenario con/Users/quien-corrió-la-tool/…dentro deja de resolver en cualquier otro checkout. Y un patch que borramodelorunse rechaza: el esquema los deja opcionales porque un archivo conextendslos hereda, pero un escenario aplanado sin ellos no se puede simular. - Con
saveTo(modo b) — crea un archivo nuevo que declaraextendshaciaextendsFrom(por defecto, el propioscenario) y contiene solo las claves que tocó el patch, comoexamples/pedido/to-be-3-cajeros.scenario.json. La ruta deextendsse escribe relativa al archivo nuevo (docs/SCENARIO_FORMAT.md§ 6), sin importar en qué directorio estésaveTo. Unremovese escribe comonull, que es comoextendsborra una clave heredada (§ 6). SisaveToapunta al propioscenario(o aextendsFrom), el archivo heredaría de sí mismo: es un ciclo deextendsy la tool falla sin escribir — para parchear en sitio, se omitesaveTo. Un archivo de destino existente se sobrescribe: «nuevo» describe el escenario derivado, no una garantía de creación exclusiva.
El patch soporta
add/replace/remove/test(RFC 6902) con punteros RFC 6901 (/resources/cajero/capacity); no soportamovenicopy— son las dos operaciones que leen de una ubicación distinta a la que escriben, y ningún caso de uso de esta tool las necesita (packages/mcp/src/json-patch.ts). Un patch que deja el escenario inválido —probabilityfuera de[0, 1],capacity < 1, unarefque no existe enresources, unidque no existe en el modelo, … — esisError: truey no escribe nada, en ninguno de los dos modos.- Sin
Las tres tools de LILA-054/055 reutilizan @lila-modeler/engine/cli-shared, extraído de cli.ts en
LILA-054 sin cambiar su salida: runCommand/compareCommand y las tools corren exactamente el
mismo pipeline (loadResolvedScenario, validateScenario, writeJsonAtomic).
export_diagram({ project | path, process?, saveTo?, overwrite?, locale? })— dibuja el diagrama de un proceso de un.lila(project) o de un.bpmn(path) como SVG, con el renderizador propio del motor. Devuelve{ process, svg }o, consaveTo,{ process, file }.export_document({ project, process?, run?, scenario?, format, saveTo, overwrite?, locale? })— escribe el documento del proceso (format:docxohtml) con los resultados de una corrida guardada. Devuelve{ file, format, project, process, run, notes }.export_results({ project, process?, run?, scenario?, format, saveTo, overwrite?, locale? })— escribe los resultados de una corrida guardada (format:xlsx, ocsven el directoriosaveTo). Devuelve{ files, format, project, process, run }.create_process({ outline, project, process?, name?, dryRun?, locale? })(#97) — convierte un esquema del proceso —carriles más una lista ordenada de pasos— en un proceso BPMN maquetado y validado, y lo escribe como un proceso nuevo del.lilaproject(o en un.lilanuevo). Nunca reemplaza un proceso. Devuelve{ file, slug, name, newFile, dryRun, warnings, notes, outline, summary, slugs }. Ver Crear un proceso desde un esquema.get_process_outline({ project, process?, locale? })(#97) — lee un proceso de un.lilacomo esquema:{ file, slug, name, outline, warnings }.edit_process({ project, process?, operations, dryRun?, layout?, locale? })(#98) — aplica una lista de operaciones (add, connect, remove, rename, setType, moveToLane, addLane) a un proceso de un.lila, todas o ninguna. Devuelve{ file, slug, name, dryRun, summary, changes, removed, scenarioRemovals, notes, warnings, outline }. Ver Editar un proceso.annotate_element({ project, process?, elementId, documentation?, responsibilities?, refs?, attributes?, dryRun?, locale? })— escribe la descripción, el RACI, las referencias al catálogo y los atributos extendidos de un elemento de un proceso de un.lila. Devuelve{ file, process, elementId, documentationOn?, dryRun, changed, written, before, after }.get_raci_matrix({ project, process?, locale? })— devuelve la matriz RACI del proceso:{ file, process, roles, rows }.import_scenario_sheet({ project, process?, scenario, sheet, dryRun?, locale? })— aplica una hoja de escenario (.xlsx/.csv) a un escenario del.lila. Devuelve{ file, process, scenario, sheet, dryRun, written, tables, changes, issues }.export_scenario_template({ project, process?, scenario, saveTo, overwrite?, locale? })— escribe esa hoja como.xlsx, rellenada. Devuelve{ file, project, process, scenario }.create_project({ path, name, bpmn, scenarios?, overwrite?, locale? })— crea un.lilanuevo a partir de un BPMN que ya existe (un agente que parte de pasos usacreate_process) y escenarios. Devuelve{ file, name, processId, laidOut, warnings, scenarios }.get_project({ project, locale? })(#628) — lista lo que tiene un.lilasin escribir nada: sus procesos, sus escenarios y sus corridas guardadas, vigentes u obsoletas. Devuelve{ file, name, formatVersion, processes }.get_scenario({ project, process?, scenario, resolved?, locale? })(#628) — lee un escenario como está guardado, o resuelto porextends, con las notas del validador, sin escribir nada. Devuelve{ file, process, entry, revision, resolved, scenario, runnable, notes }.
Las tres de exportación se explican en Exportar sin la app; las cinco siguientes en Herramientas de modelado para agentes; las dos últimas en Leer un proyecto.
Toda tool que recibe un modelo acepta también un proyecto .lila: ver
Un .lila como entrada.
Un .lila como entrada
Un .lila (docs/PROJECT_FORMAT.md) se acepta donde una tool recibe un modelo (#466): path de
validate_bpmn/describe_process, model de run_simulation/compare_scenarios y project
de patch_scenario. Las reglas son las de la CLI (docs/CLI.md, «Un .lila como entrada»):
processes el slug del proceso cuando el proyecto tiene varios (un repositorio de versión 2). Con un solo proceso es implícito; con varios y sinprocess, la tool falla con un mensaje que lista los slugs.processcon un.bpmnes un error, no se ignora.- Un escenario como texto es una ruta
.jsonsi ese archivo existe y, si no, el nombre de un escenario del proceso: su nombre de entrada (as-is.scenario.json), ese nombre sin.scenario.json(as-is) o su"name"("AS-IS", único en el proceso). Un escenario del disco se simula contra el proceso del archivo; uno del archivo resuelvemodelyextendsdentro de él. - Un escenario inline queda anclado dentro del proceso:
extends: "as-is.scenario.json"nombra un escenario del archivo, y si faltamodeles elmodel.bpmndel proceso. patch_scenarioconprojectleescenario(yextendsFrom) como nombres dentro del proceso —nunca como rutas del disco— ysaveTocomo el nombre de la entrada nueva (se añade.scenario.jsonsi falta; sin carpetas). Valida contra el modelo del proceso y solo escribe un resultado válido, igual que en disco. La escritura pasa por el códec del motor (encodeLila) a un temporal que luego se renombra sobre el.lila, así que un fallo nunca deja un archivo a medias. La revisión del escenario sube en uno, para que la app vea como desactualizadas las corridas de la versión anterior; un patch que no cambia nada no se escribe y deja la revisión como estaba. Los demás procesos, escenarios y corridas pasan sin cambios y, en un archivo escrito por la app o el motor, byte a byte; las entradas que no son parte del formato se descartan, como en cualquier guardado de un.lila. Si el archivo cambió en disco mientras la tool trabajaba (otro programa, otro servidor MCP, la CLI o la app de escritorio), no se escribe nada y la tool falla: vuelve a llamarla. Quienes escriben el mismo.lilase turnan con un archivo de bloqueo junto a él (<archivo>.lila.lock, nunca dentro del archivo); si otro lo retiene más de 3 segundos, la tool falla con «otro programa está guardando» sin escribir nada. Un bloqueo sin tocar durante 10 segundos se toma como restos de un cierre inesperado y se borra. Unprojectque no es un.lilase rechaza.
{ "name": "run_simulation", "arguments": { "model": "examples/pedido.lila", "scenario": "to-be-3-cajeros", "seed": 42, "replications": 3 } }
{ "name": "patch_scenario", "arguments": { "project": "proyecto.lila", "process": "pedido", "scenario": "as-is",
"saveTo": "to-be-4", "patch": [{ "op": "replace", "path": "/resources/cajero/capacity", "value": 4 }] } }
Leer un proyecto
get_project y get_scenario (#628) son lila project show y lila scenario show
(docs/es/CLI.md) como tools. Solo leen: ni escritura ni cambio de revisión, el .lila queda byte
a byte como estaba. Llama primero a get_project sobre un archivo que ya existe, en vez de
descubrir sus procesos, escenarios y corridas por los errores de otras tools.
get_projectdevuelve{ file, name, formatVersion, processes }.formatVersiones 2 para un repositorio de varios procesos y 1 si no. Cada proceso traeslug(lo que recibeprocess),name,processId(subpmn:process),modelRevision,elements(nodos de flujo: tareas, eventos, compuertas;nullsi el modelo no se puede leer),scenarios(entry,name,revision,extends) yruns(id,scenario,start,replications,current). Una corrida tienecurrent: false(obsoleta) cuando el modelo o su escenario cambiaron después de ella:export_resultsyexport_documentsolo la toman por id. Un.lilano guarda la fecha en que se tomó una corrida;startes el inicio simulado (run.start) del escenario que corrió.get_scenariodevuelve{ file, process, entry, revision, resolved, scenario, runnable, notes }.scenarioes la entrada como está guardada (con suextends), o conresolved: truefusionada porextendscon los valores por defecto del motor: el objeto que parcheapatch_scenario, conmodelrelativo al escenario.notesson las del validador contra el modelo del proceso —primero los errores (E-…), luego los avisos (W-…), las comprobaciones que hacerun_simulationantes de simular—, así que sirve de lint de escenarios;runnableesfalsecuando hay un error. Un escenario cuyoextendsno se puede resolver vuelve conscenario: null(al resolverlo) y el motivo ennotes.
Para leer un escenario usa get_scenario, no un patch_scenario con solo test: ese tampoco
escribe nada (#627), pero no es para leer.
{ "name": "get_project", "arguments": { "project": "credito.lila" } }
{ "name": "get_scenario", "arguments": { "project": "credito.lila", "scenario": "as-is", "resolved": true } }
Exportar sin la app
export_diagram, export_document y export_results (#538) son lila export diagram|doc|results
(docs/es/CLI.md) como tools. No necesitan la app ni un navegador:
- El diagrama sale del renderizador SVG del motor: la geometría del DI de BPMN con los colores
de Lila Light sobre blanco. El documento HTML lo incrusta. El Word va sin diagrama y ninguno
de los dos lleva las gráficas de la corrida: Word necesita un PNG y el motor no rasteriza.
notesdice qué quedó fuera. - Las corridas son las que la app guardó en el
.lila.run: "latest"(por defecto) es la corrida del modelo y escenario actuales.scenariola acota a un escenario, y hace falta cuando varios tienen una corrida actual.run: "<id>"elige una, y un id desconocido lista los ids;runyscenariojuntos son un error. El documento solo admite una corrida actual; sin ella va sin resultados y elrunde la respuesta esnull.export_resultssin corrida esisError, con un mensaje que lo dice. Los resultados de una corrida anterior se exportan contra el modelo con el que corrió. - No se sobrescribe nada. Si ya existe un archivo en
saveTo(o, en CSV, cualquiera de los cuatro del directorio), la tool devuelveisErrory no escribe nada, salvo conoverwrite: true. El proyecto (o.bpmn) que se exporta nunca es un destino, conoverwriteo sin él. Las escrituras son atómicas: primero un temporal y luego se publica.
{ "name": "export_diagram", "arguments": { "project": "examples/pedido.lila", "saveTo": "out/pedido.svg" } }
{ "name": "export_document", "arguments": { "project": "proyecto.lila", "format": "html", "saveTo": "out/pedido.html" } }
{ "name": "export_results", "arguments": { "project": "proyecto.lila", "scenario": "as-is", "format": "xlsx", "saveTo": "out/as-is.xlsx" } }
Guardar una corrida
run_simulation con saveRun: true (y lila run … --save) guarda la corrida en el .lila
exactamente como la app: la misma forma, las revisiones de modelo y escenario con que corrió, el
XML del modelo y el escenario resuelto (con model: "model.bpmn"), construida por la misma función
del motor que usa la app (storedRun). La app la abre como la corrida actual de ese escenario, y
export_document / export_results la usan. Necesita un model .lila y un escenario del
archivo (por nombre); un .bpmn, un escenario en disco o uno inline son isError y no se escribe
nada. La corrida guardada es siempre el RunResult completo; la respuesta (su texto y su
structuredContent) es el resumen o el resultado completo según detail. Un segundo bloque de
texto dice { "savedRun": { "id", "file", "process", "scenario" } }.
La escritura es atómica y bajo el candado del archivo. Si otro escritor guardó el .lila entre
tanto, se vuelve a leer y la corrida se añade a lo que hay ahora, así que las escrituras concurrentes
llegan todas. Si el modelo o ese escenario cambiaron entre tanto, la corrida ya es vieja y no se
guarda (isError). La app conserva todas las corridas (no hay límite) y esto también.
{ "name": "run_simulation", "arguments": { "model": "proyecto.lila", "scenario": "as-is", "seed": 42, "replications": 5, "saveRun": true } }
Crear un proceso desde un esquema
Un agente describe el proceso como datos en vez de escribir XML BPMN (#97). create_process arma
el BPMN semántico con bpmn-moddle, lo maqueta con bpmn-auto-layout (pool, carriles y flujos
incluidos), lo valida como validate_bpmn y lo escribe en el .lila. lila process create
(docs/es/CLI.md) hace lo mismo desde una terminal.
{ "name": "Credit application",
"lanes": ["Customer", "Analyst"],
"steps": [
{ "id": "receive", "name": "Receive application", "lane": "Analyst" },
{ "id": "check", "name": "Check bureau", "lane": "Analyst", "duration": "normal(20m, 5m)" },
{ "id": "ok", "type": "xor", "name": "Approved?",
"branches": [ { "label": "Yes", "to": "issue" }, { "label": "No", "to": "reject", "probability": 0.3 } ] },
{ "id": "issue", "name": "Issue card", "end": true },
{ "id": "reject", "name": "Notify rejection", "lane": "Customer", "end": true } ] }
- El orden de la lista es el flujo: cada paso sigue al anterior salvo que tenga
next(un id, o varios después de una compuerta),branches(solo compuertas:{ label?, to, probability? }, o{ label?, end: true, probability? }para una rama que termina el proceso) oend: true. El último paso termina el proceso. Los eventos de inicio y fin se añaden solos. typeestaskpor defecto; los demás sonuserTask,serviceTask,callActivity,xor,and,or,timer(un evento intermedio de temporizador) ysubprocess(un subproceso embebido colapsado, con un inicio y un fin de paso dentro).- Los ids de los pasos son los ids BPMN, así que los escenarios se refieren a ellos. Deben ser
ids BPMN válidos y no chocar con los que Lila genera (
StartEvent,EndEvent_<paso>,Flow_…,Lane_<n>,Process_…,<id>_di). lanees por defecto el carril del paso anterior (el primero para el primer paso). Un carril que no está enlaneses un error silanesviene, y se agrega en orden si no viene.- El escenario base. El proceso recibe
as-is.scenario.json: las llegadas por defecto de la app para un proceso nuevo (20 casos, uno por minuto), cadadurationcomoprocessingTime(tareas) o demora (temporizadores), cada entrada deresources(un nombre, o{ name, quantity }; el recurso se crea con la capacidad de su mayor pedido; con varios,selection: "or"toma cualquiera de ellos en vez de todos) y cadaprobabilityde rama como la probabilidad de su flujo. Las ramas de un XOR sin probabilidad se reparten lo que falta para 1. Unadurationes un número de segundos, un tiempo con unidad (90s,20m,1.5h,1h30m,1d; las unidades de las hojas de escenario, en español incluidas), una distribución del formato de escenario escrita corta —normal(20m, 5m),triangular(1m, 2m, 5m),exponential(mean=4m), posicional en el orden dedocs/es/SCENARIO_FORMAT.mdo con nombre— o un objeto de distribución en segundos. - El slug del nuevo
processes/<slug>/esprocess, o uno derivado del nombre (name, si nooutline.name). Un slug que ya está en el archivo es un error y no se escribe nada. Unprojectque no existe se crea como un.lilade un proceso cuyo slug sale del nombre (ahí unprocessdistinto es un error: un archivo de un proceso no puede llevar su propio slug). Todos los demás procesos quedan byte a byte como estaban; la escritura es la misma, atómica y con candado, que la depatch_scenario. - Un esquema mal formado falla con todos sus problemas a la vez, en el idioma de la llamada,
cada uno con su ruta (
steps[2].branches[0].to: …,steps[0].duraton: campo desconocido "duraton".), y no se escribe nada.outlinese declara a propósito como un objeto suelto, para que el SDK de MCP nunca rechace la llamada antes de estas comprobaciones. Un paso desde el que ningún camino llega a un fin (un ciclo sin salida) también es un problema, uno por paso atascado, y lo mismo un esquema cuyo modelo no valida (los errores del validador vuelven con sus códigos, en el paso del que hablan). notesson los avisos propios de Lila sobre un esquema aceptado, como una unión paralela (and) que espera ramas de una sola compuerta exclusiva (xor), donde los casos esperarían para siempre.
{ "name": "create_process", "arguments": { "project": "credit.lila", "outline": { "name": "Credit application", "lanes": ["Customer", "Analyst"], "steps": [ … ] } } }
get_process_outline devuelve el esquema en forma normal: cada paso dice su carril, type se
omite para task, next aparece solo cuando no es simplemente el paso siguiente, todo paso que
termina el proceso lleva end: true, las salidas de las compuertas son branches, las duraciones
son objetos de distribución en segundos y las probabilidades del XOR vienen completas. Es la forma
que create_process devuelve como outline, así que un proceso va y vuelve igual. Duraciones,
recursos y probabilidades (y la selection de recursos) salen del as-is.scenario.json del
proceso cuando lo tiene. Para un proceso que Lila no generó, warnings dice una vez por tipo lo
que el esquema deja fuera: otros pools, flujos de mensaje, los nombres de los eventos de inicio y
fin, anotaciones, marcas de flujo por defecto, otros tipos de evento, carriles anidados y lo del
escenario que un esquema no lleva (llegadas, calendarios, costos, capacidades y tipos de recurso,
ruteo condicionado; el escenario no se toca). Un proceso sin nombre toma el del .lila, si no su
id. Los pasos conservan el orden del documento.
Herramientas de modelado para agentes
annotate_element, get_raci_matrix, import_scenario_sheet, export_scenario_template y
create_project (#99, #403, #514) dejan que un agente construya y documente un proyecto sin la
app. La CLI tiene las cuatro primeras como lila process annotate|raci y
lila scenario import|template (docs/es/CLI.md). Toda escritura pasa por el mismo guardado
atómico y con candado del .lila que patch_scenario (ver
Un .lila como entrada): solo cambia el proceso tocado, los demás pasan
byte a byte, y una entrada inválida es isError sin escribir nada. dryRun: true responde qué
cambiaría y no escribe nada.
annotate_elementbusca el elemento por su id BPMN (una tarea, evento, compuerta, flujo, carril, pool, el proceso…; un id desconocido es un error).documentationreemplaza la descripción (""la quita).responsibilitieses toda la lista RACI,[{ "type": "R"|"A"|"C"|"I", "roleRef": "cajero" }]([]la vacía).refsasocia un tipo (systemRef,documentRef,riskRef,controlRef,kpiRef,input,output) con sus ids y reemplaza solo los tipos dados.attributesasocia un atributo extendido (su id, o su nombre si solo uno lo tiene) con un valor y se fusiona:""quita un valor, los atributos que no se dan se quedan. Cada valor se comprueba contra las definiciones de atributos del proyecto, como en el panel de propiedades de la app: unnumberes un decimal simple (12,-3.5), undateesAAAA-MM-DD, un valor delistes una de sus opciones, y el atributo debe aplicarse al tipo del elemento. La revisión del modelo sube en uno, como cuando la app guarda una edición. Una anotación que no cambia nada no escribe nada (changed: false). Escribe donde la app lee: la descripción de un pool es la del proceso al que apunta (se escribe ahí y se nombra endocumentationOn), y un proceso dentro de un pool guarda su RACI y sus referencias en el pool, así queresponsibilitiesorefssobre ese proceso se rechazan con un mensaje que nombra el pool.get_raci_matrixlista lo que el documento del proceso (export_document) lista en «Responsabilidades (RACI)», en el mismo orden: una fila por elemento con responsabilidades, con suid,name,lane(si tiene), lasresponsibilitiestal cual ycells(rol →"R", o"A, C"si un rol tiene varios tipos).rolesson los roles en orden de aparición.import_scenario_sheetes «Importar Excel/CSV…» de la app (docs/es/SCENARIO_SHEETS.md): la hoja se planifica contra el escenario resuelto y cada cambio se escribe en el archivo propio del escenario, su delta cuando haceextendsde otro (el padre no se toca).changestrae elbefore, elaftery untextlegible de cada campo (Cajero (cajero) · capacity: 2 → 3, una distribución en la unidad de su fila);issueslas filas no aplicadas (error,unmatched,ambiguous), las notas (warning) y los errores que tendría el resultado (lint). Un plan con algúnlintse rechaza (undryRunlo informa). Como en la app, las filas válidas se escriben aunque otras no se apliquen, así que revisaissues. La revisión del escenario sube en uno.export_scenario_templatees «Descargar plantilla» de la app: dásela a una persona e importa lo que devuelva. UnsaveToque ya existe se rechaza salvo conoverwrite.create_projectrecibebpmncomo XML (empieza por<) o como ruta a un.bpmn. Un modelo con errores de validación se rechaza.scenariosson[{ "name", "scenario" }], guardados como<name>.scenario.json; un escenario sinmodelniextendsrecibe"model": "model.bpmn", y uno que nombra otro modelo se rechaza. Un escenario que todavía no puede correr es un borrador: se guarda y vuelve conrunnable: falsey suserrors. El archivo se escribe entero o nada, y uno existente se rechaza salvo conoverwrite. Las apps web y de escritorio lo abren. Un modelo sin diagrama (sin formasbpmndi) recibe uno, dibujado con el mismo maquetado automático queedit_processconlayout: true, para el proceso que simula, así queexport_diagramfunciona con él;laidOut: truelo indica (#630). Un modelo que el maquetador no puede colocar se guarda tal como llegó.
{ "name": "create_project", "arguments": { "path": "proyecto.lila", "name": "Pedidos", "bpmn": "examples/pedido/model.bpmn",
"scenarios": [{ "name": "as-is", "scenario": { "version": 1, "name": "AS-IS", "run": { "start": "2026-10-05T08:00:00-06:00", "duration": 28800, "seed": 1 },
"elements": { "StartEvent_Pedido": { "interTriggerTimer": { "type": "exponential", "mean": 300 } } } } }] } }
{ "name": "annotate_element", "arguments": { "project": "proyecto.lila", "elementId": "Task_TomarPedido",
"documentation": "Toma el pedido en el mostrador.", "responsibilities": [{ "type": "R", "roleRef": "cajero" }, { "type": "A", "roleRef": "gerente" }],
"refs": { "systemRef": ["POS"] }, "locale": "es" } }
{ "name": "get_raci_matrix", "arguments": { "project": "proyecto.lila" } }
{ "name": "export_scenario_template", "arguments": { "project": "proyecto.lila", "scenario": "as-is", "saveTo": "as-is.xlsx" } }
{ "name": "import_scenario_sheet", "arguments": { "project": "proyecto.lila", "scenario": "as-is", "sheet": "as-is.xlsx", "dryRun": true } }
Editar un proceso
edit_process (#98) cambia un proceso que ya existe, lo haya hecho quien sea: create_process, la
app o una importación de Bizagi. Recibe una lista de operations, las aplica en orden sobre el
modelo en memoria, valida el resultado y lo escribe solo si todas las operaciones salieron bien, el
modelo no tiene ningún error de validación que no tuviera antes y cada escenario del proceso sigue
simulando. Si no, no escribe nada y la llamada falla con todos los problemas a la vez —los de forma y
los de significado en una sola pasada, en el idioma de la llamada—, cada uno con el índice de su
operación y una ruta (operations[2].after: …, operations[3].nombre: campo desconocido "nombre".,
operations[0].bpmn.Process_1: E-SIN-START: …). Un operations que no es una lista también es un
error (operations: debe ser una lista no vacía de operaciones.). lila process edit (docs/es/CLI.md) hace lo
mismo desde una terminal.
{ "name": "edit_process", "arguments": { "project": "credit.lila", "operations": [
{ "op": "add", "step": { "id": "verify", "name": "Verify identity", "duration": "5m", "resources": ["Analyst"] }, "after": "receive" },
{ "op": "connect", "from": "ok", "to": "verify", "label": "Retry", "probability": 0.1 },
{ "op": "rename", "id": "issue", "name": "Issue the card" },
{ "op": "setType", "id": "check", "type": "serviceTask" },
{ "op": "addLane", "name": "Back office" },
{ "op": "moveToLane", "id": "issue", "lane": "Back office" },
{ "op": "remove", "id": "reject" } ] } }
| Operación | Campos | Qué hace |
|---|---|---|
add |
step, after? o between? |
Agrega un paso. step es un paso de esquema sin sus conexiones: { id, name?, type?, lane?, duration?, resources? }. Con after: id, el paso nuevo toma el flujo de salida de ese paso (se rechaza si tiene varios: usa between). Con between: [from, to], va sobre el flujo from → to. El flujo donde cae conserva su id, nombre y probabilidad; un flujo nuevo sigue hasta el sucesor de antes. Sin ninguno de los dos, se agrega sin conectar. lane es, por defecto, el carril del paso al que sigue. |
connect |
from, to, label?, probability?, id? |
Agrega un flujo de secuencia (id Flow_<from>_<to> por defecto). probability solo a la salida de un gateway xor u or. |
remove |
id |
Quita un paso y reconecta: sus flujos de entrada van a su sucesor cuando tiene a lo sumo uno; un paso sin flujos de entrada pierde los de salida; un paso con varios de entrada y varios de salida (un split tras un join) se rechaza. Un paso con eventos de borde se rechaza. Los flujos de mensaje y asociaciones que cuelgan de él se van con él. También se puede quitar un flujo de secuencia. |
rename |
id, name |
Renombra cualquier elemento: paso, flujo, carril, pool, proceso. "" borra el nombre. |
setType |
id, type |
Uno de los tipos del esquema. Se quedan el nombre, la documentación, los elementos de extensión y los flujos; un subproceso convertido en otra cosa pierde su contenido (va en removed). |
moveToLane |
id, lane |
Carril por nombre o id. |
addLane |
name, id?, after? o before? |
Agrega un carril (abajo, por defecto). El primer carril de un proceso sin carriles contiene todos sus pasos, y un proceso sin pool recibe uno. |
- Los ids no cambian. Un paso renombrado conserva sus entradas de escenario, y uno con otro tipo
también, en lo que siga aplicando. El
duration, losresourcesy elselectionde un paso agregado y laprobabilityde una conexión van alas-is.scenario.jsondel proceso, como encreate_process(un recurso con el mismo nombre o clave se reutiliza). Un proceso sin ese escenario rechaza esos campos. - Después de editar, el proceso sigue simulando. Las entradas de escenario que ya no aplican se
quitan de todos los escenarios del proceso: la entrada completa de un elemento que la edición
quitó, y los campos que un elemento con otro tipo ya no admite (recursos en un gateway, un
subproceso o un temporizador; una duración en un subproceso…). Nada se va en silencio:
scenarioRemovalslista cada una como{ scenario, id, removed, entry }, con los campos quitados y sus valores anteriores, para que un agente los ponga en otro lado conpatch_scenario;notesdice lo mismo en palabras, y undryRundevuelve el mismo reporte. Después se valida cada escenario contra el modelo editado, y un error que no tenía antes rechaza la edición.removedlista cada id que salió del modelo.notesavisa además de un XOR cuyas probabilidades de rama ya no suman 1, con los flujos a corregir. - Lo que no se toca se queda: documentación, anotaciones
lila:(RACI, referencias, atributos extendidos), extensiones de otros fabricantes, anotaciones de texto, objetos de datos, otros pools y flujos de mensaje. Un modelo que el lector BPMN no puede leer completo se rechaza en vez de reescribirse con algo de menos. - Maquetado. Por defecto (
layout: true) el proceso editado se maqueta de nuevo con el mismo maquetador y el mismo arreglo de carriles quecreate_process: se pierden las posiciones puestas a mano en ese proceso; las anotaciones de texto y los objetos de datos siguen a su elemento, y los que aún no tienen figura (un modelo que llegó sin diagrama) reciben una junto a él, o en una fila bajo el dibujo si no cuelgan de nada (#630); los pools de abajo (o de al lado) se mueven en bloque lo que creció. Conlayout: falsecada figura conserva su lugar: un paso nuevo va a la derecha del paso al que sigue, en su carril (que crece una fila si está lleno), y solo se vuelven a trazar los flujos cuyos extremos cambiaron. Para hacer lugar a un paso insertado entre dos pasos muy juntos, lo que está a su derecha en ese pool se corre a la derecha en un solo tanto; pools y carriles solo se ensanchan o se alargan. - Qué proceso:
processcuando el.lilatiene varios. Dentro de su BPMN, el proceso que lee el simulador (el ejecutable y no vacío); no se pueden editar pasos de otros pools. - La escritura es la misma escritura atómica y con candado que la de
patch_scenario: si el archivo cambió en disco desde que se leyó (otro agente, la app), la llamada se rechaza y no se pierde nada. La revisión del modelo sube, así que la app ve sus corridas viejas como desactualizadas. Todos los demás procesos quedan byte a byte. CondryRun, se comprueba y devuelve todo y no se escribe nada.
Resumen o resultado completo
Un RunResult completo lista cada elemento y cada flujo con todas sus estadísticas: unos 70 KB (unos
20 mil tokens) para un proceso de diez pasos, y una comparación completa más del doble. Por eso
run_simulation y compare_scenarios responden por defecto con un resumen (#629), y
detail: "full" devuelve el resultado completo de siempre. saveTo y saveRun guardan siempre el
resultado completo, diga lo que diga detail, así que un cliente con sistema de archivos puede
conservarlo todo y leer solo el resumen.
El resumen de run_simulation ("detail": "summary"):
process: tiempo de ciclo, espera, throughput, costo por caso ybyEndEvent, como en el resultado completo;bottlenecks, como en el resultado completo;resources: por recurso,utilization,resourceWaitTotal(la espera por recurso, en segundos, de los elementos que lo piden; un elemento que pide dos recursos cuenta para los dos) ytotalCost;topWaits: los cinco elementos que más esperan (espera por recurso más espera fuera de horario), con suname,completed,resourceWaitMean,resourceWaitTotal,offHoursWaitMeanyqueueLengthMean;replications, el número de replicaciones (cuando hay varias), ycancelled/completedReplicationscomo en el resultado completo;warningscomo{ message, count }: un aviso que cada replicación repite con otros números (… (2941 veces),… 1457 casos …) sale una vez, con el primer mensaje y cuántas veces apareció.
El resumen de compare_scenarios conserva { comparison, notes }: comparison trae count, las
filas de alcance process y resources, toda otra fila marcada significant en algún escenario y
omittedRows, el número de filas que se dejaron fuera. Las filas son las del resultado completo, sin
cambios.
Para el proceso de tarjeta de crédito del flujo de la guía para agentes
(agent-flow.e2e.test.ts) el resumen ocupa unos 4.4 KB frente a 68 KB completo; un test lo mantiene
por debajo de 8 KB.
{ "name": "run_simulation", "arguments": { "scenario": "examples/pedido/as-is.scenario.json", "seed": 42, "detail": "full" } }
Recursos y prompt
El servidor ofrece como recursos MCP los documentos que un agente necesita, en el idioma del
servidor (lila mcp --lang, LILA_LANG…; inglés por defecto), para que un cliente que solo tiene
MCP no tenga que adivinar los formatos a partir de las descripciones de las tools:
| URI | Contenido |
|---|---|
lila://guide |
La guía para agentes (docs/AGENT_GUIDE.md en inglés). |
lila://outline-format |
El esquema: el ejemplo y la tabla de campos de la guía, y después las reglas. |
lila://scenario-format |
SCENARIO_FORMAT.md. |
lila://scenario-schema |
scenario.schema.json, el JSON Schema de un escenario. |
El prompt interview-to-process (argumentos transcript y project, los dos opcionales) es el
flujo de la guía, pasos 1 a 8, seguido de la entrevista; sin transcript pide la transcripción
primero.
Son los propios documentos: desde un checkout el servidor lee docs/, y el paquete publicado lleva
copias de los mismos archivos en dist/resources/ (prepack corre
packages/mcp/scripts/bundle-resources.mjs).
{ "method": "resources/read", "params": { "uri": "lila://guide" } }
Progreso y cancelación
run_simulation y compare_scenarios simulan en un worker thread, así que el servidor sigue
respondiendo (un ping, otras llamadas) mientras corre una simulación. Cuando la llamada trae
_meta.progressToken, el servidor manda notifications/progress una vez por replicación terminada,
con progress (replicaciones hechas) y total (run.replications, o la suma sobre los escenarios
de una comparación). Un notifications/cancelled del cliente detiene la corrida en el acto: el
worker se termina, no se guarda nada y, como pide MCP, no se responde a la petición cancelada.
{ "method": "tools/call", "params": { "name": "run_simulation", "arguments": { "scenario": "examples/pedido/as-is.scenario.json", "replications": 30 }, "_meta": { "progressToken": 1 } } }
Idioma
El title, la description y los describe() de cada tool están fijos en inglés: son la
superficie del protocolo, lo que el cliente MCP leyó una sola vez en tools/list, y ninguna
llamada suelta puede reescribirlos. Lo que sí cambia de idioma es el contenido de las respuestas:
el resumen de describe_process, los mensajes de isError y los problemas del motor (E-…/W-…
de docs/SEMANTICS.md § 17).
- El idioma del servidor sale de quien lo arrancó:
lila mcp --lang esse lo pasa hecho, y el binlila-mcp(que no tiene línea de comandos) lo resuelve del entorno con la misma regla que la CLI:LILA_LANG,LC_ALL,LC_MESSAGES,LANG; inglés si no hay ninguna. - El idioma de una llamada es
locale: "en" | "es", opcional en todas las tools. Solo afecta a esa respuesta; no cambia el idioma del servidor para las siguientes.
La clave resumen de describe_process no se renombra al traducir: es contrato desde
LILA-053 y hay agentes y tests que la leen por nombre. Cambia su contenido, no su etiqueta.
Instalación
El servidor está publicado en npm como paquete propio, @lila-modeler/mcp (ADR-030), con Node 22 o
posterior. Un cliente MCP lo arranca con:
npx -y @lila-modeler/mcp
-y deja que npx lo instale sin preguntar. El paquete fija la versión exacta de
@lila-modeler/engine con la que se publicó, así que el servidor y la CLI lila de la misma
versión dan los mismos números.
lila mcp, el subcomando del motor (LILA-056), arranca el mismo servidor cuando están
instalados los dos paquetes (npm install -g @lila-modeler/engine @lila-modeler/mcp). Como
@lila-modeler/mcp depende de @lila-modeler/engine, importarlo estáticamente desde cli.ts sería
un ciclo entre paquetes: se carga con import() dinámico (packages/engine/src/cli.ts,
dispatchMcp) y, si el paquete no está, el comando lo dice por stderr y sale con 1 en vez de
romperse.
Para correr cambios aún sin publicar, arráncalo desde un clon del repo:
git clone https://github.com/AlambritoDito/lila-modeler.git
cd lila-modeler
npm ci
npm run build
node packages/engine/bin/lila.js mcp # o ./node_modules/.bin/lila-mcp, el mismo servidor
Todos los clientes de abajo arrancan el servidor como subproceso por stdio: command es npx y
args son -y y @lila-modeler/mcp. Cuando el cliente no hereda tu PATH, da command como ruta
absoluta (which npx), y agrega el directorio de node al PATH del servidor si aun así no lo
encuentra. Desde un clon, command es node y args son la ruta absoluta a
packages/engine/bin/lila.js y mcp. Define la variable LILA_LANG=es para los mensajes en
español (lila mcp acepta además --lang es).
lila mcp habla MCP por stdout: nada más puede escribir ahí. Todo diagnóstico (paquete
ausente, fallo de arranque) sale por stderr, que es lo único que ve quien registró el servidor.
Registro en Claude Code
Por línea de comandos:
claude mcp add lila -- npx -y @lila-modeler/mcp
-s user lo deja disponible en todos los proyectos; sin -s, solo en el directorio actual.
Comprobación: claude mcp list debe mostrar lila: ... - ✓ Connected.
Para el propio repo no hace falta: la raíz trae un .mcp.json de proyecto que arranca el
servidor del clon, así que al abrir Claude Code aquí el servidor aparece solo (Claude Code pide aprobar los servidores de .mcp.json
la primera vez).
{
"mcpServers": {
"lila": {
"command": "node",
"args": ["packages/engine/bin/lila.js", "mcp"]
}
}
}
Las rutas de args son relativas porque Claude Code lanza los servidores de proyecto con el cwd en
la raíz del proyecto — que es además el cwd contra el que las tools resuelven path y scenario
(ver § Rutas). En un .mcp.json de otro repo, usa rutas absolutas.
Registro en Claude Desktop
Claude Desktop no tiene CLI: se edita a mano
~/Library/Application Support/Claude/claude_desktop_config.json (macOS) o
%APPDATA%\Claude\claude_desktop_config.json (Windows), y se reinicia la app. Claude Desktop no
hereda el PATH de tu shell, así que da npx con ruta absoluta (which npx):
{
"mcpServers": {
"lila": {
"command": "/ruta/absoluta/a/npx",
"args": ["-y", "@lila-modeler/mcp"]
}
}
}
Pasa rutas absolutas de modelo y escenario en cada llamada, sin depender del directorio de trabajo que elija el cliente.
Registro en Codex
codex mcp add lila -- npx -y @lila-modeler/mcp
o, en ~/.codex/config.toml:
[mcp_servers.lila]
command = "npx"
args = ["-y", "@lila-modeler/mcp"]
Registro en Hermes Agent
Hermes Agent lee los
servidores stdio de mcp_servers en ~/.hermes/config.yaml:
mcp_servers:
lila:
command: "/ruta/absoluta/a/npx"
args: ["-y", "@lila-modeler/mcp"]
cwd: "/ruta/absoluta/a/tus/proyectos"
env:
LILA_LANG: "es"
timeout: 300
- Hermes no le pasa al servidor todo tu entorno: le pasa una base segura (
PATH,HOME,LANG,TMPDIR,XDG_*y similares) más las variables que aparecen enenv. Pon enenvLILA_LANGo lo que Lila deba ver, y dacommandcomo ruta absoluta anpx(which npx) cuando elPATHcon que corre Hermes pueda no encontrarlo. cwdes donde caen las rutas relativas de las tools (project: "tarjeta.lila",saveTo). Solo las versiones recientes de Hermes se lo pasan al servidor; las anteriores (la v0.17.0, por ejemplo) lo ignoran. Las rutas absolutas en las llamadas funcionan con cualquier versión, así que prefiérelas.timeoutes por llamada, en segundos: unrun_simulationocompare_scenarioslargo bloquea el servidor hasta terminar, así que deja margen.- Después de editar el archivo, corre
/reload-mcpen una sesión de Hermes. Hermes nombra las toolsmcp_lila_<tool>(mcp_lila_create_process); los argumentos son los de esta página. tools: { include: [...] }limita lo que ve el agente; por ejemplo, a las de solo lecturaget_project,get_scenario,validate_bpmn,describe_process,get_process_outline,get_raci_matrixyrun_simulation(que solo escribe consaveToosaveRun).
El mismo bloque, con command y args, es la configuración stdio genérica para cualquier otro
cliente MCP.
Ejemplos
Una llamada por cada tool que las secciones de arriba no muestran ya, escrita como la manda un
agente (name y arguments); las rutas son relativas al directorio de trabajo del servidor.
{ "name": "validate_bpmn", "arguments": { "path": "examples/pedido/model.bpmn" } }
{ "name": "describe_process", "arguments": { "path": "examples/pedido/model.bpmn", "scenario": "examples/pedido/as-is.scenario.json" } }
// run_simulation
{ "name": "run_simulation", "arguments": { "scenario": "examples/pedido/as-is.scenario.json", "seed": 42 } }
// -> { "elements": {...}, "flows": {...}, "resources": {...}, "process": {...},
// "bottlenecks": [...], "replications": {...}, "warnings": [...] }
// compare_scenarios (el primero es la base)
{ "name": "compare_scenarios", "arguments": {
"scenarios": ["examples/pedido/as-is.scenario.json", "examples/pedido/to-be-3-cajeros.scenario.json"], "seed": 42 } }
// -> { "comparison": { "count": 2, "rows": [...] }, "notes": [...] }
// patch_scenario, modo (b): crea un TO-BE con extends al AS-IS
{ "name": "patch_scenario", "arguments": {
"scenario": "examples/pedido/as-is.scenario.json",
"patch": [{ "op": "replace", "path": "/resources/cajero/capacity", "value": 3 }],
"saveTo": "examples/pedido/to-be-3-cajeros-copia.scenario.json",
"name": "TO-BE 3 cajeros (copia)" } }
// -> escribe { "version": 1, "name": "TO-BE 3 cajeros (copia)",
// "extends": "as-is.scenario.json", "resources": { "cajero": { "capacity": 3 } } }
// y devuelve { "scenario": {...resuelto...}, "file": "/ruta/.../to-be-3-cajeros-copia.scenario.json",
// "notes": [...] }
saveTo es un archivo nuevo: nunca lo apuntes a un escenario que el repo versiona (como
to-be-3-cajeros.scenario.json), porque se sobrescribe.
{ "name": "get_process_outline", "arguments": { "project": "credit.lila", "process": "credit-application" } }
Los dos flujos de la aceptación de M4, como conversación
Son los dos que prueba packages/mcp/test/e2e.test.ts contra lila mcp por stdio, con los números
reales de examples/pedido con seed: 42.
"Simula examples/pedido con as-is y dime el cuello de botella"
Una sola llamada:
run_simulation({ "scenario": "examples/pedido/as-is.scenario.json", "seed": 42 }). El agente leebottlenecks[0]del JSON:{ "elementId": "Task_Preparar", "resourceWaitTotal": 267417737.56, "utilization": 0.3435 }, idéntico al delila run … --json. Responde: el cuello de botella esTask_Preparar(Preparar), por espera decocinero/horno; el segundo esTask_TomarPedido.
Si el modelo no fuera simulable, la llamada sería isError: true y el paso previo sería
validate_bpmn (§ Qué significa isError). Para "cuéntame qué hace este proceso" antes de
simular, describe_process.
"Qué pasa si agrego un cajero"
Dos llamadas más, sin tocar el AS-IS:
patch_scenario({ "scenario": "examples/pedido/as-is.scenario.json", "patch": [{ "op": "replace", "path": "/resources/cajero/capacity", "value": 3 }], "saveTo": "examples/pedido/to-be-3-cajeros-copia.scenario.json", "name": "TO-BE 3 cajeros (copia)" })— escribe{ "version": 1, "name": "TO-BE 3 cajeros (copia)", "extends": "as-is.scenario.json", "resources": { "cajero": { "capacity": 3 } } }: solo el delta, conextendsal AS-IS. Un patch que deja el escenario inválido se rechaza sin escribir nada.compare_scenarios({ "scenarios": ["examples/pedido/as-is.scenario.json", "examples/pedido/to-be-3-cajeros-copia.scenario.json"], "seed": 42 })— 255 filas; la que importa eselements.Task_TomarPedido.resourceWait.mean: 14.97 → 2.18 minutos, −85.4 %, consignificant: [false, true](los IC 95 % no se solapan). El agente responde: un cajero más quita casi toda la cola del mostrador, y el cuello de botella se queda enTask_Preparar.
Un escenario inline (sin archivo en disco) es un objeto JS con el mismo ResolvedScenario que
produciría resolveExtends: { "scenario": { "version": 1, "name": "...", "model": "...", "run": {...}, "elements": {...} } }. Si declara model relativo, se resuelve contra el cwd del proceso
servidor, igual que una ruta.
Qué significa isError
isError: true marca que falló la tool: faltan argumentos o sobran (path y xml a la vez),
el archivo no existe o no se puede leer, el XML es impenetrable. Un modelo que la validación
rechaza no es un fallo de la tool: validate_bpmn responde isError: false con el reporte
completo y sus errors[] (mismo criterio que el catálogo de docs/SEMANTICS.md § 17), igual que
describe_process, que lo dice además en su resumen. Así un agente distingue "la herramienta se
rompió" (reintentar, corregir la llamada) de "el modelo tiene errores" (leer errors[] y arreglar
el .bpmn). Ojo: la CLI sí sale con código 1 en ese caso — el código de salida y isError no son
lo mismo.
run_simulation y compare_scenarios mantienen el mismo criterio, y por eso responden
isError: true cuando el modelo o el escenario no pasan la validación: a diferencia de
validate_bpmn, ahí no hay resultado que devolver —no se puede simular—, así que el fallo es
de la llamada, no un reporte válido. El mensaje lleva el nombre de la tool, el escenario culpable
(su ruta, o scenarios[n] si vino inline) y los errores en JSON. Para saber por qué un modelo no
se puede simular sin gastar una simulación, la tool es validate_bpmn, que devuelve el reporte
completo con isError: false.
patch_scenario sigue el mismo criterio que run_simulation/compare_scenarios: un patch que
deja el escenario inválido (estructuralmente, por docs/SCENARIO_FORMAT.md § 5, o porque la
propia operación de JSON Patch no se puede aplicar — un path inexistente en replace, una
operación desconocida) es isError: true, sin escribir nada. No hay ambigüedad "resultado
correcto pero el modelo tiene errores" aquí: si el escenario resultante no valida, no hay nada que
devolver. Los defectos del esquema salen por parseScenario (LILA-202): con los
códigos de docs/SEMANTICS.md § 17 (E-CLAVE-DESCONOCIDA: …), el mismo texto que la CLI y en el
idioma que pidió la llamada (§ Idioma).
saveTo
saveTo escribe en el sistema de archivos del proceso servidor, con sus permisos, la ruta que
se le pase (relativa al cwd, como todo lo demás). Sobrescribe un archivo existente sin preguntar,
igual que lila run --json <ruta>, y publica con rename desde un temporal en el mismo
directorio: nadie llega a leer un JSON a medias. Un directorio con ese nombre es un error de la
tool, no un borrado. Las tools de exportación son la excepción: nunca reemplazan un archivo
salvo con overwrite: true (ver Exportar sin la app). run_simulation y compare_scenarios guardan el resultado
completo diga lo que diga detail. compare_scenarios guarda ahí solo comparison, sin notes: los mismos
bytes que lila compare --json <ruta>. En patch_scenario, saveTo cambia el modo de la tool
(§ arriba): sin saveTo se sobrescribe scenario; con saveTo se crea un archivo nuevo con
extends y solo el delta del patch.
Rutas
path y scenario se resuelven contra el cwd del proceso servidor, no contra el del cliente
ni la raíz del repo; dentro de un .lila, model y extends se resuelven respecto del lugar del
escenario en el archivo. En Claude Code eso es el directorio desde el que se lanzó el servidor; en la
duda, pasa rutas absolutas.
Paquete del SDK
@modelcontextprotocol/server 2.0.0 (no @modelcontextprotocol/sdk, que es un paquete distinto
del mismo repo/mantenedores, más orientado a cliente+servidor combinados en 1.x). El BACKLOG pedía
"@modelcontextprotocol/server 2.x"; se verificó con npm view que existe exactamente en esa
versión, mantenido por el mismo equipo de Anthropic/MCP. Los tests usan además
@modelcontextprotocol/client 2.0.0 (mismo repo) como devDependency.
Probarlo a mano
Desde la raíz del repo, tras npm run build.
Lo más rápido, sin instalar nada y sin navegador — un cliente stdio de diez líneas:
node --input-type=module -e "
const { Client } = await import('@modelcontextprotocol/client');
const { StdioClientTransport } = await import('@modelcontextprotocol/client/stdio');
const client = new Client({ name: 'manual', version: '0' });
await client.connect(new StdioClientTransport({ command: 'node', args: ['packages/engine/bin/lila.js', 'mcp'] }));
console.log((await client.listTools()).tools.map((t) => t.name));
const r = await client.callTool({ name: 'validate_bpmn', arguments: { path: 'examples/pedido/model.bpmn' } });
console.log(r.isError, r.content[0].text.slice(0, 200));
await client.close();
"
O a pelo, una línea de JSON-RPC por stdin:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"x","version":"0"}}}' | node packages/engine/bin/lila.js mcp
Y con Claude Code de verdad, sin abrir una sesión interactiva (el .mcp.json del repo ya está):
claude -p "Simula examples/pedido con el escenario as-is y dime el cuello de botella." \
--mcp-config .mcp.json --strict-mcp-config --allowedTools "mcp__lila__run_simulation"
Devuelve Task_Preparar, el mismo bottlenecks[0].elementId que lila run. El nombre de la tool
en --allowedTools es mcp__<servidor>__<tool>; sin él, claude -p no puede aprobar la llamada y
se queda sin usar el servidor.
El inspector oficial (npx @modelcontextprotocol/inspector …) también sirve, pero abre una UI en
el navegador y se queda en primer plano: no lo lances desde un agente ni en CI.
Límites conocidos
- Con
detail: "full"el resultado viaja dos veces.run_simulationmanda el mismo objeto como texto y comostructuredContent, que es lo que recomienda la especificación MCP por compatibilidad: paraexamples/pedidocon 30 replicaciones el resultado completo son ~93 KB de mensaje. El resumen por defecto lo mantiene pequeño. compare_scenariosno publicaoutputSchema(no hay esquema zod deCompareResulten el repo), aunque sí mandastructuredContent. Un cliente estricto puede rechazarlo.- Todo pasa por el disco del servidor.
saveToescribe con los permisos del proceso servidor y sobrescribe sin preguntar; no hay sandbox de rutas. create_processmaqueta conbpmn-auto-layout1.3, que no dibuja pool ni carriles: Lila conserva sus columnas y filas, mueve cada nodo a su carril, vuelve a trazar los flujos en ángulo recto (rodeando las figuras que estorban) y pone la etiqueta de cada rama sobre su propia rama. Los carriles pueden salir más altos de lo necesario, el nombre de una compuerta puede quedar sobre un flujo que sale por abajo, y un subproceso se crea colapsado con un paso vacío dentro. Si el dibujo importa, se acomoda en la app.edit_processconlayout: truemaqueta de nuevo todo el proceso editado (se pierden las posiciones a mano en él); un subproceso expandido queda colapsado, y los pools verticales (como los dibuja Bizagi) quedan horizontales. Conlayout: falsela colocación es simple: a la derecha del predecesor, en su carril; no desenreda cruces. Solo se puede editar el proceso principal del BPMN, y los carriles anidados solo a través de sus carriles hoja.