@cocaxcode/api-testing-mcp 0.8.2 → 0.9.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
- 27 tools &middot; Zero config &middot; Zero dependencies &middot; Everything runs inside your AI conversation.
5
+ 29 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-27-blueviolet?style=flat-square" alt="27 tools" />
15
- <img src="https://img.shields.io/badge/tests-83-brightgreen?style=flat-square" alt="83 tests" />
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" />
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, 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, export to Postman, 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,7 +47,7 @@ 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 | 27 tools | 1-11 tools |
50
+ | HTTP requests with auth | 29 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
53
  | Collections with tags and CRUD | Full CRUD + tag filtering | Import from Postman or none |
@@ -57,6 +57,7 @@ There are other API testing MCP servers out there. Here's why this one is differ
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
61
  | cURL export | With resolved variables | Not available |
61
62
  | Project-scoped environments | Per-directory context | Not available |
62
63
  | External dependencies | **Zero** — just Node.js | Playwright, Jest, pytest... |
@@ -182,6 +183,8 @@ You don't need to memorize tool names, parameters, or JSON structures — just t
182
183
  | *"How fast is the health endpoint under load?"* | Fires 50 concurrent requests, reports p50/p95/p99 latencies |
183
184
  | *"Run all my saved smoke tests"* | Executes every request tagged `smoke`, reports pass/fail |
184
185
  | *"Export the create-user request as curl"* | Builds a ready-to-paste cURL command with resolved variables |
186
+ | *"Export my collection to Postman"* | Writes a `.postman_collection.json` file ready to import |
187
+ | *"Export the dev environment for Postman"* | Writes a `.postman_environment.json` file ready to import |
185
188
  | *"Compare the users endpoint between dev and prod"* | Hits both URLs, diffs status codes, body, and timing |
186
189
  | *"Switch to the production environment"* | Changes active env — all subsequent requests use prod URLs and tokens |
187
190
 
@@ -396,6 +399,54 @@ BULK TEST — 8/8 passed | 1.2s total
396
399
  login — POST /auth/login → 200 (156ms)
397
400
  ```
398
401
 
402
+ ### Postman Export
403
+
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.
405
+
406
+ ```
407
+ "Export my collection to Postman"
408
+ "Export only the smoke tests to Postman"
409
+ "Export the dev environment for Postman"
410
+ ```
411
+
412
+ **What you get:**
413
+
414
+ ```
415
+ your-project/
416
+ └── postman/
417
+ ├── my-api.postman_collection.json ← Import in Postman: File → Import
418
+ └── dev.postman_environment.json ← Import in Postman: File → Import
419
+ ```
420
+
421
+ **Collection features:**
422
+ - Requests grouped in **folders by tag** (smoke, auth, users, etc.)
423
+ - Auth (Bearer, API Key, Basic) mapped to Postman's native auth format
424
+ - `{{variables}}` preserved as-is (Postman uses the same syntax)
425
+ - Headers, query params, and JSON body included
426
+ - Collection variables from your active environment
427
+
428
+ **Environment features:**
429
+ - All variables exported with `enabled: true`
430
+ - Postman-compatible format (`_postman_variable_scope: "environment"`)
431
+ - Works with any environment (active or by name)
432
+
433
+ <details>
434
+ <summary>Example: exporting with a specific tag</summary>
435
+
436
+ ```
437
+ You: "Export my smoke tests to Postman as 'Smoke Tests'"
438
+ ```
439
+
440
+ This creates `postman/smoke-tests.postman_collection.json` with only the requests tagged `smoke`, grouped in folders.
441
+
442
+ You can also specify a custom output directory:
443
+
444
+ ```
445
+ You: "Export my collection to Postman in the exports folder"
446
+ ```
447
+
448
+ </details>
449
+
399
450
  ### cURL Export
400
451
 
401
452
  Convert any saved request into a ready-to-paste cURL command with resolved variables.
@@ -443,7 +494,7 @@ Resolution order: project-specific environment → global active environment.
443
494
 
444
495
  ## Tool Reference
445
496
 
446
- 27 tools organized in 8 categories:
497
+ 29 tools organized in 8 categories:
447
498
 
448
499
  | Category | Tools | Count |
449
500
  |----------|-------|-------|
@@ -454,7 +505,7 @@ Resolution order: project-specific environment → global active environment.
454
505
  | **Environments** | `env_create` `env_list` `env_set` `env_get` `env_switch` `env_rename` `env_delete` `env_spec` `env_project_clear` `env_project_list` | 10 |
455
506
  | **API Specs** | `api_import` `api_spec_list` `api_endpoints` `api_endpoint_detail` | 4 |
456
507
  | **Mock** | `mock` | 1 |
457
- | **Utilities** | `load_test` `export_curl` `diff_responses` `bulk_test` | 4 |
508
+ | **Utilities** | `load_test` `export_curl` `diff_responses` `bulk_test` `export_postman_collection` `export_postman_environment` | 6 |
458
509
 
459
510
  You don't need to call these tools directly. Just describe what you want and the AI picks the right one.
460
511
 
@@ -490,14 +541,14 @@ Override the default directory in your MCP config:
490
541
  Built for reliability and testability:
491
542
 
492
543
  - **Zero runtime dependencies** — only `@modelcontextprotocol/sdk` and `zod`
493
- - **83 integration tests** with mocked fetch (no network calls in CI)
544
+ - **96 integration tests** with mocked fetch (no network calls in CI)
494
545
  - **Factory pattern** — `createServer(storageDir?)` for isolated test instances
495
546
  - **Strict TypeScript** — zero `any`, full type coverage
496
- - **< 90KB** bundled output via tsup
547
+ - **< 95KB** bundled output via tsup
497
548
 
498
549
  ```
499
550
  src/
500
- ├── tools/ # 27 MCP tool handlers (one file per category)
551
+ ├── tools/ # 29 MCP tool handlers (one file per category)
501
552
  ├── lib/ # Business logic (no MCP dependency)
502
553
  │ ├── http-client # fetch wrapper with timing
503
554
  │ ├── storage # JSON file storage engine
@@ -506,7 +557,7 @@ src/
506
557
  │ ├── path # Dot-notation accessor (body.data.0.id)
507
558
  │ ├── interpolation # {{variable}} resolver
508
559
  │ └── openapi-parser # $ref + allOf/oneOf/anyOf resolution
509
- └── __tests__/ # 10 test suites, 83 tests
560
+ └── __tests__/ # 10 test suites, 96 tests
510
561
  ```
511
562
 
512
563
  ---
@@ -527,7 +578,7 @@ src/
527
578
  git clone https://github.com/cocaxcode/api-testing-mcp.git
528
579
  cd api-testing-mcp
529
580
  npm install
530
- npm test # 83 tests across 10 suites
581
+ npm test # 96 tests across 10 suites
531
582
  npm run build # ESM bundle via tsup
532
583
  npm run typecheck # Strict TypeScript
533
584
  ```
package/dist/index.js CHANGED
@@ -424,7 +424,8 @@ function interpolateRequest(config, variables) {
424
424
  url: interpolateString(config.url, variables),
425
425
  headers: interpolateRecord(config.headers, variables),
426
426
  query: interpolateRecord(config.query, variables),
427
- body: config.body !== void 0 ? interpolateValue(config.body, variables) : void 0
427
+ body: config.body !== void 0 ? interpolateValue(config.body, variables) : void 0,
428
+ auth: config.auth ? interpolateValue(config.auth, variables) : void 0
428
429
  };
429
430
  }
430
431
 
@@ -1859,6 +1860,8 @@ function registerFlowTool(server, storage) {
1859
1860
 
1860
1861
  // src/tools/utilities.ts
1861
1862
  import { z as z8 } from "zod";
1863
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1864
+ import { join as join2 } from "path";
1862
1865
  function registerUtilityTools(server, storage) {
1863
1866
  server.tool(
1864
1867
  "export_curl",
@@ -2138,6 +2141,251 @@ ${curlCommand}`
2138
2141
  }
2139
2142
  }
2140
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
+ "export_postman_environment",
2238
+ "Exporta un entorno como Postman Environment (JSON). Escribe el archivo en disco, importable directamente en Postman.",
2239
+ {
2240
+ name: z8.string().optional().describe("Nombre del entorno a exportar (default: entorno activo)"),
2241
+ output_dir: z8.string().optional().describe("Directorio donde guardar el archivo (default: ./postman/)")
2242
+ },
2243
+ async (params) => {
2244
+ try {
2245
+ let envName = params.name;
2246
+ if (!envName) {
2247
+ envName = await storage.getActiveEnvironment() ?? void 0;
2248
+ if (!envName) {
2249
+ return {
2250
+ content: [
2251
+ {
2252
+ type: "text",
2253
+ text: 'No hay entorno activo. Especifica un nombre con el par\xE1metro "name".'
2254
+ }
2255
+ ],
2256
+ isError: true
2257
+ };
2258
+ }
2259
+ }
2260
+ const env = await storage.getEnvironment(envName);
2261
+ if (!env) {
2262
+ return {
2263
+ content: [
2264
+ {
2265
+ type: "text",
2266
+ text: `Entorno '${envName}' no encontrado.`
2267
+ }
2268
+ ],
2269
+ isError: true
2270
+ };
2271
+ }
2272
+ const postmanEnv = {
2273
+ name: env.name,
2274
+ values: Object.entries(env.variables).map(([key, value]) => ({
2275
+ key,
2276
+ value,
2277
+ type: "default",
2278
+ enabled: true
2279
+ })),
2280
+ _postman_variable_scope: "environment"
2281
+ };
2282
+ const json = JSON.stringify(postmanEnv, null, 2);
2283
+ const outputDir = params.output_dir ?? join2(process.cwd(), "postman");
2284
+ await mkdir2(outputDir, { recursive: true });
2285
+ const fileName = env.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + ".postman_environment.json";
2286
+ const filePath = join2(outputDir, fileName);
2287
+ await writeFile2(filePath, json, "utf-8");
2288
+ return {
2289
+ content: [
2290
+ {
2291
+ type: "text",
2292
+ text: `Postman Environment "${env.name}" exportado (${Object.keys(env.variables).length} variables).
2293
+
2294
+ Archivo: ${filePath}
2295
+
2296
+ Importa este archivo en Postman: File \u2192 Import \u2192 selecciona el archivo.`
2297
+ }
2298
+ ]
2299
+ };
2300
+ } catch (error) {
2301
+ const message = error instanceof Error ? error.message : String(error);
2302
+ return {
2303
+ content: [{ type: "text", text: `Error: ${message}` }],
2304
+ isError: true
2305
+ };
2306
+ }
2307
+ }
2308
+ );
2309
+ }
2310
+ function buildPostmanItem(saved, variables, resolveVars) {
2311
+ let config = saved.request;
2312
+ if (resolveVars) {
2313
+ const resolvedUrl = resolveUrl(config.url, variables);
2314
+ config = { ...config, url: resolvedUrl };
2315
+ config = interpolateRequest(config, variables);
2316
+ }
2317
+ const item = {
2318
+ name: saved.name,
2319
+ request: buildPostmanRequest(config)
2320
+ };
2321
+ return item;
2322
+ }
2323
+ function buildPostmanRequest(config) {
2324
+ const request = {
2325
+ method: config.method,
2326
+ header: buildPostmanHeaders(config.headers),
2327
+ url: buildPostmanUrl(config.url, config.query)
2328
+ };
2329
+ if (config.body !== void 0 && config.body !== null) {
2330
+ request.body = {
2331
+ mode: "raw",
2332
+ raw: typeof config.body === "string" ? config.body : JSON.stringify(config.body, null, 2),
2333
+ options: { raw: { language: "json" } }
2334
+ };
2335
+ const headers = request.header;
2336
+ if (!headers.some((h) => h.key.toLowerCase() === "content-type")) {
2337
+ headers.push({ key: "Content-Type", value: "application/json" });
2338
+ }
2339
+ }
2340
+ if (config.auth) {
2341
+ request.auth = buildPostmanAuth(config.auth);
2342
+ }
2343
+ return request;
2344
+ }
2345
+ function buildPostmanHeaders(headers) {
2346
+ if (!headers) return [];
2347
+ return Object.entries(headers).map(([key, value]) => ({ key, value }));
2348
+ }
2349
+ function buildPostmanUrl(rawUrl, query) {
2350
+ const url = { raw: rawUrl };
2351
+ const match = rawUrl.match(/^(https?):\/\/([^/]+)(\/.*)?$/);
2352
+ if (match) {
2353
+ url.protocol = match[1];
2354
+ url.host = match[2].split(".");
2355
+ url.path = match[3] ? match[3].slice(1).split("/") : [];
2356
+ }
2357
+ if (query && Object.keys(query).length > 0) {
2358
+ url.query = Object.entries(query).map(([key, value]) => ({ key, value }));
2359
+ const queryStr = Object.entries(query).map(([k, v]) => `${k}=${v}`).join("&");
2360
+ url.raw = rawUrl + (rawUrl.includes("?") ? "&" : "?") + queryStr;
2361
+ }
2362
+ return url;
2363
+ }
2364
+ function buildPostmanAuth(auth) {
2365
+ switch (auth.type) {
2366
+ case "bearer":
2367
+ return {
2368
+ type: "bearer",
2369
+ bearer: [{ key: "token", value: auth.token ?? "", type: "string" }]
2370
+ };
2371
+ case "api-key":
2372
+ return {
2373
+ type: "apikey",
2374
+ apikey: [
2375
+ { key: "key", value: auth.key ?? "", type: "string" },
2376
+ { key: "value", value: auth.header ?? "X-API-Key", type: "string" },
2377
+ { key: "in", value: "header", type: "string" }
2378
+ ]
2379
+ };
2380
+ case "basic":
2381
+ return {
2382
+ type: "basic",
2383
+ basic: [
2384
+ { key: "username", value: auth.username ?? "", type: "string" },
2385
+ { key: "password", value: auth.password ?? "", type: "string" }
2386
+ ]
2387
+ };
2388
+ }
2141
2389
  }
2142
2390
 
2143
2391
  // src/tools/mock.ts