@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/README.md +18 -4
- package/dist/index.js +372 -278
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
195
|
+
try {
|
|
196
|
+
const globalActive = await readFile(this.activeEnvFile, "utf-8");
|
|
197
|
+
if (globalActive.trim() === name) {
|
|
158
198
|
await unlink(this.activeEnvFile);
|
|
159
|
-
}
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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/
|
|
380
|
-
|
|
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:
|
|
394
|
-
url:
|
|
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:
|
|
398
|
-
body:
|
|
399
|
-
query:
|
|
400
|
-
timeout:
|
|
401
|
-
auth:
|
|
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
|
-
|
|
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
|
|
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:
|
|
457
|
-
request:
|
|
458
|
-
method:
|
|
459
|
-
url:
|
|
460
|
-
headers:
|
|
461
|
-
body:
|
|
462
|
-
query:
|
|
463
|
-
auth:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
610
|
-
variables:
|
|
611
|
-
spec:
|
|
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:
|
|
677
|
-
value:
|
|
678
|
-
environment:
|
|
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:
|
|
717
|
-
environment:
|
|
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:
|
|
790
|
-
environment:
|
|
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:
|
|
825
|
-
new_name:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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:
|
|
1047
|
-
source:
|
|
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:
|
|
1184
|
-
tag:
|
|
1185
|
-
method:
|
|
1186
|
-
path:
|
|
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
|
|
1191
|
-
if (
|
|
1358
|
+
const resolved = await resolveSpecName(params.name, storage);
|
|
1359
|
+
if (resolved.error) {
|
|
1192
1360
|
return {
|
|
1193
|
-
content: [{ type: "text", text:
|
|
1361
|
+
content: [{ type: "text", text: resolved.error }],
|
|
1194
1362
|
isError: true
|
|
1195
1363
|
};
|
|
1196
1364
|
}
|
|
1197
|
-
const
|
|
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 '${
|
|
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:
|
|
1265
|
-
method:
|
|
1266
|
-
path:
|
|
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
|
|
1271
|
-
if (
|
|
1439
|
+
const resolved = await resolveSpecName(params.name, storage);
|
|
1440
|
+
if (resolved.error) {
|
|
1272
1441
|
return {
|
|
1273
|
-
content: [{ type: "text", text:
|
|
1442
|
+
content: [{ type: "text", text: resolved.error }],
|
|
1274
1443
|
isError: true
|
|
1275
1444
|
};
|
|
1276
1445
|
}
|
|
1277
|
-
const
|
|
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 '${
|
|
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
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
|
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:
|
|
1526
|
-
url:
|
|
1527
|
-
headers:
|
|
1528
|
-
body:
|
|
1529
|
-
query:
|
|
1530
|
-
auth:
|
|
1531
|
-
|
|
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
|
-
|
|
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
|
|
1591
|
-
var FlowStepSchema =
|
|
1592
|
-
name:
|
|
1593
|
-
method:
|
|
1594
|
-
url:
|
|
1595
|
-
headers:
|
|
1596
|
-
body:
|
|
1597
|
-
query:
|
|
1598
|
-
auth:
|
|
1599
|
-
|
|
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:
|
|
1633
|
-
stop_on_error:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
1729
|
-
resolve_variables:
|
|
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
|
-
|
|
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:
|
|
1822
|
-
|
|
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
|
-
|
|
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:
|
|
1955
|
-
expected_status:
|
|
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
|
-
|
|
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
|
|
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:
|
|
2110
|
-
method:
|
|
2111
|
-
path:
|
|
2112
|
-
target:
|
|
2113
|
-
status:
|
|
2114
|
-
count:
|
|
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
|
|
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:
|
|
2241
|
-
url:
|
|
2242
|
-
headers:
|
|
2243
|
-
body:
|
|
2244
|
-
query:
|
|
2245
|
-
auth:
|
|
2246
|
-
|
|
2247
|
-
|
|
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
|
-
|
|
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.
|
|
2452
|
+
var VERSION = "0.8.0";
|
|
2359
2453
|
function createServer(storageDir) {
|
|
2360
2454
|
const server = new McpServer({
|
|
2361
2455
|
name: "api-testing-mcp",
|