@lila-modeler/mcp 0.0.0-stage → 1.0.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +71 -2
- package/bin/lila-mcp.js +15 -0
- package/dist/agent-tools.d.ts +15 -0
- package/dist/agent-tools.js +162 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.js +10 -0
- package/dist/json-patch.d.ts +26 -0
- package/dist/json-patch.js +174 -0
- package/dist/scenario-input.d.ts +12 -0
- package/dist/scenario-input.js +40 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +1077 -0
- package/package.json +52 -4
package/dist/server.js
ADDED
|
@@ -0,0 +1,1077 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Servidor MCP de Lila Modeler: tools sobre `@lila-modeler/engine`, sin lógica propia. `validate_bpmn` y
|
|
3
|
+
* `describe_process` son de LILA-053; `run_simulation` y `compare_scenarios` de LILA-054, sobre
|
|
4
|
+
* `@lila-modeler/engine/cli-shared` (extraído de `cli.ts` en el mismo ticket, sin cambiar su salida).
|
|
5
|
+
* `createServer()` solo registra tools; conectar un transporte (stdio, in-memory para tests) es
|
|
6
|
+
* responsabilidad de quien lo use — ver `src/bin.ts` para el caso stdio real.
|
|
7
|
+
*
|
|
8
|
+
* Idioma (LILA-211, parte 2): `title`, `description` y los `.describe()` son **fijos en inglés**
|
|
9
|
+
* —son la superficie del protocolo, la que un cliente ya leyó en `tools/list`, y un `locale` por
|
|
10
|
+
* llamada no puede reescribirla—. Todo lo demás (el resumen de `describe_process`, los mensajes de
|
|
11
|
+
* `isError` y los problemas que devuelve el motor) sale del catálogo: `createServer({ locale })`
|
|
12
|
+
* fija el idioma por defecto del servidor y cada tool acepta `locale` para una llamada suelta.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { posix } from 'node:path';
|
|
16
|
+
import { validateBpmnXml } from '@lila-modeler/engine/bpmn';
|
|
17
|
+
import { absolutePath, comparablePath, compareWarnings, loadModelSource, loadResolvedScenario, loadValidatedModel, readJsonFile, resultWithBoundaryWarnings, scenarioSource, targetScenarios, validatedModelOf, withRunOverrides, writeJsonAtomic, } from '@lila-modeler/engine/cli-shared';
|
|
18
|
+
import { createLilaProcess, editLilaProcess, findLilaScenario, isLilaPath, lilaScenarioEntryName, lilaScenarioPath, lilaScenarioReader, openLilaProcess, readLilaOutline, writeLilaScenario, exportDiagram, exportDocument, exportResults, saveSimulationRun, writeExportDirectory, writeExportFile, } from '@lila-modeler/engine/project-fs';
|
|
19
|
+
import { runResultSchema } from '@lila-modeler/engine/result-schema';
|
|
20
|
+
import { resolveExtends, schemaIssueLines, scenarioErrors, parseScenario, validateScenario, } from '@lila-modeler/engine/schema';
|
|
21
|
+
import { compare, simulate } from '@lila-modeler/engine';
|
|
22
|
+
import { messages, resolveLocale } from '@lila-modeler/engine/messages';
|
|
23
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
24
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
import { registerAgentTools } from './agent-tools.js';
|
|
27
|
+
import { applyJsonPatch, buildPatchDelta } from './json-patch.js';
|
|
28
|
+
import { resolveScenarioInput } from './scenario-input.js';
|
|
29
|
+
const NAME = 'lila-mcp';
|
|
30
|
+
const VERSION = '1.0.0-beta.22';
|
|
31
|
+
/** Idioma por llamada: sobrescribe el del servidor solo para esa respuesta. */
|
|
32
|
+
const localeSchema = z
|
|
33
|
+
.enum(['en', 'es'])
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Language of the texts this call returns. Defaults to the server locale (LILA_LANG).');
|
|
36
|
+
function message(error) {
|
|
37
|
+
return error instanceof Error ? error.message : String(error);
|
|
38
|
+
}
|
|
39
|
+
/** Los mensajes del catálogo son cuerpos; el nombre de la tool lo pone siempre quien llama. */
|
|
40
|
+
function toolMessage(tool, body) {
|
|
41
|
+
return `${tool}: ${body}`;
|
|
42
|
+
}
|
|
43
|
+
function textResult(payload) {
|
|
44
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: false };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `isError` marca que **la tool** falló (argumentos malos, archivo ilegible, XML impenetrable,
|
|
48
|
+
* escenario inválido), no que el modelo tenga errores de validación en `validate_bpmn`: eso es un
|
|
49
|
+
* resultado correcto y viaja en el JSON (`errors[]`). Ver `docs/MCP.md`.
|
|
50
|
+
*/
|
|
51
|
+
function errorResult(message) {
|
|
52
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
53
|
+
}
|
|
54
|
+
/** Lee un archivo del que ya se validó la existencia; devuelve el error legible si no se puede. */
|
|
55
|
+
function readModel(tool, file, locale) {
|
|
56
|
+
const M = messages(locale).mcp;
|
|
57
|
+
if (!existsSync(file))
|
|
58
|
+
return { error: toolMessage(tool, M.fileMissing(file)) };
|
|
59
|
+
try {
|
|
60
|
+
return { xml: readFileSync(file, 'utf8') };
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return { error: toolMessage(tool, message(error)) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resuelve el par `path` | `xml` que aceptan `validate_bpmn` y `describe_process`: exactamente uno
|
|
68
|
+
* de los dos, nunca los dos a la vez (pasar ambos sería una precedencia silenciosa sobre un modelo
|
|
69
|
+
* que el usuario no pidió).
|
|
70
|
+
*/
|
|
71
|
+
async function modelXml(tool, path, xml, process, locale) {
|
|
72
|
+
const M = messages(locale).mcp;
|
|
73
|
+
if (path !== undefined && xml !== undefined) {
|
|
74
|
+
return { error: toolMessage(tool, M.bothPathAndXml()) };
|
|
75
|
+
}
|
|
76
|
+
// `process` selects a process of a `.lila` (#466); with a `.bpmn` or inline XML it is a mistake.
|
|
77
|
+
if (process !== undefined && (path === undefined || !isLilaPath(path))) {
|
|
78
|
+
return { error: toolMessage(tool, messages(locale).cli.processOnlyForLila()) };
|
|
79
|
+
}
|
|
80
|
+
if (xml !== undefined)
|
|
81
|
+
return { xml };
|
|
82
|
+
if (path !== undefined && isLilaPath(path)) {
|
|
83
|
+
try {
|
|
84
|
+
const source = await loadModelSource(path, { process, locale });
|
|
85
|
+
return { xml: source.xml, lila: source.lila };
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return { error: toolMessage(tool, message(error)) };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (path !== undefined)
|
|
92
|
+
return readModel(tool, absolutePath(path), locale);
|
|
93
|
+
return { error: toolMessage(tool, M.pathOrXml()) };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The `.lila` named by `model` (#466), opened on `process`; `undefined` for a `.bpmn` or no model.
|
|
97
|
+
* A `process` without a `.lila` model is refused instead of being ignored.
|
|
98
|
+
*/
|
|
99
|
+
async function lilaModel(model, process, locale) {
|
|
100
|
+
if (model !== undefined && isLilaPath(model))
|
|
101
|
+
return openLilaProcess(model, { process, locale });
|
|
102
|
+
if (process !== undefined)
|
|
103
|
+
throw new Error(messages(locale).cli.processOnlyForLila());
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const GATEWAY_TYPES = new Set(['xor', 'or', 'and', 'eventGateway']);
|
|
107
|
+
/** Resumen legible pedido por LILA-053: nodos, gateways, lanes, subprocesos, en `locale`. */
|
|
108
|
+
function describeIr(report, resources, scenarioError, locale) {
|
|
109
|
+
const catalog = messages(locale);
|
|
110
|
+
const M = catalog.mcp;
|
|
111
|
+
const C = catalog.cli;
|
|
112
|
+
const { ir, ignoredProcessIds, errors, warnings } = report;
|
|
113
|
+
const lines = [];
|
|
114
|
+
lines.push(C.process(`${ir.id}${ir.name === '' ? '' : ` (${ir.name})`}`));
|
|
115
|
+
const counts = new Map();
|
|
116
|
+
for (const node of Object.values(ir.nodes))
|
|
117
|
+
counts.set(node.type, (counts.get(node.type) ?? 0) + 1);
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push(M.nodes(Object.keys(ir.nodes).length));
|
|
120
|
+
for (const [type, n] of [...counts].sort(([a], [b]) => a.localeCompare(b))) {
|
|
121
|
+
lines.push(` ${M.nodeType(type)}: ${n}`);
|
|
122
|
+
}
|
|
123
|
+
const gatewayIds = Object.entries(ir.nodes).filter(([, node]) => GATEWAY_TYPES.has(node.type));
|
|
124
|
+
if (gatewayIds.length > 0) {
|
|
125
|
+
lines.push('');
|
|
126
|
+
lines.push(M.gateways());
|
|
127
|
+
for (const [id, node] of gatewayIds) {
|
|
128
|
+
lines.push(` ${id}${node.name === '' ? '' : ` (${node.name})`} [${node.type}]:`);
|
|
129
|
+
for (const flowId of node.outgoing) {
|
|
130
|
+
const flow = ir.flows[flowId];
|
|
131
|
+
if (flow === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
const mark = flow.isDefault ? ` ${C.defaultFlow()}` : '';
|
|
134
|
+
lines.push(` -> ${flow.to}${flow.name === '' ? '' : ` ${flow.name}`}${mark}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const lanes = new Map();
|
|
139
|
+
for (const [id, node] of Object.entries(ir.nodes)) {
|
|
140
|
+
if (node.lane === undefined)
|
|
141
|
+
continue;
|
|
142
|
+
(lanes.get(node.lane) ?? lanes.set(node.lane, []).get(node.lane))?.push(id);
|
|
143
|
+
}
|
|
144
|
+
if (lanes.size > 0) {
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(M.lanes());
|
|
147
|
+
for (const [lane, ids] of lanes)
|
|
148
|
+
lines.push(` ${lane}: ${ids.join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
const subprocesses = new Map();
|
|
151
|
+
for (const [id, node] of Object.entries(ir.nodes)) {
|
|
152
|
+
if (node.subprocessId === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
(subprocesses.get(node.subprocessId) ?? subprocesses.set(node.subprocessId, []).get(node.subprocessId))?.push(id);
|
|
155
|
+
}
|
|
156
|
+
if (subprocesses.size > 0) {
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push(M.embeddedSubprocesses());
|
|
159
|
+
for (const [id, ids] of subprocesses)
|
|
160
|
+
lines.push(` ${id}: ${ids.join(', ')}`);
|
|
161
|
+
}
|
|
162
|
+
if (ignoredProcessIds.length > 0) {
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push(C.otherProcesses(ignoredProcessIds.join(', ')));
|
|
165
|
+
}
|
|
166
|
+
// Un modelo fuera del perfil se describe igual, pero no se puede simular: decirlo aquí evita
|
|
167
|
+
// que un agente encadene describe_process -> run_simulation sobre un modelo que no corre.
|
|
168
|
+
lines.push('');
|
|
169
|
+
const counted = `${M.errorCount(errors.length)}, ${M.warningCount(warnings.length)}`;
|
|
170
|
+
lines.push(errors.length > 0 ? M.validationWithErrors(counted) : M.validation(counted));
|
|
171
|
+
lines.push('');
|
|
172
|
+
if (scenarioError !== undefined) {
|
|
173
|
+
lines.push(M.referencedResourcesUnreadable(scenarioError));
|
|
174
|
+
}
|
|
175
|
+
else if (resources.length > 0) {
|
|
176
|
+
lines.push(M.referencedResources());
|
|
177
|
+
for (const line of resources)
|
|
178
|
+
lines.push(` ${line}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
lines.push(M.referencedResourcesNone());
|
|
182
|
+
}
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
/** Referencias a recursos por elemento, a partir de un escenario ya resuelto (`extends` incluido). */
|
|
186
|
+
function resourceLines(scenario) {
|
|
187
|
+
const lines = [];
|
|
188
|
+
for (const [elementId, element] of Object.entries(scenario.elements ?? {})) {
|
|
189
|
+
const refs = element.resources ?? [];
|
|
190
|
+
if (refs.length === 0)
|
|
191
|
+
continue;
|
|
192
|
+
const parts = refs.map((use) => `${use.ref} x${use.quantity}`);
|
|
193
|
+
lines.push(`${elementId}: ${parts.join(', ')}`);
|
|
194
|
+
}
|
|
195
|
+
return lines;
|
|
196
|
+
}
|
|
197
|
+
/** Recursos por elemento del escenario, o el motivo por el que no se pudo leer. */
|
|
198
|
+
function scenarioResources(file, locale, lila) {
|
|
199
|
+
try {
|
|
200
|
+
const source = scenarioSource(file, lila, locale);
|
|
201
|
+
const raw = resolveExtends(source.path, source.read);
|
|
202
|
+
const parsed = parseScenario(raw, { locale });
|
|
203
|
+
if (parsed.success)
|
|
204
|
+
return { resources: resourceLines(parsed.data), scenario: parsed.data };
|
|
205
|
+
// `$` para la raíz, igual que `schemaIssueLines` del motor: no es texto traducible.
|
|
206
|
+
const detail = parsed.error.issues
|
|
207
|
+
.map((issue) => `${issue.path.join('.') || '$'}: ${issue.message}`)
|
|
208
|
+
.join('; ');
|
|
209
|
+
return { error: detail };
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return { error: message(error) };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/* ------------------------------------------------------------------ *
|
|
216
|
+
* `run_simulation` / `compare_scenarios` (LILA-054)
|
|
217
|
+
* ------------------------------------------------------------------ */
|
|
218
|
+
const scenarioInputSchema = z.union([
|
|
219
|
+
z.string().describe('Path to a .json scenario, relative to the cwd of the server process.'),
|
|
220
|
+
z.record(z.string(), z.unknown()).describe('The already resolved scenario, inline.'),
|
|
221
|
+
]);
|
|
222
|
+
/**
|
|
223
|
+
* Igual pipeline que `runCommand` de `cli.ts` (compartida en `@lila-modeler/engine/cli-shared`): valida
|
|
224
|
+
* modelo y escenario, aplica overrides y simula con `log: false`. Ningún campo de nivel 2/3 se
|
|
225
|
+
* rechaza (LILA-184): `resources` y `calendars` los simula el motor desde LILA-033…036 y LILA-041.
|
|
226
|
+
* El `RunResult` devuelto es el mismo objeto que produce `lila run --json`.
|
|
227
|
+
*/
|
|
228
|
+
async function runSimulation({ model, process, scenario, seed, replications, saveTo, saveRun }, locale) {
|
|
229
|
+
const M = messages(locale).mcp;
|
|
230
|
+
const fail = (body) => errorResult(toolMessage('run_simulation', body));
|
|
231
|
+
let lila;
|
|
232
|
+
let resolved;
|
|
233
|
+
try {
|
|
234
|
+
lila = await lilaModel(model, process, locale);
|
|
235
|
+
resolved = resolveScenarioInput(scenario, locale, lila);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return fail(message(error));
|
|
239
|
+
}
|
|
240
|
+
// `saveRun` (#538) stores the run in the archive: it needs a `.lila` and one of its scenarios.
|
|
241
|
+
if (saveRun === true && lila === undefined)
|
|
242
|
+
return fail(messages(locale).cli.saveRunNeedsLila());
|
|
243
|
+
if (saveRun === true && typeof scenario !== 'string') {
|
|
244
|
+
return fail(messages(locale).cli.saveRunNeedsArchiveScenario(M.inlineScenario()));
|
|
245
|
+
}
|
|
246
|
+
const modelPath = lila !== undefined ? lila.modelPath : model === undefined ? resolved.model : absolutePath(model);
|
|
247
|
+
if (model !== undefined && comparablePath(modelPath) !== comparablePath(resolved.model)) {
|
|
248
|
+
return fail(M.modelMismatch(modelPath, resolved.model));
|
|
249
|
+
}
|
|
250
|
+
let ir;
|
|
251
|
+
let modelValidation;
|
|
252
|
+
let elsewhere;
|
|
253
|
+
try {
|
|
254
|
+
// #546: the process the scenario targets is the one simulated.
|
|
255
|
+
({ ir, validation: modelValidation, elsewhere } =
|
|
256
|
+
lila === undefined
|
|
257
|
+
? await loadValidatedModel(modelPath, locale, { scenarios: [resolved] })
|
|
258
|
+
: await validatedModelOf({ path: lila.modelPath, xml: lila.process.model.xml, lila }, locale, [resolved]));
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return fail(message(error));
|
|
262
|
+
}
|
|
263
|
+
if (modelValidation.errors.length > 0) {
|
|
264
|
+
return fail(M.modelInvalid(JSON.stringify(modelValidation.errors)));
|
|
265
|
+
}
|
|
266
|
+
const withOverrides = withRunOverrides(resolved, { seed, replications });
|
|
267
|
+
const scenarioProblems = validateScenario(withOverrides, ir, { locale, elsewhere });
|
|
268
|
+
const scenarioIssues = scenarioErrors(scenarioProblems);
|
|
269
|
+
if (scenarioIssues.length > 0)
|
|
270
|
+
return fail(M.scenarioInvalid(JSON.stringify(scenarioIssues)));
|
|
271
|
+
let result;
|
|
272
|
+
try {
|
|
273
|
+
const simulated = simulate(ir, withOverrides, { log: false, locale });
|
|
274
|
+
result = resultWithBoundaryWarnings(simulated, modelValidation, scenarioProblems);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
return fail(message(error));
|
|
278
|
+
}
|
|
279
|
+
if (saveTo !== undefined) {
|
|
280
|
+
try {
|
|
281
|
+
writeJsonAtomic(saveTo, result, locale);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
return fail(message(error));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const content = [{ type: 'text', text: JSON.stringify(result, null, 2) }];
|
|
288
|
+
if (saveRun === true) {
|
|
289
|
+
try {
|
|
290
|
+
// The `RunResult` stays the structured content (its `outputSchema`); where the run went is a
|
|
291
|
+
// second text block.
|
|
292
|
+
const savedRun = await saveSimulationRun(lila, scenario, withOverrides, result, locale);
|
|
293
|
+
content.push({ type: 'text', text: JSON.stringify({ savedRun }, null, 2) });
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
return fail(message(error));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return { content, isError: false, structuredContent: result };
|
|
300
|
+
}
|
|
301
|
+
/** Etiqueta de un escenario en los mensajes de error: su ruta, o su posición si vino inline. */
|
|
302
|
+
function scenarioLabel(entry, index) {
|
|
303
|
+
return typeof entry === 'string' ? entry : `scenarios[${index}]`;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Igual pipeline que `compareCommand` de `cli.ts`: todo escenario se resuelve y valida contra el
|
|
307
|
+
* mismo modelo antes de simular ninguno (un escenario inválido en la posición n no cuesta simular
|
|
308
|
+
* los n − 1 anteriores). El `CompareResult` devuelto es el mismo objeto que produce
|
|
309
|
+
* `lila compare --json`; los avisos que la CLI imprime aparte de la tabla (semillas distintas,
|
|
310
|
+
* `baseTimeUnit` distinto, réplicas insuficientes) van en `notes`, sin tocar `comparison`.
|
|
311
|
+
*/
|
|
312
|
+
async function compareScenarios({ model, process, scenarios, seed, replications, saveTo }, locale) {
|
|
313
|
+
const M = messages(locale).mcp;
|
|
314
|
+
const fail = (body) => errorResult(toolMessage('compare_scenarios', body));
|
|
315
|
+
if (scenarios.length < 2)
|
|
316
|
+
return fail(M.atLeastTwoScenarios());
|
|
317
|
+
let lila;
|
|
318
|
+
try {
|
|
319
|
+
lila = await lilaModel(model, process, locale);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
return fail(message(error));
|
|
323
|
+
}
|
|
324
|
+
const resolved = [];
|
|
325
|
+
for (const [index, entry] of scenarios.entries()) {
|
|
326
|
+
const label = scenarioLabel(entry, index);
|
|
327
|
+
try {
|
|
328
|
+
resolved.push({ label, scenario: resolveScenarioInput(entry, locale, lila) });
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
return fail(`${label}: ${message(error)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const referenceModel = lila !== undefined
|
|
335
|
+
? lila.modelPath
|
|
336
|
+
: model === undefined
|
|
337
|
+
? resolved[0].scenario.model
|
|
338
|
+
: absolutePath(model);
|
|
339
|
+
for (const { label, scenario } of resolved) {
|
|
340
|
+
if (comparablePath(referenceModel) !== comparablePath(scenario.model)) {
|
|
341
|
+
return fail(`${label}: ${M.modelMismatch(referenceModel, scenario.model)}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
let ir;
|
|
345
|
+
let modelValidation;
|
|
346
|
+
let elsewhere;
|
|
347
|
+
const all = resolved.map(({ scenario }) => scenario);
|
|
348
|
+
try {
|
|
349
|
+
({ ir, validation: modelValidation, elsewhere } =
|
|
350
|
+
lila === undefined
|
|
351
|
+
? await loadValidatedModel(referenceModel, locale, { scenarios: all })
|
|
352
|
+
: await validatedModelOf({ path: lila.modelPath, xml: lila.process.model.xml, lila }, locale, all));
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
return fail(message(error));
|
|
356
|
+
}
|
|
357
|
+
if (modelValidation.errors.length > 0) {
|
|
358
|
+
return fail(M.modelInvalid(JSON.stringify(modelValidation.errors)));
|
|
359
|
+
}
|
|
360
|
+
// Todo se valida antes de simular nada, igual que `compareCommand`.
|
|
361
|
+
const validated = [];
|
|
362
|
+
for (const { label, scenario } of resolved) {
|
|
363
|
+
const withOverrides = withRunOverrides(scenario, { seed, replications });
|
|
364
|
+
const problems = validateScenario(withOverrides, ir, { locale, elsewhere });
|
|
365
|
+
const issues = scenarioErrors(problems);
|
|
366
|
+
if (issues.length > 0)
|
|
367
|
+
return fail(`${label}: ${M.scenarioInvalid(JSON.stringify(issues))}`);
|
|
368
|
+
validated.push({ label, scenario: withOverrides, problems });
|
|
369
|
+
}
|
|
370
|
+
let loaded;
|
|
371
|
+
try {
|
|
372
|
+
loaded = validated.map(({ label, scenario, problems }) => ({
|
|
373
|
+
file: label,
|
|
374
|
+
scenario,
|
|
375
|
+
result: resultWithBoundaryWarnings(simulate(ir, scenario, { log: false, locale }), modelValidation, problems),
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
return fail(message(error));
|
|
380
|
+
}
|
|
381
|
+
let comparison;
|
|
382
|
+
try {
|
|
383
|
+
comparison = compare(loaded.map((entry) => entry.result), { locale });
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
// E-COMPARE-VACIO u otro error de `compare()`: no debería pasar tras el check de arriba, pero
|
|
387
|
+
// se atrapa igual para no tumbar el servidor (mismo criterio que #53).
|
|
388
|
+
return fail(message(error));
|
|
389
|
+
}
|
|
390
|
+
const notes = compareWarnings(loaded, loaded[0].scenario.run.baseTimeUnit, locale);
|
|
391
|
+
if (saveTo !== undefined) {
|
|
392
|
+
try {
|
|
393
|
+
writeJsonAtomic(saveTo, comparison, locale);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
return fail(message(error));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const payload = { comparison, notes };
|
|
400
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], isError: false, structuredContent: payload };
|
|
401
|
+
}
|
|
402
|
+
/* ------------------------------------------------------------------ *
|
|
403
|
+
* `patch_scenario` (LILA-055)
|
|
404
|
+
* ------------------------------------------------------------------ */
|
|
405
|
+
function isPlainObject(value) {
|
|
406
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Esquema + modelo + `validateScenario`, en una sola pasada: la misma comprobación sirve
|
|
410
|
+
* para el modo (a) sobre el escenario ya parcheado, y para el modo (b) sobre el candidato que
|
|
411
|
+
* realmente se va a escribir (que puede no coincidir con el parcheado si `extendsFrom` no es el
|
|
412
|
+
* mismo archivo que `scenario`). Solo si esto no devuelve error se escribe algo a disco.
|
|
413
|
+
* El esquema se pasa por `parseScenario` (LILA-202), no por `ScenarioSchema.safeParse`: los
|
|
414
|
+
* defectos del escenario parcheado salen en español, igual que en la CLI y en `run_simulation`.
|
|
415
|
+
*/
|
|
416
|
+
async function validatePatchedScenario(raw, locale, lila) {
|
|
417
|
+
const M = messages(locale).mcp;
|
|
418
|
+
const parsed = parseScenario(raw, { locale });
|
|
419
|
+
if (!parsed.success) {
|
|
420
|
+
// `schemaIssueLines` es la que pone los códigos de § 17 (`E-CLAVE-DESCONOCIDA`) que la CLI ya
|
|
421
|
+
// emite (LILA-198); formatear los defectos aquí a mano los perdía.
|
|
422
|
+
const detail = schemaIssueLines(parsed.error.issues, { locale }).join('; ');
|
|
423
|
+
return { ok: false, error: `${M.invalidAfterPatchLabel()} ${detail}` };
|
|
424
|
+
}
|
|
425
|
+
// `ScenarioSchema` tiene `model` y `run` opcionales (un archivo con `extends` los hereda), así
|
|
426
|
+
// que un patch puede borrarlos y aun así pasar el esquema. `loadResolvedScenario` los exige y es
|
|
427
|
+
// lo que hace el modo (b); sin este par de guardas el modo (a) escribía un escenario que la
|
|
428
|
+
// propia `run_simulation` rechaza después — y el `remove /model` filtraba el TypeError de
|
|
429
|
+
// `node:path` en vez de un mensaje del dominio.
|
|
430
|
+
if (parsed.data.model === undefined)
|
|
431
|
+
return { ok: false, error: M.patchedMissingModel() };
|
|
432
|
+
if (parsed.data.run === undefined)
|
|
433
|
+
return { ok: false, error: M.patchedMissingRun() };
|
|
434
|
+
const scenario = parsed.data;
|
|
435
|
+
let ir;
|
|
436
|
+
let modelValidation;
|
|
437
|
+
let elsewhere;
|
|
438
|
+
// In a `.lila` the model is the process's own `model.bpmn` (#466): a patch that points `model`
|
|
439
|
+
// elsewhere is refused, like `run_simulation` refuses a scenario of another model.
|
|
440
|
+
if (lila !== undefined && comparablePath(scenario.model) !== comparablePath(lila.modelPath)) {
|
|
441
|
+
return { ok: false, error: M.modelMismatch(lila.modelPath, scenario.model) };
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
({ ir, validation: modelValidation, elsewhere } =
|
|
445
|
+
lila === undefined
|
|
446
|
+
? await loadValidatedModel(scenario.model, locale, { scenarios: [scenario] })
|
|
447
|
+
: await validatedModelOf({ path: lila.modelPath, xml: lila.process.model.xml, lila }, locale, [scenario]));
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
return { ok: false, error: message(error) };
|
|
451
|
+
}
|
|
452
|
+
if (modelValidation.errors.length > 0) {
|
|
453
|
+
return { ok: false, error: M.modelInvalid(JSON.stringify(modelValidation.errors)) };
|
|
454
|
+
}
|
|
455
|
+
const problems = validateScenario(scenario, ir, { locale, elsewhere });
|
|
456
|
+
const issues = scenarioErrors(problems);
|
|
457
|
+
if (issues.length > 0) {
|
|
458
|
+
return { ok: false, error: `${M.invalidAfterPatchLabel()} ${JSON.stringify(issues)}` };
|
|
459
|
+
}
|
|
460
|
+
const notes = problems.filter((p) => p.severity === 'warning').map((p) => `${p.code}: ${p.message}`);
|
|
461
|
+
return { ok: true, scenario, notes };
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* `loadResolvedScenario` prefija sus errores con la ruta del archivo. En el modo (b) esa ruta es
|
|
465
|
+
* la de `saveTo`: un archivo que todavía no existe y que, si el escenario no valida, no se va a
|
|
466
|
+
* escribir. Citarlo manda al agente a abrir una ruta fantasma — el mismo problema que el escenario
|
|
467
|
+
* inline de LILA-054. Se le quita el prefijo y se alinea el texto con el del modo (a).
|
|
468
|
+
*/
|
|
469
|
+
function withoutPhantomFile(error, phantom, locale) {
|
|
470
|
+
const catalog = messages(locale);
|
|
471
|
+
const raw = message(error).replaceAll('\n', ' ');
|
|
472
|
+
return raw.startsWith(`${phantom}: `)
|
|
473
|
+
? raw
|
|
474
|
+
.slice(phantom.length + 2)
|
|
475
|
+
.replace(catalog.cli.invalidScenarioLabel(), catalog.mcp.invalidAfterPatchLabel())
|
|
476
|
+
: raw;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* El escenario resuelto trae `model` como ruta absoluta: `resolveExtends` la ancla al archivo que
|
|
480
|
+
* la declara. Escribirla tal cual en el modo (a) vuelve el archivo dependiente de la máquina que
|
|
481
|
+
* corrió la tool (`/Users/quien-sea/...`), y deja de resolver en cualquier otro checkout. Se
|
|
482
|
+
* reescribe relativa al propio escenario, que es como la declaran los del repo y la misma regla
|
|
483
|
+
* que el modo (b) aplica a `extends`.
|
|
484
|
+
*/
|
|
485
|
+
function withRelativeModel(scenario, file) {
|
|
486
|
+
return { ...scenario, model: posix.relative(posix.dirname(file), scenario.model) };
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Dos modos (LILA-055): sin `saveTo`, parchea `scenario` en sitio (escenario resuelto completo,
|
|
490
|
+
* sin `extends`: el patch se aplica sobre el escenario ya fusionado). Con `saveTo`, crea un
|
|
491
|
+
* archivo nuevo que declara `extends: extendsFrom` (por defecto, el propio `scenario`) y contiene
|
|
492
|
+
* solo el delta del patch — como `to-be-3-cajeros.scenario.json`. En ambos casos, solo se escribe
|
|
493
|
+
* si el escenario resultante valida contra el modelo; nunca dos veces (primero se valida, después
|
|
494
|
+
* se escribe una única vez).
|
|
495
|
+
*/
|
|
496
|
+
async function patchScenario(input, locale) {
|
|
497
|
+
if (input.project !== undefined) {
|
|
498
|
+
if (!isLilaPath(input.project)) {
|
|
499
|
+
return errorResult(toolMessage('patch_scenario', messages(locale).mcp.projectNotLila(input.project)));
|
|
500
|
+
}
|
|
501
|
+
return patchLilaScenario(input, input.project, locale);
|
|
502
|
+
}
|
|
503
|
+
if (input.process !== undefined) {
|
|
504
|
+
return errorResult(toolMessage('patch_scenario', messages(locale).cli.processOnlyForLila()));
|
|
505
|
+
}
|
|
506
|
+
const { scenario, patch, saveTo, extendsFrom, name, description } = input;
|
|
507
|
+
const M = messages(locale).mcp;
|
|
508
|
+
const fail = (body) => errorResult(toolMessage('patch_scenario', body));
|
|
509
|
+
const scenarioPath = absolutePath(scenario);
|
|
510
|
+
let base;
|
|
511
|
+
try {
|
|
512
|
+
base = loadResolvedScenario(scenarioPath, undefined, locale);
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
return fail(message(error));
|
|
516
|
+
}
|
|
517
|
+
let patchedRaw;
|
|
518
|
+
try {
|
|
519
|
+
patchedRaw = applyJsonPatch(base, patch);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
return fail(message(error));
|
|
523
|
+
}
|
|
524
|
+
if (saveTo === undefined) {
|
|
525
|
+
// Modo (a): patchear en sitio.
|
|
526
|
+
if (isPlainObject(patchedRaw)) {
|
|
527
|
+
if (name !== undefined)
|
|
528
|
+
patchedRaw.name = name;
|
|
529
|
+
if (description !== undefined)
|
|
530
|
+
patchedRaw.description = description;
|
|
531
|
+
}
|
|
532
|
+
const outcome = await validatePatchedScenario(patchedRaw, locale);
|
|
533
|
+
if (!outcome.ok)
|
|
534
|
+
return fail(outcome.error);
|
|
535
|
+
try {
|
|
536
|
+
const file = writeJsonAtomic(scenarioPath, withRelativeModel(outcome.scenario, scenarioPath), locale);
|
|
537
|
+
return textResult({ scenario: outcome.scenario, file, notes: outcome.notes });
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
return fail(message(error));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Modo (b): archivo nuevo con `extends` al padre, solo el delta del patch.
|
|
544
|
+
if (!isPlainObject(patchedRaw))
|
|
545
|
+
return fail(M.patchNotAnObject());
|
|
546
|
+
const saveToPath = absolutePath(saveTo);
|
|
547
|
+
const extendsFromPath = absolutePath(extendsFrom ?? scenario);
|
|
548
|
+
const relExtends = posix.relative(posix.dirname(saveToPath), extendsFromPath);
|
|
549
|
+
const delta = buildPatchDelta(patch, patchedRaw);
|
|
550
|
+
const candidate = { version: base.version, extends: relExtends, ...delta };
|
|
551
|
+
candidate['name'] =
|
|
552
|
+
name ?? (typeof delta['name'] === 'string' ? delta['name'] : M.patchedName(base.name));
|
|
553
|
+
if (description !== undefined)
|
|
554
|
+
candidate['description'] = description;
|
|
555
|
+
let resolvedCandidate;
|
|
556
|
+
try {
|
|
557
|
+
resolvedCandidate = loadResolvedScenario(saveToPath, (file) => (file === saveToPath ? candidate : readJsonFile(file, locale)), locale);
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
return fail(withoutPhantomFile(error, saveToPath, locale));
|
|
561
|
+
}
|
|
562
|
+
const outcome = await validatePatchedScenario(resolvedCandidate, locale);
|
|
563
|
+
if (!outcome.ok)
|
|
564
|
+
return fail(outcome.error);
|
|
565
|
+
try {
|
|
566
|
+
const file = writeJsonAtomic(saveToPath, candidate, locale);
|
|
567
|
+
return textResult({ scenario: outcome.scenario, file, notes: outcome.notes });
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
return fail(message(error));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* `patch_scenario` on a scenario inside a `.lila` (#466). The same two modes as on disk, with every
|
|
575
|
+
* name inside the archive: `scenario` and `extendsFrom` name scenarios of the process
|
|
576
|
+
* (`findLilaScenario`), and `saveTo` is the name of the new entry. Validation runs against the
|
|
577
|
+
* process's own model, and only a scenario that validates is written — through `writeLilaScenario`,
|
|
578
|
+
* which saves the whole archive atomically and carries every other process through unchanged.
|
|
579
|
+
*/
|
|
580
|
+
async function patchLilaScenario({ scenario, process, patch, saveTo, extendsFrom, name, description }, project, locale) {
|
|
581
|
+
const M = messages(locale).mcp;
|
|
582
|
+
const fail = (body) => errorResult(toolMessage('patch_scenario', body));
|
|
583
|
+
let lila;
|
|
584
|
+
let entry;
|
|
585
|
+
let base;
|
|
586
|
+
try {
|
|
587
|
+
lila = await openLilaProcess(project, { process, locale });
|
|
588
|
+
entry = findLilaScenario(lila, scenario, locale);
|
|
589
|
+
base = loadResolvedScenario(lilaScenarioPath(lila, entry), lilaScenarioReader(lila, (file) => readJsonFile(file, locale), locale), locale);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
return fail(message(error));
|
|
593
|
+
}
|
|
594
|
+
const read = lilaScenarioReader(lila, (file) => readJsonFile(file, locale), locale);
|
|
595
|
+
const written = (target, resolvedScenario, notes) => textResult({ scenario: resolvedScenario, file: lila.file, process: lila.process.slug, entry: target, notes });
|
|
596
|
+
let patchedRaw;
|
|
597
|
+
try {
|
|
598
|
+
patchedRaw = applyJsonPatch(base, patch);
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
return fail(message(error));
|
|
602
|
+
}
|
|
603
|
+
if (saveTo === undefined) {
|
|
604
|
+
if (isPlainObject(patchedRaw)) {
|
|
605
|
+
if (name !== undefined)
|
|
606
|
+
patchedRaw.name = name;
|
|
607
|
+
if (description !== undefined)
|
|
608
|
+
patchedRaw.description = description;
|
|
609
|
+
}
|
|
610
|
+
const outcome = await validatePatchedScenario(patchedRaw, locale, lila);
|
|
611
|
+
if (!outcome.ok)
|
|
612
|
+
return fail(outcome.error);
|
|
613
|
+
try {
|
|
614
|
+
const path = lilaScenarioPath(lila, entry);
|
|
615
|
+
await writeLilaScenario(lila, entry, withRelativeModel(outcome.scenario, path), locale);
|
|
616
|
+
return written(entry, outcome.scenario, outcome.notes);
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
return fail(message(error));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (!isPlainObject(patchedRaw))
|
|
623
|
+
return fail(M.patchNotAnObject());
|
|
624
|
+
let target;
|
|
625
|
+
let parent;
|
|
626
|
+
try {
|
|
627
|
+
target = lilaScenarioEntryName(saveTo, locale);
|
|
628
|
+
parent = extendsFrom === undefined ? entry : findLilaScenario(lila, extendsFrom, locale);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
return fail(message(error));
|
|
632
|
+
}
|
|
633
|
+
const delta = buildPatchDelta(patch, patchedRaw);
|
|
634
|
+
// Both entries live in the same folder of the archive, so `extends` is the parent's entry name.
|
|
635
|
+
const candidate = { version: base.version, extends: parent, ...delta };
|
|
636
|
+
candidate['name'] = name ?? (typeof delta['name'] === 'string' ? delta['name'] : M.patchedName(base.name));
|
|
637
|
+
if (description !== undefined)
|
|
638
|
+
candidate['description'] = description;
|
|
639
|
+
const targetPath = lilaScenarioPath(lila, target);
|
|
640
|
+
let resolvedCandidate;
|
|
641
|
+
try {
|
|
642
|
+
resolvedCandidate = loadResolvedScenario(targetPath, (file) => (file === targetPath ? candidate : read(file)), locale);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
return fail(withoutPhantomFile(error, targetPath, locale));
|
|
646
|
+
}
|
|
647
|
+
const outcome = await validatePatchedScenario(resolvedCandidate, locale, lila);
|
|
648
|
+
if (!outcome.ok)
|
|
649
|
+
return fail(outcome.error);
|
|
650
|
+
try {
|
|
651
|
+
await writeLilaScenario(lila, target, candidate, locale);
|
|
652
|
+
return written(target, outcome.scenario, outcome.notes);
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
return fail(message(error));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
export function createServer(options = {}) {
|
|
659
|
+
const server = new McpServer({ name: NAME, version: VERSION }, { capabilities: { tools: {} } });
|
|
660
|
+
// El idioma del servidor: lo que pidió `lila mcp --lang`/`LILA_LANG`, o inglés.
|
|
661
|
+
const fallbackLocale = options.locale ?? 'en';
|
|
662
|
+
const localeOf = (locale) => locale ?? fallbackLocale;
|
|
663
|
+
server.registerTool('validate_bpmn', {
|
|
664
|
+
title: 'Validate BPMN',
|
|
665
|
+
description: 'Parses and validates a .bpmn file or one process of a .lila project (by path or inline ' +
|
|
666
|
+
'XML, one of the two) and returns the ' +
|
|
667
|
+
'same JSON as `lila validate --json`: the IR, the ignored processes, and structured ' +
|
|
668
|
+
'errors/warnings. A model with a non-empty `errors` cannot be simulated, but that is a ' +
|
|
669
|
+
'valid result of the tool: isError only marks that the tool failed (bad arguments, ' +
|
|
670
|
+
'unreadable file, impenetrable XML).',
|
|
671
|
+
inputSchema: z.object({
|
|
672
|
+
path: z
|
|
673
|
+
.string()
|
|
674
|
+
.optional()
|
|
675
|
+
.describe('Path to the .bpmn or .lila, relative to the cwd of the server process.'),
|
|
676
|
+
xml: z.string().optional().describe('XML content of the .bpmn, instead of a path.'),
|
|
677
|
+
process: z
|
|
678
|
+
.string()
|
|
679
|
+
.optional()
|
|
680
|
+
.describe('Slug of the process when the .lila holds several; implicit when it holds one.'),
|
|
681
|
+
locale: localeSchema,
|
|
682
|
+
}),
|
|
683
|
+
}, async ({ path, xml, process, locale }) => {
|
|
684
|
+
const language = localeOf(locale);
|
|
685
|
+
const read = await modelXml('validate_bpmn', path, xml, process, language);
|
|
686
|
+
if ('error' in read)
|
|
687
|
+
return errorResult(read.error);
|
|
688
|
+
try {
|
|
689
|
+
// #546: in a `.lila`, the process its scenarios target, as `run_simulation` simulates it.
|
|
690
|
+
return textResult(await validateBpmnXml(read.xml, { locale: language, scenarios: targetScenarios(read.lila) }));
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
return errorResult(toolMessage('validate_bpmn', message(error)));
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
server.registerTool('describe_process', {
|
|
697
|
+
title: 'Describe process',
|
|
698
|
+
description: 'Parses a .bpmn or one process of a .lila (by path or inline XML, one of the two) and ' +
|
|
699
|
+
'returns its IR (ProcessIR) ' +
|
|
700
|
+
'together with a readable summary (the `resumen` key): node count by type, gateways with ' +
|
|
701
|
+
'their outgoing flows, lanes, subprocesses, other processes in the file and whether the ' +
|
|
702
|
+
'model passes validation. With the optional `scenario` (path to a .json scenario, resolves ' +
|
|
703
|
+
'`extends`; with a .lila, also the name of a scenario of that process) it adds the ' +
|
|
704
|
+
'resources referenced by element.',
|
|
705
|
+
inputSchema: z.object({
|
|
706
|
+
path: z
|
|
707
|
+
.string()
|
|
708
|
+
.optional()
|
|
709
|
+
.describe('Path to the .bpmn or .lila, relative to the cwd of the server process.'),
|
|
710
|
+
xml: z.string().optional().describe('XML content of the .bpmn, instead of a path.'),
|
|
711
|
+
process: z
|
|
712
|
+
.string()
|
|
713
|
+
.optional()
|
|
714
|
+
.describe('Slug of the process when the .lila holds several; implicit when it holds one.'),
|
|
715
|
+
scenario: z
|
|
716
|
+
.string()
|
|
717
|
+
.optional()
|
|
718
|
+
.describe('Path to a .json scenario, relative to the cwd; with a .lila, or a scenario name in it.'),
|
|
719
|
+
locale: localeSchema,
|
|
720
|
+
}),
|
|
721
|
+
}, async ({ path, xml, process, scenario, locale }) => {
|
|
722
|
+
const language = localeOf(locale);
|
|
723
|
+
const read = await modelXml('describe_process', path, xml, process, language);
|
|
724
|
+
if ('error' in read)
|
|
725
|
+
return errorResult(read.error);
|
|
726
|
+
const scenarioResult = scenario === undefined ? undefined : scenarioResources(scenario, language, read.lila);
|
|
727
|
+
// #546: the process described is the one the scenarios target, as `run_simulation` runs it.
|
|
728
|
+
const given = scenarioResult !== undefined && 'scenario' in scenarioResult ? [scenarioResult.scenario] : [];
|
|
729
|
+
let report;
|
|
730
|
+
try {
|
|
731
|
+
report = await validateBpmnXml(read.xml, { locale: language, scenarios: targetScenarios(read.lila, given) });
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
return errorResult(toolMessage('describe_process', message(error)));
|
|
735
|
+
}
|
|
736
|
+
const resources = scenarioResult !== undefined && 'resources' in scenarioResult ? scenarioResult.resources : [];
|
|
737
|
+
const scenarioError = scenarioResult !== undefined && 'error' in scenarioResult ? scenarioResult.error : undefined;
|
|
738
|
+
// `resumen` es el nombre de la clave desde LILA-053 y es contrato documentado
|
|
739
|
+
// (`docs/MCP.md`): no se renombra al traducir, solo cambia el idioma de su contenido.
|
|
740
|
+
return textResult({ ir: report.ir, resumen: describeIr(report, resources, scenarioError, language) });
|
|
741
|
+
});
|
|
742
|
+
server.registerTool('run_simulation', {
|
|
743
|
+
title: 'Run simulation',
|
|
744
|
+
description: 'Validates model and scenario, simulates with `log: false` and returns the same `RunResult` ' +
|
|
745
|
+
'as `lila run --json` (elements, flows, resources, process, bottlenecks and warnings). ' +
|
|
746
|
+
'`scenario` accepts a .json path (resolves `extends`) or the already resolved scenario as an ' +
|
|
747
|
+
'inline object. `model` may be a .lila project: `scenario` is then also the name of a ' +
|
|
748
|
+
'scenario of that process (a .json path that exists wins), and `process` picks the process ' +
|
|
749
|
+
'when there are several. `saveTo` writes the same JSON atomically, like `lila run --json <path>`; ' +
|
|
750
|
+
'`saveRun` stores the run in the .lila, like `lila run --save`. ' +
|
|
751
|
+
'isError only marks that the tool failed (invalid model/scenario, unreadable file).',
|
|
752
|
+
inputSchema: z.object({
|
|
753
|
+
model: z.string().optional().describe('Path to the .bpmn or .lila; defaults to scenario.model.'),
|
|
754
|
+
process: z
|
|
755
|
+
.string()
|
|
756
|
+
.optional()
|
|
757
|
+
.describe('Slug of the process when the .lila holds several; implicit when it holds one.'),
|
|
758
|
+
scenario: scenarioInputSchema,
|
|
759
|
+
seed: z.number().int().optional().describe('Overrides run.seed.'),
|
|
760
|
+
replications: z.number().int().min(1).optional().describe('Overrides run.replications.'),
|
|
761
|
+
saveTo: z.string().optional().describe('Path to write the RunResult as JSON.'),
|
|
762
|
+
saveRun: z
|
|
763
|
+
.boolean()
|
|
764
|
+
.optional()
|
|
765
|
+
.describe('With a .lila `model` and a scenario of it: store the run in the project, as the app does ' +
|
|
766
|
+
'(it opens as the current run; export_document/export_results use it). A second content ' +
|
|
767
|
+
'block returns { savedRun: { id, file, process, scenario } }.'),
|
|
768
|
+
locale: localeSchema,
|
|
769
|
+
}),
|
|
770
|
+
outputSchema: runResultSchema,
|
|
771
|
+
}, async (input) => runSimulation(input, localeOf(input.locale)));
|
|
772
|
+
server.registerTool('compare_scenarios', {
|
|
773
|
+
title: 'Compare scenarios',
|
|
774
|
+
description: 'Validates and simulates two or more scenarios on the same model (the first one is the ' +
|
|
775
|
+
'base) and returns the same `CompareResult` as `lila compare --json`, plus `notes`: the ' +
|
|
776
|
+
'warnings the CLI prints beside the table (different seeds, different `baseTimeUnit`, too ' +
|
|
777
|
+
'few replications for a 95% CI). `scenarios` accepts .json paths and inline objects, mixed; ' +
|
|
778
|
+
'with a .lila `model`, also scenario names of that process (`process` picks it). ' +
|
|
779
|
+
'`saveTo` writes `comparison` as JSON, like `lila compare --json`.',
|
|
780
|
+
inputSchema: z.object({
|
|
781
|
+
model: z
|
|
782
|
+
.string()
|
|
783
|
+
.optional()
|
|
784
|
+
.describe('Path to the .bpmn or .lila; defaults to the model of the first scenario.'),
|
|
785
|
+
process: z
|
|
786
|
+
.string()
|
|
787
|
+
.optional()
|
|
788
|
+
.describe('Slug of the process when the .lila holds several; implicit when it holds one.'),
|
|
789
|
+
// Sin `.min(2)`: así el mensaje lo da `compareScenarios` en el idioma pedido, no el
|
|
790
|
+
// validador del SDK, que solo habla inglés.
|
|
791
|
+
scenarios: z.array(scenarioInputSchema),
|
|
792
|
+
seed: z.number().int().optional().describe('Overrides run.seed in every scenario.'),
|
|
793
|
+
replications: z.number().int().min(1).optional().describe('Overrides run.replications in every scenario.'),
|
|
794
|
+
saveTo: z.string().optional().describe('Path to write the CompareResult as JSON.'),
|
|
795
|
+
locale: localeSchema,
|
|
796
|
+
}),
|
|
797
|
+
}, async (input) => compareScenarios(input, localeOf(input.locale)));
|
|
798
|
+
server.registerTool('patch_scenario', {
|
|
799
|
+
title: 'Patch scenario',
|
|
800
|
+
description: 'Applies a JSON Patch (RFC 6902; only add/replace/remove/test, no move/copy) to a scenario, ' +
|
|
801
|
+
'validates the result against its model and writes it to disk only if it validates. Without ' +
|
|
802
|
+
'`saveTo` it patches `scenario` in place (the full resolved scenario, without `extends`). ' +
|
|
803
|
+
'With `saveTo` it creates a new file with `extends` towards `extendsFrom` (by default, ' +
|
|
804
|
+
'`scenario` itself) and only the keys the patch touched — like ' +
|
|
805
|
+
'`to-be-3-cajeros.scenario.json`. Returns the resulting resolved scenario, the path written ' +
|
|
806
|
+
'and `notes` with the lint warnings. A patch that leaves the scenario invalid (`probability` ' +
|
|
807
|
+
'outside `[0,1]`, `capacity` < 1, a `ref` that does not exist in `resources`, …) is rejected ' +
|
|
808
|
+
'without writing anything. With `project` (a .lila), `scenario`, `extendsFrom` and `saveTo` ' +
|
|
809
|
+
'are scenario names inside that process, and the archive is rewritten atomically with every ' +
|
|
810
|
+
'other process unchanged.',
|
|
811
|
+
inputSchema: z.object({
|
|
812
|
+
scenario: z
|
|
813
|
+
.string()
|
|
814
|
+
.describe('Path to the .json scenario to read and patch, relative to the cwd; with `project`, its name.'),
|
|
815
|
+
project: z
|
|
816
|
+
.string()
|
|
817
|
+
.optional()
|
|
818
|
+
.describe('Path to a .lila: patch a scenario inside it instead of a .json on disk.'),
|
|
819
|
+
process: z
|
|
820
|
+
.string()
|
|
821
|
+
.optional()
|
|
822
|
+
.describe('Slug of the process of `project` when it holds several; implicit when it holds one.'),
|
|
823
|
+
patch: z
|
|
824
|
+
.array(z.object({
|
|
825
|
+
op: z.string().describe('add | replace | remove | test (move/copy are not supported).'),
|
|
826
|
+
path: z.string().describe('RFC 6901 pointer, e.g. "/resources/cajero/capacity".'),
|
|
827
|
+
value: z.unknown().optional().describe('Required by add/replace/test.'),
|
|
828
|
+
from: z.string().optional().describe('Unused: move/copy are not implemented.'),
|
|
829
|
+
}))
|
|
830
|
+
.min(1),
|
|
831
|
+
saveTo: z
|
|
832
|
+
.string()
|
|
833
|
+
.optional()
|
|
834
|
+
.describe('Absent: writes over `scenario`. Present: creates a new scenario with `extends` there.'),
|
|
835
|
+
extendsFrom: z
|
|
836
|
+
.string()
|
|
837
|
+
.optional()
|
|
838
|
+
.describe('Parent of the `extends` of the new file (only with `saveTo`). Defaults to `scenario`.'),
|
|
839
|
+
name: z.string().optional().describe('Name of the resulting scenario; defaults to a generated one.'),
|
|
840
|
+
description: z.string().optional().describe('Description of the resulting scenario.'),
|
|
841
|
+
locale: localeSchema,
|
|
842
|
+
}),
|
|
843
|
+
}, async (input) => patchScenario(input, localeOf(input.locale)));
|
|
844
|
+
// App-free exports (#538): thin layers over `@lila-modeler/engine/project-fs`, like `lila export`.
|
|
845
|
+
const processSchema = z
|
|
846
|
+
.string()
|
|
847
|
+
.optional()
|
|
848
|
+
.describe('Slug of the process when the .lila holds several; implicit when it holds one.');
|
|
849
|
+
const runSchema = z
|
|
850
|
+
.string()
|
|
851
|
+
.optional()
|
|
852
|
+
.describe('A stored run id, or "latest" (default): the run of the current model and scenario.');
|
|
853
|
+
const scenarioSchema = z
|
|
854
|
+
.string()
|
|
855
|
+
.optional()
|
|
856
|
+
.describe('With "latest", the run of this scenario (needed when several scenarios have one).');
|
|
857
|
+
const overwriteSchema = z.boolean().optional().describe('Replace an existing file. Default false: existing files are never overwritten.');
|
|
858
|
+
server.registerTool('export_diagram', {
|
|
859
|
+
title: 'Export diagram',
|
|
860
|
+
description: 'Draws the diagram of a .bpmn, or of one process of a .lila, as SVG with the engine\'s own ' +
|
|
861
|
+
'renderer (no app, no browser): the BPMN DI geometry in the Lila Light colours on white. ' +
|
|
862
|
+
'Without `saveTo` the SVG comes back in `svg`; with it, it is written atomically and `file` ' +
|
|
863
|
+
'is the absolute path. Like `lila export diagram`.',
|
|
864
|
+
inputSchema: z.object({
|
|
865
|
+
project: z.string().optional().describe('Path to a .lila, relative to the cwd of the server process.'),
|
|
866
|
+
path: z.string().optional().describe('Path to a .bpmn (or .lila), instead of `project`.'),
|
|
867
|
+
process: processSchema,
|
|
868
|
+
saveTo: z.string().optional().describe('Path of the .svg to write.'),
|
|
869
|
+
overwrite: overwriteSchema,
|
|
870
|
+
locale: localeSchema,
|
|
871
|
+
}),
|
|
872
|
+
}, async ({ project, path, process, saveTo, overwrite, locale }) => {
|
|
873
|
+
const language = localeOf(locale);
|
|
874
|
+
const M = messages(language).mcp;
|
|
875
|
+
if (project !== undefined && path !== undefined)
|
|
876
|
+
return errorResult(toolMessage('export_diagram', M.bothProjectAndPath()));
|
|
877
|
+
const file = project ?? path;
|
|
878
|
+
if (file === undefined)
|
|
879
|
+
return errorResult(toolMessage('export_diagram', M.projectOrPath()));
|
|
880
|
+
try {
|
|
881
|
+
const { svg, process: slug, file: source } = await exportDiagram({ file, process, locale: language });
|
|
882
|
+
if (saveTo === undefined)
|
|
883
|
+
return textResult({ process: slug ?? null, svg });
|
|
884
|
+
return textResult({ process: slug ?? null, file: writeExportFile(saveTo, svg, { overwrite, source, locale: language }) });
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
return errorResult(toolMessage('export_diagram', message(error)));
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
server.registerTool('create_process', {
|
|
891
|
+
title: 'Create process',
|
|
892
|
+
description: 'Creates a process from an outline — lanes plus an ordered list of steps with branches — ' +
|
|
893
|
+
'laid out automatically (pool, lanes, flows) and validated, and writes it as a new ' +
|
|
894
|
+
'`processes/<slug>/` of the .lila `project`, or as a new .lila when the file does not exist. ' +
|
|
895
|
+
'It never replaces an existing process: a slug already in the file is an error and nothing ' +
|
|
896
|
+
'is written. List order is the flow: each step follows the previous one unless it has ' +
|
|
897
|
+
'`next`, `branches` or `end`. Start and end events are added. Step ids become the BPMN ids. ' +
|
|
898
|
+
'The process gets a base scenario `as-is.scenario.json` with arrivals and every ' +
|
|
899
|
+
'`duration`, `resources` and branch `probability` given. Returns the slug, the outline as ' +
|
|
900
|
+
'stored (normal form), the validator warnings and a one-line `summary`. With `dryRun`, ' +
|
|
901
|
+
'builds and checks everything and writes nothing.',
|
|
902
|
+
inputSchema: z.object({
|
|
903
|
+
// A plain object on purpose (QA of #553): a strict schema here would let the SDK reject the
|
|
904
|
+
// call before the handler, in English and one problem at a time. `createLilaProcess` checks
|
|
905
|
+
// everything in one pass, in the call's language, with `steps[i].field` paths.
|
|
906
|
+
outline: z
|
|
907
|
+
.record(z.string(), z.unknown())
|
|
908
|
+
.describe('The process: {name, lanes?: [lane names], steps: [{id, name?, lane?, type? (task | userTask | ' +
|
|
909
|
+
'serviceTask | callActivity | xor | and | or | timer | subprocess), next? (id or ids), branches? ' +
|
|
910
|
+
'(gateways: [{label?, to | end: true, probability?}]), end?: true, duration? ("20m", ' +
|
|
911
|
+
'"normal(20m, 5m)", seconds or a distribution object), resources? ([name | {name, quantity}]), ' +
|
|
912
|
+
'selection? ("and" | "or")}]}. List order is the flow. See docs/MCP.md.'),
|
|
913
|
+
project: z.string().describe('Path to the .lila (created if missing), relative to the cwd of the server process.'),
|
|
914
|
+
process: z
|
|
915
|
+
.string()
|
|
916
|
+
.optional()
|
|
917
|
+
.describe('Slug of the new process folder in an existing .lila. Defaults to one derived from the name.'),
|
|
918
|
+
name: z.string().optional().describe('Name of the process; defaults to outline.name.'),
|
|
919
|
+
dryRun: z.boolean().optional().describe('true: build and validate, write nothing.'),
|
|
920
|
+
locale: localeSchema,
|
|
921
|
+
}),
|
|
922
|
+
}, async ({ outline, project, process, name, dryRun, locale }) => {
|
|
923
|
+
const language = localeOf(locale);
|
|
924
|
+
try {
|
|
925
|
+
const created = await createLilaProcess(project, outline, { process, name, dryRun, locale: language });
|
|
926
|
+
return textResult(created);
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
return errorResult(toolMessage('create_process', message(error)));
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
server.registerTool('export_document', {
|
|
933
|
+
title: 'Export process document',
|
|
934
|
+
description: 'Writes the process document of one process of a .lila, like the app\'s Export document: ' +
|
|
935
|
+
'diagram, element descriptions in flow order, the scenario and the results of a stored run. ' +
|
|
936
|
+
'`run` "latest" (default) takes the run of the current model and scenario, and the document ' +
|
|
937
|
+
'goes without results when there is none; an older run is refused. `format` docx or html; ' +
|
|
938
|
+
'the HTML embeds the SVG diagram, the Word file has no diagram and neither has the run\'s ' +
|
|
939
|
+
'charts (no rasteriser): `notes` says what was left out. Like `lila export doc`.',
|
|
940
|
+
inputSchema: z.object({
|
|
941
|
+
project: z.string().describe('Path to the .lila, relative to the cwd of the server process.'),
|
|
942
|
+
process: processSchema,
|
|
943
|
+
run: runSchema,
|
|
944
|
+
scenario: scenarioSchema,
|
|
945
|
+
format: z.enum(['docx', 'html']),
|
|
946
|
+
saveTo: z.string().describe('Path of the .docx or .html to write.'),
|
|
947
|
+
overwrite: overwriteSchema,
|
|
948
|
+
locale: localeSchema,
|
|
949
|
+
}),
|
|
950
|
+
}, async ({ project, process, run, scenario, format, saveTo, overwrite, locale }) => {
|
|
951
|
+
const language = localeOf(locale);
|
|
952
|
+
try {
|
|
953
|
+
const doc = await exportDocument({ file: project, process, run, scenario, format, locale: language });
|
|
954
|
+
const file = writeExportFile(saveTo, doc.data, { overwrite, source: doc.file, locale: language });
|
|
955
|
+
return textResult({ file, format, project: doc.file, process: doc.process, run: doc.run, notes: doc.notes });
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
return errorResult(toolMessage('export_document', message(error)));
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
server.registerTool('export_results', {
|
|
962
|
+
title: 'Export results',
|
|
963
|
+
description: 'Writes the results of a run stored in a .lila, like `lila run --xlsx`/`--csv`: `xlsx` is ' +
|
|
964
|
+
'one workbook (Summary, Elements, Flows, Resources, Parameters) at `saveTo`; `csv` writes ' +
|
|
965
|
+
'elements, flows, resources and process .csv into the directory `saveTo` (a stored run ' +
|
|
966
|
+
'keeps no event log). `run` "latest" (default) is the run of the current model and ' +
|
|
967
|
+
'scenario; an older run is exported by id against the model it ran on. An error says so ' +
|
|
968
|
+
'when the process has no run. Like `lila export results`.',
|
|
969
|
+
inputSchema: z.object({
|
|
970
|
+
project: z.string().describe('Path to the .lila, relative to the cwd of the server process.'),
|
|
971
|
+
process: processSchema,
|
|
972
|
+
run: runSchema,
|
|
973
|
+
scenario: scenarioSchema,
|
|
974
|
+
format: z.enum(['xlsx', 'csv']),
|
|
975
|
+
saveTo: z.string().describe('Path of the .xlsx, or the directory for the CSV files.'),
|
|
976
|
+
overwrite: overwriteSchema,
|
|
977
|
+
locale: localeSchema,
|
|
978
|
+
}),
|
|
979
|
+
}, async ({ project, process, run, scenario, format, saveTo, overwrite, locale }) => {
|
|
980
|
+
const language = localeOf(locale);
|
|
981
|
+
try {
|
|
982
|
+
const results = await exportResults({ file: project, process, run, scenario, format, locale: language });
|
|
983
|
+
const files = results.data instanceof Uint8Array
|
|
984
|
+
? [writeExportFile(saveTo, results.data, { overwrite, source: results.file, locale: language })]
|
|
985
|
+
: writeExportDirectory(saveTo, results.data, { overwrite, source: results.file, locale: language });
|
|
986
|
+
return textResult({ files, format, project: results.file, process: results.process, run: results.run });
|
|
987
|
+
}
|
|
988
|
+
catch (error) {
|
|
989
|
+
return errorResult(toolMessage('export_results', message(error)));
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
server.registerTool('get_process_outline', {
|
|
993
|
+
title: 'Get process outline',
|
|
994
|
+
description: 'Reads one process of a .lila as an outline (the format `create_process` takes, in normal ' +
|
|
995
|
+
'form: every step names its lane, `next` only when it is not the following step, `end: ' +
|
|
996
|
+
'true` where the process ends, durations as scenario distributions in seconds). Durations, ' +
|
|
997
|
+
'resources and branch probabilities come from its `as-is.scenario.json` when it has one. ' +
|
|
998
|
+
'`warnings` lists what the outline could not carry (other event types, nested lanes…).',
|
|
999
|
+
inputSchema: z.object({
|
|
1000
|
+
project: z.string().describe('Path to the .lila, relative to the cwd of the server process.'),
|
|
1001
|
+
process: z
|
|
1002
|
+
.string()
|
|
1003
|
+
.optional()
|
|
1004
|
+
.describe('Slug of the process when the .lila holds several; implicit when it holds one.'),
|
|
1005
|
+
locale: localeSchema,
|
|
1006
|
+
}),
|
|
1007
|
+
}, async ({ project, process, locale }) => {
|
|
1008
|
+
const language = localeOf(locale);
|
|
1009
|
+
try {
|
|
1010
|
+
return textResult(await readLilaOutline(project, { process, locale: language }));
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
return errorResult(toolMessage('get_process_outline', message(error)));
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
// Annotate, RACI, scenario sheets and project creation (#99, #403, #514).
|
|
1017
|
+
registerAgentTools(server, localeOf);
|
|
1018
|
+
server.registerTool('edit_process', {
|
|
1019
|
+
title: 'Edit process',
|
|
1020
|
+
description: 'Applies a list of operations to one process of a .lila, all or none: `add` (a step, as in ' +
|
|
1021
|
+
'an outline, `after` a step or `between` two connected steps, or unconnected), `connect`, ' +
|
|
1022
|
+
'`remove` (a step, reconnecting its predecessors to its successor when that is unambiguous, ' +
|
|
1023
|
+
'or a flow), `rename`, `setType`, `moveToLane` and `addLane`. Ids never change, so renamed ' +
|
|
1024
|
+
'or retyped steps keep their scenario entries; durations, resources and probabilities given ' +
|
|
1025
|
+
'go into the base scenario `as-is.scenario.json`. The result is validated; any problem ' +
|
|
1026
|
+
'refuses the whole edit, writes nothing and lists every issue with the index of its ' +
|
|
1027
|
+
'operation (`operations[i]`). Documentation, annotations, extended attributes and other ' +
|
|
1028
|
+
'pools are kept. With `layout` (default true) the process is laid out again; with ' +
|
|
1029
|
+
'`layout: false` every shape keeps its place and only new ones are placed. Returns what ' +
|
|
1030
|
+
'each operation did (`changes`), the removed ids, the scenario entries of removed ids (kept, ' +
|
|
1031
|
+
'not deleted), notes, the validator warnings and the outline after the edit. With `dryRun`, ' +
|
|
1032
|
+
'nothing is written.',
|
|
1033
|
+
inputSchema: z.object({
|
|
1034
|
+
project: z.string().describe('Path to the .lila, relative to the cwd of the server process.'),
|
|
1035
|
+
process: z
|
|
1036
|
+
.string()
|
|
1037
|
+
.optional()
|
|
1038
|
+
.describe('Slug of the process when the .lila holds several; implicit when it holds one.'),
|
|
1039
|
+
// Loose on purpose: the engine checks the operations itself and reports every problem with
|
|
1040
|
+
// the index of its operation and a catalog message, instead of the SDK's schema error.
|
|
1041
|
+
operations: z
|
|
1042
|
+
.unknown()
|
|
1043
|
+
.describe('In order. Each one of: {op:"add", step:{id, name?, type?, lane?, duration?, resources?}, after?:id | between?:[from,to]}; ' +
|
|
1044
|
+
'{op:"connect", from, to, label?, probability?, id?}; {op:"remove", id}; {op:"rename", id, name}; ' +
|
|
1045
|
+
'{op:"setType", id, type: task|userTask|serviceTask|callActivity|xor|and|or|timer|subprocess}; ' +
|
|
1046
|
+
'{op:"moveToLane", id, lane}; {op:"addLane", name, id?, after?|before?}. E.g. [{"op":"add","step":{"id":"verify","name":"Verify ID","duration":"5m"},"after":"receive"}, ' +
|
|
1047
|
+
'{"op":"connect","from":"ok","to":"verify","label":"Retry","probability":0.1}, {"op":"remove","id":"check"}, ' +
|
|
1048
|
+
'{"op":"rename","id":"issue","name":"Issue card"}, {"op":"setType","id":"check","type":"serviceTask"}, ' +
|
|
1049
|
+
'{"op":"addLane","name":"Back office"}, {"op":"moveToLane","id":"issue","lane":"Back office"}].'),
|
|
1050
|
+
dryRun: z.boolean().optional().describe('true: apply and validate in memory, write nothing.'),
|
|
1051
|
+
layout: z.boolean().optional().describe('Default true: lay the process out again. false: keep positions, place only new shapes.'),
|
|
1052
|
+
locale: localeSchema,
|
|
1053
|
+
}),
|
|
1054
|
+
}, async ({ project, process, operations, dryRun, layout, locale }) => {
|
|
1055
|
+
const language = localeOf(locale);
|
|
1056
|
+
try {
|
|
1057
|
+
return textResult(await editLilaProcess(project, operations, { process, dryRun, layout, locale: language }));
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
return errorResult(toolMessage('edit_process', message(error)));
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
return server;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Arranca el servidor sobre stdio. Lo usan el bin `lila-mcp` de este paquete y el subcomando
|
|
1067
|
+
* `lila mcp` de `@lila-modeler/engine` (LILA-056), que lo carga con `import()` dinámico para no crear un
|
|
1068
|
+
* ciclo de dependencia entre los dos paquetes. stdout es el transporte: nada más puede escribir ahí.
|
|
1069
|
+
*
|
|
1070
|
+
* Sin `locale`, el idioma sale del entorno (`LILA_LANG`, `LC_ALL`, `LC_MESSAGES`, `LANG`), la misma
|
|
1071
|
+
* regla que la CLI: quien lo lanza desde `lila mcp` ya resolvió `--lang` y lo pasa explícito.
|
|
1072
|
+
*/
|
|
1073
|
+
export async function startStdioServer(options = {}) {
|
|
1074
|
+
const locale = options.locale ?? resolveLocale(undefined, process.env);
|
|
1075
|
+
await createServer({ locale }).connect(new StdioServerTransport());
|
|
1076
|
+
}
|
|
1077
|
+
//# sourceMappingURL=server.js.map
|