@cocaxcode/api-testing-mcp 0.8.3 → 0.10.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
@@ -1860,6 +1860,8 @@ function registerFlowTool(server, storage) {
1860
1860
 
1861
1861
  // src/tools/utilities.ts
1862
1862
  import { z as z8 } from "zod";
1863
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1864
+ import { join as join2 } from "path";
1863
1865
  function registerUtilityTools(server, storage) {
1864
1866
  server.tool(
1865
1867
  "export_curl",
@@ -2139,6 +2141,511 @@ ${curlCommand}`
2139
2141
  }
2140
2142
  }
2141
2143
  );
2144
+ server.tool(
2145
+ "export_postman_collection",
2146
+ "Exporta los requests guardados como una Postman Collection v2.1 (JSON). Escribe el archivo en disco, importable directamente en Postman.",
2147
+ {
2148
+ name: z8.string().optional().describe('Nombre de la colecci\xF3n (default: "API Testing Collection")'),
2149
+ tag: z8.string().optional().describe("Filtrar requests por tag"),
2150
+ output_dir: z8.string().optional().describe("Directorio donde guardar el archivo (default: ./postman/)"),
2151
+ resolve_variables: z8.boolean().optional().describe("Resolver {{variables}} del entorno activo (default: false)")
2152
+ },
2153
+ async (params) => {
2154
+ try {
2155
+ const collections = await storage.listCollections(params.tag);
2156
+ if (collections.length === 0) {
2157
+ return {
2158
+ content: [
2159
+ {
2160
+ type: "text",
2161
+ text: params.tag ? `No hay requests guardados con tag '${params.tag}'.` : "No hay requests guardados en la colecci\xF3n."
2162
+ }
2163
+ ]
2164
+ };
2165
+ }
2166
+ const resolveVars = params.resolve_variables ?? false;
2167
+ const variables = resolveVars ? await storage.getActiveVariables() : {};
2168
+ const savedRequests = [];
2169
+ for (const item of collections) {
2170
+ const saved = await storage.getCollection(item.name);
2171
+ if (saved) savedRequests.push(saved);
2172
+ }
2173
+ const tagged = /* @__PURE__ */ new Map();
2174
+ const untagged = [];
2175
+ for (const saved of savedRequests) {
2176
+ if (saved.tags && saved.tags.length > 0) {
2177
+ const tag = saved.tags[0];
2178
+ if (!tagged.has(tag)) tagged.set(tag, []);
2179
+ tagged.get(tag).push(saved);
2180
+ } else {
2181
+ untagged.push(saved);
2182
+ }
2183
+ }
2184
+ const items = [];
2185
+ for (const [tag, requests] of tagged) {
2186
+ items.push({
2187
+ name: tag,
2188
+ item: requests.map((r) => buildPostmanItem(r, variables, resolveVars))
2189
+ });
2190
+ }
2191
+ for (const saved of untagged) {
2192
+ items.push(buildPostmanItem(saved, variables, resolveVars));
2193
+ }
2194
+ const activeVars = await storage.getActiveVariables();
2195
+ const collectionVars = Object.entries(activeVars).map(([key, value]) => ({
2196
+ key,
2197
+ value,
2198
+ type: "string"
2199
+ }));
2200
+ const collectionName = params.name ?? "API Testing Collection";
2201
+ const postmanCollection = {
2202
+ info: {
2203
+ name: collectionName,
2204
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
2205
+ },
2206
+ item: items,
2207
+ variable: collectionVars
2208
+ };
2209
+ const json = JSON.stringify(postmanCollection, null, 2);
2210
+ const outputDir = params.output_dir ?? join2(process.cwd(), "postman");
2211
+ await mkdir2(outputDir, { recursive: true });
2212
+ const fileName = collectionName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + ".postman_collection.json";
2213
+ const filePath = join2(outputDir, fileName);
2214
+ await writeFile2(filePath, json, "utf-8");
2215
+ return {
2216
+ content: [
2217
+ {
2218
+ type: "text",
2219
+ text: `Postman Collection v2.1 exportada (${savedRequests.length} requests).
2220
+
2221
+ Archivo: ${filePath}
2222
+
2223
+ Importa este archivo en Postman: File \u2192 Import \u2192 selecciona el archivo.`
2224
+ }
2225
+ ]
2226
+ };
2227
+ } catch (error) {
2228
+ const message = error instanceof Error ? error.message : String(error);
2229
+ return {
2230
+ content: [{ type: "text", text: `Error: ${message}` }],
2231
+ isError: true
2232
+ };
2233
+ }
2234
+ }
2235
+ );
2236
+ server.tool(
2237
+ "import_postman_collection",
2238
+ "Importa una Postman Collection v2.1 (JSON) como requests guardados en la colecci\xF3n. Soporta folders, auth, headers, body y query params.",
2239
+ {
2240
+ file: z8.string().describe("Ruta al archivo .postman_collection.json exportado de Postman"),
2241
+ tag: z8.string().optional().describe("Tag adicional para aplicar a todos los requests importados"),
2242
+ overwrite: z8.boolean().optional().describe("Sobreescribir requests existentes con el mismo nombre (default: false)")
2243
+ },
2244
+ async (params) => {
2245
+ try {
2246
+ const raw = await readFile3(params.file, "utf-8");
2247
+ const collection = JSON.parse(raw);
2248
+ if (!collection.item || !Array.isArray(collection.item)) {
2249
+ return {
2250
+ content: [
2251
+ {
2252
+ type: "text",
2253
+ text: 'Error: El archivo no parece ser una Postman Collection v\xE1lida. Falta la propiedad "item".'
2254
+ }
2255
+ ],
2256
+ isError: true
2257
+ };
2258
+ }
2259
+ const overwrite = params.overwrite ?? false;
2260
+ const extraTag = params.tag;
2261
+ const flatItems = flattenPostmanItems(collection.item, collection.auth);
2262
+ let imported = 0;
2263
+ let skipped = 0;
2264
+ const errors = [];
2265
+ for (const item of flatItems) {
2266
+ try {
2267
+ const saved = parsePostmanItem(item, extraTag);
2268
+ if (!saved) continue;
2269
+ if (!overwrite) {
2270
+ const existing = await storage.getCollection(saved.name);
2271
+ if (existing) {
2272
+ skipped++;
2273
+ continue;
2274
+ }
2275
+ }
2276
+ await storage.saveCollection(saved);
2277
+ imported++;
2278
+ } catch (err) {
2279
+ const msg = err instanceof Error ? err.message : String(err);
2280
+ errors.push(`${item.name ?? "unknown"}: ${msg}`);
2281
+ }
2282
+ }
2283
+ const lines = [
2284
+ `Postman Collection importada: ${imported} requests guardados.`
2285
+ ];
2286
+ if (skipped > 0) lines.push(`${skipped} requests omitidos (ya exist\xEDan, usa overwrite: true para sobreescribir).`);
2287
+ if (errors.length > 0) {
2288
+ lines.push(`${errors.length} errores:`);
2289
+ for (const e of errors) lines.push(` - ${e}`);
2290
+ }
2291
+ if (collection.info?.name) lines.push(`
2292
+ Colecci\xF3n: ${collection.info.name}`);
2293
+ return {
2294
+ content: [{ type: "text", text: lines.join("\n") }]
2295
+ };
2296
+ } catch (error) {
2297
+ const message = error instanceof Error ? error.message : String(error);
2298
+ return {
2299
+ content: [{ type: "text", text: `Error: ${message}` }],
2300
+ isError: true
2301
+ };
2302
+ }
2303
+ }
2304
+ );
2305
+ server.tool(
2306
+ "import_postman_environment",
2307
+ "Importa un Postman Environment (JSON) como entorno local. Soporta variables con valores initial/current.",
2308
+ {
2309
+ file: z8.string().describe("Ruta al archivo .postman_environment.json exportado de Postman"),
2310
+ name: z8.string().optional().describe("Nombre para el entorno (default: usa el nombre del archivo Postman)"),
2311
+ overwrite: z8.boolean().optional().describe("Sobreescribir si ya existe un entorno con el mismo nombre (default: false)"),
2312
+ activate: z8.boolean().optional().describe("Activar el entorno importado como entorno activo (default: false)")
2313
+ },
2314
+ async (params) => {
2315
+ try {
2316
+ const raw = await readFile3(params.file, "utf-8");
2317
+ const postmanEnv = JSON.parse(raw);
2318
+ if (!postmanEnv.values || !Array.isArray(postmanEnv.values)) {
2319
+ return {
2320
+ content: [
2321
+ {
2322
+ type: "text",
2323
+ text: 'Error: El archivo no parece ser un Postman Environment v\xE1lido. Falta la propiedad "values".'
2324
+ }
2325
+ ],
2326
+ isError: true
2327
+ };
2328
+ }
2329
+ const envName = params.name ?? postmanEnv.name ?? "postman-import";
2330
+ const overwrite = params.overwrite ?? false;
2331
+ const existing = await storage.getEnvironment(envName);
2332
+ if (existing && !overwrite) {
2333
+ return {
2334
+ content: [
2335
+ {
2336
+ type: "text",
2337
+ text: `Error: Ya existe un entorno '${envName}'. Usa overwrite: true para sobreescribir.`
2338
+ }
2339
+ ],
2340
+ isError: true
2341
+ };
2342
+ }
2343
+ const variables = {};
2344
+ for (const v of postmanEnv.values) {
2345
+ if (!v.key) continue;
2346
+ if (v.enabled === false) continue;
2347
+ variables[v.key] = String(v.currentValue ?? v.value ?? "");
2348
+ }
2349
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2350
+ await storage.createEnvironment({
2351
+ name: envName,
2352
+ variables,
2353
+ createdAt: now,
2354
+ updatedAt: now
2355
+ });
2356
+ if (params.activate) {
2357
+ await storage.setActiveEnvironment(envName);
2358
+ }
2359
+ const lines = [
2360
+ `Postman Environment "${envName}" importado (${Object.keys(variables).length} variables).`
2361
+ ];
2362
+ if (params.activate) lines.push("Entorno activado como activo.");
2363
+ return {
2364
+ content: [{ type: "text", text: lines.join("\n") }]
2365
+ };
2366
+ } catch (error) {
2367
+ const message = error instanceof Error ? error.message : String(error);
2368
+ return {
2369
+ content: [{ type: "text", text: `Error: ${message}` }],
2370
+ isError: true
2371
+ };
2372
+ }
2373
+ }
2374
+ );
2375
+ server.tool(
2376
+ "export_postman_environment",
2377
+ "Exporta un entorno como Postman Environment (JSON). Escribe el archivo en disco, importable directamente en Postman.",
2378
+ {
2379
+ name: z8.string().optional().describe("Nombre del entorno a exportar (default: entorno activo)"),
2380
+ output_dir: z8.string().optional().describe("Directorio donde guardar el archivo (default: ./postman/)")
2381
+ },
2382
+ async (params) => {
2383
+ try {
2384
+ let envName = params.name;
2385
+ if (!envName) {
2386
+ envName = await storage.getActiveEnvironment() ?? void 0;
2387
+ if (!envName) {
2388
+ return {
2389
+ content: [
2390
+ {
2391
+ type: "text",
2392
+ text: 'No hay entorno activo. Especifica un nombre con el par\xE1metro "name".'
2393
+ }
2394
+ ],
2395
+ isError: true
2396
+ };
2397
+ }
2398
+ }
2399
+ const env = await storage.getEnvironment(envName);
2400
+ if (!env) {
2401
+ return {
2402
+ content: [
2403
+ {
2404
+ type: "text",
2405
+ text: `Entorno '${envName}' no encontrado.`
2406
+ }
2407
+ ],
2408
+ isError: true
2409
+ };
2410
+ }
2411
+ const postmanEnv = {
2412
+ name: env.name,
2413
+ values: Object.entries(env.variables).map(([key, value]) => ({
2414
+ key,
2415
+ value,
2416
+ type: "default",
2417
+ enabled: true
2418
+ })),
2419
+ _postman_variable_scope: "environment"
2420
+ };
2421
+ const json = JSON.stringify(postmanEnv, null, 2);
2422
+ const outputDir = params.output_dir ?? join2(process.cwd(), "postman");
2423
+ await mkdir2(outputDir, { recursive: true });
2424
+ const fileName = env.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + ".postman_environment.json";
2425
+ const filePath = join2(outputDir, fileName);
2426
+ await writeFile2(filePath, json, "utf-8");
2427
+ return {
2428
+ content: [
2429
+ {
2430
+ type: "text",
2431
+ text: `Postman Environment "${env.name}" exportado (${Object.keys(env.variables).length} variables).
2432
+
2433
+ Archivo: ${filePath}
2434
+
2435
+ Importa este archivo en Postman: File \u2192 Import \u2192 selecciona el archivo.`
2436
+ }
2437
+ ]
2438
+ };
2439
+ } catch (error) {
2440
+ const message = error instanceof Error ? error.message : String(error);
2441
+ return {
2442
+ content: [{ type: "text", text: `Error: ${message}` }],
2443
+ isError: true
2444
+ };
2445
+ }
2446
+ }
2447
+ );
2448
+ }
2449
+ function buildPostmanItem(saved, variables, resolveVars) {
2450
+ let config = saved.request;
2451
+ if (resolveVars) {
2452
+ const resolvedUrl = resolveUrl(config.url, variables);
2453
+ config = { ...config, url: resolvedUrl };
2454
+ config = interpolateRequest(config, variables);
2455
+ }
2456
+ const item = {
2457
+ name: saved.name,
2458
+ request: buildPostmanRequest(config)
2459
+ };
2460
+ return item;
2461
+ }
2462
+ function buildPostmanRequest(config) {
2463
+ const request = {
2464
+ method: config.method,
2465
+ header: buildPostmanHeaders(config.headers),
2466
+ url: buildPostmanUrl(config.url, config.query)
2467
+ };
2468
+ if (config.body !== void 0 && config.body !== null) {
2469
+ request.body = {
2470
+ mode: "raw",
2471
+ raw: typeof config.body === "string" ? config.body : JSON.stringify(config.body, null, 2),
2472
+ options: { raw: { language: "json" } }
2473
+ };
2474
+ const headers = request.header;
2475
+ if (!headers.some((h) => h.key.toLowerCase() === "content-type")) {
2476
+ headers.push({ key: "Content-Type", value: "application/json" });
2477
+ }
2478
+ }
2479
+ if (config.auth) {
2480
+ request.auth = buildPostmanAuth(config.auth);
2481
+ }
2482
+ return request;
2483
+ }
2484
+ function buildPostmanHeaders(headers) {
2485
+ if (!headers) return [];
2486
+ return Object.entries(headers).map(([key, value]) => ({ key, value }));
2487
+ }
2488
+ function buildPostmanUrl(rawUrl, query) {
2489
+ const url = { raw: rawUrl };
2490
+ const match = rawUrl.match(/^(https?):\/\/([^/]+)(\/.*)?$/);
2491
+ if (match) {
2492
+ url.protocol = match[1];
2493
+ url.host = match[2].split(".");
2494
+ url.path = match[3] ? match[3].slice(1).split("/") : [];
2495
+ }
2496
+ if (query && Object.keys(query).length > 0) {
2497
+ url.query = Object.entries(query).map(([key, value]) => ({ key, value }));
2498
+ const queryStr = Object.entries(query).map(([k, v]) => `${k}=${v}`).join("&");
2499
+ url.raw = rawUrl + (rawUrl.includes("?") ? "&" : "?") + queryStr;
2500
+ }
2501
+ return url;
2502
+ }
2503
+ function buildPostmanAuth(auth) {
2504
+ switch (auth.type) {
2505
+ case "bearer":
2506
+ return {
2507
+ type: "bearer",
2508
+ bearer: [{ key: "token", value: auth.token ?? "", type: "string" }]
2509
+ };
2510
+ case "api-key":
2511
+ return {
2512
+ type: "apikey",
2513
+ apikey: [
2514
+ { key: "key", value: auth.key ?? "", type: "string" },
2515
+ { key: "value", value: auth.header ?? "X-API-Key", type: "string" },
2516
+ { key: "in", value: "header", type: "string" }
2517
+ ]
2518
+ };
2519
+ case "basic":
2520
+ return {
2521
+ type: "basic",
2522
+ basic: [
2523
+ { key: "username", value: auth.username ?? "", type: "string" },
2524
+ { key: "password", value: auth.password ?? "", type: "string" }
2525
+ ]
2526
+ };
2527
+ }
2528
+ }
2529
+ function flattenPostmanItems(items, parentAuth, parentTags = []) {
2530
+ const result = [];
2531
+ for (const item of items) {
2532
+ if (item.item && Array.isArray(item.item)) {
2533
+ const folderTags = item.name ? [...parentTags, item.name] : parentTags;
2534
+ const folderAuth = item.auth ?? parentAuth;
2535
+ result.push(...flattenPostmanItems(item.item, folderAuth, folderTags));
2536
+ } else if (item.request) {
2537
+ result.push({
2538
+ ...item,
2539
+ _folderTags: parentTags,
2540
+ _inheritedAuth: item.request?.auth ? void 0 : parentAuth
2541
+ });
2542
+ }
2543
+ }
2544
+ return result;
2545
+ }
2546
+ var VALID_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2547
+ function parsePostmanItem(item, extraTag) {
2548
+ const req = item.request;
2549
+ if (!req) return null;
2550
+ const method = typeof req.method === "string" ? req.method.toUpperCase() : "GET";
2551
+ if (!VALID_METHODS2.has(method)) return null;
2552
+ const url = parsePostmanUrl(req.url);
2553
+ if (!url) return null;
2554
+ const headers = {};
2555
+ if (Array.isArray(req.header)) {
2556
+ for (const h of req.header) {
2557
+ if (h.disabled) continue;
2558
+ if (h.key && h.value !== void 0) {
2559
+ headers[h.key] = String(h.value);
2560
+ }
2561
+ }
2562
+ }
2563
+ const query = {};
2564
+ if (req.url && typeof req.url === "object" && Array.isArray(req.url.query)) {
2565
+ for (const q of req.url.query) {
2566
+ if (q.disabled) continue;
2567
+ if (q.key) {
2568
+ query[q.key] = String(q.value ?? "");
2569
+ }
2570
+ }
2571
+ }
2572
+ let body = void 0;
2573
+ if (req.body) {
2574
+ if (req.body.mode === "raw" && typeof req.body.raw === "string") {
2575
+ try {
2576
+ body = JSON.parse(req.body.raw);
2577
+ } catch {
2578
+ body = req.body.raw;
2579
+ }
2580
+ } else if (req.body.mode === "urlencoded" && Array.isArray(req.body.urlencoded)) {
2581
+ const formData = {};
2582
+ for (const p of req.body.urlencoded) {
2583
+ if (p.key) formData[p.key] = String(p.value ?? "");
2584
+ }
2585
+ body = formData;
2586
+ } else if (req.body.mode === "formdata" && Array.isArray(req.body.formdata)) {
2587
+ const formData = {};
2588
+ for (const p of req.body.formdata) {
2589
+ if (p.key && p.type !== "file") formData[p.key] = String(p.value ?? "");
2590
+ }
2591
+ body = formData;
2592
+ }
2593
+ }
2594
+ const authSource = req.auth ?? item._inheritedAuth;
2595
+ const auth = parsePostmanAuth(authSource);
2596
+ const tags = [...item._folderTags ?? []];
2597
+ if (extraTag && !tags.includes(extraTag)) tags.push(extraTag);
2598
+ const name = item.name || `${method} ${url}`;
2599
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2600
+ const config = { method, url };
2601
+ if (Object.keys(headers).length > 0) config.headers = headers;
2602
+ if (Object.keys(query).length > 0) config.query = query;
2603
+ if (body !== void 0) config.body = body;
2604
+ if (auth) config.auth = auth;
2605
+ return {
2606
+ name,
2607
+ request: config,
2608
+ tags: tags.length > 0 ? tags : void 0,
2609
+ createdAt: now,
2610
+ updatedAt: now
2611
+ };
2612
+ }
2613
+ function parsePostmanUrl(url) {
2614
+ if (typeof url === "string") return url || null;
2615
+ if (url && typeof url === "object") {
2616
+ if (typeof url.raw === "string") return url.raw || null;
2617
+ const protocol = url.protocol ?? "https";
2618
+ const host = Array.isArray(url.host) ? url.host.join(".") : url.host;
2619
+ const path = Array.isArray(url.path) ? url.path.join("/") : url.path ?? "";
2620
+ if (host) return `${protocol}://${host}${path ? "/" + path : ""}`;
2621
+ }
2622
+ return null;
2623
+ }
2624
+ function parsePostmanAuth(auth) {
2625
+ if (!auth || !auth.type) return void 0;
2626
+ const getVal = (arr, key) => {
2627
+ if (!Array.isArray(arr)) return void 0;
2628
+ const item = arr.find((a) => a.key === key);
2629
+ return item?.value ? String(item.value) : void 0;
2630
+ };
2631
+ switch (auth.type) {
2632
+ case "bearer": {
2633
+ const token = getVal(auth.bearer, "token");
2634
+ return token ? { type: "bearer", token } : void 0;
2635
+ }
2636
+ case "apikey": {
2637
+ const key = getVal(auth.apikey, "key");
2638
+ const headerName = getVal(auth.apikey, "value");
2639
+ return key ? { type: "api-key", key, header: headerName } : void 0;
2640
+ }
2641
+ case "basic": {
2642
+ const username = getVal(auth.basic, "username");
2643
+ const password = getVal(auth.basic, "password");
2644
+ return username ? { type: "basic", username, password } : void 0;
2645
+ }
2646
+ default:
2647
+ return void 0;
2648
+ }
2142
2649
  }
2143
2650
 
2144
2651
  // src/tools/mock.ts
@@ -2450,7 +2957,7 @@ function registerLoadTestTool(server, storage) {
2450
2957
  }
2451
2958
 
2452
2959
  // src/server.ts
2453
- var VERSION = "0.8.0";
2960
+ var VERSION = "0.10.0";
2454
2961
  function createServer(storageDir) {
2455
2962
  const server = new McpServer({
2456
2963
  name: "api-testing-mcp",