@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.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * JSON Patch (RFC 6902) mínimo para `patch_scenario` (LILA-055): `add`/`replace`/`remove`/`test`
3
+ * sobre objetos y arrays planos, con punteros RFC 6901. Sin `move`/`copy` (ponytail): son las dos
4
+ * operaciones que leen de una ubicación distinta a la que escriben, duplicarían la navegación, y
5
+ * ningún caso de uso de `patch_scenario` las necesita — documentado en `docs/MCP.md`.
6
+ *
7
+ * También arma el delta mínimo de un patch, para el modo "crear escenario con `extends`" de
8
+ * `patch_scenario`: la ruta de cada operación, anidada, con una clave borrada traducida a `null`
9
+ * (mismo borrado que usa `extends`, `docs/SCENARIO_FORMAT.md` § 6) y un array tocado en cualquier
10
+ * profundidad reemplazado entero (los arrays no se fusionan por índice en `extends`).
11
+ */
12
+ const SUPPORTED_OPS = new Set(['add', 'replace', 'remove', 'test']);
13
+ function isPlainObject(value) {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ }
16
+ /**
17
+ * RFC 6901 no reserva ningún nombre de clave, pero en JavaScript `obj['__proto__'] = x` no crea
18
+ * una clave: escribe en el prototipo. Un patch con `/__proto__/loQueSea` contaminaba
19
+ * `Object.prototype` del **proceso servidor** entero —un servidor MCP por stdio es de vida larga—
20
+ * y dejaba colgada la llamada y todas las siguientes. `constructor` y `prototype` van a la misma
21
+ * lista: ningún escenario tiene claves así (`docs/SCENARIO_FORMAT.md` § 2).
22
+ */
23
+ const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
24
+ function parsePointer(pointer) {
25
+ if (pointer === '')
26
+ throw new Error('json patch: la raíz del documento no se puede parchear.');
27
+ if (!pointer.startsWith('/'))
28
+ throw new Error(`json patch: puntero inválido: ${pointer}`);
29
+ return pointer
30
+ .slice(1)
31
+ .split('/')
32
+ .map((segment) => {
33
+ const key = segment.replaceAll('~1', '/').replaceAll('~0', '~');
34
+ if (FORBIDDEN_SEGMENTS.has(key)) {
35
+ throw new Error(`json patch: segmento prohibido "${key}" en ${pointer}: escribiría en el prototipo del objeto.`);
36
+ }
37
+ return key;
38
+ });
39
+ }
40
+ /**
41
+ * El puntero ya no puede nombrar esas claves, pero el `value` de un `add`/`replace` es JSON del
42
+ * cliente y `JSON.parse('{"__proto__":{…}}')` **sí** crea una clave propia: se escribía tal cual
43
+ * en el escenario guardado y `deepMerge` (LILA-204) se la comía en silencio al resolver el
44
+ * `extends`, dejando en disco un archivo con una clave que el motor ignora. Se rechaza igual que
45
+ * el segmento: aquí el patch es una orden explícita del agente, no un archivo heredado.
46
+ */
47
+ function assertValueSinClavesDePrototipo(value, path) {
48
+ if (Array.isArray(value)) {
49
+ for (const item of value)
50
+ assertValueSinClavesDePrototipo(item, path);
51
+ return;
52
+ }
53
+ if (!isPlainObject(value))
54
+ return;
55
+ for (const [key, nested] of Object.entries(value)) {
56
+ if (FORBIDDEN_SEGMENTS.has(key)) {
57
+ throw new Error(`json patch: clave prohibida "${key}" en el value de ${path}: escribiría en el prototipo del objeto.`);
58
+ }
59
+ assertValueSinClavesDePrototipo(nested, path);
60
+ }
61
+ }
62
+ function child(container, key) {
63
+ if (Array.isArray(container))
64
+ return container[Number(key)];
65
+ if (isPlainObject(container))
66
+ return container[key];
67
+ return undefined;
68
+ }
69
+ function get(root, segments) {
70
+ let cursor = root;
71
+ for (const segment of segments)
72
+ cursor = child(cursor, segment);
73
+ return cursor;
74
+ }
75
+ /** Contenedor y clave del último segmento; lanza si algún tramo intermedio no existe. */
76
+ function navigate(root, segments) {
77
+ let cursor = root;
78
+ for (const segment of segments.slice(0, -1)) {
79
+ cursor = child(cursor, segment);
80
+ if (cursor === undefined)
81
+ throw new Error(`json patch: no existe la ruta hasta "${segment}".`);
82
+ }
83
+ if (!isPlainObject(cursor) && !Array.isArray(cursor)) {
84
+ throw new Error('json patch: la ruta no llega a un objeto o array.');
85
+ }
86
+ return { parent: cursor, key: segments[segments.length - 1] ?? '' };
87
+ }
88
+ /** Aplica el patch a una copia profunda de `target`; nunca muta el original. */
89
+ export function applyJsonPatch(target, patch) {
90
+ const root = structuredClone(target);
91
+ for (const op of patch) {
92
+ if (!SUPPORTED_OPS.has(op.op)) {
93
+ throw new Error(`json patch: operación no soportada: "${op.op}" (solo add/replace/remove/test; move/copy no están implementadas).`);
94
+ }
95
+ // RFC 6902 § 4: `add`, `replace` y `test` requieren `value`. Sin esto, un `add` sin `value`
96
+ // escribía `undefined`, que `JSON.stringify` descarta: un borrado silencioso en vez de error.
97
+ if (op.op !== 'remove' && op.value === undefined) {
98
+ throw new Error(`json patch: la operación "${op.op}" requiere "value" (${op.path}).`);
99
+ }
100
+ if (op.op !== 'remove')
101
+ assertValueSinClavesDePrototipo(op.value, op.path);
102
+ const segments = parsePointer(op.path);
103
+ if (op.op === 'test') {
104
+ const { parent, key } = navigate(root, segments);
105
+ if (JSON.stringify(child(parent, key)) !== JSON.stringify(op.value)) {
106
+ throw new Error(`json patch: test falló en ${op.path}.`);
107
+ }
108
+ continue;
109
+ }
110
+ const { parent, key } = navigate(root, segments);
111
+ if (Array.isArray(parent)) {
112
+ const index = key === '-' ? parent.length : Number(key);
113
+ if (!Number.isInteger(index) || index < 0 || index > parent.length) {
114
+ throw new Error(`json patch: índice inválido en ${op.path}.`);
115
+ }
116
+ if (op.op === 'add')
117
+ parent.splice(index, 0, op.value);
118
+ else if (index >= parent.length)
119
+ throw new Error(`json patch: no existe la ruta ${op.path}.`);
120
+ else if (op.op === 'replace')
121
+ parent[index] = op.value;
122
+ else
123
+ parent.splice(index, 1); // remove
124
+ }
125
+ else if (op.op === 'add') {
126
+ parent[key] = op.value;
127
+ }
128
+ else if (key in parent) {
129
+ if (op.op === 'replace')
130
+ parent[key] = op.value;
131
+ else
132
+ delete parent[key]; // remove
133
+ }
134
+ else {
135
+ throw new Error(`json patch: no existe la ruta ${op.path}.`);
136
+ }
137
+ }
138
+ return root;
139
+ }
140
+ /**
141
+ * Delta mínimo de un patch: solo las rutas que tocó alguna operación, leídas del documento **ya
142
+ * parcheado** (`patchedRoot`) para reflejar el estado final aunque varias operaciones compongan la
143
+ * misma ruta. Una clave que ya no existe en `patchedRoot` (un `remove`) se traduce a `null`.
144
+ */
145
+ export function buildPatchDelta(patch, patchedRoot) {
146
+ const delta = {};
147
+ for (const op of patch) {
148
+ if (op.op === 'test' || !SUPPORTED_OPS.has(op.op))
149
+ continue;
150
+ const segments = parsePointer(op.path);
151
+ let deltaCursor = delta;
152
+ let patchedCursor = patchedRoot;
153
+ let stoppedAtArray = false;
154
+ for (const segment of segments.slice(0, -1)) {
155
+ const next = child(patchedCursor, segment);
156
+ if (Array.isArray(next)) {
157
+ deltaCursor[segment] = next; // los arrays se reemplazan enteros, nunca por índice (§ 6).
158
+ stoppedAtArray = true;
159
+ break;
160
+ }
161
+ if (!isPlainObject(deltaCursor[segment]))
162
+ deltaCursor[segment] = {};
163
+ deltaCursor = deltaCursor[segment];
164
+ patchedCursor = next;
165
+ }
166
+ if (stoppedAtArray)
167
+ continue;
168
+ const lastKey = segments[segments.length - 1];
169
+ const leaf = get(patchedRoot, segments);
170
+ deltaCursor[lastKey] = leaf === undefined ? null : leaf;
171
+ }
172
+ return delta;
173
+ }
174
+ //# sourceMappingURL=json-patch.js.map
@@ -0,0 +1,12 @@
1
+ import { type Locale } from '@lila-modeler/engine/messages';
2
+ import { type LilaProcess } from '@lila-modeler/engine/project-fs';
3
+ import type { ResolvedScenario } from '@lila-modeler/engine/schema';
4
+ export type ScenarioInput = string | Record<string, unknown>;
5
+ /**
6
+ * With `lila` (the model is a `.lila`, #466), a string is a `.json` path or a scenario name of
7
+ * the process (`resolveScenarioArgument`), and an inline object is anchored inside the process's
8
+ * folder of the archive instead of the cwd: its `extends: "as-is.scenario.json"` names a scenario
9
+ * of the process, and a missing `model` means the process's `model.bpmn`.
10
+ */
11
+ export declare function resolveScenarioInput(input: ScenarioInput, locale?: Locale, lila?: LilaProcess | undefined): ResolvedScenario;
12
+ //# sourceMappingURL=scenario-input.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Resuelve el parámetro `scenario` de `run_simulation`/`compare_scenarios` (LILA-054): una ruta
3
+ * `.json` (relativa al cwd del proceso, con `extends` igual que la CLI) o el escenario ya resuelto
4
+ * como objeto inline.
5
+ *
6
+ * Un objeto inline se ancla a un archivo virtual `<escenario-inline>.json` en el cwd del proceso:
7
+ * si declara `extends`, esa referencia (y cualquier `model` relativo) se resuelve contra el cwd,
8
+ * igual que si el agente hubiera guardado el objeto ahí antes de pasarlo. `loadResolvedScenario`
9
+ * (de `@lila-modeler/engine/cli-shared`, LILA-054) hace la misma validación de escenario resuelto para
10
+ * ambos casos: no se duplica frente a la CLI.
11
+ */
12
+ import { absolutePath, loadResolvedScenario, readJsonFile, resolveScenarioArgument, } from '@lila-modeler/engine/cli-shared';
13
+ import { messages } from '@lila-modeler/engine/messages';
14
+ import { lilaScenarioReader } from '@lila-modeler/engine/project-fs';
15
+ /** Nombre del ancla virtual: no existe en disco, así que nunca debe salir en un mensaje. */
16
+ const INLINE_FILE = '<escenario-inline>.json';
17
+ /**
18
+ * With `lila` (the model is a `.lila`, #466), a string is a `.json` path or a scenario name of
19
+ * the process (`resolveScenarioArgument`), and an inline object is anchored inside the process's
20
+ * folder of the archive instead of the cwd: its `extends: "as-is.scenario.json"` names a scenario
21
+ * of the process, and a missing `model` means the process's `model.bpmn`.
22
+ */
23
+ export function resolveScenarioInput(input, locale = 'en', lila) {
24
+ if (typeof input === 'string')
25
+ return resolveScenarioArgument(input, lila, locale).scenario;
26
+ const disk = (file) => readJsonFile(file, locale);
27
+ const virtualPath = lila === undefined ? absolutePath(INLINE_FILE) : `${lila.root}${INLINE_FILE}`;
28
+ const read = lila === undefined ? disk : lilaScenarioReader(lila, disk, locale);
29
+ const anchored = lila !== undefined && !('model' in input) ? { ...input, model: 'model.bpmn' } : input;
30
+ try {
31
+ return loadResolvedScenario(virtualPath, (file) => (file === virtualPath ? anchored : read(file)), locale);
32
+ }
33
+ catch (error) {
34
+ // Los mensajes de `loadResolvedScenario`/`resolveExtends` citan el archivo. El ancla no existe
35
+ // en disco: dejarla en el mensaje manda al agente a leer una ruta inventada.
36
+ const detail = error instanceof Error ? error.message : String(error);
37
+ throw new Error(detail.replaceAll(virtualPath, messages(locale).mcp.inlineScenario()));
38
+ }
39
+ }
40
+ //# sourceMappingURL=scenario-input.js.map
@@ -0,0 +1,18 @@
1
+ import { type Locale } from '@lila-modeler/engine/messages';
2
+ import { McpServer } from '@modelcontextprotocol/server';
3
+ /** Opciones de arranque comunes a `createServer` y `startStdioServer`. */
4
+ export interface ServerOptions {
5
+ /** Idioma por defecto de las respuestas; cada llamada puede pedir otro con `locale`. */
6
+ locale?: Locale | undefined;
7
+ }
8
+ export declare function createServer(options?: ServerOptions): McpServer;
9
+ /**
10
+ * Arranca el servidor sobre stdio. Lo usan el bin `lila-mcp` de este paquete y el subcomando
11
+ * `lila mcp` de `@lila-modeler/engine` (LILA-056), que lo carga con `import()` dinámico para no crear un
12
+ * ciclo de dependencia entre los dos paquetes. stdout es el transporte: nada más puede escribir ahí.
13
+ *
14
+ * Sin `locale`, el idioma sale del entorno (`LILA_LANG`, `LC_ALL`, `LC_MESSAGES`, `LANG`), la misma
15
+ * regla que la CLI: quien lo lanza desde `lila mcp` ya resolvió `--lang` y lo pasa explícito.
16
+ */
17
+ export declare function startStdioServer(options?: ServerOptions): Promise<void>;
18
+ //# sourceMappingURL=server.d.ts.map