@cocaxcode/api-testing-mcp 0.2.0 → 0.4.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
@@ -14,11 +14,13 @@ var Storage = class {
14
14
  baseDir;
15
15
  collectionsDir;
16
16
  environmentsDir;
17
+ specsDir;
17
18
  activeEnvFile;
18
19
  constructor(baseDir) {
19
20
  this.baseDir = baseDir ?? process.env.API_TESTING_DIR ?? join(process.cwd(), ".api-testing");
20
21
  this.collectionsDir = join(this.baseDir, "collections");
21
22
  this.environmentsDir = join(this.baseDir, "environments");
23
+ this.specsDir = join(this.baseDir, "specs");
22
24
  this.activeEnvFile = join(this.baseDir, "active-env");
23
25
  }
24
26
  // ── Collections ──
@@ -119,6 +121,41 @@ var Storage = class {
119
121
  const env = await this.getEnvironment(activeName);
120
122
  return env?.variables ?? {};
121
123
  }
124
+ // ── API Specs ──
125
+ async saveSpec(spec) {
126
+ await this.ensureDir("specs");
127
+ const filePath = join(this.specsDir, `${this.sanitizeName(spec.name)}.json`);
128
+ await this.writeJson(filePath, spec);
129
+ }
130
+ async getSpec(name) {
131
+ const filePath = join(this.specsDir, `${this.sanitizeName(name)}.json`);
132
+ return this.readJson(filePath);
133
+ }
134
+ async listSpecs() {
135
+ await this.ensureDir("specs");
136
+ const files = await this.listJsonFiles(this.specsDir);
137
+ const items = [];
138
+ for (const file of files) {
139
+ const spec = await this.readJson(join(this.specsDir, file));
140
+ if (!spec) continue;
141
+ items.push({
142
+ name: spec.name,
143
+ source: spec.source,
144
+ endpointCount: spec.endpoints.length,
145
+ version: spec.version
146
+ });
147
+ }
148
+ return items;
149
+ }
150
+ async deleteSpec(name) {
151
+ const filePath = join(this.specsDir, `${this.sanitizeName(name)}.json`);
152
+ try {
153
+ await unlink(filePath);
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
122
159
  // ── Internal ──
123
160
  async ensureDir(subdir) {
124
161
  const dir = subdir ? join(this.baseDir, subdir) : this.baseDir;
@@ -721,8 +758,1396 @@ function registerEnvironmentTools(server, storage) {
721
758
  );
722
759
  }
723
760
 
761
+ // src/tools/api-spec.ts
762
+ import { z as z4 } from "zod";
763
+
764
+ // src/lib/openapi-parser.ts
765
+ var VALID_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
766
+ function resolveRef(ref, root) {
767
+ const parts = ref.replace(/^#\//, "").split("/");
768
+ let current = root;
769
+ for (const part of parts) {
770
+ if (current && typeof current === "object" && part in current) {
771
+ current = current[part];
772
+ } else {
773
+ return void 0;
774
+ }
775
+ }
776
+ return current;
777
+ }
778
+ function resolveSchema(schema, root, depth = 0) {
779
+ if (!schema || depth > 10) return schema;
780
+ if (schema.$ref) {
781
+ const resolved = resolveRef(schema.$ref, root);
782
+ if (resolved) {
783
+ return resolveSchema(resolved, root, depth + 1);
784
+ }
785
+ return { type: "object", description: `Unresolved: ${schema.$ref}` };
786
+ }
787
+ const result = { ...schema };
788
+ if (result.properties) {
789
+ const resolvedProps = {};
790
+ for (const [key, prop] of Object.entries(result.properties)) {
791
+ resolvedProps[key] = resolveSchema(prop, root, depth + 1) ?? prop;
792
+ }
793
+ result.properties = resolvedProps;
794
+ }
795
+ if (result.items) {
796
+ result.items = resolveSchema(result.items, root, depth + 1) ?? result.items;
797
+ }
798
+ return result;
799
+ }
800
+ function parseOpenApiSpec(doc, name, source) {
801
+ const info = doc.info;
802
+ const paths = doc.paths;
803
+ const components = doc.components;
804
+ const rawSchemas = components?.schemas ?? {};
805
+ const schemas = {};
806
+ for (const [schemaName, schema] of Object.entries(rawSchemas)) {
807
+ schemas[schemaName] = resolveSchema(schema, doc, 0) ?? schema;
808
+ }
809
+ const endpoints = [];
810
+ if (paths) {
811
+ for (const [path, pathItem] of Object.entries(paths)) {
812
+ for (const [method, operation] of Object.entries(pathItem)) {
813
+ const upperMethod = method.toUpperCase();
814
+ if (!VALID_METHODS.includes(upperMethod)) continue;
815
+ const op = operation;
816
+ const rawParams = op.parameters ?? [];
817
+ const parameters = rawParams.map((p) => ({
818
+ name: p.name,
819
+ in: p.in,
820
+ required: p.required,
821
+ description: p.description,
822
+ schema: resolveSchema(p.schema, doc)
823
+ }));
824
+ const rawBody = op.requestBody;
825
+ let requestBody = void 0;
826
+ if (rawBody) {
827
+ const bodyContent = rawBody.content;
828
+ const resolvedContent = {};
829
+ if (bodyContent) {
830
+ for (const [contentType, mediaType] of Object.entries(bodyContent)) {
831
+ resolvedContent[contentType] = {
832
+ schema: resolveSchema(mediaType.schema, doc)
833
+ };
834
+ }
835
+ }
836
+ requestBody = {
837
+ required: rawBody.required,
838
+ description: rawBody.description,
839
+ content: resolvedContent
840
+ };
841
+ }
842
+ const rawResponses = op.responses ?? {};
843
+ const responses = {};
844
+ for (const [statusCode, resp] of Object.entries(rawResponses)) {
845
+ const respContent = resp.content;
846
+ const resolvedRespContent = {};
847
+ if (respContent) {
848
+ for (const [contentType, mediaType] of Object.entries(respContent)) {
849
+ resolvedRespContent[contentType] = {
850
+ schema: resolveSchema(mediaType.schema, doc)
851
+ };
852
+ }
853
+ }
854
+ responses[statusCode] = {
855
+ description: resp.description,
856
+ content: respContent ? resolvedRespContent : void 0
857
+ };
858
+ }
859
+ endpoints.push({
860
+ method: upperMethod,
861
+ path,
862
+ summary: op.summary,
863
+ description: op.description,
864
+ tags: op.tags,
865
+ parameters,
866
+ requestBody,
867
+ responses
868
+ });
869
+ }
870
+ }
871
+ }
872
+ const now = (/* @__PURE__ */ new Date()).toISOString();
873
+ return {
874
+ name,
875
+ source,
876
+ version: info?.version,
877
+ endpoints,
878
+ schemas,
879
+ importedAt: now,
880
+ updatedAt: now
881
+ };
882
+ }
883
+
884
+ // src/tools/api-spec.ts
885
+ import { readFile as readFile2 } from "fs/promises";
886
+ function registerApiSpecTools(server, storage) {
887
+ server.tool(
888
+ "api_import",
889
+ "Importa un spec OpenAPI/Swagger desde una URL o archivo local. Guarda los endpoints y schemas para consulta.",
890
+ {
891
+ name: z4.string().describe('Nombre para identificar este API (ej: "mi-backend", "cocaxcode-api")'),
892
+ source: z4.string().describe(
893
+ "URL del spec OpenAPI JSON (ej: http://localhost:3001/api-docs-json) o ruta a archivo local"
894
+ )
895
+ },
896
+ async (params) => {
897
+ try {
898
+ let rawDoc;
899
+ if (params.source.startsWith("http://") || params.source.startsWith("https://")) {
900
+ const controller = new AbortController();
901
+ const timeout = setTimeout(() => controller.abort(), 3e4);
902
+ try {
903
+ const response = await fetch(params.source, { signal: controller.signal });
904
+ if (!response.ok) {
905
+ return {
906
+ content: [
907
+ {
908
+ type: "text",
909
+ text: `Error: No se pudo descargar el spec. Status: ${response.status} ${response.statusText}`
910
+ }
911
+ ],
912
+ isError: true
913
+ };
914
+ }
915
+ rawDoc = await response.json();
916
+ } finally {
917
+ clearTimeout(timeout);
918
+ }
919
+ } else {
920
+ try {
921
+ const content = await readFile2(params.source, "utf-8");
922
+ rawDoc = JSON.parse(content);
923
+ } catch {
924
+ return {
925
+ content: [
926
+ {
927
+ type: "text",
928
+ text: `Error: No se pudo leer el archivo '${params.source}'. Verifica que existe y es JSON v\xE1lido.`
929
+ }
930
+ ],
931
+ isError: true
932
+ };
933
+ }
934
+ }
935
+ if (!rawDoc.openapi && !rawDoc.swagger) {
936
+ return {
937
+ content: [
938
+ {
939
+ type: "text",
940
+ text: 'Error: El documento no parece ser un spec OpenAPI/Swagger v\xE1lido. Falta la propiedad "openapi" o "swagger".'
941
+ }
942
+ ],
943
+ isError: true
944
+ };
945
+ }
946
+ const spec = parseOpenApiSpec(rawDoc, params.name, params.source);
947
+ await storage.saveSpec(spec);
948
+ const tagCounts = {};
949
+ for (const ep of spec.endpoints) {
950
+ for (const tag of ep.tags ?? ["sin-tag"]) {
951
+ tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
952
+ }
953
+ }
954
+ const tagSummary = Object.entries(tagCounts).map(([tag, count]) => ` - ${tag}: ${count} endpoints`).join("\n");
955
+ const schemaCount = Object.keys(spec.schemas).length;
956
+ return {
957
+ content: [
958
+ {
959
+ type: "text",
960
+ text: [
961
+ `API '${params.name}' importada correctamente.`,
962
+ "",
963
+ `Versi\xF3n: ${spec.version ?? "no especificada"}`,
964
+ `Endpoints: ${spec.endpoints.length}`,
965
+ `Schemas: ${schemaCount}`,
966
+ "",
967
+ "Endpoints por tag:",
968
+ tagSummary,
969
+ "",
970
+ "Usa api_endpoints para ver los endpoints disponibles.",
971
+ "Usa api_endpoint_detail para ver el detalle de un endpoint espec\xEDfico."
972
+ ].join("\n")
973
+ }
974
+ ]
975
+ };
976
+ } catch (error) {
977
+ const message = error instanceof Error ? error.message : String(error);
978
+ return {
979
+ content: [{ type: "text", text: `Error: ${message}` }],
980
+ isError: true
981
+ };
982
+ }
983
+ }
984
+ );
985
+ server.tool(
986
+ "api_endpoints",
987
+ "Lista los endpoints de un API importada. Filtra por tag, m\xE9todo o path.",
988
+ {
989
+ name: z4.string().describe("Nombre del API importada"),
990
+ tag: z4.string().optional().describe('Filtrar por tag (ej: "blog", "auth", "users")'),
991
+ method: z4.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).optional().describe("Filtrar por m\xE9todo HTTP"),
992
+ path: z4.string().optional().describe('Filtrar por path (b\xFAsqueda parcial, ej: "/blog" muestra todos los que contienen /blog)')
993
+ },
994
+ async (params) => {
995
+ try {
996
+ const spec = await storage.getSpec(params.name);
997
+ if (!spec) {
998
+ return {
999
+ content: [
1000
+ {
1001
+ type: "text",
1002
+ text: `Error: API '${params.name}' no encontrada. Usa api_import para importarla primero.`
1003
+ }
1004
+ ],
1005
+ isError: true
1006
+ };
1007
+ }
1008
+ let endpoints = spec.endpoints;
1009
+ if (params.tag) {
1010
+ endpoints = endpoints.filter(
1011
+ (ep) => (ep.tags ?? []).some((t) => t.toLowerCase() === params.tag.toLowerCase())
1012
+ );
1013
+ }
1014
+ if (params.method) {
1015
+ endpoints = endpoints.filter((ep) => ep.method === params.method);
1016
+ }
1017
+ if (params.path) {
1018
+ const search = params.path.toLowerCase();
1019
+ endpoints = endpoints.filter((ep) => ep.path.toLowerCase().includes(search));
1020
+ }
1021
+ if (endpoints.length === 0) {
1022
+ return {
1023
+ content: [
1024
+ {
1025
+ type: "text",
1026
+ text: "No se encontraron endpoints con los filtros aplicados."
1027
+ }
1028
+ ]
1029
+ };
1030
+ }
1031
+ const lines = endpoints.map((ep) => {
1032
+ const tags = ep.tags?.length ? ` [${ep.tags.join(", ")}]` : "";
1033
+ const summary = ep.summary ? ` \u2014 ${ep.summary}` : "";
1034
+ return `${ep.method.padEnd(7)} ${ep.path}${summary}${tags}`;
1035
+ });
1036
+ return {
1037
+ content: [
1038
+ {
1039
+ type: "text",
1040
+ text: [
1041
+ `API: ${spec.name} (${endpoints.length} endpoints)`,
1042
+ "",
1043
+ ...lines,
1044
+ "",
1045
+ "Usa api_endpoint_detail para ver par\xE1metros, body y respuestas de un endpoint."
1046
+ ].join("\n")
1047
+ }
1048
+ ]
1049
+ };
1050
+ } catch (error) {
1051
+ const message = error instanceof Error ? error.message : String(error);
1052
+ return {
1053
+ content: [{ type: "text", text: `Error: ${message}` }],
1054
+ isError: true
1055
+ };
1056
+ }
1057
+ }
1058
+ );
1059
+ server.tool(
1060
+ "api_endpoint_detail",
1061
+ "Muestra el detalle completo de un endpoint: par\xE1metros, body schema, y respuestas. \xDAtil para saber qu\xE9 datos enviar.",
1062
+ {
1063
+ name: z4.string().describe("Nombre del API importada"),
1064
+ method: z4.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("M\xE9todo HTTP del endpoint"),
1065
+ path: z4.string().describe('Path exacto del endpoint (ej: "/blog", "/auth/login")')
1066
+ },
1067
+ async (params) => {
1068
+ try {
1069
+ const spec = await storage.getSpec(params.name);
1070
+ if (!spec) {
1071
+ return {
1072
+ content: [
1073
+ {
1074
+ type: "text",
1075
+ text: `Error: API '${params.name}' no encontrada. Usa api_import para importarla primero.`
1076
+ }
1077
+ ],
1078
+ isError: true
1079
+ };
1080
+ }
1081
+ const endpoint = spec.endpoints.find(
1082
+ (ep) => ep.method === params.method && ep.path === params.path
1083
+ );
1084
+ if (!endpoint) {
1085
+ const similar = spec.endpoints.filter(
1086
+ (ep) => ep.path.includes(params.path) || params.path.includes(ep.path)
1087
+ );
1088
+ const suggestion = similar.length > 0 ? `
1089
+
1090
+ Endpoints similares:
1091
+ ${similar.map((ep) => ` ${ep.method} ${ep.path}`).join("\n")}` : "";
1092
+ return {
1093
+ content: [
1094
+ {
1095
+ type: "text",
1096
+ text: `Error: Endpoint ${params.method} ${params.path} no encontrado.${suggestion}`
1097
+ }
1098
+ ],
1099
+ isError: true
1100
+ };
1101
+ }
1102
+ const sections = [];
1103
+ sections.push(`## ${endpoint.method} ${endpoint.path}`);
1104
+ if (endpoint.summary) sections.push(`**${endpoint.summary}**`);
1105
+ if (endpoint.description) sections.push(endpoint.description);
1106
+ if (endpoint.tags?.length) sections.push(`Tags: ${endpoint.tags.join(", ")}`);
1107
+ if (endpoint.parameters?.length) {
1108
+ sections.push("");
1109
+ sections.push("### Par\xE1metros");
1110
+ for (const param of endpoint.parameters) {
1111
+ const required = param.required ? " (requerido)" : " (opcional)";
1112
+ const type = param.schema?.type ?? "string";
1113
+ const desc = param.description ? ` \u2014 ${param.description}` : "";
1114
+ sections.push(`- **${param.name}** [${param.in}] ${type}${required}${desc}`);
1115
+ }
1116
+ }
1117
+ if (endpoint.requestBody) {
1118
+ sections.push("");
1119
+ sections.push("### Body");
1120
+ const required = endpoint.requestBody.required ? " (requerido)" : " (opcional)";
1121
+ sections.push(`Body${required}`);
1122
+ if (endpoint.requestBody.content) {
1123
+ for (const [contentType, media] of Object.entries(endpoint.requestBody.content)) {
1124
+ sections.push(`
1125
+ Content-Type: ${contentType}`);
1126
+ if (media.schema) {
1127
+ sections.push("```json");
1128
+ sections.push(formatSchema(media.schema));
1129
+ sections.push("```");
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ if (endpoint.responses) {
1135
+ sections.push("");
1136
+ sections.push("### Respuestas");
1137
+ for (const [status, resp] of Object.entries(endpoint.responses)) {
1138
+ const desc = resp.description ? ` \u2014 ${resp.description}` : "";
1139
+ sections.push(`
1140
+ **${status}**${desc}`);
1141
+ if (resp.content) {
1142
+ for (const [, media] of Object.entries(resp.content)) {
1143
+ if (media.schema) {
1144
+ sections.push("```json");
1145
+ sections.push(formatSchema(media.schema));
1146
+ sections.push("```");
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ return {
1153
+ content: [
1154
+ {
1155
+ type: "text",
1156
+ text: sections.join("\n")
1157
+ }
1158
+ ]
1159
+ };
1160
+ } catch (error) {
1161
+ const message = error instanceof Error ? error.message : String(error);
1162
+ return {
1163
+ content: [{ type: "text", text: `Error: ${message}` }],
1164
+ isError: true
1165
+ };
1166
+ }
1167
+ }
1168
+ );
1169
+ }
1170
+ function formatSchema(schema, depth = 0) {
1171
+ if (depth > 5) return '"..."';
1172
+ const indent = " ".repeat(depth);
1173
+ const innerIndent = " ".repeat(depth + 1);
1174
+ if (schema.example !== void 0) {
1175
+ return JSON.stringify(schema.example, null, 2).split("\n").map((line, i) => i === 0 ? line : indent + line).join("\n");
1176
+ }
1177
+ if (schema.enum) {
1178
+ return JSON.stringify(schema.enum[0]);
1179
+ }
1180
+ if (schema.type === "object" && schema.properties) {
1181
+ const props = Object.entries(schema.properties);
1182
+ if (props.length === 0) return "{}";
1183
+ const requiredFields = new Set(schema.required ?? []);
1184
+ const lines = ["{"];
1185
+ for (const [key, prop] of props) {
1186
+ const isRequired = requiredFields.has(key);
1187
+ const comment = [];
1188
+ if (prop.description) comment.push(prop.description);
1189
+ if (!isRequired) comment.push("opcional");
1190
+ const commentStr = comment.length > 0 ? ` // ${comment.join(" \u2014 ")}` : "";
1191
+ const value = formatSchema(prop, depth + 1);
1192
+ lines.push(`${innerIndent}"${key}": ${value},${commentStr}`);
1193
+ }
1194
+ lines.push(`${indent}}`);
1195
+ return lines.join("\n");
1196
+ }
1197
+ if (schema.type === "array" && schema.items) {
1198
+ const itemValue = formatSchema(schema.items, depth + 1);
1199
+ return `[${itemValue}]`;
1200
+ }
1201
+ switch (schema.type) {
1202
+ case "string":
1203
+ if (schema.format === "date-time") return '"2024-01-01T00:00:00Z"';
1204
+ if (schema.format === "email") return '"user@example.com"';
1205
+ if (schema.format === "uri" || schema.format === "url") return '"https://example.com"';
1206
+ return '"string"';
1207
+ case "number":
1208
+ case "integer":
1209
+ return "0";
1210
+ case "boolean":
1211
+ return "true";
1212
+ default:
1213
+ return "null";
1214
+ }
1215
+ }
1216
+
1217
+ // src/tools/assert.ts
1218
+ import { z as z5 } from "zod";
1219
+ var AssertionSchema = z5.object({
1220
+ path: z5.string().describe(
1221
+ 'JSONPath al valor a validar: "status", "body.data.id", "headers.content-type", "timing.total_ms"'
1222
+ ),
1223
+ operator: z5.enum(["eq", "neq", "gt", "gte", "lt", "lte", "contains", "not_contains", "exists", "type"]).describe(
1224
+ "Operador: eq (igual), neq (no igual), gt/gte/lt/lte (num\xE9ricos), contains/not_contains (strings/arrays), exists (campo existe), type (typeof)"
1225
+ ),
1226
+ expected: z5.any().optional().describe('Valor esperado (no necesario para "exists")')
1227
+ });
1228
+ function getByPath(obj, path) {
1229
+ const parts = path.split(".");
1230
+ let current = obj;
1231
+ for (const part of parts) {
1232
+ if (current === null || current === void 0) return void 0;
1233
+ if (typeof current === "object") {
1234
+ current = current[part];
1235
+ } else {
1236
+ return void 0;
1237
+ }
1238
+ }
1239
+ return current;
1240
+ }
1241
+ function evaluateAssertion(response, assertion) {
1242
+ const actual = getByPath(response, assertion.path);
1243
+ switch (assertion.operator) {
1244
+ case "eq":
1245
+ return {
1246
+ pass: actual === assertion.expected,
1247
+ message: actual === assertion.expected ? `${assertion.path} === ${JSON.stringify(assertion.expected)}` : `${assertion.path}: esperado ${JSON.stringify(assertion.expected)}, recibido ${JSON.stringify(actual)}`
1248
+ };
1249
+ case "neq":
1250
+ return {
1251
+ pass: actual !== assertion.expected,
1252
+ message: actual !== assertion.expected ? `${assertion.path} !== ${JSON.stringify(assertion.expected)}` : `${assertion.path}: no deber\xEDa ser ${JSON.stringify(assertion.expected)}`
1253
+ };
1254
+ case "gt":
1255
+ return {
1256
+ pass: typeof actual === "number" && actual > assertion.expected,
1257
+ message: `${assertion.path}: ${actual} > ${assertion.expected} \u2192 ${typeof actual === "number" && actual > assertion.expected}`
1258
+ };
1259
+ case "gte":
1260
+ return {
1261
+ pass: typeof actual === "number" && actual >= assertion.expected,
1262
+ message: `${assertion.path}: ${actual} >= ${assertion.expected} \u2192 ${typeof actual === "number" && actual >= assertion.expected}`
1263
+ };
1264
+ case "lt":
1265
+ return {
1266
+ pass: typeof actual === "number" && actual < assertion.expected,
1267
+ message: `${assertion.path}: ${actual} < ${assertion.expected} \u2192 ${typeof actual === "number" && actual < assertion.expected}`
1268
+ };
1269
+ case "lte":
1270
+ return {
1271
+ pass: typeof actual === "number" && actual <= assertion.expected,
1272
+ message: `${assertion.path}: ${actual} <= ${assertion.expected} \u2192 ${typeof actual === "number" && actual <= assertion.expected}`
1273
+ };
1274
+ case "contains": {
1275
+ let pass = false;
1276
+ if (typeof actual === "string") {
1277
+ pass = actual.includes(String(assertion.expected));
1278
+ } else if (Array.isArray(actual)) {
1279
+ pass = actual.includes(assertion.expected);
1280
+ }
1281
+ return {
1282
+ pass,
1283
+ message: pass ? `${assertion.path} contiene ${JSON.stringify(assertion.expected)}` : `${assertion.path}: no contiene ${JSON.stringify(assertion.expected)}`
1284
+ };
1285
+ }
1286
+ case "not_contains": {
1287
+ let pass = true;
1288
+ if (typeof actual === "string") {
1289
+ pass = !actual.includes(String(assertion.expected));
1290
+ } else if (Array.isArray(actual)) {
1291
+ pass = !actual.includes(assertion.expected);
1292
+ }
1293
+ return {
1294
+ pass,
1295
+ message: pass ? `${assertion.path} no contiene ${JSON.stringify(assertion.expected)}` : `${assertion.path}: contiene ${JSON.stringify(assertion.expected)} (no deber\xEDa)`
1296
+ };
1297
+ }
1298
+ case "exists":
1299
+ return {
1300
+ pass: actual !== void 0 && actual !== null,
1301
+ message: actual !== void 0 && actual !== null ? `${assertion.path} existe` : `${assertion.path}: no existe`
1302
+ };
1303
+ case "type":
1304
+ return {
1305
+ pass: typeof actual === assertion.expected,
1306
+ message: typeof actual === assertion.expected ? `${assertion.path} es tipo ${assertion.expected}` : `${assertion.path}: esperado tipo ${assertion.expected}, recibido ${typeof actual}`
1307
+ };
1308
+ default:
1309
+ return { pass: false, message: `Operador desconocido: ${assertion.operator}` };
1310
+ }
1311
+ }
1312
+ function registerAssertTool(server, storage) {
1313
+ server.tool(
1314
+ "assert",
1315
+ "Ejecuta un request y valida la respuesta con assertions. Retorna resultado pass/fail por cada assertion.",
1316
+ {
1317
+ method: z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
1318
+ url: z5.string().describe("URL del endpoint (soporta /relativa y {{variables}})"),
1319
+ headers: z5.record(z5.string()).optional().describe("Headers HTTP"),
1320
+ body: z5.any().optional().describe("Body del request (JSON)"),
1321
+ query: z5.record(z5.string()).optional().describe("Query parameters"),
1322
+ auth: z5.object({
1323
+ type: z5.enum(["bearer", "api-key", "basic"]),
1324
+ token: z5.string().optional(),
1325
+ key: z5.string().optional(),
1326
+ header: z5.string().optional(),
1327
+ username: z5.string().optional(),
1328
+ password: z5.string().optional()
1329
+ }).optional().describe("Autenticaci\xF3n"),
1330
+ assertions: z5.array(AssertionSchema).describe("Lista de assertions a validar contra la respuesta")
1331
+ },
1332
+ async (params) => {
1333
+ try {
1334
+ const variables = await storage.getActiveVariables();
1335
+ let resolvedUrl = params.url;
1336
+ if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1337
+ const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1338
+ resolvedUrl = `${baseUrl}${resolvedUrl}`;
1339
+ }
1340
+ const config = {
1341
+ method: params.method,
1342
+ url: resolvedUrl,
1343
+ headers: params.headers,
1344
+ body: params.body,
1345
+ query: params.query,
1346
+ auth: params.auth
1347
+ };
1348
+ const interpolated = interpolateRequest(config, variables);
1349
+ const response = await executeRequest(interpolated);
1350
+ const results = params.assertions.map((assertion) => {
1351
+ const result = evaluateAssertion(response, assertion);
1352
+ return { ...result, assertion };
1353
+ });
1354
+ const passed = results.filter((r) => r.pass).length;
1355
+ const failed = results.filter((r) => !r.pass).length;
1356
+ const allPassed = failed === 0;
1357
+ const lines = [
1358
+ `${allPassed ? "\u2705 PASS" : "\u274C FAIL"} \u2014 ${passed}/${results.length} assertions passed`,
1359
+ `${params.method} ${params.url} \u2192 ${response.status} ${response.statusText} (${response.timing.total_ms}ms)`,
1360
+ ""
1361
+ ];
1362
+ for (const r of results) {
1363
+ const icon = r.pass ? "\u2705" : "\u274C";
1364
+ lines.push(`${icon} ${r.message}`);
1365
+ }
1366
+ return {
1367
+ content: [{ type: "text", text: lines.join("\n") }],
1368
+ isError: !allPassed
1369
+ };
1370
+ } catch (error) {
1371
+ const message = error instanceof Error ? error.message : String(error);
1372
+ return {
1373
+ content: [{ type: "text", text: `Error: ${message}` }],
1374
+ isError: true
1375
+ };
1376
+ }
1377
+ }
1378
+ );
1379
+ }
1380
+
1381
+ // src/tools/flow.ts
1382
+ import { z as z6 } from "zod";
1383
+ var FlowStepSchema = z6.object({
1384
+ name: z6.string().describe('Nombre del paso (ej: "login", "crear-post")'),
1385
+ method: z6.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
1386
+ url: z6.string().describe("URL del endpoint"),
1387
+ headers: z6.record(z6.string()).optional().describe("Headers HTTP"),
1388
+ body: z6.any().optional().describe("Body del request"),
1389
+ query: z6.record(z6.string()).optional().describe("Query parameters"),
1390
+ auth: z6.object({
1391
+ type: z6.enum(["bearer", "api-key", "basic"]),
1392
+ token: z6.string().optional(),
1393
+ key: z6.string().optional(),
1394
+ header: z6.string().optional(),
1395
+ username: z6.string().optional(),
1396
+ password: z6.string().optional()
1397
+ }).optional().describe("Autenticaci\xF3n"),
1398
+ extract: z6.record(z6.string()).optional().describe(
1399
+ 'Variables a extraer de la respuesta para pasos siguientes. Key = nombre variable, value = path (ej: { "TOKEN": "body.token", "USER_ID": "body.data.id" })'
1400
+ )
1401
+ });
1402
+ function getByPath2(obj, path) {
1403
+ const parts = path.split(".");
1404
+ let current = obj;
1405
+ for (const part of parts) {
1406
+ if (current === null || current === void 0) return void 0;
1407
+ if (typeof current === "object") {
1408
+ if (Array.isArray(current) && /^\d+$/.test(part)) {
1409
+ current = current[parseInt(part)];
1410
+ } else {
1411
+ current = current[part];
1412
+ }
1413
+ } else {
1414
+ return void 0;
1415
+ }
1416
+ }
1417
+ return current;
1418
+ }
1419
+ function registerFlowTool(server, storage) {
1420
+ server.tool(
1421
+ "flow_run",
1422
+ "Ejecuta una secuencia de requests en orden. Extrae variables de cada respuesta para usar en pasos siguientes con {{variable}}.",
1423
+ {
1424
+ steps: z6.array(FlowStepSchema).describe("Pasos a ejecutar en orden"),
1425
+ stop_on_error: z6.boolean().optional().describe("Detener al primer error (default: true)")
1426
+ },
1427
+ async (params) => {
1428
+ try {
1429
+ const stopOnError = params.stop_on_error ?? true;
1430
+ const envVariables = await storage.getActiveVariables();
1431
+ const flowVariables = { ...envVariables };
1432
+ const results = [];
1433
+ for (const step of params.steps) {
1434
+ try {
1435
+ let resolvedUrl = step.url;
1436
+ if (resolvedUrl.startsWith("/") && flowVariables.BASE_URL) {
1437
+ const baseUrl = flowVariables.BASE_URL.replace(/\/+$/, "");
1438
+ resolvedUrl = `${baseUrl}${resolvedUrl}`;
1439
+ }
1440
+ const config = {
1441
+ method: step.method,
1442
+ url: resolvedUrl,
1443
+ headers: step.headers,
1444
+ body: step.body,
1445
+ query: step.query,
1446
+ auth: step.auth
1447
+ };
1448
+ const interpolated = interpolateRequest(config, flowVariables);
1449
+ const response = await executeRequest(interpolated);
1450
+ const extracted = {};
1451
+ if (step.extract) {
1452
+ for (const [varName, path] of Object.entries(step.extract)) {
1453
+ const value = getByPath2(response, path);
1454
+ if (value !== void 0 && value !== null) {
1455
+ extracted[varName] = String(value);
1456
+ flowVariables[varName] = String(value);
1457
+ }
1458
+ }
1459
+ }
1460
+ results.push({
1461
+ name: step.name,
1462
+ status: response.status,
1463
+ timing: response.timing.total_ms,
1464
+ extracted
1465
+ });
1466
+ } catch (error) {
1467
+ const message = error instanceof Error ? error.message : String(error);
1468
+ results.push({
1469
+ name: step.name,
1470
+ status: 0,
1471
+ timing: 0,
1472
+ extracted: {},
1473
+ error: message
1474
+ });
1475
+ if (stopOnError) break;
1476
+ }
1477
+ }
1478
+ const allOk = results.every((r) => !r.error && r.status >= 200 && r.status < 400);
1479
+ const lines = [
1480
+ `${allOk ? "\u2705 FLOW COMPLETO" : "\u274C FLOW CON ERRORES"} \u2014 ${results.length}/${params.steps.length} pasos ejecutados`,
1481
+ ""
1482
+ ];
1483
+ for (let i = 0; i < results.length; i++) {
1484
+ const r = results[i];
1485
+ const icon = r.error ? "\u274C" : r.status >= 200 && r.status < 400 ? "\u2705" : "\u26A0\uFE0F";
1486
+ lines.push(`${icon} Paso ${i + 1}: ${r.name}`);
1487
+ if (r.error) {
1488
+ lines.push(` Error: ${r.error}`);
1489
+ } else {
1490
+ lines.push(` Status: ${r.status} | Tiempo: ${r.timing}ms`);
1491
+ }
1492
+ if (Object.keys(r.extracted).length > 0) {
1493
+ const vars = Object.entries(r.extracted).map(([k, v]) => `${k}=${v.length > 50 ? v.substring(0, 50) + "..." : v}`).join(", ");
1494
+ lines.push(` Extra\xEDdo: ${vars}`);
1495
+ }
1496
+ lines.push("");
1497
+ }
1498
+ return {
1499
+ content: [{ type: "text", text: lines.join("\n") }],
1500
+ isError: !allOk
1501
+ };
1502
+ } catch (error) {
1503
+ const message = error instanceof Error ? error.message : String(error);
1504
+ return {
1505
+ content: [{ type: "text", text: `Error: ${message}` }],
1506
+ isError: true
1507
+ };
1508
+ }
1509
+ }
1510
+ );
1511
+ }
1512
+
1513
+ // src/tools/utilities.ts
1514
+ import { z as z7 } from "zod";
1515
+ function registerUtilityTools(server, storage) {
1516
+ server.tool(
1517
+ "export_curl",
1518
+ "Genera un comando cURL a partir de un request guardado en la colecci\xF3n. Listo para copiar y pegar.",
1519
+ {
1520
+ name: z7.string().describe("Nombre del request guardado en la colecci\xF3n"),
1521
+ resolve_variables: z7.boolean().optional().describe("Resolver {{variables}} del entorno activo (default: true)")
1522
+ },
1523
+ async (params) => {
1524
+ try {
1525
+ const saved = await storage.getCollection(params.name);
1526
+ if (!saved) {
1527
+ return {
1528
+ content: [
1529
+ {
1530
+ type: "text",
1531
+ text: `Error: Request '${params.name}' no encontrado en la colecci\xF3n.`
1532
+ }
1533
+ ],
1534
+ isError: true
1535
+ };
1536
+ }
1537
+ let config = saved.request;
1538
+ const resolveVars = params.resolve_variables ?? true;
1539
+ if (resolveVars) {
1540
+ const variables = await storage.getActiveVariables();
1541
+ let resolvedUrl = config.url;
1542
+ if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1543
+ const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1544
+ resolvedUrl = `${baseUrl}${resolvedUrl}`;
1545
+ }
1546
+ config = { ...config, url: resolvedUrl };
1547
+ config = interpolateRequest(config, variables);
1548
+ }
1549
+ const parts = ["curl"];
1550
+ if (config.method !== "GET") {
1551
+ parts.push(`-X ${config.method}`);
1552
+ }
1553
+ let url = config.url;
1554
+ if (config.query && Object.keys(config.query).length > 0) {
1555
+ const queryStr = Object.entries(config.query).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1556
+ url += (url.includes("?") ? "&" : "?") + queryStr;
1557
+ }
1558
+ parts.push(`'${url}'`);
1559
+ if (config.headers) {
1560
+ for (const [key, value] of Object.entries(config.headers)) {
1561
+ parts.push(`-H '${key}: ${value}'`);
1562
+ }
1563
+ }
1564
+ if (config.auth) {
1565
+ switch (config.auth.type) {
1566
+ case "bearer":
1567
+ if (config.auth.token) {
1568
+ parts.push(`-H 'Authorization: Bearer ${config.auth.token}'`);
1569
+ }
1570
+ break;
1571
+ case "api-key":
1572
+ if (config.auth.key) {
1573
+ const header = config.auth.header ?? "X-API-Key";
1574
+ parts.push(`-H '${header}: ${config.auth.key}'`);
1575
+ }
1576
+ break;
1577
+ case "basic":
1578
+ if (config.auth.username && config.auth.password) {
1579
+ parts.push(`-u '${config.auth.username}:${config.auth.password}'`);
1580
+ }
1581
+ break;
1582
+ }
1583
+ }
1584
+ if (config.body !== void 0 && config.body !== null) {
1585
+ const bodyStr = typeof config.body === "string" ? config.body : JSON.stringify(config.body);
1586
+ parts.push(`-H 'Content-Type: application/json'`);
1587
+ parts.push(`-d '${bodyStr}'`);
1588
+ }
1589
+ const curlCommand = parts.join(" \\\n ");
1590
+ return {
1591
+ content: [
1592
+ {
1593
+ type: "text",
1594
+ text: `cURL para '${params.name}':
1595
+
1596
+ ${curlCommand}`
1597
+ }
1598
+ ]
1599
+ };
1600
+ } catch (error) {
1601
+ const message = error instanceof Error ? error.message : String(error);
1602
+ return {
1603
+ content: [{ type: "text", text: `Error: ${message}` }],
1604
+ isError: true
1605
+ };
1606
+ }
1607
+ }
1608
+ );
1609
+ server.tool(
1610
+ "diff_responses",
1611
+ "Ejecuta dos requests y compara sus respuestas. \xDAtil para detectar regresiones o comparar entornos.",
1612
+ {
1613
+ request_a: z7.object({
1614
+ label: z7.string().optional().describe('Etiqueta (ej: "antes", "dev", "v1")'),
1615
+ method: z7.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
1616
+ url: z7.string(),
1617
+ headers: z7.record(z7.string()).optional(),
1618
+ body: z7.any().optional(),
1619
+ query: z7.record(z7.string()).optional(),
1620
+ auth: z7.object({
1621
+ type: z7.enum(["bearer", "api-key", "basic"]),
1622
+ token: z7.string().optional(),
1623
+ key: z7.string().optional(),
1624
+ header: z7.string().optional(),
1625
+ username: z7.string().optional(),
1626
+ password: z7.string().optional()
1627
+ }).optional()
1628
+ }).describe("Primer request"),
1629
+ request_b: z7.object({
1630
+ label: z7.string().optional().describe('Etiqueta (ej: "despu\xE9s", "prod", "v2")'),
1631
+ method: z7.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]),
1632
+ url: z7.string(),
1633
+ headers: z7.record(z7.string()).optional(),
1634
+ body: z7.any().optional(),
1635
+ query: z7.record(z7.string()).optional(),
1636
+ auth: z7.object({
1637
+ type: z7.enum(["bearer", "api-key", "basic"]),
1638
+ token: z7.string().optional(),
1639
+ key: z7.string().optional(),
1640
+ header: z7.string().optional(),
1641
+ username: z7.string().optional(),
1642
+ password: z7.string().optional()
1643
+ }).optional()
1644
+ }).describe("Segundo request")
1645
+ },
1646
+ async (params) => {
1647
+ try {
1648
+ const variables = await storage.getActiveVariables();
1649
+ const executeOne = async (req) => {
1650
+ let resolvedUrl = req.url;
1651
+ if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1652
+ const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1653
+ resolvedUrl = `${baseUrl}${resolvedUrl}`;
1654
+ }
1655
+ const config = {
1656
+ method: req.method,
1657
+ url: resolvedUrl,
1658
+ headers: req.headers,
1659
+ body: req.body,
1660
+ query: req.query,
1661
+ auth: req.auth
1662
+ };
1663
+ return executeRequest(interpolateRequest(config, variables));
1664
+ };
1665
+ const [responseA, responseB] = await Promise.all([
1666
+ executeOne(params.request_a),
1667
+ executeOne(params.request_b)
1668
+ ]);
1669
+ const labelA = params.request_a.label ?? "A";
1670
+ const labelB = params.request_b.label ?? "B";
1671
+ const diffs = [];
1672
+ if (responseA.status !== responseB.status) {
1673
+ diffs.push(
1674
+ `Status: ${labelA}=${responseA.status} vs ${labelB}=${responseB.status}`
1675
+ );
1676
+ }
1677
+ const timingDiff = Math.abs(responseA.timing.total_ms - responseB.timing.total_ms);
1678
+ if (timingDiff > 100) {
1679
+ diffs.push(
1680
+ `Timing: ${labelA}=${responseA.timing.total_ms}ms vs ${labelB}=${responseB.timing.total_ms}ms (\u0394${Math.round(timingDiff)}ms)`
1681
+ );
1682
+ }
1683
+ const bodyA = JSON.stringify(responseA.body, null, 2);
1684
+ const bodyB = JSON.stringify(responseB.body, null, 2);
1685
+ if (bodyA !== bodyB) {
1686
+ diffs.push("Body: diferente");
1687
+ if (typeof responseA.body === "object" && typeof responseB.body === "object" && responseA.body && responseB.body) {
1688
+ const keysA = new Set(Object.keys(responseA.body));
1689
+ const keysB = new Set(Object.keys(responseB.body));
1690
+ const onlyInA = [...keysA].filter((k) => !keysB.has(k));
1691
+ const onlyInB = [...keysB].filter((k) => !keysA.has(k));
1692
+ const common = [...keysA].filter((k) => keysB.has(k));
1693
+ if (onlyInA.length > 0) diffs.push(` Solo en ${labelA}: ${onlyInA.join(", ")}`);
1694
+ if (onlyInB.length > 0) diffs.push(` Solo en ${labelB}: ${onlyInB.join(", ")}`);
1695
+ for (const key of common) {
1696
+ const valA = JSON.stringify(
1697
+ responseA.body[key]
1698
+ );
1699
+ const valB = JSON.stringify(
1700
+ responseB.body[key]
1701
+ );
1702
+ if (valA !== valB) {
1703
+ const shortA = valA.length > 50 ? valA.substring(0, 50) + "..." : valA;
1704
+ const shortB = valB.length > 50 ? valB.substring(0, 50) + "..." : valB;
1705
+ diffs.push(` ${key}: ${labelA}=${shortA} vs ${labelB}=${shortB}`);
1706
+ }
1707
+ }
1708
+ }
1709
+ }
1710
+ const sizeDiff = Math.abs(responseA.size_bytes - responseB.size_bytes);
1711
+ if (sizeDiff > 0) {
1712
+ diffs.push(
1713
+ `Size: ${labelA}=${responseA.size_bytes}B vs ${labelB}=${responseB.size_bytes}B`
1714
+ );
1715
+ }
1716
+ const identical = diffs.length === 0;
1717
+ const lines = [
1718
+ identical ? "\u2705 ID\xC9NTICAS" : `\u26A0\uFE0F ${diffs.length} DIFERENCIAS ENCONTRADAS`,
1719
+ "",
1720
+ `${labelA}: ${params.request_a.method} ${params.request_a.url} \u2192 ${responseA.status} (${responseA.timing.total_ms}ms)`,
1721
+ `${labelB}: ${params.request_b.method} ${params.request_b.url} \u2192 ${responseB.status} (${responseB.timing.total_ms}ms)`
1722
+ ];
1723
+ if (!identical) {
1724
+ lines.push("");
1725
+ lines.push("Diferencias:");
1726
+ for (const diff of diffs) {
1727
+ lines.push(` ${diff}`);
1728
+ }
1729
+ }
1730
+ return {
1731
+ content: [{ type: "text", text: lines.join("\n") }]
1732
+ };
1733
+ } catch (error) {
1734
+ const message = error instanceof Error ? error.message : String(error);
1735
+ return {
1736
+ content: [{ type: "text", text: `Error: ${message}` }],
1737
+ isError: true
1738
+ };
1739
+ }
1740
+ }
1741
+ );
1742
+ server.tool(
1743
+ "bulk_test",
1744
+ "Ejecuta todos los requests guardados en la colecci\xF3n y reporta resultados. Filtrable por tag.",
1745
+ {
1746
+ tag: z7.string().optional().describe("Filtrar por tag"),
1747
+ expected_status: z7.number().optional().describe("Status HTTP esperado para todos (default: cualquier 2xx)")
1748
+ },
1749
+ async (params) => {
1750
+ try {
1751
+ const collections = await storage.listCollections(params.tag);
1752
+ if (collections.length === 0) {
1753
+ return {
1754
+ content: [
1755
+ {
1756
+ type: "text",
1757
+ text: params.tag ? `No hay requests guardados con tag '${params.tag}'.` : "No hay requests guardados en la colecci\xF3n."
1758
+ }
1759
+ ]
1760
+ };
1761
+ }
1762
+ const variables = await storage.getActiveVariables();
1763
+ const results = [];
1764
+ for (const item of collections) {
1765
+ const saved = await storage.getCollection(item.name);
1766
+ if (!saved) continue;
1767
+ try {
1768
+ let config = saved.request;
1769
+ let resolvedUrl = config.url;
1770
+ if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
1771
+ const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
1772
+ resolvedUrl = `${baseUrl}${resolvedUrl}`;
1773
+ }
1774
+ config = { ...config, url: resolvedUrl };
1775
+ const interpolated = interpolateRequest(config, variables);
1776
+ const response = await executeRequest(interpolated);
1777
+ const pass = params.expected_status ? response.status === params.expected_status : response.status >= 200 && response.status < 300;
1778
+ results.push({
1779
+ name: item.name,
1780
+ method: config.method,
1781
+ url: item.url,
1782
+ status: response.status,
1783
+ timing: response.timing.total_ms,
1784
+ pass
1785
+ });
1786
+ } catch (error) {
1787
+ const message = error instanceof Error ? error.message : String(error);
1788
+ results.push({
1789
+ name: item.name,
1790
+ method: item.method,
1791
+ url: item.url,
1792
+ status: 0,
1793
+ timing: 0,
1794
+ pass: false,
1795
+ error: message
1796
+ });
1797
+ }
1798
+ }
1799
+ const passed = results.filter((r) => r.pass).length;
1800
+ const failed = results.filter((r) => !r.pass).length;
1801
+ const totalTime = Math.round(results.reduce((sum, r) => sum + r.timing, 0) * 100) / 100;
1802
+ const lines = [
1803
+ `${failed === 0 ? "\u2705" : "\u274C"} BULK TEST \u2014 ${passed}/${results.length} passed | ${totalTime}ms total`,
1804
+ ""
1805
+ ];
1806
+ for (const r of results) {
1807
+ const icon = r.pass ? "\u2705" : "\u274C";
1808
+ if (r.error) {
1809
+ lines.push(`${icon} ${r.name} \u2014 ERROR: ${r.error}`);
1810
+ } else {
1811
+ lines.push(`${icon} ${r.name} \u2014 ${r.method} ${r.url} \u2192 ${r.status} (${r.timing}ms)`);
1812
+ }
1813
+ }
1814
+ return {
1815
+ content: [{ type: "text", text: lines.join("\n") }],
1816
+ isError: failed > 0
1817
+ };
1818
+ } catch (error) {
1819
+ const message = error instanceof Error ? error.message : String(error);
1820
+ return {
1821
+ content: [{ type: "text", text: `Error: ${message}` }],
1822
+ isError: true
1823
+ };
1824
+ }
1825
+ }
1826
+ );
1827
+ }
1828
+
1829
+ // src/tools/mock.ts
1830
+ import { z as z8 } from "zod";
1831
+ function generateMockData(schema, depth = 0) {
1832
+ if (depth > 8) return null;
1833
+ if (schema.example !== void 0) return schema.example;
1834
+ if (schema.enum && schema.enum.length > 0) {
1835
+ return schema.enum[Math.floor(Math.random() * schema.enum.length)];
1836
+ }
1837
+ switch (schema.type) {
1838
+ case "object": {
1839
+ if (!schema.properties) return {};
1840
+ const obj = {};
1841
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
1842
+ obj[key] = generateMockData(propSchema, depth + 1);
1843
+ }
1844
+ return obj;
1845
+ }
1846
+ case "array": {
1847
+ if (!schema.items) return [];
1848
+ const count = Math.floor(Math.random() * 3) + 1;
1849
+ return Array.from(
1850
+ { length: count },
1851
+ () => generateMockData(schema.items, depth + 1)
1852
+ );
1853
+ }
1854
+ case "string": {
1855
+ switch (schema.format) {
1856
+ case "date-time":
1857
+ return (/* @__PURE__ */ new Date()).toISOString();
1858
+ case "date":
1859
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1860
+ case "email":
1861
+ return `user${Math.floor(Math.random() * 1e3)}@example.com`;
1862
+ case "uri":
1863
+ case "url":
1864
+ return "https://example.com/resource";
1865
+ case "uuid":
1866
+ return crypto.randomUUID();
1867
+ case "ipv4":
1868
+ return `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`;
1869
+ default: {
1870
+ const desc = (schema.description ?? "").toLowerCase();
1871
+ if (desc.includes("name") || desc.includes("nombre"))
1872
+ return `Test User ${Math.floor(Math.random() * 100)}`;
1873
+ if (desc.includes("title") || desc.includes("t\xEDtulo"))
1874
+ return `Test Title ${Math.floor(Math.random() * 100)}`;
1875
+ if (desc.includes("description") || desc.includes("descripci\xF3n"))
1876
+ return "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
1877
+ if (desc.includes("password") || desc.includes("contrase\xF1a"))
1878
+ return "TestPass123!";
1879
+ if (desc.includes("slug"))
1880
+ return `test-slug-${Math.floor(Math.random() * 1e3)}`;
1881
+ if (desc.includes("phone") || desc.includes("tel\xE9fono"))
1882
+ return "+34612345678";
1883
+ return `mock-string-${Math.floor(Math.random() * 1e4)}`;
1884
+ }
1885
+ }
1886
+ }
1887
+ case "number":
1888
+ case "integer":
1889
+ return Math.floor(Math.random() * 1e3);
1890
+ case "boolean":
1891
+ return Math.random() > 0.5;
1892
+ default:
1893
+ return null;
1894
+ }
1895
+ }
1896
+ function registerMockTool(server, storage) {
1897
+ server.tool(
1898
+ "mock",
1899
+ "Genera datos mock/fake para un endpoint bas\xE1ndose en su spec OpenAPI importada. \xDAtil para frontend sin backend.",
1900
+ {
1901
+ name: z8.string().describe("Nombre del API importada"),
1902
+ method: z8.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("M\xE9todo HTTP del endpoint"),
1903
+ path: z8.string().describe('Path del endpoint (ej: "/users", "/blog")'),
1904
+ target: z8.enum(["request", "response"]).optional().describe("Generar mock del body de request o de la response (default: response)"),
1905
+ status: z8.string().optional().describe('Status code de la respuesta a mockear (default: "200" o "201")'),
1906
+ count: z8.number().optional().describe("N\xFAmero de items mock a generar si el schema es un array (default: 3)")
1907
+ },
1908
+ async (params) => {
1909
+ try {
1910
+ const spec = await storage.getSpec(params.name);
1911
+ if (!spec) {
1912
+ return {
1913
+ content: [
1914
+ {
1915
+ type: "text",
1916
+ text: `Error: API '${params.name}' no encontrada. Usa api_import para importarla primero.`
1917
+ }
1918
+ ],
1919
+ isError: true
1920
+ };
1921
+ }
1922
+ const endpoint = spec.endpoints.find(
1923
+ (ep) => ep.method === params.method && ep.path === params.path
1924
+ );
1925
+ if (!endpoint) {
1926
+ return {
1927
+ content: [
1928
+ {
1929
+ type: "text",
1930
+ text: `Error: Endpoint ${params.method} ${params.path} no encontrado en '${params.name}'.`
1931
+ }
1932
+ ],
1933
+ isError: true
1934
+ };
1935
+ }
1936
+ const target = params.target ?? "response";
1937
+ let schema;
1938
+ if (target === "request") {
1939
+ const content = endpoint.requestBody?.content;
1940
+ if (content) {
1941
+ const jsonContent = content["application/json"];
1942
+ schema = jsonContent?.schema;
1943
+ }
1944
+ if (!schema) {
1945
+ return {
1946
+ content: [
1947
+ {
1948
+ type: "text",
1949
+ text: `Error: El endpoint ${params.method} ${params.path} no tiene un body schema definido.`
1950
+ }
1951
+ ],
1952
+ isError: true
1953
+ };
1954
+ }
1955
+ } else {
1956
+ const statusCode = params.status ?? (params.method === "POST" ? "201" : "200");
1957
+ const response = endpoint.responses?.[statusCode];
1958
+ if (!response) {
1959
+ const twoXX = Object.keys(endpoint.responses ?? {}).find((s) => s.startsWith("2"));
1960
+ if (twoXX && endpoint.responses) {
1961
+ const fallbackResp = endpoint.responses[twoXX];
1962
+ const content = fallbackResp?.content;
1963
+ if (content) {
1964
+ const jsonContent = content["application/json"];
1965
+ schema = jsonContent?.schema;
1966
+ }
1967
+ }
1968
+ } else {
1969
+ const content = response.content;
1970
+ if (content) {
1971
+ const jsonContent = content["application/json"];
1972
+ schema = jsonContent?.schema;
1973
+ }
1974
+ }
1975
+ if (!schema) {
1976
+ return {
1977
+ content: [
1978
+ {
1979
+ type: "text",
1980
+ text: `Error: No se encontr\xF3 un schema de respuesta para ${params.method} ${params.path}.`
1981
+ }
1982
+ ],
1983
+ isError: true
1984
+ };
1985
+ }
1986
+ }
1987
+ let mockData;
1988
+ if (schema.type === "array" && params.count) {
1989
+ mockData = Array.from(
1990
+ { length: params.count },
1991
+ () => generateMockData(schema.items ?? { type: "object" })
1992
+ );
1993
+ } else {
1994
+ mockData = generateMockData(schema);
1995
+ }
1996
+ const label = target === "request" ? "REQUEST BODY" : "RESPONSE";
1997
+ return {
1998
+ content: [
1999
+ {
2000
+ type: "text",
2001
+ text: [
2002
+ `Mock ${label} para ${params.method} ${params.path}:`,
2003
+ "",
2004
+ "```json",
2005
+ JSON.stringify(mockData, null, 2),
2006
+ "```",
2007
+ "",
2008
+ "Datos generados autom\xE1ticamente desde el spec OpenAPI.",
2009
+ target === "request" ? "Puedes usar estos datos directamente en un request." : "Estos son datos de ejemplo que devolver\xEDa el endpoint."
2010
+ ].join("\n")
2011
+ }
2012
+ ]
2013
+ };
2014
+ } catch (error) {
2015
+ const message = error instanceof Error ? error.message : String(error);
2016
+ return {
2017
+ content: [{ type: "text", text: `Error: ${message}` }],
2018
+ isError: true
2019
+ };
2020
+ }
2021
+ }
2022
+ );
2023
+ }
2024
+
2025
+ // src/tools/load-test.ts
2026
+ import { z as z9 } from "zod";
2027
+ function registerLoadTestTool(server, storage) {
2028
+ server.tool(
2029
+ "load_test",
2030
+ "Lanza N requests concurrentes al mismo endpoint y mide tiempos promedio, percentiles y tasa de errores.",
2031
+ {
2032
+ method: z9.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).describe("HTTP method"),
2033
+ url: z9.string().describe("URL del endpoint"),
2034
+ headers: z9.record(z9.string()).optional().describe("Headers HTTP"),
2035
+ body: z9.any().optional().describe("Body del request"),
2036
+ query: z9.record(z9.string()).optional().describe("Query parameters"),
2037
+ auth: z9.object({
2038
+ type: z9.enum(["bearer", "api-key", "basic"]),
2039
+ token: z9.string().optional(),
2040
+ key: z9.string().optional(),
2041
+ header: z9.string().optional(),
2042
+ username: z9.string().optional(),
2043
+ password: z9.string().optional()
2044
+ }).optional().describe("Autenticaci\xF3n"),
2045
+ concurrent: z9.number().describe("N\xFAmero de requests concurrentes a lanzar (max: 100)"),
2046
+ timeout: z9.number().optional().describe("Timeout por request en ms (default: 30000)")
2047
+ },
2048
+ async (params) => {
2049
+ try {
2050
+ const concurrentCount = Math.min(Math.max(params.concurrent, 1), 100);
2051
+ const variables = await storage.getActiveVariables();
2052
+ let resolvedUrl = params.url;
2053
+ if (resolvedUrl.startsWith("/") && variables.BASE_URL) {
2054
+ const baseUrl = variables.BASE_URL.replace(/\/+$/, "");
2055
+ resolvedUrl = `${baseUrl}${resolvedUrl}`;
2056
+ }
2057
+ const baseConfig = {
2058
+ method: params.method,
2059
+ url: resolvedUrl,
2060
+ headers: params.headers,
2061
+ body: params.body,
2062
+ query: params.query,
2063
+ auth: params.auth,
2064
+ timeout: params.timeout
2065
+ };
2066
+ const interpolated = interpolateRequest(baseConfig, variables);
2067
+ const startTotal = performance.now();
2068
+ const promises = Array.from(
2069
+ { length: concurrentCount },
2070
+ () => executeRequest(interpolated).then((response) => ({
2071
+ status: response.status,
2072
+ timing: response.timing.total_ms,
2073
+ error: void 0
2074
+ })).catch((error) => ({
2075
+ status: 0,
2076
+ timing: 0,
2077
+ error: error instanceof Error ? error.message : String(error)
2078
+ }))
2079
+ );
2080
+ const results = await Promise.all(promises);
2081
+ const endTotal = performance.now();
2082
+ const wallTime = Math.round((endTotal - startTotal) * 100) / 100;
2083
+ const successful = results.filter((r) => !r.error);
2084
+ const failed = results.filter((r) => r.error);
2085
+ const timings = successful.map((r) => r.timing).sort((a, b) => a - b);
2086
+ const statusCounts = {};
2087
+ for (const r of successful) {
2088
+ statusCounts[r.status] = (statusCounts[r.status] ?? 0) + 1;
2089
+ }
2090
+ const avg = timings.length > 0 ? Math.round(timings.reduce((s, t) => s + t, 0) / timings.length * 100) / 100 : 0;
2091
+ const min = timings.length > 0 ? timings[0] : 0;
2092
+ const max = timings.length > 0 ? timings[timings.length - 1] : 0;
2093
+ const p50 = timings.length > 0 ? timings[Math.floor(timings.length * 0.5)] : 0;
2094
+ const p95 = timings.length > 0 ? timings[Math.floor(timings.length * 0.95)] : 0;
2095
+ const p99 = timings.length > 0 ? timings[Math.floor(timings.length * 0.99)] : 0;
2096
+ const rps = wallTime > 0 ? Math.round(successful.length / (wallTime / 1e3) * 100) / 100 : 0;
2097
+ const lines = [
2098
+ `\u{1F4CA} LOAD TEST \u2014 ${params.method} ${params.url}`,
2099
+ "",
2100
+ `Requests: ${concurrentCount} concurrentes`,
2101
+ `Exitosos: ${successful.length} | Fallidos: ${failed.length}`,
2102
+ `Tiempo total: ${wallTime}ms`,
2103
+ `Requests/segundo: ${rps}`,
2104
+ "",
2105
+ "\u23F1\uFE0F Tiempos de respuesta:",
2106
+ ` Min: ${min}ms`,
2107
+ ` Avg: ${avg}ms`,
2108
+ ` p50: ${p50}ms`,
2109
+ ` p95: ${p95}ms`,
2110
+ ` p99: ${p99}ms`,
2111
+ ` Max: ${max}ms`
2112
+ ];
2113
+ if (Object.keys(statusCounts).length > 0) {
2114
+ lines.push("");
2115
+ lines.push("\u{1F4CB} Status codes:");
2116
+ for (const [status, count] of Object.entries(statusCounts)) {
2117
+ const pct = Math.round(count / concurrentCount * 100);
2118
+ lines.push(` ${status}: ${count} (${pct}%)`);
2119
+ }
2120
+ }
2121
+ if (failed.length > 0) {
2122
+ lines.push("");
2123
+ lines.push("\u274C Errores:");
2124
+ const errorCounts = {};
2125
+ for (const r of failed) {
2126
+ const errMsg = r.error ?? "Unknown";
2127
+ errorCounts[errMsg] = (errorCounts[errMsg] ?? 0) + 1;
2128
+ }
2129
+ for (const [err, count] of Object.entries(errorCounts)) {
2130
+ lines.push(` ${err}: ${count}x`);
2131
+ }
2132
+ }
2133
+ return {
2134
+ content: [{ type: "text", text: lines.join("\n") }],
2135
+ isError: failed.length > successful.length
2136
+ // More than 50% failed
2137
+ };
2138
+ } catch (error) {
2139
+ const message = error instanceof Error ? error.message : String(error);
2140
+ return {
2141
+ content: [{ type: "text", text: `Error: ${message}` }],
2142
+ isError: true
2143
+ };
2144
+ }
2145
+ }
2146
+ );
2147
+ }
2148
+
724
2149
  // src/server.ts
725
- var VERSION = "0.1.0";
2150
+ var VERSION = "0.4.0";
726
2151
  function createServer(storageDir) {
727
2152
  const server = new McpServer({
728
2153
  name: "api-testing-mcp",
@@ -732,6 +2157,12 @@ function createServer(storageDir) {
732
2157
  registerRequestTool(server, storage);
733
2158
  registerCollectionTools(server, storage);
734
2159
  registerEnvironmentTools(server, storage);
2160
+ registerApiSpecTools(server, storage);
2161
+ registerAssertTool(server, storage);
2162
+ registerFlowTool(server, storage);
2163
+ registerUtilityTools(server, storage);
2164
+ registerMockTool(server, storage);
2165
+ registerLoadTestTool(server, storage);
735
2166
  return server;
736
2167
  }
737
2168