@cocaxcode/api-testing-mcp 0.9.0 → 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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <h1 align="center">@cocaxcode/api-testing-mcp</h1>
3
3
  <p align="center">
4
4
  <strong>The most complete API testing MCP server available.</strong><br/>
5
- 29 tools &middot; Zero config &middot; Zero dependencies &middot; Everything runs inside your AI conversation.
5
+ 31 tools &middot; Zero config &middot; Zero dependencies &middot; Everything runs inside your AI conversation.
6
6
  </p>
7
7
  </p>
8
8
 
@@ -11,8 +11,8 @@
11
11
  <a href="https://www.npmjs.com/package/@cocaxcode/api-testing-mcp"><img src="https://img.shields.io/npm/dm/@cocaxcode/api-testing-mcp.svg?style=flat-square" alt="npm downloads" /></a>
12
12
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="License" /></a>
13
13
  <img src="https://img.shields.io/badge/node-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node" />
14
- <img src="https://img.shields.io/badge/tools-29-blueviolet?style=flat-square" alt="29 tools" />
15
- <img src="https://img.shields.io/badge/tests-96-brightgreen?style=flat-square" alt="96 tests" />
14
+ <img src="https://img.shields.io/badge/tools-31-blueviolet?style=flat-square" alt="29 tools" />
15
+ <img src="https://img.shields.io/badge/tests-110-brightgreen?style=flat-square" alt="96 tests" />
16
16
  </p>
17
17
 
18
18
  <p align="center">
@@ -29,7 +29,7 @@
29
29
 
30
30
  ## What is this?
31
31
 
32
- An [MCP server](https://modelcontextprotocol.io) that gives your AI assistant the ability to **test, validate, mock, chain, diff, load-test, export to Postman, and manage** any API — all from natural language.
32
+ An [MCP server](https://modelcontextprotocol.io) that gives your AI assistant the ability to **test, validate, mock, chain, diff, load-test, import/export Postman collections, and manage** any API — all from natural language.
33
33
 
34
34
  You describe what you need. The AI figures out the rest.
35
35
 
@@ -47,17 +47,17 @@ There are other API testing MCP servers out there. Here's why this one is differ
47
47
 
48
48
  | Capability | @cocaxcode/api-testing-mcp | Others |
49
49
  |---|:---:|:---:|
50
- | HTTP requests with auth | 29 tools | 1-11 tools |
50
+ | HTTP requests with auth | 31 tools | 1-11 tools |
51
51
  | Assertions (eq, neq, gt, lt, contains, exists, type...) | 10 operators | Status code only or none |
52
52
  | Request flows with variable extraction | `flow_run` with `extract` | Not available |
53
- | Collections with tags and CRUD | Full CRUD + tag filtering | Import from Postman or none |
53
+ | Collections with tags and CRUD | Full CRUD + tag filtering | Basic or none |
54
54
  | Environments with variable interpolation | CRUD + project-scoped | Manual `set_env_vars` or none |
55
55
  | OpenAPI import with `$ref`, `allOf`, `oneOf`, `anyOf` | ~95% real-world coverage | Basic or none |
56
56
  | Mock data generation from schemas | Types, formats, enums | Not available |
57
57
  | Load testing with percentiles | p50/p95/p99 + req/s | Basic or none |
58
58
  | Response diffing | Field-by-field comparison | Not available |
59
59
  | Bulk testing by tag | Collection-wide pass/fail | Not available |
60
- | **Postman export (collection + environment)** | **Files ready to import** | **Not available** |
60
+ | **Postman import + export** | **Bidirectional: import from & export to Postman** | **Not available** |
61
61
  | cURL export | With resolved variables | Not available |
62
62
  | Project-scoped environments | Per-directory context | Not available |
63
63
  | External dependencies | **Zero** — just Node.js | Playwright, Jest, pytest... |
@@ -183,6 +183,8 @@ You don't need to memorize tool names, parameters, or JSON structures — just t
183
183
  | *"How fast is the health endpoint under load?"* | Fires 50 concurrent requests, reports p50/p95/p99 latencies |
184
184
  | *"Run all my saved smoke tests"* | Executes every request tagged `smoke`, reports pass/fail |
185
185
  | *"Export the create-user request as curl"* | Builds a ready-to-paste cURL command with resolved variables |
186
+ | *"Import my Postman collection from exported.json"* | Reads a Postman Collection v2.1, converts all requests into saved collection items |
187
+ | *"Import the Postman environment from prod.postman_environment.json"* | Imports variables from a Postman Environment file |
186
188
  | *"Export my collection to Postman"* | Writes a `.postman_collection.json` file ready to import |
187
189
  | *"Export the dev environment for Postman"* | Writes a `.postman_environment.json` file ready to import |
188
190
  | *"Compare the users endpoint between dev and prod"* | Hits both URLs, diffs status codes, body, and timing |
@@ -399,9 +401,33 @@ BULK TEST — 8/8 passed | 1.2s total
399
401
  login — POST /auth/login → 200 (156ms)
400
402
  ```
401
403
 
402
- ### Postman Export
404
+ ### Postman Import & Export
403
405
 
404
- Export your saved requests and environments as Postman-compatible JSON files. The files are written to a `postman/` folder in your project, ready to import in Postman.
406
+ **Bidirectional Postman support.** Import existing Postman collections and environments, or export yours for use in Postman. Migrate seamlessly between Postman and your AI workflow.
407
+
408
+ #### Import from Postman
409
+
410
+ ```
411
+ "Import my Postman collection from ./exported.postman_collection.json"
412
+ "Import the collection and tag everything as legacy"
413
+ "Import the Postman environment from ./prod.postman_environment.json and activate it"
414
+ ```
415
+
416
+ **Collection import features:**
417
+ - Postman Collection **v2.1** format (the default Postman export)
418
+ - **Folders become tags** — a request inside `Users > Admin` gets tags `["Users", "Admin"]`
419
+ - Auth inherited from folders/collection level (Bearer, API Key, Basic)
420
+ - Body parsing: raw JSON, x-www-form-urlencoded, form-data
421
+ - Query params, headers, and disabled items handled correctly
422
+ - `overwrite` option to update existing requests
423
+
424
+ **Environment import features:**
425
+ - Prefers `currentValue` over `value` (matches Postman runtime behavior)
426
+ - Skips disabled variables
427
+ - Optional `activate` flag to make it the active environment immediately
428
+ - Custom name override
429
+
430
+ #### Export to Postman
405
431
 
406
432
  ```
407
433
  "Export my collection to Postman"
@@ -418,31 +444,30 @@ your-project/
418
444
  └── dev.postman_environment.json ← Import in Postman: File → Import
419
445
  ```
420
446
 
421
- **Collection features:**
447
+ **Collection export features:**
422
448
  - Requests grouped in **folders by tag** (smoke, auth, users, etc.)
423
449
  - Auth (Bearer, API Key, Basic) mapped to Postman's native auth format
424
450
  - `{{variables}}` preserved as-is (Postman uses the same syntax)
425
451
  - Headers, query params, and JSON body included
426
452
  - Collection variables from your active environment
427
453
 
428
- **Environment features:**
454
+ **Environment export features:**
429
455
  - All variables exported with `enabled: true`
430
456
  - Postman-compatible format (`_postman_variable_scope: "environment"`)
431
457
  - Works with any environment (active or by name)
432
458
 
433
459
  <details>
434
- <summary>Example: exporting with a specific tag</summary>
460
+ <summary>Example: round-trip workflow</summary>
435
461
 
436
462
  ```
437
- You: "Export my smoke tests to Postman as 'Smoke Tests'"
438
- ```
463
+ You: "Import the Postman collection from ./legacy-api.postman_collection.json with tag migrated"
464
+ → 47 requests imported with tag "migrated"
439
465
 
440
- This creates `postman/smoke-tests.postman_collection.json` with only the requests tagged `smoke`, grouped in folders.
466
+ You: "Run all migrated requests"
467
+ → Bulk test: 45/47 passed
441
468
 
442
- You can also specify a custom output directory:
443
-
444
- ```
445
- You: "Export my collection to Postman in the exports folder"
469
+ You: "Fix the failing ones and export back to Postman"
470
+ → Updated collection exported to postman/legacy-api.postman_collection.json
446
471
  ```
447
472
 
448
473
  </details>
@@ -494,7 +519,7 @@ Resolution order: project-specific environment → global active environment.
494
519
 
495
520
  ## Tool Reference
496
521
 
497
- 29 tools organized in 8 categories:
522
+ 31 tools organized in 8 categories:
498
523
 
499
524
  | Category | Tools | Count |
500
525
  |----------|-------|-------|
@@ -505,7 +530,7 @@ Resolution order: project-specific environment → global active environment.
505
530
  | **Environments** | `env_create` `env_list` `env_set` `env_get` `env_switch` `env_rename` `env_delete` `env_spec` `env_project_clear` `env_project_list` | 10 |
506
531
  | **API Specs** | `api_import` `api_spec_list` `api_endpoints` `api_endpoint_detail` | 4 |
507
532
  | **Mock** | `mock` | 1 |
508
- | **Utilities** | `load_test` `export_curl` `diff_responses` `bulk_test` `export_postman_collection` `export_postman_environment` | 6 |
533
+ | **Utilities** | `load_test` `export_curl` `diff_responses` `bulk_test` `import_postman_collection` `import_postman_environment` `export_postman_collection` `export_postman_environment` | 8 |
509
534
 
510
535
  You don't need to call these tools directly. Just describe what you want and the AI picks the right one.
511
536
 
@@ -541,14 +566,14 @@ Override the default directory in your MCP config:
541
566
  Built for reliability and testability:
542
567
 
543
568
  - **Zero runtime dependencies** — only `@modelcontextprotocol/sdk` and `zod`
544
- - **96 integration tests** with mocked fetch (no network calls in CI)
569
+ - **110 integration tests** with mocked fetch (no network calls in CI)
545
570
  - **Factory pattern** — `createServer(storageDir?)` for isolated test instances
546
571
  - **Strict TypeScript** — zero `any`, full type coverage
547
572
  - **< 95KB** bundled output via tsup
548
573
 
549
574
  ```
550
575
  src/
551
- ├── tools/ # 29 MCP tool handlers (one file per category)
576
+ ├── tools/ # 31 MCP tool handlers (one file per category)
552
577
  ├── lib/ # Business logic (no MCP dependency)
553
578
  │ ├── http-client # fetch wrapper with timing
554
579
  │ ├── storage # JSON file storage engine
@@ -557,7 +582,7 @@ src/
557
582
  │ ├── path # Dot-notation accessor (body.data.0.id)
558
583
  │ ├── interpolation # {{variable}} resolver
559
584
  │ └── openapi-parser # $ref + allOf/oneOf/anyOf resolution
560
- └── __tests__/ # 10 test suites, 96 tests
585
+ └── __tests__/ # 10 test suites, 110 tests
561
586
  ```
562
587
 
563
588
  ---
@@ -578,7 +603,7 @@ src/
578
603
  git clone https://github.com/cocaxcode/api-testing-mcp.git
579
604
  cd api-testing-mcp
580
605
  npm install
581
- npm test # 96 tests across 10 suites
606
+ npm test # 110 tests across 10 suites
582
607
  npm run build # ESM bundle via tsup
583
608
  npm run typecheck # Strict TypeScript
584
609
  ```
package/dist/index.js CHANGED
@@ -1860,7 +1860,7 @@ 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, writeFile as writeFile2 } from "fs/promises";
1863
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1864
1864
  import { join as join2 } from "path";
1865
1865
  function registerUtilityTools(server, storage) {
1866
1866
  server.tool(
@@ -2233,6 +2233,145 @@ Importa este archivo en Postman: File \u2192 Import \u2192 selecciona el archivo
2233
2233
  }
2234
2234
  }
2235
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
+ );
2236
2375
  server.tool(
2237
2376
  "export_postman_environment",
2238
2377
  "Exporta un entorno como Postman Environment (JSON). Escribe el archivo en disco, importable directamente en Postman.",
@@ -2387,6 +2526,127 @@ function buildPostmanAuth(auth) {
2387
2526
  };
2388
2527
  }
2389
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
+ }
2649
+ }
2390
2650
 
2391
2651
  // src/tools/mock.ts
2392
2652
  import { z as z9 } from "zod";
@@ -2697,7 +2957,7 @@ function registerLoadTestTool(server, storage) {
2697
2957
  }
2698
2958
 
2699
2959
  // src/server.ts
2700
- var VERSION = "0.8.0";
2960
+ var VERSION = "0.10.0";
2701
2961
  function createServer(storageDir) {
2702
2962
  const server = new McpServer({
2703
2963
  name: "api-testing-mcp",