mcp-state-machine-test-framework 1.2.1 → 1.2.3

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.
Files changed (3) hide show
  1. package/README.md +3 -4
  2. package/index.js +148 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,13 +49,12 @@ If you are an AI agent helping with this project, follow these rules:
49
49
  5. **Workflow**: Use `inspect_framework` to understand the current state before proposing changes.
50
50
 
51
51
  ## 🚀 Mission Bootstrap (Copy & Paste to any AI Agent)
52
- > **⚠️ CRITICAL SCHEMA WARNING**: This framework **DOES NOT** use `nodes[]` or `edges[]` arrays. It uses a `nodos` object map. **ALWAYS** use the `upsert_node` tool.
53
- >
54
52
  > **YOUR ONLY MISSION**:
55
53
  > 1. Run `npx -y -p mcp-state-machine-test-framework mcp-sms-init` to setup EVERYTHING.
56
- > 2. Read `.mcp_agent_rules` to understand the workflow.
54
+ > 2. Use `design_wizard` (action: 'start') to design nodes step-by-step.
55
+ > 3. **STRICT RULE**: Never edit JSON files manually.
57
56
  >
58
- > "Run `npx -y -p mcp-state-machine-test-framework mcp-sms-init`. This will setup the project, folders, templates and `mcp_config.json` automatically. Then read `.mcp_agent_rules` and start mapping using ONLY the provided MCP tools."
57
+ > "Run `npx -y -p mcp-state-machine-test-framework mcp-sms-init`. Then start a design session using `design_wizard` (action: 'start'). Follow the wizard steps to create nodes and transitions."
59
58
 
60
59
  ---
61
60
  *Developed with ❤️ for maximum automation efficiency.*
package/index.js CHANGED
@@ -78,10 +78,10 @@ export class MaquinaDeEstados {
78
78
  async loadExternalData(dataPath) {
79
79
  const fullPath = path.isAbsolute(dataPath) ? dataPath : path.join(__dirname, dataPath);
80
80
  if (!fsSync.existsSync(fullPath)) return null;
81
-
81
+
82
82
  const content = await fs.readFile(fullPath, "utf-8");
83
83
  if (fullPath.endsWith(".json")) return JSON.parse(content);
84
-
84
+
85
85
  if (fullPath.endsWith(".csv")) {
86
86
  const lines = content.split("\n").map(l => l.trim()).filter(l => l.length > 0);
87
87
  if (lines.length < 2) return [];
@@ -104,7 +104,7 @@ export class MaquinaDeEstados {
104
104
  async ejecutarSuite(suiteName) {
105
105
  const suite = this.suites.get(suiteName);
106
106
  if (!suite) throw new Error(`Suite '${suiteName}' no encontrada.`);
107
-
107
+
108
108
  if (suite.state_map) {
109
109
  process.stderr.write(`\n📍 [V12.3] Pre-flight Integrity Check for: ${suite.state_map}\n`);
110
110
  // Simplified validation: Ensure the file exists and has the required structure
@@ -129,8 +129,8 @@ export class MaquinaDeEstados {
129
129
  if (typeof text !== "string") return text;
130
130
  // Enmascarar tokens de seguridad y JWTs
131
131
  return text.replace(/("securityToken"\s*:\s*")[^"]+(")/g, '$1********$2')
132
- .replace(/("token"\s*:\s*")[^"]+(")/g, '$1********$2')
133
- .replace(/eyJhbGciOi[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*/g, '********[JWT_MASKED]********');
132
+ .replace(/("token"\s*:\s*")[^"]+(")/g, '$1********$2')
133
+ .replace(/eyJhbGciOi[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*/g, '********[JWT_MASKED]********');
134
134
  };
135
135
 
136
136
  const runActions = async (actions, targetRes, data) => {
@@ -145,7 +145,7 @@ export class MaquinaDeEstados {
145
145
  const executeAction = async (act) => {
146
146
  const subRes = (act === action) ? actionRes : { action: act, status: "passed" };
147
147
  if (act !== action) targetRes.push(subRes);
148
-
148
+
149
149
  let finalAction = act;
150
150
  try {
151
151
  if (act.startsWith("transicion:")) {
@@ -153,7 +153,7 @@ export class MaquinaDeEstados {
153
153
  const mapPath = path.join(__dirname, 'maps', suite.state_map);
154
154
  const mapData = JSON.parse(await fs.readFile(mapPath, 'utf8'));
155
155
  const nodes = mapData.nodos || mapData;
156
-
156
+
157
157
  let foundAction = null;
158
158
  for (const node of Object.values(nodes)) {
159
159
  if (node.transiciones && node.transiciones[transName]) {
@@ -187,18 +187,18 @@ export class MaquinaDeEstados {
187
187
 
188
188
  const { client } = await this.getMcpClient(serverName);
189
189
  lastResult = await client.callTool({ name: toolName, arguments: toolArgs }, undefined, { timeout: 600000 });
190
-
190
+
191
191
  if (lastResult.isError && toolName !== "close_session") {
192
192
  throw new Error(lastResult.content?.[0]?.text || "Error desconocido en herramienta MCP");
193
193
  }
194
-
194
+
195
195
  if (lastResult.content) {
196
- for (const item of lastResult.content) {
197
- if (item.text) {
198
- subRes.output = (subRes.output || "") + "\n" + maskSecrets(item.text);
199
- }
200
- }
201
- }
196
+ for (const item of lastResult.content) {
197
+ if (item.text) {
198
+ subRes.output = (subRes.output || "") + "\n" + maskSecrets(item.text);
199
+ }
200
+ }
201
+ }
202
202
 
203
203
  if (lastResult.content) {
204
204
  let base64 = "";
@@ -233,19 +233,19 @@ export class MaquinaDeEstados {
233
233
  for (const caseName of suite.tests) {
234
234
  const testCase = this.casosPrueba.get(caseName);
235
235
  if (!testCase) continue;
236
-
236
+
237
237
  let dataRows = [null];
238
238
  if (testCase.data) {
239
239
  if (Array.isArray(testCase.data)) dataRows = testCase.data;
240
240
  else if (typeof testCase.data === "string") dataRows = await this.loadExternalData(testCase.data) || [null];
241
241
  }
242
-
242
+
243
243
  for (let i = 0; i < dataRows.length; i++) {
244
244
  const row = dataRows[i];
245
- const suffix = row ? ` [Iteration ${i+1}]` : "";
245
+ const suffix = row ? ` [Iteration ${i + 1}]` : "";
246
246
  const caseRes = { name: testCase.name + suffix, steps: [], status: "passed", hooks: { beforeCase: [], afterCase: [] } };
247
247
  results.cases.push(caseRes);
248
-
248
+
249
249
  try {
250
250
  await runActions(suite.beforeCase, caseRes.hooks.beforeCase, row);
251
251
  for (const step of testCase.steps) {
@@ -326,20 +326,20 @@ export class MaquinaDeEstados {
326
326
 
327
327
  const maquina = new MaquinaDeEstados();
328
328
  const server = new McpServer({ name: "demo-state-machine", version: "11.1.0" });
329
- server.tool("execute_suite", "Ejecución de Suite Completa", {
330
- name: z.string().describe("Nombre de la suite a ejecutar (ej: 'Suite_Login'). Buscará el archivo en /suites.")
329
+ server.tool("execute_suite", "Ejecución de Suite Completa", {
330
+ name: z.string().describe("Nombre de la suite a ejecutar (ej: 'Suite_Login'). Buscará el archivo en /suites.")
331
331
  }, async ({ name }) => {
332
332
  await maquina.cargar();
333
333
  const dir = await maquina.ejecutarSuite(name);
334
334
  return { content: [{ type: "text", text: `Reporte: ${dir}` }] };
335
335
  });
336
336
 
337
- server.tool("upsert_node", "Añadir o actualizar un nodo en el mapa de estados.", {
338
- mapName: z.string().describe("Nombre del archivo del mapa (ej: 'home_map.json'). El servidor lo buscará en la carpeta /maps."),
339
- nodeName: z.string().describe("Identificador único del nodo (ej: 'LOGIN_PAGE', 'HOME')."),
337
+ server.tool("upsert_node", "Añadir o actualizar un nodo en el mapa de estados.", {
338
+ mapName: z.string().describe("Nombre del archivo del mapa (ej: 'home_map.json'). El servidor lo buscará en la carpeta /maps."),
339
+ nodeName: z.string().describe("Identificador único del nodo (ej: 'LOGIN_PAGE', 'HOME')."),
340
340
  nodeData: z.object({
341
341
  transiciones: z.any().optional().describe("Objeto que define las salidas del nodo. Ej: { 'IR_A_LOGIN': { 'destino': 'LOGIN_PAGE', 'accion': 'mcp:wdio-mcp/click_element ...' } }")
342
- }).passthrough().describe("Datos completos del nodo, incluyendo transiciones y metadatos.")
342
+ }).passthrough().describe("Datos completos del nodo, incluyendo transiciones y metadatos.")
343
343
  }, async ({ mapName, nodeName, nodeData }) => {
344
344
  const mapPath = path.join(__dirname, 'maps', mapName);
345
345
  let map = { nodos: {} };
@@ -347,8 +347,8 @@ server.tool("upsert_node", "Añadir o actualizar un nodo en el mapa de estados."
347
347
  const content = await fs.readFile(mapPath, 'utf8');
348
348
  map = JSON.parse(content);
349
349
  if (!map.nodos) map = { nodos: map };
350
- } catch (e) {}
351
-
350
+ } catch (e) { }
351
+
352
352
  map.nodos[nodeName] = nodeData;
353
353
  await fs.mkdir(path.dirname(mapPath), { recursive: true });
354
354
  await fs.writeFile(mapPath, JSON.stringify(map, null, 2));
@@ -372,8 +372,8 @@ server.tool("upsert_node", "Añadir o actualizar un nodo en el mapa de estados."
372
372
  return { content: [{ type: "text", text: `Nodo '${nodeName}' actualizado. Diagrama visual regenerado en maps/${mapName.replace('.json', '.md')}` }] };
373
373
  });
374
374
 
375
- server.tool("inspect_framework", "Inspeccionar integridad y listar entidades del framework", {
376
- filter: z.optional(z.string())
375
+ server.tool("inspect_framework", "Inspeccionar integridad y listar entidades del framework", {
376
+ filter: z.optional(z.string())
377
377
  }, async () => {
378
378
  const entities = { maps: [], test_cases: [], suites: [], health: [] };
379
379
  const getFiles = async (dir) => {
@@ -402,15 +402,46 @@ server.tool("inspect_framework", "Inspeccionar integridad y listar entidades del
402
402
 
403
403
  if (entities.health.length === 0) entities.health.push("✅ Estructura íntegra. Todos los vínculos son correctos.");
404
404
 
405
+ // Validación de integridad de Grafos (Nodos Huérfanos)
406
+ for (const mapFile of entities.maps) {
407
+ try {
408
+ const mapData = JSON.parse(await fs.readFile(path.join(__dirname, 'maps', mapFile), 'utf8'));
409
+ const nodos = mapData.nodos || mapData;
410
+ if (nodos && nodos["HOME"]) {
411
+ const reachable = new Set(["HOME"]);
412
+ const stack = ["HOME"];
413
+ while (stack.length > 0) {
414
+ const current = stack.pop();
415
+ const node = nodos[current];
416
+ if (node && node.transiciones) {
417
+ for (const t of Object.values(node.transiciones)) {
418
+ if (t.destino && nodos[t.destino] && !reachable.has(t.destino)) {
419
+ reachable.add(t.destino);
420
+ stack.push(t.destino);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ const totalNodes = Object.keys(nodos);
426
+ const unreachable = totalNodes.filter(n => !reachable.has(n));
427
+ if (unreachable.length > 0) {
428
+ entities.health.push(`⚠️ Mapa '${mapFile}': Nodos inalcanzables desde HOME: ${unreachable.join(', ')}`);
429
+ }
430
+ } else if (nodos && !nodos["HOME"]) {
431
+ entities.health.push(`⚠️ Mapa '${mapFile}': No tiene un nodo 'HOME' (inicio), no se puede validar el grafo.`);
432
+ }
433
+ } catch (e) { entities.health.push(`❌ Error validando grafo de '${mapFile}': ${e.message}`); }
434
+ }
435
+
405
436
  return { content: [{ type: "text", text: JSON.stringify(entities, null, 2) }] };
406
437
  });
407
438
 
408
- server.tool("save_test_case", "Crear o actualizar un caso de prueba.", {
409
- name: z.string().describe("Nombre identificador del test (ej: 'TC_Login_Exitoso')."),
439
+ server.tool("save_test_case", "Crear o actualizar un caso de prueba.", {
440
+ name: z.string().describe("Nombre identificador del test (ej: 'TC_Login_Exitoso')."),
410
441
  steps: z.array(z.object({
411
442
  name: z.string().optional().describe("Descripción amigable del paso (ej: 'Ingresar Usuario')."),
412
443
  action: z.string().optional().describe("Acción a realizar. Puede ser un nombre de transición del mapa o un comando mcp:wdio-mcp/...")
413
- }).passthrough()).describe("Lista ordenada de pasos lógicos a ejecutar.")
444
+ }).passthrough()).describe("Lista ordenada de pasos lógicos a ejecutar.")
414
445
  }, async ({ name, steps }) => {
415
446
  const fileName = name.endsWith('.json') ? name : `${name}.json`;
416
447
  const filePath = path.join(__dirname, 'test_cases', fileName);
@@ -420,8 +451,8 @@ server.tool("save_test_case", "Crear o actualizar un caso de prueba.", {
420
451
  return { content: [{ type: "text", text: `Caso de prueba '${name}' guardado correctamente.` }] };
421
452
  });
422
453
 
423
- server.tool("save_suite", "Crear o actualizar una suite de pruebas.", {
424
- name: z.string().describe("Nombre de la suite (ej: 'Suite_E2E_Sanity')."),
454
+ server.tool("save_suite", "Crear o actualizar una suite de pruebas.", {
455
+ name: z.string().describe("Nombre de la suite (ej: 'Suite_E2E_Sanity')."),
425
456
  state_map: z.string().describe("Archivo del mapa de estados a usar (ej: 'mob_perfecto_map.json')."),
426
457
  tests: z.array(z.string()).describe("Lista de nombres de casos de prueba a incluir en la suite."),
427
458
  beforeSuite: z.array(z.string()).optional().default([]).describe("Acciones globales antes de la suite (ej: iniciar sesión)."),
@@ -435,11 +466,71 @@ server.tool("save_suite", "Crear o actualizar una suite de pruebas.", {
435
466
  return { content: [{ type: "text", text: `Suite '${name}' guardada correctamente.` }] };
436
467
  });
437
468
 
438
- server.tool("init_project", "Inicializa el proyecto con carpetas y archivos de plantilla", {
439
- force: z.optional(z.boolean()).default(false)
469
+ server.tool("design_wizard", "Asistente paso a paso para diseñar la máquina de estados", {
470
+ action: z.enum(["start", "add_node", "add_transition", "save"]),
471
+ data: z.record(z.any()).optional().describe("Datos según la fase: { mapName }, { nodeName }, { label, destino, accion }")
472
+ }, async ({ action, data }) => {
473
+ const contextPath = path.join(__dirname, 'data', '.design_context.json');
474
+ let context = { step: "IDLE", mapName: "", nodeName: "", transitions: {} };
475
+
476
+ try { context = JSON.parse(await fs.readFile(contextPath, 'utf8')); } catch (e) { }
477
+
478
+ if (action === "start") {
479
+ context = { step: "SELECT_NODE", mapName: data.mapName || "default_map.json", nodeName: "", transitions: {} };
480
+ await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
481
+ return { content: [{ type: "text", text: `🧙‍♂️ Wizard Iniciado: Trabajando en '${context.mapName}'.\n\nPASO 1: ¿Qué nombre le damos al nodo? (Usa action: 'add_node')` }] };
482
+ }
483
+
484
+ if (action === "add_node" && context.step === "SELECT_NODE") {
485
+ context.nodeName = data.nodeName;
486
+ context.step = "ADD_TRANSITIONS";
487
+ await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
488
+ return { content: [{ type: "text", text: `📍 Nodo '${context.nodeName}' identificado.\n\nPASO 2: Define una transición. (Usa action: 'add_transition' con { label, destino, accion }). Puedes llamar a esto varias veces.` }] };
489
+ }
490
+
491
+ if (action === "add_transition" && context.step === "ADD_TRANSITIONS") {
492
+ context.transitions[data.label] = { destino: data.destino, accion: data.accion };
493
+ await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
494
+ return { content: [{ type: "text", text: `🔄 Transición '${data.label}' añadida.\n\n¿Quieres añadir otra o guardar? (Usa action: 'save' para finalizar)` }] };
495
+ }
496
+
497
+ if (action === "save" && context.step === "ADD_TRANSITIONS") {
498
+ const result = await server.callTool("upsert_node", {
499
+ mapName: context.mapName,
500
+ nodeName: context.nodeName,
501
+ nodeData: { transiciones: context.transitions }
502
+ });
503
+ await fs.unlink(contextPath); // Reset wizard
504
+ return result;
505
+ }
506
+
507
+ return { content: [{ type: "text", text: `❌ Acción no permitida en el estado actual (${context.step}).` }] };
508
+ });
509
+
510
+ server.tool("begin_design", "Iniciar una sesión de diseño guiado para una aplicación", {
511
+ appName: z.string().describe("Nombre de la aplicación a diseñar (ej: 'AdvantageShopping').")
512
+ }, async ({ appName }) => {
513
+ const rules = `
514
+ 🎭 HAS ENTRADO EN MODO DISEÑO (SMS ARCHITECT) 🎭
515
+ Estás diseñando la estructura para: ${appName}
516
+
517
+ REGLAS CRÍTICAS DE SUPERVIVENCIA:
518
+ 1. ❌ PROHIBIDO editar archivos JSON manualmente.
519
+ 2. ❌ PROHIBIDO usar los campos 'nodes' o 'edges'. NO SOMOS UNA LIBRERÍA DE GRÁFICOS.
520
+ 3. ✅ USA EXCLUSIVAMENTE 'upsert_node' para crear o actualizar estados.
521
+ 4. ✅ ESTRUCTURA OBLIGATORIA: { "nodos": { "NOMBRE_NODO": { "transiciones": {} } } }
522
+ 5. 🔄 FLUJO: Primero propón el diseño en texto -> Pide confirmación -> Usa la herramienta.
523
+
524
+ Dime qué pantalla o flujo quieres empezar a mapear para ${appName}.
525
+ `;
526
+ return { content: [{ type: "text", text: rules }] };
527
+ });
528
+
529
+ server.tool("init_project", "Inicializa el proyecto con carpetas y archivos de plantilla", {
530
+ force: z.optional(z.boolean()).default(false)
440
531
  }, async ({ force }) => {
441
532
  await ensureDirectories();
442
-
533
+
443
534
  const templates = {
444
535
  'maps/template_map.json': {
445
536
  nodos: {
@@ -478,7 +569,7 @@ server.tool("init_project", "Inicializa el proyecto con carpetas y archivos de p
478
569
  try {
479
570
  await fs.access(fullPath);
480
571
  continue; // Skip if exists
481
- } catch (e) {}
572
+ } catch (e) { }
482
573
  }
483
574
  await fs.writeFile(fullPath, JSON.stringify(content, null, 2));
484
575
  } catch (e) {
@@ -486,7 +577,21 @@ server.tool("init_project", "Inicializa el proyecto con carpetas y archivos de p
486
577
  }
487
578
  }
488
579
 
489
- return { content: [{ type: "text", text: "✅ Proyecto inicializado. Se han creado las carpetas y los archivos de plantilla (templates) para guiar al agente." }] };
580
+ return {
581
+ content: [{
582
+ type: "text",
583
+ text: "✅ Instalación completada con éxito.\n\nComo este es un proyecto nuevo, el siguiente paso es definir tu aplicación. Por favor, llama a la herramienta `begin_design` para empezar a mapear tus primeros estados."
584
+ }]
585
+ };
586
+ });
587
+
588
+ server.tool("framework_menu", "Panel de control para elegir entre Modo Diseño o Modo Ejecución", {}, async () => {
589
+ return {
590
+ content: [{
591
+ type: "text",
592
+ text: "🎮 **Framework Control Panel**\n\n¿Qué deseas hacer ahora?\n1. 🎨 **Modo Diseño**: Añadir o modificar nodos y mapas (Usa `begin_design`).\n2. 🚀 **Modo Ejecución**: Lanzar suites de pruebas existentes (Usa `execute_suite`).\n3. 🔍 **Auditoría**: Revisar la integridad del sistema (Usa `inspect_framework`)."
593
+ }]
594
+ };
490
595
  });
491
596
 
492
597
  async function ensureDirectories() {
@@ -501,10 +606,10 @@ async function ensureDirectories() {
501
606
  }
502
607
  }
503
608
 
504
- async function main() {
609
+ async function main() {
505
610
  await ensureDirectories();
506
- await maquina.cargar();
507
- const transport = new StdioServerTransport();
508
- await server.connect(transport);
611
+ await maquina.cargar();
612
+ const transport = new StdioServerTransport();
613
+ await server.connect(transport);
509
614
  }
510
615
  main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-state-machine-test-framework",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "High-fidelity State Machine MCP Server for autonomous E2E testing orchestration.",
5
5
  "main": "index.js",
6
6
  "type": "module",