@cocaxcode/api-testing-mcp 0.7.0 → 0.8.1

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/dist/index.js CHANGED
@@ -38,19 +38,19 @@ var Storage = class {
38
38
  async listCollections(tag) {
39
39
  await this.ensureDir("collections");
40
40
  const files = await this.listJsonFiles(this.collectionsDir);
41
- const items = [];
42
- for (const file of files) {
43
- const saved = await this.readJson(join(this.collectionsDir, file));
44
- if (!saved) continue;
45
- if (tag && !(saved.tags ?? []).includes(tag)) continue;
46
- items.push({
47
- name: saved.name,
48
- method: saved.request.method,
49
- url: saved.request.url,
50
- tags: saved.tags ?? []
51
- });
52
- }
53
- return items;
41
+ const allSaved = await Promise.all(
42
+ files.map((file) => this.readJson(join(this.collectionsDir, file)))
43
+ );
44
+ return allSaved.filter((saved) => {
45
+ if (!saved) return false;
46
+ if (tag && !(saved.tags ?? []).includes(tag)) return false;
47
+ return true;
48
+ }).map((saved) => ({
49
+ name: saved.name,
50
+ method: saved.request.method,
51
+ url: saved.request.url,
52
+ tags: saved.tags ?? []
53
+ }));
54
54
  }
55
55
  async deleteCollection(name) {
56
56
  const filePath = join(this.collectionsDir, `${this.sanitizeName(name)}.json`);
@@ -75,18 +75,15 @@ var Storage = class {
75
75
  await this.ensureDir("environments");
76
76
  const files = await this.listJsonFiles(this.environmentsDir);
77
77
  const activeEnv = await this.getActiveEnvironment();
78
- const items = [];
79
- for (const file of files) {
80
- const env = await this.readJson(join(this.environmentsDir, file));
81
- if (!env) continue;
82
- items.push({
83
- name: env.name,
84
- active: env.name === activeEnv,
85
- variableCount: Object.keys(env.variables).length,
86
- spec: env.spec
87
- });
88
- }
89
- return items;
78
+ const allEnvs = await Promise.all(
79
+ files.map((file) => this.readJson(join(this.environmentsDir, file)))
80
+ );
81
+ return allEnvs.filter((env) => env !== null).map((env) => ({
82
+ name: env.name,
83
+ active: env.name === activeEnv,
84
+ variableCount: Object.keys(env.variables).length,
85
+ spec: env.spec
86
+ }));
90
87
  }
91
88
  async updateEnvironment(name, variables) {
92
89
  const env = await this.getEnvironment(name);
@@ -237,18 +234,15 @@ var Storage = class {
237
234
  async listSpecs() {
238
235
  await this.ensureDir("specs");
239
236
  const files = await this.listJsonFiles(this.specsDir);
240
- const items = [];
241
- for (const file of files) {
242
- const spec = await this.readJson(join(this.specsDir, file));
243
- if (!spec) continue;
244
- items.push({
245
- name: spec.name,
246
- source: spec.source,
247
- endpointCount: spec.endpoints.length,
248
- version: spec.version
249
- });
250
- }
251
- return items;
237
+ const allSpecs = await Promise.all(
238
+ files.map((file) => this.readJson(join(this.specsDir, file)))
239
+ );
240
+ return allSpecs.filter((spec) => spec !== null).map((spec) => ({
241
+ name: spec.name,
242
+ source: spec.source,
243
+ endpointCount: spec.endpoints.length,
244
+ version: spec.version
245
+ }));
252
246
  }
253
247
  async deleteSpec(name) {
254
248
  const filePath = join(this.specsDir, `${this.sanitizeName(name)}.json`);
@@ -288,12 +282,16 @@ var Storage = class {
288
282
  * Reemplaza caracteres no alfanuméricos por guiones.
289
283
  */
290
284
  sanitizeName(name) {
291
- return name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
285
+ const sanitized = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
286
+ if (!sanitized) {
287
+ throw new Error(`Nombre inv\xE1lido: '${name}'`);
288
+ }
289
+ return sanitized;
292
290
  }
293
291
  };
294
292
 
295
293
  // src/tools/request.ts
296
- import { z } from "zod";
294
+ import { z as z2 } from "zod";
297
295
 
298
296
  // src/lib/http-client.ts
299
297
  var DEFAULT_TIMEOUT = 3e4;
@@ -430,38 +428,56 @@ function interpolateRequest(config, variables) {
430
428
  };
431
429
  }
432
430
 
433
- // src/tools/request.ts
434
- var AuthSchema = {
431
+ // src/lib/url.ts
432
+ function resolveUrl(url, variables) {
433
+ if (url.startsWith("/") && variables.BASE_URL) {
434
+ const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
435
+ return `${baseUrl}${url}`;
436
+ }
437
+ return url;
438
+ }
439
+
440
+ // src/lib/schemas.ts
441
+ import { z } from "zod";
442
+ var HttpMethodSchema = z.enum([
443
+ "GET",
444
+ "POST",
445
+ "PUT",
446
+ "PATCH",
447
+ "DELETE",
448
+ "HEAD",
449
+ "OPTIONS"
450
+ ]);
451
+ var AuthSchema = z.object({
435
452
  type: z.enum(["bearer", "api-key", "basic"]).describe("Tipo de autenticaci\xF3n"),
436
453
  token: z.string().optional().describe("Token para Bearer auth"),
437
454
  key: z.string().optional().describe("API key value"),
438
455
  header: z.string().optional().describe("Header name para API key (default: X-API-Key)"),
439
456
  username: z.string().optional().describe("Username para Basic auth"),
440
457
  password: z.string().optional().describe("Password para Basic auth")
441
- };
458
+ });
459
+ var AuthSchemaShape = AuthSchema.shape;
460
+
461
+ // src/tools/request.ts
442
462
  function registerRequestTool(server, storage) {
443
463
  server.tool(
444
464
  "request",
445
465
  "Ejecuta un HTTP request. URLs relativas (/path) usan BASE_URL del entorno activo. Soporta {{variables}}.",
446
466
  {
447
- method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
448
- url: z.string().describe(
467
+ method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
468
+ url: z2.string().describe(
449
469
  "URL del endpoint. Si empieza con / se antepone BASE_URL del entorno activo. Soporta {{variables}}."
450
470
  ),
451
- headers: z.record(z.string()).optional().describe("Headers HTTP como key-value pairs"),
452
- body: z.any().optional().describe("Body del request (JSON). Soporta {{variables}}"),
453
- query: z.record(z.string()).optional().describe("Query parameters como key-value pairs"),
454
- timeout: z.number().optional().describe("Timeout en milisegundos (default: 30000)"),
455
- auth: z.object(AuthSchema).optional().describe("Configuraci\xF3n de autenticaci\xF3n")
471
+ headers: z2.record(z2.string()).optional().describe("Headers HTTP como key-value pairs"),
472
+ body: z2.any().optional().describe("Body del request (JSON). Soporta {{variables}}"),
473
+ query: z2.record(z2.string()).optional().describe("Query parameters como key-value pairs"),
474
+ timeout: z2.number().optional().describe("Timeout en milisegundos (default: 30000)"),
475
+ auth: z2.object(AuthSchemaShape).optional().describe("Configuraci\xF3n de autenticaci\xF3n")
456
476
  },
457
477
  async (params) => {
458
478
  try {
459
479
  const variables = await storage.getActiveVariables();
460
- let resolvedUrl = params.url;
461
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
462
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
463
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
464
- }
480
+ const resolvedUrl = resolveUrl(params.url, variables);
465
481
  const config = {
466
482
  method: params.method,
467
483
  url: resolvedUrl,
@@ -493,30 +509,22 @@ function registerRequestTool(server, storage) {
493
509
  }
494
510
 
495
511
  // src/tools/collection.ts
496
- import { z as z2 } from "zod";
497
- var AuthSchema2 = {
498
- type: z2.enum(["bearer", "api-key", "basic"]).describe("Tipo de autenticaci\xF3n"),
499
- token: z2.string().optional().describe("Token para Bearer auth"),
500
- key: z2.string().optional().describe("API key value"),
501
- header: z2.string().optional().describe("Header name para API key (default: X-API-Key)"),
502
- username: z2.string().optional().describe("Username para Basic auth"),
503
- password: z2.string().optional().describe("Password para Basic auth")
504
- };
512
+ import { z as z3 } from "zod";
505
513
  function registerCollectionTools(server, storage) {
506
514
  server.tool(
507
515
  "collection_save",
508
516
  "Guarda un request en la colecci\xF3n local. Si ya existe un request con el mismo nombre, lo sobreescribe.",
509
517
  {
510
- name: z2.string().describe("Nombre \xFAnico del request guardado"),
511
- request: z2.object({
512
- method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
513
- url: z2.string(),
514
- headers: z2.record(z2.string()).optional(),
515
- body: z2.any().optional(),
516
- query: z2.record(z2.string()).optional(),
517
- auth: z2.object(AuthSchema2).optional()
518
+ name: z3.string().describe("Nombre \xFAnico del request guardado"),
519
+ request: z3.object({
520
+ method: z3.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
521
+ url: z3.string(),
522
+ headers: z3.record(z3.string()).optional(),
523
+ body: z3.any().optional(),
524
+ query: z3.record(z3.string()).optional(),
525
+ auth: z3.object(AuthSchemaShape).optional()
518
526
  }).describe("Configuraci\xF3n del request a guardar"),
519
- tags: z2.array(z2.string()).optional().describe('Tags para organizar (ej: ["auth", "users"])')
527
+ tags: z3.array(z3.string()).optional().describe('Tags para organizar (ej: ["auth", "users"])')
520
528
  },
521
529
  async (params) => {
522
530
  try {
@@ -551,7 +559,7 @@ function registerCollectionTools(server, storage) {
551
559
  "collection_list",
552
560
  "Lista todos los requests guardados en la colecci\xF3n. Opcionalmente filtra por tag.",
553
561
  {
554
- tag: z2.string().optional().describe("Filtrar por tag")
562
+ tag: z3.string().optional().describe("Filtrar por tag")
555
563
  },
556
564
  async (params) => {
557
565
  try {
@@ -581,7 +589,7 @@ function registerCollectionTools(server, storage) {
581
589
  "collection_get",
582
590
  "Obtiene los detalles completos de un request guardado por su nombre.",
583
591
  {
584
- name: z2.string().describe("Nombre del request guardado")
592
+ name: z3.string().describe("Nombre del request guardado")
585
593
  },
586
594
  async (params) => {
587
595
  try {
@@ -618,7 +626,7 @@ function registerCollectionTools(server, storage) {
618
626
  "collection_delete",
619
627
  "Elimina un request guardado de la colecci\xF3n.",
620
628
  {
621
- name: z2.string().describe("Nombre del request a eliminar")
629
+ name: z3.string().describe("Nombre del request a eliminar")
622
630
  },
623
631
  async (params) => {
624
632
  try {
@@ -654,15 +662,15 @@ function registerCollectionTools(server, storage) {
654
662
  }
655
663
 
656
664
  // src/tools/environment.ts
657
- import { z as z3 } from "zod";
665
+ import { z as z4 } from "zod";
658
666
  function registerEnvironmentTools(server, storage) {
659
667
  server.tool(
660
668
  "env_create",
661
669
  "Crea un nuevo entorno (ej: dev, staging, prod) con variables opcionales.",
662
670
  {
663
- name: z3.string().describe("Nombre del entorno (ej: dev, staging, prod)"),
664
- variables: z3.record(z3.string()).optional().describe("Variables iniciales como key-value"),
665
- spec: z3.string().optional().describe('Nombre del spec API asociado (ej: "cocaxcode-api")')
671
+ name: z4.string().describe("Nombre del entorno (ej: dev, staging, prod)"),
672
+ variables: z4.record(z4.string()).optional().describe("Variables iniciales como key-value"),
673
+ spec: z4.string().optional().describe('Nombre del spec API asociado (ej: "cocaxcode-api")')
666
674
  },
667
675
  async (params) => {
668
676
  try {
@@ -727,9 +735,9 @@ function registerEnvironmentTools(server, storage) {
727
735
  "env_set",
728
736
  "Establece una variable en un entorno. Si no se especifica entorno, usa el activo.",
729
737
  {
730
- key: z3.string().describe("Nombre de la variable"),
731
- value: z3.string().describe("Valor de la variable"),
732
- environment: z3.string().optional().describe("Entorno destino (default: entorno activo)")
738
+ key: z4.string().describe("Nombre de la variable"),
739
+ value: z4.string().describe("Valor de la variable"),
740
+ environment: z4.string().optional().describe("Entorno destino (default: entorno activo)")
733
741
  },
734
742
  async (params) => {
735
743
  try {
@@ -767,8 +775,8 @@ function registerEnvironmentTools(server, storage) {
767
775
  "env_get",
768
776
  "Obtiene una variable espec\xEDfica o todas las variables de un entorno.",
769
777
  {
770
- key: z3.string().optional().describe("Variable espec\xEDfica. Si se omite, retorna todas"),
771
- environment: z3.string().optional().describe("Entorno a consultar (default: entorno activo)")
778
+ key: z4.string().optional().describe("Variable espec\xEDfica. Si se omite, retorna todas"),
779
+ environment: z4.string().optional().describe("Entorno a consultar (default: entorno activo)")
772
780
  },
773
781
  async (params) => {
774
782
  try {
@@ -840,8 +848,8 @@ function registerEnvironmentTools(server, storage) {
840
848
  "env_spec",
841
849
  "Asocia o desasocia un spec API a un entorno. Si no se especifica entorno, usa el activo.",
842
850
  {
843
- spec: z3.string().optional().describe("Nombre del spec a asociar. Si se omite, desasocia el spec actual"),
844
- environment: z3.string().optional().describe("Entorno destino (default: entorno activo)")
851
+ spec: z4.string().optional().describe("Nombre del spec a asociar. Si se omite, desasocia el spec actual"),
852
+ environment: z4.string().optional().describe("Entorno destino (default: entorno activo)")
845
853
  },
846
854
  async (params) => {
847
855
  try {
@@ -875,8 +883,8 @@ function registerEnvironmentTools(server, storage) {
875
883
  "env_rename",
876
884
  "Renombra un entorno existente. Si es el entorno activo, actualiza la referencia.",
877
885
  {
878
- name: z3.string().describe("Nombre actual del entorno"),
879
- new_name: z3.string().describe("Nuevo nombre para el entorno")
886
+ name: z4.string().describe("Nombre actual del entorno"),
887
+ new_name: z4.string().describe("Nuevo nombre para el entorno")
880
888
  },
881
889
  async (params) => {
882
890
  try {
@@ -902,7 +910,7 @@ function registerEnvironmentTools(server, storage) {
902
910
  "env_delete",
903
911
  "Elimina un entorno y todas sus variables. Si es el entorno activo, lo desactiva.",
904
912
  {
905
- name: z3.string().describe("Nombre del entorno a eliminar")
913
+ name: z4.string().describe("Nombre del entorno a eliminar")
906
914
  },
907
915
  async (params) => {
908
916
  try {
@@ -928,8 +936,8 @@ function registerEnvironmentTools(server, storage) {
928
936
  "env_switch",
929
937
  "Cambia el entorno activo. Si se especifica project, solo aplica a ese directorio de proyecto.",
930
938
  {
931
- name: z3.string().describe("Nombre del entorno a activar"),
932
- project: z3.string().optional().describe("Ruta del proyecto (ej: C:/cocaxcode). Si se omite, cambia el entorno global")
939
+ name: z4.string().describe("Nombre del entorno a activar"),
940
+ project: z4.string().optional().describe("Ruta del proyecto (ej: C:/cocaxcode). Si se omite, cambia el entorno global")
933
941
  },
934
942
  async (params) => {
935
943
  try {
@@ -956,7 +964,7 @@ function registerEnvironmentTools(server, storage) {
956
964
  "env_project_clear",
957
965
  "Elimina la asociaci\xF3n de entorno espec\xEDfico de un proyecto. El proyecto usar\xE1 el entorno global.",
958
966
  {
959
- project: z3.string().describe("Ruta del proyecto del que eliminar la asociaci\xF3n")
967
+ project: z4.string().describe("Ruta del proyecto del que eliminar la asociaci\xF3n")
960
968
  },
961
969
  async (params) => {
962
970
  try {
@@ -1030,7 +1038,7 @@ function registerEnvironmentTools(server, storage) {
1030
1038
  }
1031
1039
 
1032
1040
  // src/tools/api-spec.ts
1033
- import { z as z4 } from "zod";
1041
+ import { z as z5 } from "zod";
1034
1042
 
1035
1043
  // src/lib/openapi-parser.ts
1036
1044
  var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
@@ -1056,6 +1064,35 @@ function resolveSchema(schema, root, depth = 0) {
1056
1064
  return { type: "object", description: `Unresolved: ${schema.$ref}` };
1057
1065
  }
1058
1066
  const result = { ...schema };
1067
+ const rawAllOf = schema.allOf;
1068
+ if (rawAllOf && Array.isArray(rawAllOf)) {
1069
+ const merged = { type: "object" };
1070
+ const mergedProps = {};
1071
+ const mergedRequired = [];
1072
+ for (const sub of rawAllOf) {
1073
+ const resolved = resolveSchema(sub, root, depth + 1);
1074
+ if (resolved?.properties) {
1075
+ Object.assign(mergedProps, resolved.properties);
1076
+ }
1077
+ if (resolved?.required) {
1078
+ mergedRequired.push(...resolved.required);
1079
+ }
1080
+ if (resolved?.description && !merged.description) {
1081
+ merged.description = resolved.description;
1082
+ }
1083
+ }
1084
+ merged.properties = { ...result.properties ?? {}, ...mergedProps };
1085
+ if (mergedRequired.length > 0) {
1086
+ merged.required = [.../* @__PURE__ */ new Set([...result.required ?? [], ...mergedRequired])];
1087
+ }
1088
+ return merged;
1089
+ }
1090
+ const rawOneOf = schema.oneOf;
1091
+ const rawAnyOf = schema.anyOf;
1092
+ const unionSchemas = rawOneOf ?? rawAnyOf;
1093
+ if (unionSchemas && Array.isArray(unionSchemas) && unionSchemas.length > 0) {
1094
+ return resolveSchema(unionSchemas[0], root, depth + 1);
1095
+ }
1059
1096
  if (result.properties) {
1060
1097
  const resolvedProps = {};
1061
1098
  for (const [key, prop] of Object.entries(result.properties)) {
@@ -1174,8 +1211,8 @@ function registerApiSpecTools(server, storage) {
1174
1211
  "api_import",
1175
1212
  "Importa un spec OpenAPI/Swagger desde una URL o archivo local. Guarda los endpoints y schemas para consulta.",
1176
1213
  {
1177
- name: z4.string().describe('Nombre para identificar este API (ej: "mi-backend", "cocaxcode-api")'),
1178
- source: z4.string().describe(
1214
+ name: z5.string().describe('Nombre para identificar este API (ej: "mi-backend", "cocaxcode-api")'),
1215
+ source: z5.string().describe(
1179
1216
  "URL del spec OpenAPI JSON (ej: http://localhost:3001/api-docs-json) o ruta a archivo local"
1180
1217
  )
1181
1218
  },
@@ -1311,10 +1348,10 @@ function registerApiSpecTools(server, storage) {
1311
1348
  "api_endpoints",
1312
1349
  "Lista los endpoints de un API importada. Filtra por tag, m\xE9todo o path. Si no se especifica nombre y solo hay un spec importado, lo usa autom\xE1ticamente.",
1313
1350
  {
1314
- name: z4.string().optional().describe("Nombre del API importada. Si se omite y solo hay un spec, lo usa autom\xE1ticamente"),
1315
- tag: z4.string().optional().describe('Filtrar por tag (ej: "blog", "auth", "users")'),
1316
- method: z4.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).optional().describe("Filtrar por m\xE9todo HTTP"),
1317
- path: z4.string().optional().describe('Filtrar por path (b\xFAsqueda parcial, ej: "/blog" muestra todos los que contienen /blog)')
1351
+ name: z5.string().optional().describe("Nombre del API importada. Si se omite y solo hay un spec, lo usa autom\xE1ticamente"),
1352
+ tag: z5.string().optional().describe('Filtrar por tag (ej: "blog", "auth", "users")'),
1353
+ method: z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).optional().describe("Filtrar por m\xE9todo HTTP"),
1354
+ path: z5.string().optional().describe('Filtrar por path (b\xFAsqueda parcial, ej: "/blog" muestra todos los que contienen /blog)')
1318
1355
  },
1319
1356
  async (params) => {
1320
1357
  try {
@@ -1393,9 +1430,9 @@ function registerApiSpecTools(server, storage) {
1393
1430
  "api_endpoint_detail",
1394
1431
  "Muestra el detalle completo de un endpoint: par\xE1metros, body schema, y respuestas. \xDAtil para saber qu\xE9 datos enviar.",
1395
1432
  {
1396
- name: z4.string().optional().describe("Nombre del API importada. Si se omite y solo hay un spec, lo usa autom\xE1ticamente"),
1397
- method: z4.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("M\xE9todo HTTP del endpoint"),
1398
- path: z4.string().describe('Path exacto del endpoint (ej: "/blog", "/auth/login")')
1433
+ name: z5.string().optional().describe("Nombre del API importada. Si se omite y solo hay un spec, lo usa autom\xE1ticamente"),
1434
+ method: z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("M\xE9todo HTTP del endpoint"),
1435
+ path: z5.string().describe('Path exacto del endpoint (ej: "/blog", "/auth/login")')
1399
1436
  },
1400
1437
  async (params) => {
1401
1438
  try {
@@ -1556,29 +1593,37 @@ function formatSchema(schema, depth = 0) {
1556
1593
  }
1557
1594
 
1558
1595
  // src/tools/assert.ts
1559
- import { z as z5 } from "zod";
1560
- var AssertionSchema = z5.object({
1561
- path: z5.string().describe(
1562
- 'JSONPath al valor a validar: "status", "body.data.id", "headers.content-type", "timing.total_ms"'
1563
- ),
1564
- operator: z5.enum(["eq", "neq", "gt", "gte", "lt", "lte", "contains", "not_contains", "exists", "type"]).describe(
1565
- "Operador: eq (igual), neq (no igual), gt/gte/lt/lte (num\xE9ricos), contains/not_contains (strings/arrays), exists (campo existe), type (typeof)"
1566
- ),
1567
- expected: z5.any().optional().describe('Valor esperado (no necesario para "exists")')
1568
- });
1596
+ import { z as z6 } from "zod";
1597
+
1598
+ // src/lib/path.ts
1569
1599
  function getByPath(obj, path) {
1570
1600
  const parts = path.split(".");
1571
1601
  let current = obj;
1572
1602
  for (const part of parts) {
1573
1603
  if (current === null || current === void 0) return void 0;
1574
1604
  if (typeof current === "object") {
1575
- current = current[part];
1605
+ if (Array.isArray(current) && /^\d+$/.test(part)) {
1606
+ current = current[parseInt(part)];
1607
+ } else {
1608
+ current = current[part];
1609
+ }
1576
1610
  } else {
1577
1611
  return void 0;
1578
1612
  }
1579
1613
  }
1580
1614
  return current;
1581
1615
  }
1616
+
1617
+ // src/tools/assert.ts
1618
+ var AssertionSchema = z6.object({
1619
+ path: z6.string().describe(
1620
+ 'JSONPath al valor a validar: "status", "body.data.id", "headers.content-type", "timing.total_ms"'
1621
+ ),
1622
+ operator: z6.enum(["eq", "neq", "gt", "gte", "lt", "lte", "contains", "not_contains", "exists", "type"]).describe(
1623
+ "Operador: eq (igual), neq (no igual), gt/gte/lt/lte (num\xE9ricos), contains/not_contains (strings/arrays), exists (campo existe), type (typeof)"
1624
+ ),
1625
+ expected: z6.any().optional().describe('Valor esperado (no necesario para "exists")')
1626
+ });
1582
1627
  function evaluateAssertion(response, assertion) {
1583
1628
  const actual = getByPath(response, assertion.path);
1584
1629
  switch (assertion.operator) {
@@ -1655,29 +1700,18 @@ function registerAssertTool(server, storage) {
1655
1700
  "assert",
1656
1701
  "Ejecuta un request y valida la respuesta con assertions. Retorna resultado pass/fail por cada assertion.",
1657
1702
  {
1658
- method: z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
1659
- url: z5.string().describe("URL del endpoint (soporta /relativa y {{variables}})"),
1660
- headers: z5.record(z5.string()).optional().describe("Headers HTTP"),
1661
- body: z5.any().optional().describe("Body del request (JSON)"),
1662
- query: z5.record(z5.string()).optional().describe("Query parameters"),
1663
- auth: z5.object({
1664
- type: z5.enum(["bearer", "api-key", "basic"]),
1665
- token: z5.string().optional(),
1666
- key: z5.string().optional(),
1667
- header: z5.string().optional(),
1668
- username: z5.string().optional(),
1669
- password: z5.string().optional()
1670
- }).optional().describe("Autenticaci\xF3n"),
1671
- assertions: z5.array(AssertionSchema).describe("Lista de assertions a validar contra la respuesta")
1703
+ method: z6.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
1704
+ url: z6.string().describe("URL del endpoint (soporta /relativa y {{variables}})"),
1705
+ headers: z6.record(z6.string()).optional().describe("Headers HTTP"),
1706
+ body: z6.any().optional().describe("Body del request (JSON)"),
1707
+ query: z6.record(z6.string()).optional().describe("Query parameters"),
1708
+ auth: AuthSchema.optional().describe("Autenticaci\xF3n"),
1709
+ assertions: z6.array(AssertionSchema).describe("Lista de assertions a validar contra la respuesta")
1672
1710
  },
1673
1711
  async (params) => {
1674
1712
  try {
1675
1713
  const variables = await storage.getActiveVariables();
1676
- let resolvedUrl = params.url;
1677
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1678
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1679
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1680
- }
1714
+ const resolvedUrl = resolveUrl(params.url, variables);
1681
1715
  const config = {
1682
1716
  method: params.method,
1683
1717
  url: resolvedUrl,
@@ -1720,50 +1754,26 @@ function registerAssertTool(server, storage) {
1720
1754
  }
1721
1755
 
1722
1756
  // src/tools/flow.ts
1723
- import { z as z6 } from "zod";
1724
- var FlowStepSchema = z6.object({
1725
- name: z6.string().describe('Nombre del paso (ej: "login", "crear-post")'),
1726
- method: z6.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
1727
- url: z6.string().describe("URL del endpoint"),
1728
- headers: z6.record(z6.string()).optional().describe("Headers HTTP"),
1729
- body: z6.any().optional().describe("Body del request"),
1730
- query: z6.record(z6.string()).optional().describe("Query parameters"),
1731
- auth: z6.object({
1732
- type: z6.enum(["bearer", "api-key", "basic"]),
1733
- token: z6.string().optional(),
1734
- key: z6.string().optional(),
1735
- header: z6.string().optional(),
1736
- username: z6.string().optional(),
1737
- password: z6.string().optional()
1738
- }).optional().describe("Autenticaci\xF3n"),
1739
- extract: z6.record(z6.string()).optional().describe(
1757
+ import { z as z7 } from "zod";
1758
+ var FlowStepSchema = z7.object({
1759
+ name: z7.string().describe('Nombre del paso (ej: "login", "crear-post")'),
1760
+ method: z7.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
1761
+ url: z7.string().describe("URL del endpoint"),
1762
+ headers: z7.record(z7.string()).optional().describe("Headers HTTP"),
1763
+ body: z7.any().optional().describe("Body del request"),
1764
+ query: z7.record(z7.string()).optional().describe("Query parameters"),
1765
+ auth: AuthSchema.optional().describe("Autenticaci\xF3n"),
1766
+ extract: z7.record(z7.string()).optional().describe(
1740
1767
  'Variables a extraer de la respuesta para pasos siguientes. Key = nombre variable, value = path (ej: { "TOKEN": "body.token", "USER_ID": "body.data.id" })'
1741
1768
  )
1742
1769
  });
1743
- function getByPath2(obj, path) {
1744
- const parts = path.split(".");
1745
- let current = obj;
1746
- for (const part of parts) {
1747
- if (current === null || current === void 0) return void 0;
1748
- if (typeof current === "object") {
1749
- if (Array.isArray(current) && /^\d+$/.test(part)) {
1750
- current = current[parseInt(part)];
1751
- } else {
1752
- current = current[part];
1753
- }
1754
- } else {
1755
- return void 0;
1756
- }
1757
- }
1758
- return current;
1759
- }
1760
1770
  function registerFlowTool(server, storage) {
1761
1771
  server.tool(
1762
1772
  "flow_run",
1763
1773
  "Ejecuta una secuencia de requests en orden. Extrae variables de cada respuesta para usar en pasos siguientes con {{variable}}.",
1764
1774
  {
1765
- steps: z6.array(FlowStepSchema).describe("Pasos a ejecutar en orden"),
1766
- stop_on_error: z6.boolean().optional().describe("Detener al primer error (default: true)")
1775
+ steps: z7.array(FlowStepSchema).describe("Pasos a ejecutar en orden"),
1776
+ stop_on_error: z7.boolean().optional().describe("Detener al primer error (default: true)")
1767
1777
  },
1768
1778
  async (params) => {
1769
1779
  try {
@@ -1773,11 +1783,7 @@ function registerFlowTool(server, storage) {
1773
1783
  const results = [];
1774
1784
  for (const step of params.steps) {
1775
1785
  try {
1776
- let resolvedUrl = step.url;
1777
- if (resolvedUrl.startsWith("/") && flowVariables.BASE_URL) {
1778
- const baseUrl = flowVariables.BASE_URL.replace(/\/+$/, "");
1779
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1780
- }
1786
+ const resolvedUrl = resolveUrl(step.url, flowVariables);
1781
1787
  const config = {
1782
1788
  method: step.method,
1783
1789
  url: resolvedUrl,
@@ -1791,7 +1797,7 @@ function registerFlowTool(server, storage) {
1791
1797
  const extracted = {};
1792
1798
  if (step.extract) {
1793
1799
  for (const [varName, path] of Object.entries(step.extract)) {
1794
- const value = getByPath2(response, path);
1800
+ const value = getByPath(response, path);
1795
1801
  if (value !== void 0 && value !== null) {
1796
1802
  extracted[varName] = String(value);
1797
1803
  flowVariables[varName] = String(value);
@@ -1852,14 +1858,14 @@ function registerFlowTool(server, storage) {
1852
1858
  }
1853
1859
 
1854
1860
  // src/tools/utilities.ts
1855
- import { z as z7 } from "zod";
1861
+ import { z as z8 } from "zod";
1856
1862
  function registerUtilityTools(server, storage) {
1857
1863
  server.tool(
1858
1864
  "export_curl",
1859
1865
  "Genera un comando cURL a partir de un request guardado en la colecci\xF3n. Listo para copiar y pegar.",
1860
1866
  {
1861
- name: z7.string().describe("Nombre del request guardado en la colecci\xF3n"),
1862
- resolve_variables: z7.boolean().optional().describe("Resolver {{variables}} del entorno activo (default: true)")
1867
+ name: z8.string().describe("Nombre del request guardado en la colecci\xF3n"),
1868
+ resolve_variables: z8.boolean().optional().describe("Resolver {{variables}} del entorno activo (default: true)")
1863
1869
  },
1864
1870
  async (params) => {
1865
1871
  try {
@@ -1879,11 +1885,7 @@ function registerUtilityTools(server, storage) {
1879
1885
  const resolveVars = params.resolve_variables ?? true;
1880
1886
  if (resolveVars) {
1881
1887
  const variables = await storage.getActiveVariables();
1882
- let resolvedUrl = config.url;
1883
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1884
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1885
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1886
- }
1888
+ const resolvedUrl = resolveUrl(config.url, variables);
1887
1889
  config = { ...config, url: resolvedUrl };
1888
1890
  config = interpolateRequest(config, variables);
1889
1891
  }
@@ -1947,52 +1949,27 @@ ${curlCommand}`
1947
1949
  }
1948
1950
  }
1949
1951
  );
1952
+ const DiffRequestSchema = z8.object({
1953
+ label: z8.string().optional().describe('Etiqueta (ej: "antes", "dev", "v1")'),
1954
+ method: z8.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
1955
+ url: z8.string(),
1956
+ headers: z8.record(z8.string()).optional(),
1957
+ body: z8.any().optional(),
1958
+ query: z8.record(z8.string()).optional(),
1959
+ auth: AuthSchema.optional()
1960
+ });
1950
1961
  server.tool(
1951
1962
  "diff_responses",
1952
1963
  "Ejecuta dos requests y compara sus respuestas. \xDAtil para detectar regresiones o comparar entornos.",
1953
1964
  {
1954
- request_a: z7.object({
1955
- label: z7.string().optional().describe('Etiqueta (ej: "antes", "dev", "v1")'),
1956
- method: z7.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
1957
- url: z7.string(),
1958
- headers: z7.record(z7.string()).optional(),
1959
- body: z7.any().optional(),
1960
- query: z7.record(z7.string()).optional(),
1961
- auth: z7.object({
1962
- type: z7.enum(["bearer", "api-key", "basic"]),
1963
- token: z7.string().optional(),
1964
- key: z7.string().optional(),
1965
- header: z7.string().optional(),
1966
- username: z7.string().optional(),
1967
- password: z7.string().optional()
1968
- }).optional()
1969
- }).describe("Primer request"),
1970
- request_b: z7.object({
1971
- label: z7.string().optional().describe('Etiqueta (ej: "despu\xE9s", "prod", "v2")'),
1972
- method: z7.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
1973
- url: z7.string(),
1974
- headers: z7.record(z7.string()).optional(),
1975
- body: z7.any().optional(),
1976
- query: z7.record(z7.string()).optional(),
1977
- auth: z7.object({
1978
- type: z7.enum(["bearer", "api-key", "basic"]),
1979
- token: z7.string().optional(),
1980
- key: z7.string().optional(),
1981
- header: z7.string().optional(),
1982
- username: z7.string().optional(),
1983
- password: z7.string().optional()
1984
- }).optional()
1985
- }).describe("Segundo request")
1965
+ request_a: DiffRequestSchema.describe("Primer request"),
1966
+ request_b: DiffRequestSchema.describe("Segundo request")
1986
1967
  },
1987
1968
  async (params) => {
1988
1969
  try {
1989
1970
  const variables = await storage.getActiveVariables();
1990
1971
  const executeOne = async (req) => {
1991
- let resolvedUrl = req.url;
1992
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1993
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1994
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1995
- }
1972
+ const resolvedUrl = resolveUrl(req.url, variables);
1996
1973
  const config = {
1997
1974
  method: req.method,
1998
1975
  url: resolvedUrl,
@@ -2084,8 +2061,8 @@ ${curlCommand}`
2084
2061
  "bulk_test",
2085
2062
  "Ejecuta todos los requests guardados en la colecci\xF3n y reporta resultados. Filtrable por tag.",
2086
2063
  {
2087
- tag: z7.string().optional().describe("Filtrar por tag"),
2088
- expected_status: z7.number().optional().describe("Status HTTP esperado para todos (default: cualquier 2xx)")
2064
+ tag: z8.string().optional().describe("Filtrar por tag"),
2065
+ expected_status: z8.number().optional().describe("Status HTTP esperado para todos (default: cualquier 2xx)")
2089
2066
  },
2090
2067
  async (params) => {
2091
2068
  try {
@@ -2107,11 +2084,7 @@ ${curlCommand}`
2107
2084
  if (!saved) continue;
2108
2085
  try {
2109
2086
  let config = saved.request;
2110
- let resolvedUrl = config.url;
2111
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
2112
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
2113
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
2114
- }
2087
+ const resolvedUrl = resolveUrl(config.url, variables);
2115
2088
  config = { ...config, url: resolvedUrl };
2116
2089
  const interpolated = interpolateRequest(config, variables);
2117
2090
  const response = await executeRequest(interpolated);
@@ -2168,7 +2141,7 @@ ${curlCommand}`
2168
2141
  }
2169
2142
 
2170
2143
  // src/tools/mock.ts
2171
- import { z as z8 } from "zod";
2144
+ import { z as z9 } from "zod";
2172
2145
  function generateMockData(schema, depth = 0) {
2173
2146
  if (depth > 8) return null;
2174
2147
  if (schema.example !== void 0) return schema.example;
@@ -2239,12 +2212,12 @@ function registerMockTool(server, storage) {
2239
2212
  "mock",
2240
2213
  "Genera datos mock/fake para un endpoint bas\xE1ndose en su spec OpenAPI importada. \xDAtil para frontend sin backend.",
2241
2214
  {
2242
- name: z8.string().describe("Nombre del API importada"),
2243
- method: z8.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("M\xE9todo HTTP del endpoint"),
2244
- path: z8.string().describe('Path del endpoint (ej: "/users", "/blog")'),
2245
- target: z8.enum(["request", "response"]).optional().describe("Generar mock del body de request o de la response (default: response)"),
2246
- status: z8.string().optional().describe('Status code de la respuesta a mockear (default: "200" o "201")'),
2247
- count: z8.number().optional().describe("N\xFAmero de items mock a generar si el schema es un array (default: 3)")
2215
+ name: z9.string().describe("Nombre del API importada"),
2216
+ method: z9.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("M\xE9todo HTTP del endpoint"),
2217
+ path: z9.string().describe('Path del endpoint (ej: "/users", "/blog")'),
2218
+ target: z9.enum(["request", "response"]).optional().describe("Generar mock del body de request o de la response (default: response)"),
2219
+ status: z9.string().optional().describe('Status code de la respuesta a mockear (default: "200" o "201")'),
2220
+ count: z9.number().optional().describe("N\xFAmero de items mock a generar si el schema es un array (default: 3)")
2248
2221
  },
2249
2222
  async (params) => {
2250
2223
  try {
@@ -2364,37 +2337,26 @@ function registerMockTool(server, storage) {
2364
2337
  }
2365
2338
 
2366
2339
  // src/tools/load-test.ts
2367
- import { z as z9 } from "zod";
2340
+ import { z as z10 } from "zod";
2368
2341
  function registerLoadTestTool(server, storage) {
2369
2342
  server.tool(
2370
2343
  "load_test",
2371
2344
  "Lanza N requests concurrentes al mismo endpoint y mide tiempos promedio, percentiles y tasa de errores.",
2372
2345
  {
2373
- method: z9.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
2374
- url: z9.string().describe("URL del endpoint"),
2375
- headers: z9.record(z9.string()).optional().describe("Headers HTTP"),
2376
- body: z9.any().optional().describe("Body del request"),
2377
- query: z9.record(z9.string()).optional().describe("Query parameters"),
2378
- auth: z9.object({
2379
- type: z9.enum(["bearer", "api-key", "basic"]),
2380
- token: z9.string().optional(),
2381
- key: z9.string().optional(),
2382
- header: z9.string().optional(),
2383
- username: z9.string().optional(),
2384
- password: z9.string().optional()
2385
- }).optional().describe("Autenticaci\xF3n"),
2386
- concurrent: z9.number().describe("N\xFAmero de requests concurrentes a lanzar (max: 100)"),
2387
- timeout: z9.number().optional().describe("Timeout por request en ms (default: 30000)")
2346
+ method: z10.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
2347
+ url: z10.string().describe("URL del endpoint"),
2348
+ headers: z10.record(z10.string()).optional().describe("Headers HTTP"),
2349
+ body: z10.any().optional().describe("Body del request"),
2350
+ query: z10.record(z10.string()).optional().describe("Query parameters"),
2351
+ auth: AuthSchema.optional().describe("Autenticaci\xF3n"),
2352
+ concurrent: z10.number().describe("N\xFAmero de requests concurrentes a lanzar (max: 100)"),
2353
+ timeout: z10.number().optional().describe("Timeout por request en ms (default: 30000)")
2388
2354
  },
2389
2355
  async (params) => {
2390
2356
  try {
2391
2357
  const concurrentCount = Math.min(Math.max(params.concurrent, 1), 100);
2392
2358
  const variables = await storage.getActiveVariables();
2393
- let resolvedUrl = params.url;
2394
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
2395
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
2396
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
2397
- }
2359
+ const resolvedUrl = resolveUrl(params.url, variables);
2398
2360
  const baseConfig = {
2399
2361
  method: params.method,
2400
2362
  url: resolvedUrl,
@@ -2474,7 +2436,6 @@ function registerLoadTestTool(server, storage) {
2474
2436
  return {
2475
2437
  content: [{ type: "text", text: lines.join("\n") }],
2476
2438
  isError: failed.length > successful.length
2477
- // More than 50% failed
2478
2439
  };
2479
2440
  } catch (error) {
2480
2441
  const message = error instanceof Error ? error.message : String(error);
@@ -2488,7 +2449,7 @@ function registerLoadTestTool(server, storage) {
2488
2449
  }
2489
2450
 
2490
2451
  // src/server.ts
2491
- var VERSION = "0.7.0";
2452
+ var VERSION = "0.8.0";
2492
2453
  function createServer(storageDir) {
2493
2454
  const server = new McpServer({
2494
2455
  name: "api-testing-mcp",