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