@cocaxcode/api-testing-mcp 0.6.0 → 0.8.0

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
@@ -16,12 +16,14 @@ var Storage = class {
16
16
  environmentsDir;
17
17
  specsDir;
18
18
  activeEnvFile;
19
+ projectEnvsFile;
19
20
  constructor(baseDir) {
20
21
  this.baseDir = baseDir ?? process.env.API_TESTING_DIR ?? join(homedir(), ".api-testing");
21
22
  this.collectionsDir = join(this.baseDir, "collections");
22
23
  this.environmentsDir = join(this.baseDir, "environments");
23
24
  this.specsDir = join(this.baseDir, "specs");
24
25
  this.activeEnvFile = join(this.baseDir, "active-env");
26
+ this.projectEnvsFile = join(this.baseDir, "project-envs.json");
25
27
  }
26
28
  // ── Collections ──
27
29
  async saveCollection(saved) {
@@ -36,19 +38,19 @@ var Storage = class {
36
38
  async listCollections(tag) {
37
39
  await this.ensureDir("collections");
38
40
  const files = await this.listJsonFiles(this.collectionsDir);
39
- const items = [];
40
- for (const file of files) {
41
- const saved = await this.readJson(join(this.collectionsDir, file));
42
- if (!saved) continue;
43
- if (tag && !(saved.tags ?? []).includes(tag)) continue;
44
- items.push({
45
- name: saved.name,
46
- method: saved.request.method,
47
- url: saved.request.url,
48
- tags: saved.tags ?? []
49
- });
50
- }
51
- 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
+ }));
52
54
  }
53
55
  async deleteCollection(name) {
54
56
  const filePath = join(this.collectionsDir, `${this.sanitizeName(name)}.json`);
@@ -73,18 +75,15 @@ var Storage = class {
73
75
  await this.ensureDir("environments");
74
76
  const files = await this.listJsonFiles(this.environmentsDir);
75
77
  const activeEnv = await this.getActiveEnvironment();
76
- const items = [];
77
- for (const file of files) {
78
- const env = await this.readJson(join(this.environmentsDir, file));
79
- if (!env) continue;
80
- items.push({
81
- name: env.name,
82
- active: env.name === activeEnv,
83
- variableCount: Object.keys(env.variables).length,
84
- spec: env.spec
85
- });
86
- }
87
- 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
+ }));
88
87
  }
89
88
  async updateEnvironment(name, variables) {
90
89
  const env = await this.getEnvironment(name);
@@ -96,7 +95,14 @@ var Storage = class {
96
95
  const filePath = join(this.environmentsDir, `${this.sanitizeName(name)}.json`);
97
96
  await this.writeJson(filePath, env);
98
97
  }
99
- async getActiveEnvironment() {
98
+ async getActiveEnvironment(project) {
99
+ const projectPath = project ?? process.cwd();
100
+ const projectEnvs = await this.getProjectEnvs();
101
+ const projectEnv = projectEnvs[projectPath];
102
+ if (projectEnv) {
103
+ const env = await this.getEnvironment(projectEnv);
104
+ if (env) return projectEnv;
105
+ }
100
106
  try {
101
107
  const content = await readFile(this.activeEnvFile, "utf-8");
102
108
  return content.trim() || null;
@@ -104,13 +110,33 @@ var Storage = class {
104
110
  return null;
105
111
  }
106
112
  }
107
- async setActiveEnvironment(name) {
113
+ async setActiveEnvironment(name, project) {
108
114
  const env = await this.getEnvironment(name);
109
115
  if (!env) {
110
116
  throw new Error(`Entorno '${name}' no encontrado`);
111
117
  }
112
- await this.ensureDir("");
113
- await writeFile(this.activeEnvFile, name, "utf-8");
118
+ if (project) {
119
+ const projectEnvs = await this.getProjectEnvs();
120
+ projectEnvs[project] = name;
121
+ await this.ensureDir("");
122
+ await this.writeJson(this.projectEnvsFile, projectEnvs);
123
+ } else {
124
+ await this.ensureDir("");
125
+ await writeFile(this.activeEnvFile, name, "utf-8");
126
+ }
127
+ }
128
+ async clearProjectEnvironment(project) {
129
+ const projectEnvs = await this.getProjectEnvs();
130
+ if (!(project in projectEnvs)) return false;
131
+ delete projectEnvs[project];
132
+ await this.writeJson(this.projectEnvsFile, projectEnvs);
133
+ return true;
134
+ }
135
+ async listProjectEnvironments() {
136
+ return this.getProjectEnvs();
137
+ }
138
+ async getProjectEnvs() {
139
+ return await this.readJson(this.projectEnvsFile) ?? {};
114
140
  }
115
141
  async setEnvironmentSpec(envName, specName) {
116
142
  const env = await this.getEnvironment(envName);
@@ -141,9 +167,23 @@ var Storage = class {
141
167
  env.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
142
168
  await this.createEnvironment(env);
143
169
  await unlink(join(this.environmentsDir, `${this.sanitizeName(oldName)}.json`));
144
- const activeEnv = await this.getActiveEnvironment();
145
- if (activeEnv === oldName) {
146
- await writeFile(this.activeEnvFile, newName, "utf-8");
170
+ try {
171
+ const globalActive = await readFile(this.activeEnvFile, "utf-8");
172
+ if (globalActive.trim() === oldName) {
173
+ await writeFile(this.activeEnvFile, newName, "utf-8");
174
+ }
175
+ } catch {
176
+ }
177
+ const projectEnvs = await this.getProjectEnvs();
178
+ let changed = false;
179
+ for (const [project, envName] of Object.entries(projectEnvs)) {
180
+ if (envName === oldName) {
181
+ projectEnvs[project] = newName;
182
+ changed = true;
183
+ }
184
+ }
185
+ if (changed) {
186
+ await this.writeJson(this.projectEnvsFile, projectEnvs);
147
187
  }
148
188
  }
149
189
  async deleteEnvironment(name) {
@@ -152,13 +192,24 @@ var Storage = class {
152
192
  throw new Error(`Entorno '${name}' no encontrado`);
153
193
  }
154
194
  await unlink(join(this.environmentsDir, `${this.sanitizeName(name)}.json`));
155
- const activeEnv = await this.getActiveEnvironment();
156
- if (activeEnv === name) {
157
- try {
195
+ try {
196
+ const globalActive = await readFile(this.activeEnvFile, "utf-8");
197
+ if (globalActive.trim() === name) {
158
198
  await unlink(this.activeEnvFile);
159
- } catch {
199
+ }
200
+ } catch {
201
+ }
202
+ const projectEnvs = await this.getProjectEnvs();
203
+ let changed = false;
204
+ for (const [project, envName] of Object.entries(projectEnvs)) {
205
+ if (envName === name) {
206
+ delete projectEnvs[project];
207
+ changed = true;
160
208
  }
161
209
  }
210
+ if (changed) {
211
+ await this.writeJson(this.projectEnvsFile, projectEnvs);
212
+ }
162
213
  }
163
214
  /**
164
215
  * Carga las variables del entorno activo.
@@ -183,18 +234,15 @@ var Storage = class {
183
234
  async listSpecs() {
184
235
  await this.ensureDir("specs");
185
236
  const files = await this.listJsonFiles(this.specsDir);
186
- const items = [];
187
- for (const file of files) {
188
- const spec = await this.readJson(join(this.specsDir, file));
189
- if (!spec) continue;
190
- items.push({
191
- name: spec.name,
192
- source: spec.source,
193
- endpointCount: spec.endpoints.length,
194
- version: spec.version
195
- });
196
- }
197
- 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
+ }));
198
246
  }
199
247
  async deleteSpec(name) {
200
248
  const filePath = join(this.specsDir, `${this.sanitizeName(name)}.json`);
@@ -234,12 +282,16 @@ var Storage = class {
234
282
  * Reemplaza caracteres no alfanuméricos por guiones.
235
283
  */
236
284
  sanitizeName(name) {
237
- 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;
238
290
  }
239
291
  };
240
292
 
241
293
  // src/tools/request.ts
242
- import { z } from "zod";
294
+ import { z as z2 } from "zod";
243
295
 
244
296
  // src/lib/http-client.ts
245
297
  var DEFAULT_TIMEOUT = 3e4;
@@ -376,38 +428,56 @@ function interpolateRequest(config, variables) {
376
428
  };
377
429
  }
378
430
 
379
- // src/tools/request.ts
380
- 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({
381
452
  type: z.enum(["bearer", "api-key", "basic"]).describe("Tipo de autenticaci\xF3n"),
382
453
  token: z.string().optional().describe("Token para Bearer auth"),
383
454
  key: z.string().optional().describe("API key value"),
384
455
  header: z.string().optional().describe("Header name para API key (default: X-API-Key)"),
385
456
  username: z.string().optional().describe("Username para Basic auth"),
386
457
  password: z.string().optional().describe("Password para Basic auth")
387
- };
458
+ });
459
+ var AuthSchemaShape = AuthSchema.shape;
460
+
461
+ // src/tools/request.ts
388
462
  function registerRequestTool(server, storage) {
389
463
  server.tool(
390
464
  "request",
391
465
  "Ejecuta un HTTP request. URLs relativas (/path) usan BASE_URL del entorno activo. Soporta {{variables}}.",
392
466
  {
393
- method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
394
- url: z.string().describe(
467
+ method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
468
+ url: z2.string().describe(
395
469
  "URL del endpoint. Si empieza con / se antepone BASE_URL del entorno activo. Soporta {{variables}}."
396
470
  ),
397
- headers: z.record(z.string()).optional().describe("Headers HTTP como key-value pairs"),
398
- body: z.any().optional().describe("Body del request (JSON). Soporta {{variables}}"),
399
- query: z.record(z.string()).optional().describe("Query parameters como key-value pairs"),
400
- timeout: z.number().optional().describe("Timeout en milisegundos (default: 30000)"),
401
- 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")
402
476
  },
403
477
  async (params) => {
404
478
  try {
405
479
  const variables = await storage.getActiveVariables();
406
- let resolvedUrl = params.url;
407
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
408
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
409
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
410
- }
480
+ const resolvedUrl = resolveUrl(params.url, variables);
411
481
  const config = {
412
482
  method: params.method,
413
483
  url: resolvedUrl,
@@ -439,30 +509,22 @@ function registerRequestTool(server, storage) {
439
509
  }
440
510
 
441
511
  // src/tools/collection.ts
442
- import { z as z2 } from "zod";
443
- var AuthSchema2 = {
444
- type: z2.enum(["bearer", "api-key", "basic"]).describe("Tipo de autenticaci\xF3n"),
445
- token: z2.string().optional().describe("Token para Bearer auth"),
446
- key: z2.string().optional().describe("API key value"),
447
- header: z2.string().optional().describe("Header name para API key (default: X-API-Key)"),
448
- username: z2.string().optional().describe("Username para Basic auth"),
449
- password: z2.string().optional().describe("Password para Basic auth")
450
- };
512
+ import { z as z3 } from "zod";
451
513
  function registerCollectionTools(server, storage) {
452
514
  server.tool(
453
515
  "collection_save",
454
516
  "Guarda un request en la colecci\xF3n local. Si ya existe un request con el mismo nombre, lo sobreescribe.",
455
517
  {
456
- name: z2.string().describe("Nombre \xFAnico del request guardado"),
457
- request: z2.object({
458
- method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
459
- url: z2.string(),
460
- headers: z2.record(z2.string()).optional(),
461
- body: z2.any().optional(),
462
- query: z2.record(z2.string()).optional(),
463
- 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()
464
526
  }).describe("Configuraci\xF3n del request a guardar"),
465
- 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"])')
466
528
  },
467
529
  async (params) => {
468
530
  try {
@@ -497,7 +559,7 @@ function registerCollectionTools(server, storage) {
497
559
  "collection_list",
498
560
  "Lista todos los requests guardados en la colecci\xF3n. Opcionalmente filtra por tag.",
499
561
  {
500
- tag: z2.string().optional().describe("Filtrar por tag")
562
+ tag: z3.string().optional().describe("Filtrar por tag")
501
563
  },
502
564
  async (params) => {
503
565
  try {
@@ -527,7 +589,7 @@ function registerCollectionTools(server, storage) {
527
589
  "collection_get",
528
590
  "Obtiene los detalles completos de un request guardado por su nombre.",
529
591
  {
530
- name: z2.string().describe("Nombre del request guardado")
592
+ name: z3.string().describe("Nombre del request guardado")
531
593
  },
532
594
  async (params) => {
533
595
  try {
@@ -564,7 +626,7 @@ function registerCollectionTools(server, storage) {
564
626
  "collection_delete",
565
627
  "Elimina un request guardado de la colecci\xF3n.",
566
628
  {
567
- name: z2.string().describe("Nombre del request a eliminar")
629
+ name: z3.string().describe("Nombre del request a eliminar")
568
630
  },
569
631
  async (params) => {
570
632
  try {
@@ -600,15 +662,15 @@ function registerCollectionTools(server, storage) {
600
662
  }
601
663
 
602
664
  // src/tools/environment.ts
603
- import { z as z3 } from "zod";
665
+ import { z as z4 } from "zod";
604
666
  function registerEnvironmentTools(server, storage) {
605
667
  server.tool(
606
668
  "env_create",
607
669
  "Crea un nuevo entorno (ej: dev, staging, prod) con variables opcionales.",
608
670
  {
609
- name: z3.string().describe("Nombre del entorno (ej: dev, staging, prod)"),
610
- variables: z3.record(z3.string()).optional().describe("Variables iniciales como key-value"),
611
- 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")')
612
674
  },
613
675
  async (params) => {
614
676
  try {
@@ -673,9 +735,9 @@ function registerEnvironmentTools(server, storage) {
673
735
  "env_set",
674
736
  "Establece una variable en un entorno. Si no se especifica entorno, usa el activo.",
675
737
  {
676
- key: z3.string().describe("Nombre de la variable"),
677
- value: z3.string().describe("Valor de la variable"),
678
- 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)")
679
741
  },
680
742
  async (params) => {
681
743
  try {
@@ -713,8 +775,8 @@ function registerEnvironmentTools(server, storage) {
713
775
  "env_get",
714
776
  "Obtiene una variable espec\xEDfica o todas las variables de un entorno.",
715
777
  {
716
- key: z3.string().optional().describe("Variable espec\xEDfica. Si se omite, retorna todas"),
717
- 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)")
718
780
  },
719
781
  async (params) => {
720
782
  try {
@@ -786,8 +848,8 @@ function registerEnvironmentTools(server, storage) {
786
848
  "env_spec",
787
849
  "Asocia o desasocia un spec API a un entorno. Si no se especifica entorno, usa el activo.",
788
850
  {
789
- spec: z3.string().optional().describe("Nombre del spec a asociar. Si se omite, desasocia el spec actual"),
790
- 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)")
791
853
  },
792
854
  async (params) => {
793
855
  try {
@@ -821,8 +883,8 @@ function registerEnvironmentTools(server, storage) {
821
883
  "env_rename",
822
884
  "Renombra un entorno existente. Si es el entorno activo, actualiza la referencia.",
823
885
  {
824
- name: z3.string().describe("Nombre actual del entorno"),
825
- 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")
826
888
  },
827
889
  async (params) => {
828
890
  try {
@@ -848,7 +910,7 @@ function registerEnvironmentTools(server, storage) {
848
910
  "env_delete",
849
911
  "Elimina un entorno y todas sus variables. Si es el entorno activo, lo desactiva.",
850
912
  {
851
- name: z3.string().describe("Nombre del entorno a eliminar")
913
+ name: z4.string().describe("Nombre del entorno a eliminar")
852
914
  },
853
915
  async (params) => {
854
916
  try {
@@ -872,18 +934,95 @@ function registerEnvironmentTools(server, storage) {
872
934
  );
873
935
  server.tool(
874
936
  "env_switch",
875
- "Cambia el entorno activo. Las variables del entorno activo se usan en {{interpolaci\xF3n}}.",
937
+ "Cambia el entorno activo. Si se especifica project, solo aplica a ese directorio de proyecto.",
938
+ {
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")
941
+ },
942
+ async (params) => {
943
+ try {
944
+ await storage.setActiveEnvironment(params.name, params.project);
945
+ const scope = params.project ? ` para proyecto '${params.project}'` : " (global)";
946
+ return {
947
+ content: [
948
+ {
949
+ type: "text",
950
+ text: `Entorno activo cambiado a '${params.name}'${scope}`
951
+ }
952
+ ]
953
+ };
954
+ } catch (error) {
955
+ const message = error instanceof Error ? error.message : String(error);
956
+ return {
957
+ content: [{ type: "text", text: `Error: ${message}` }],
958
+ isError: true
959
+ };
960
+ }
961
+ }
962
+ );
963
+ server.tool(
964
+ "env_project_clear",
965
+ "Elimina la asociaci\xF3n de entorno espec\xEDfico de un proyecto. El proyecto usar\xE1 el entorno global.",
876
966
  {
877
- name: z3.string().describe("Nombre del entorno a activar")
967
+ project: z4.string().describe("Ruta del proyecto del que eliminar la asociaci\xF3n")
878
968
  },
879
969
  async (params) => {
880
970
  try {
881
- await storage.setActiveEnvironment(params.name);
971
+ const removed = await storage.clearProjectEnvironment(params.project);
972
+ if (!removed) {
973
+ return {
974
+ content: [
975
+ {
976
+ type: "text",
977
+ text: `No hay entorno espec\xEDfico para el proyecto '${params.project}'`
978
+ }
979
+ ]
980
+ };
981
+ }
982
+ return {
983
+ content: [
984
+ {
985
+ type: "text",
986
+ text: `Entorno espec\xEDfico eliminado para proyecto '${params.project}'. Usar\xE1 el entorno global.`
987
+ }
988
+ ]
989
+ };
990
+ } catch (error) {
991
+ const message = error instanceof Error ? error.message : String(error);
992
+ return {
993
+ content: [{ type: "text", text: `Error: ${message}` }],
994
+ isError: true
995
+ };
996
+ }
997
+ }
998
+ );
999
+ server.tool(
1000
+ "env_project_list",
1001
+ "Lista todos los proyectos con entornos espec\xEDficos asignados.",
1002
+ {},
1003
+ async () => {
1004
+ try {
1005
+ const projectEnvs = await storage.listProjectEnvironments();
1006
+ const entries = Object.entries(projectEnvs);
1007
+ if (entries.length === 0) {
1008
+ return {
1009
+ content: [
1010
+ {
1011
+ type: "text",
1012
+ text: "No hay entornos espec\xEDficos por proyecto. Todos usan el entorno global."
1013
+ }
1014
+ ]
1015
+ };
1016
+ }
882
1017
  return {
883
1018
  content: [
884
1019
  {
885
1020
  type: "text",
886
- text: `Entorno activo cambiado a '${params.name}'`
1021
+ text: JSON.stringify(
1022
+ entries.map(([project, env]) => ({ project, environment: env })),
1023
+ null,
1024
+ 2
1025
+ )
887
1026
  }
888
1027
  ]
889
1028
  };
@@ -899,7 +1038,7 @@ function registerEnvironmentTools(server, storage) {
899
1038
  }
900
1039
 
901
1040
  // src/tools/api-spec.ts
902
- import { z as z4 } from "zod";
1041
+ import { z as z5 } from "zod";
903
1042
 
904
1043
  // src/lib/openapi-parser.ts
905
1044
  var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
@@ -925,6 +1064,35 @@ function resolveSchema(schema, root, depth = 0) {
925
1064
  return { type: "object", description: `Unresolved: ${schema.$ref}` };
926
1065
  }
927
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
+ }
928
1096
  if (result.properties) {
929
1097
  const resolvedProps = {};
930
1098
  for (const [key, prop] of Object.entries(result.properties)) {
@@ -1043,8 +1211,8 @@ function registerApiSpecTools(server, storage) {
1043
1211
  "api_import",
1044
1212
  "Importa un spec OpenAPI/Swagger desde una URL o archivo local. Guarda los endpoints y schemas para consulta.",
1045
1213
  {
1046
- name: z4.string().describe('Nombre para identificar este API (ej: "mi-backend", "cocaxcode-api")'),
1047
- source: z4.string().describe(
1214
+ name: z5.string().describe('Nombre para identificar este API (ej: "mi-backend", "cocaxcode-api")'),
1215
+ source: z5.string().describe(
1048
1216
  "URL del spec OpenAPI JSON (ej: http://localhost:3001/api-docs-json) o ruta a archivo local"
1049
1217
  )
1050
1218
  },
@@ -1180,27 +1348,28 @@ function registerApiSpecTools(server, storage) {
1180
1348
  "api_endpoints",
1181
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.",
1182
1350
  {
1183
- name: z4.string().optional().describe("Nombre del API importada. Si se omite y solo hay un spec, lo usa autom\xE1ticamente"),
1184
- tag: z4.string().optional().describe('Filtrar por tag (ej: "blog", "auth", "users")'),
1185
- method: z4.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).optional().describe("Filtrar por m\xE9todo HTTP"),
1186
- 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)')
1187
1355
  },
1188
1356
  async (params) => {
1189
1357
  try {
1190
- const specName = await resolveSpecName(params.name, storage);
1191
- if (specName.error) {
1358
+ const resolved = await resolveSpecName(params.name, storage);
1359
+ if (resolved.error) {
1192
1360
  return {
1193
- content: [{ type: "text", text: specName.error }],
1361
+ content: [{ type: "text", text: resolved.error }],
1194
1362
  isError: true
1195
1363
  };
1196
1364
  }
1197
- const spec = await storage.getSpec(specName.name);
1365
+ const resolvedName = resolved.name;
1366
+ const spec = await storage.getSpec(resolvedName);
1198
1367
  if (!spec) {
1199
1368
  return {
1200
1369
  content: [
1201
1370
  {
1202
1371
  type: "text",
1203
- text: `Error: API '${specName.name}' no encontrada. Usa api_import para importarla primero.`
1372
+ text: `Error: API '${resolvedName}' no encontrada. Usa api_import para importarla primero.`
1204
1373
  }
1205
1374
  ],
1206
1375
  isError: true
@@ -1261,26 +1430,27 @@ function registerApiSpecTools(server, storage) {
1261
1430
  "api_endpoint_detail",
1262
1431
  "Muestra el detalle completo de un endpoint: par\xE1metros, body schema, y respuestas. \xDAtil para saber qu\xE9 datos enviar.",
1263
1432
  {
1264
- name: z4.string().optional().describe("Nombre del API importada. Si se omite y solo hay un spec, lo usa autom\xE1ticamente"),
1265
- method: z4.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("M\xE9todo HTTP del endpoint"),
1266
- 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")')
1267
1436
  },
1268
1437
  async (params) => {
1269
1438
  try {
1270
- const specName = await resolveSpecName(params.name, storage);
1271
- if (specName.error) {
1439
+ const resolved = await resolveSpecName(params.name, storage);
1440
+ if (resolved.error) {
1272
1441
  return {
1273
- content: [{ type: "text", text: specName.error }],
1442
+ content: [{ type: "text", text: resolved.error }],
1274
1443
  isError: true
1275
1444
  };
1276
1445
  }
1277
- const spec = await storage.getSpec(specName.name);
1446
+ const resolvedName = resolved.name;
1447
+ const spec = await storage.getSpec(resolvedName);
1278
1448
  if (!spec) {
1279
1449
  return {
1280
1450
  content: [
1281
1451
  {
1282
1452
  type: "text",
1283
- text: `Error: API '${specName.name}' no encontrada. Usa api_import para importarla primero.`
1453
+ text: `Error: API '${resolvedName}' no encontrada. Usa api_import para importarla primero.`
1284
1454
  }
1285
1455
  ],
1286
1456
  isError: true
@@ -1423,29 +1593,37 @@ function formatSchema(schema, depth = 0) {
1423
1593
  }
1424
1594
 
1425
1595
  // src/tools/assert.ts
1426
- import { z as z5 } from "zod";
1427
- var AssertionSchema = z5.object({
1428
- path: z5.string().describe(
1429
- 'JSONPath al valor a validar: "status", "body.data.id", "headers.content-type", "timing.total_ms"'
1430
- ),
1431
- operator: z5.enum(["eq", "neq", "gt", "gte", "lt", "lte", "contains", "not_contains", "exists", "type"]).describe(
1432
- "Operador: eq (igual), neq (no igual), gt/gte/lt/lte (num\xE9ricos), contains/not_contains (strings/arrays), exists (campo existe), type (typeof)"
1433
- ),
1434
- expected: z5.any().optional().describe('Valor esperado (no necesario para "exists")')
1435
- });
1596
+ import { z as z6 } from "zod";
1597
+
1598
+ // src/lib/path.ts
1436
1599
  function getByPath(obj, path) {
1437
1600
  const parts = path.split(".");
1438
1601
  let current = obj;
1439
1602
  for (const part of parts) {
1440
1603
  if (current === null || current === void 0) return void 0;
1441
1604
  if (typeof current === "object") {
1442
- current = current[part];
1605
+ if (Array.isArray(current) && /^\d+$/.test(part)) {
1606
+ current = current[parseInt(part)];
1607
+ } else {
1608
+ current = current[part];
1609
+ }
1443
1610
  } else {
1444
1611
  return void 0;
1445
1612
  }
1446
1613
  }
1447
1614
  return current;
1448
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
+ });
1449
1627
  function evaluateAssertion(response, assertion) {
1450
1628
  const actual = getByPath(response, assertion.path);
1451
1629
  switch (assertion.operator) {
@@ -1522,29 +1700,18 @@ function registerAssertTool(server, storage) {
1522
1700
  "assert",
1523
1701
  "Ejecuta un request y valida la respuesta con assertions. Retorna resultado pass/fail por cada assertion.",
1524
1702
  {
1525
- method: z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
1526
- url: z5.string().describe("URL del endpoint (soporta /relativa y {{variables}})"),
1527
- headers: z5.record(z5.string()).optional().describe("Headers HTTP"),
1528
- body: z5.any().optional().describe("Body del request (JSON)"),
1529
- query: z5.record(z5.string()).optional().describe("Query parameters"),
1530
- auth: z5.object({
1531
- type: z5.enum(["bearer", "api-key", "basic"]),
1532
- token: z5.string().optional(),
1533
- key: z5.string().optional(),
1534
- header: z5.string().optional(),
1535
- username: z5.string().optional(),
1536
- password: z5.string().optional()
1537
- }).optional().describe("Autenticaci\xF3n"),
1538
- 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")
1539
1710
  },
1540
1711
  async (params) => {
1541
1712
  try {
1542
1713
  const variables = await storage.getActiveVariables();
1543
- let resolvedUrl = params.url;
1544
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1545
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1546
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1547
- }
1714
+ const resolvedUrl = resolveUrl(params.url, variables);
1548
1715
  const config = {
1549
1716
  method: params.method,
1550
1717
  url: resolvedUrl,
@@ -1587,50 +1754,26 @@ function registerAssertTool(server, storage) {
1587
1754
  }
1588
1755
 
1589
1756
  // src/tools/flow.ts
1590
- import { z as z6 } from "zod";
1591
- var FlowStepSchema = z6.object({
1592
- name: z6.string().describe('Nombre del paso (ej: "login", "crear-post")'),
1593
- method: z6.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
1594
- url: z6.string().describe("URL del endpoint"),
1595
- headers: z6.record(z6.string()).optional().describe("Headers HTTP"),
1596
- body: z6.any().optional().describe("Body del request"),
1597
- query: z6.record(z6.string()).optional().describe("Query parameters"),
1598
- auth: z6.object({
1599
- type: z6.enum(["bearer", "api-key", "basic"]),
1600
- token: z6.string().optional(),
1601
- key: z6.string().optional(),
1602
- header: z6.string().optional(),
1603
- username: z6.string().optional(),
1604
- password: z6.string().optional()
1605
- }).optional().describe("Autenticaci\xF3n"),
1606
- 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(
1607
1767
  'Variables a extraer de la respuesta para pasos siguientes. Key = nombre variable, value = path (ej: { "TOKEN": "body.token", "USER_ID": "body.data.id" })'
1608
1768
  )
1609
1769
  });
1610
- function getByPath2(obj, path) {
1611
- const parts = path.split(".");
1612
- let current = obj;
1613
- for (const part of parts) {
1614
- if (current === null || current === void 0) return void 0;
1615
- if (typeof current === "object") {
1616
- if (Array.isArray(current) && /^\d+$/.test(part)) {
1617
- current = current[parseInt(part)];
1618
- } else {
1619
- current = current[part];
1620
- }
1621
- } else {
1622
- return void 0;
1623
- }
1624
- }
1625
- return current;
1626
- }
1627
1770
  function registerFlowTool(server, storage) {
1628
1771
  server.tool(
1629
1772
  "flow_run",
1630
1773
  "Ejecuta una secuencia de requests en orden. Extrae variables de cada respuesta para usar en pasos siguientes con {{variable}}.",
1631
1774
  {
1632
- steps: z6.array(FlowStepSchema).describe("Pasos a ejecutar en orden"),
1633
- 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)")
1634
1777
  },
1635
1778
  async (params) => {
1636
1779
  try {
@@ -1640,11 +1783,7 @@ function registerFlowTool(server, storage) {
1640
1783
  const results = [];
1641
1784
  for (const step of params.steps) {
1642
1785
  try {
1643
- let resolvedUrl = step.url;
1644
- if (resolvedUrl.startsWith("/") && flowVariables.BASE_URL) {
1645
- const baseUrl = flowVariables.BASE_URL.replace(/\/+$/, "");
1646
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1647
- }
1786
+ const resolvedUrl = resolveUrl(step.url, flowVariables);
1648
1787
  const config = {
1649
1788
  method: step.method,
1650
1789
  url: resolvedUrl,
@@ -1658,7 +1797,7 @@ function registerFlowTool(server, storage) {
1658
1797
  const extracted = {};
1659
1798
  if (step.extract) {
1660
1799
  for (const [varName, path] of Object.entries(step.extract)) {
1661
- const value = getByPath2(response, path);
1800
+ const value = getByPath(response, path);
1662
1801
  if (value !== void 0 && value !== null) {
1663
1802
  extracted[varName] = String(value);
1664
1803
  flowVariables[varName] = String(value);
@@ -1719,14 +1858,14 @@ function registerFlowTool(server, storage) {
1719
1858
  }
1720
1859
 
1721
1860
  // src/tools/utilities.ts
1722
- import { z as z7 } from "zod";
1861
+ import { z as z8 } from "zod";
1723
1862
  function registerUtilityTools(server, storage) {
1724
1863
  server.tool(
1725
1864
  "export_curl",
1726
1865
  "Genera un comando cURL a partir de un request guardado en la colecci\xF3n. Listo para copiar y pegar.",
1727
1866
  {
1728
- name: z7.string().describe("Nombre del request guardado en la colecci\xF3n"),
1729
- 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)")
1730
1869
  },
1731
1870
  async (params) => {
1732
1871
  try {
@@ -1746,11 +1885,7 @@ function registerUtilityTools(server, storage) {
1746
1885
  const resolveVars = params.resolve_variables ?? true;
1747
1886
  if (resolveVars) {
1748
1887
  const variables = await storage.getActiveVariables();
1749
- let resolvedUrl = config.url;
1750
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1751
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1752
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1753
- }
1888
+ const resolvedUrl = resolveUrl(config.url, variables);
1754
1889
  config = { ...config, url: resolvedUrl };
1755
1890
  config = interpolateRequest(config, variables);
1756
1891
  }
@@ -1814,52 +1949,27 @@ ${curlCommand}`
1814
1949
  }
1815
1950
  }
1816
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
+ });
1817
1961
  server.tool(
1818
1962
  "diff_responses",
1819
1963
  "Ejecuta dos requests y compara sus respuestas. \xDAtil para detectar regresiones o comparar entornos.",
1820
1964
  {
1821
- request_a: z7.object({
1822
- label: z7.string().optional().describe('Etiqueta (ej: "antes", "dev", "v1")'),
1823
- method: z7.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
1824
- url: z7.string(),
1825
- headers: z7.record(z7.string()).optional(),
1826
- body: z7.any().optional(),
1827
- query: z7.record(z7.string()).optional(),
1828
- auth: z7.object({
1829
- type: z7.enum(["bearer", "api-key", "basic"]),
1830
- token: z7.string().optional(),
1831
- key: z7.string().optional(),
1832
- header: z7.string().optional(),
1833
- username: z7.string().optional(),
1834
- password: z7.string().optional()
1835
- }).optional()
1836
- }).describe("Primer request"),
1837
- request_b: z7.object({
1838
- label: z7.string().optional().describe('Etiqueta (ej: "despu\xE9s", "prod", "v2")'),
1839
- method: z7.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
1840
- url: z7.string(),
1841
- headers: z7.record(z7.string()).optional(),
1842
- body: z7.any().optional(),
1843
- query: z7.record(z7.string()).optional(),
1844
- auth: z7.object({
1845
- type: z7.enum(["bearer", "api-key", "basic"]),
1846
- token: z7.string().optional(),
1847
- key: z7.string().optional(),
1848
- header: z7.string().optional(),
1849
- username: z7.string().optional(),
1850
- password: z7.string().optional()
1851
- }).optional()
1852
- }).describe("Segundo request")
1965
+ request_a: DiffRequestSchema.describe("Primer request"),
1966
+ request_b: DiffRequestSchema.describe("Segundo request")
1853
1967
  },
1854
1968
  async (params) => {
1855
1969
  try {
1856
1970
  const variables = await storage.getActiveVariables();
1857
1971
  const executeOne = async (req) => {
1858
- let resolvedUrl = req.url;
1859
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1860
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1861
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1862
- }
1972
+ const resolvedUrl = resolveUrl(req.url, variables);
1863
1973
  const config = {
1864
1974
  method: req.method,
1865
1975
  url: resolvedUrl,
@@ -1951,8 +2061,8 @@ ${curlCommand}`
1951
2061
  "bulk_test",
1952
2062
  "Ejecuta todos los requests guardados en la colecci\xF3n y reporta resultados. Filtrable por tag.",
1953
2063
  {
1954
- tag: z7.string().optional().describe("Filtrar por tag"),
1955
- 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)")
1956
2066
  },
1957
2067
  async (params) => {
1958
2068
  try {
@@ -1974,11 +2084,7 @@ ${curlCommand}`
1974
2084
  if (!saved) continue;
1975
2085
  try {
1976
2086
  let config = saved.request;
1977
- let resolvedUrl = config.url;
1978
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1979
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1980
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
1981
- }
2087
+ const resolvedUrl = resolveUrl(config.url, variables);
1982
2088
  config = { ...config, url: resolvedUrl };
1983
2089
  const interpolated = interpolateRequest(config, variables);
1984
2090
  const response = await executeRequest(interpolated);
@@ -2035,7 +2141,7 @@ ${curlCommand}`
2035
2141
  }
2036
2142
 
2037
2143
  // src/tools/mock.ts
2038
- import { z as z8 } from "zod";
2144
+ import { z as z9 } from "zod";
2039
2145
  function generateMockData(schema, depth = 0) {
2040
2146
  if (depth > 8) return null;
2041
2147
  if (schema.example !== void 0) return schema.example;
@@ -2106,12 +2212,12 @@ function registerMockTool(server, storage) {
2106
2212
  "mock",
2107
2213
  "Genera datos mock/fake para un endpoint bas\xE1ndose en su spec OpenAPI importada. \xDAtil para frontend sin backend.",
2108
2214
  {
2109
- name: z8.string().describe("Nombre del API importada"),
2110
- method: z8.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("M\xE9todo HTTP del endpoint"),
2111
- path: z8.string().describe('Path del endpoint (ej: "/users", "/blog")'),
2112
- target: z8.enum(["request", "response"]).optional().describe("Generar mock del body de request o de la response (default: response)"),
2113
- status: z8.string().optional().describe('Status code de la respuesta a mockear (default: "200" o "201")'),
2114
- 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)")
2115
2221
  },
2116
2222
  async (params) => {
2117
2223
  try {
@@ -2231,37 +2337,26 @@ function registerMockTool(server, storage) {
2231
2337
  }
2232
2338
 
2233
2339
  // src/tools/load-test.ts
2234
- import { z as z9 } from "zod";
2340
+ import { z as z10 } from "zod";
2235
2341
  function registerLoadTestTool(server, storage) {
2236
2342
  server.tool(
2237
2343
  "load_test",
2238
2344
  "Lanza N requests concurrentes al mismo endpoint y mide tiempos promedio, percentiles y tasa de errores.",
2239
2345
  {
2240
- method: z9.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
2241
- url: z9.string().describe("URL del endpoint"),
2242
- headers: z9.record(z9.string()).optional().describe("Headers HTTP"),
2243
- body: z9.any().optional().describe("Body del request"),
2244
- query: z9.record(z9.string()).optional().describe("Query parameters"),
2245
- auth: z9.object({
2246
- type: z9.enum(["bearer", "api-key", "basic"]),
2247
- token: z9.string().optional(),
2248
- key: z9.string().optional(),
2249
- header: z9.string().optional(),
2250
- username: z9.string().optional(),
2251
- password: z9.string().optional()
2252
- }).optional().describe("Autenticaci\xF3n"),
2253
- concurrent: z9.number().describe("N\xFAmero de requests concurrentes a lanzar (max: 100)"),
2254
- 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)")
2255
2354
  },
2256
2355
  async (params) => {
2257
2356
  try {
2258
2357
  const concurrentCount = Math.min(Math.max(params.concurrent, 1), 100);
2259
2358
  const variables = await storage.getActiveVariables();
2260
- let resolvedUrl = params.url;
2261
- if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
2262
- const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
2263
- resolvedUrl = `${baseUrl}${resolvedUrl}`;
2264
- }
2359
+ const resolvedUrl = resolveUrl(params.url, variables);
2265
2360
  const baseConfig = {
2266
2361
  method: params.method,
2267
2362
  url: resolvedUrl,
@@ -2341,7 +2436,6 @@ function registerLoadTestTool(server, storage) {
2341
2436
  return {
2342
2437
  content: [{ type: "text", text: lines.join("\n") }],
2343
2438
  isError: failed.length > successful.length
2344
- // More than 50% failed
2345
2439
  };
2346
2440
  } catch (error) {
2347
2441
  const message = error instanceof Error ? error.message : String(error);
@@ -2355,7 +2449,7 @@ function registerLoadTestTool(server, storage) {
2355
2449
  }
2356
2450
 
2357
2451
  // src/server.ts
2358
- var VERSION = "0.5.5";
2452
+ var VERSION = "0.8.0";
2359
2453
  function createServer(storageDir) {
2360
2454
  const server = new McpServer({
2361
2455
  name: "api-testing-mcp",