mpx-api 1.2.2 → 1.2.3

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
@@ -4,18 +4,23 @@
4
4
 
5
5
  No GUI. No proprietary formats. Just powerful, git-friendly API testing from your terminal.
6
6
 
7
+ Part of the [Mesaplex](https://mesaplex.com) developer toolchain.
8
+
7
9
  [![npm version](https://img.shields.io/npm/v/mpx-api.svg)](https://www.npmjs.com/package/mpx-api)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
10
+ [![License: Dual](https://img.shields.io/badge/license-Dual-blue.svg)](LICENSE)
11
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
9
12
 
10
- ## Why mpx-api?
13
+ ## Features
11
14
 
12
- - **Git-friendly**: Collections are YAML files, not proprietary blobs
13
- - **CI/CD ready**: Exit codes, JSON output, no GUI dependency
14
- - **Developer experience**: Beautiful terminal output, syntax highlighting
15
- - **Request chaining**: Use response from one request in another (`{{request.response.body.id}}`)
16
- - **Built-in mock server**: Test against OpenAPI specs without deploying
17
- - **Assertions in collections**: No separate test code needed
18
- - **Fast**: Pure Node.js, minimal dependencies, < 200ms startup
15
+ - **Git-friendly** Collections are YAML files, not proprietary blobs
16
+ - **CI/CD ready** Exit codes, JSON output, no GUI dependency
17
+ - **Beautiful terminal output** with syntax highlighting
18
+ - **Request chaining** Use response data from one request in another
19
+ - **Built-in mock server** Test against OpenAPI specs without deploying (Pro)
20
+ - **Load testing** RPS control, percentile reporting (Pro)
21
+ - **Doc generation** Generate API docs from collections (Pro)
22
+ - **MCP server** — Integrates with any MCP-compatible AI agent
23
+ - **Self-documenting** — `--schema` returns machine-readable tool description
19
24
 
20
25
  ## Installation
21
26
 
@@ -23,27 +28,26 @@ No GUI. No proprietary formats. Just powerful, git-friendly API testing from you
23
28
  npm install -g mpx-api
24
29
  ```
25
30
 
26
- Or use with `npx`:
31
+ Or run directly with npx:
27
32
 
28
33
  ```bash
29
34
  npx mpx-api get https://api.github.com/users/octocat
30
35
  ```
31
36
 
32
- ## Quick Start
37
+ **Requirements:** Node.js 18+ · No native dependencies · macOS, Linux, Windows
33
38
 
34
- ### Make HTTP Requests
39
+ ## Quick Start
35
40
 
36
41
  ```bash
37
42
  # Simple GET request
38
43
  mpx-api get https://jsonplaceholder.typicode.com/users
39
44
 
40
45
  # POST with JSON
41
- mpx-api post https://api.example.com/users --json '{"name":"Alice","email":"alice@example.com"}'
46
+ mpx-api post https://api.example.com/users --json '{"name":"Alice"}'
42
47
 
43
48
  # Custom headers
44
49
  mpx-api get https://api.example.com/protected \
45
- -H "Authorization: Bearer $TOKEN" \
46
- -H "Accept: application/json"
50
+ -H "Authorization: Bearer $TOKEN"
47
51
 
48
52
  # Verbose output (show headers)
49
53
  mpx-api get https://httpbin.org/get -v
@@ -52,7 +56,22 @@ mpx-api get https://httpbin.org/get -v
52
56
  mpx-api get https://api.example.com/data -q
53
57
  ```
54
58
 
55
- ### Create a Collection
59
+ ## Usage
60
+
61
+ ### HTTP Requests
62
+
63
+ Supports `get`, `post`, `put`, `patch`, `delete`, `head`, and `options`:
64
+
65
+ ```bash
66
+ mpx-api get https://api.example.com/users
67
+ mpx-api post https://api.example.com/users --json '{"name":"Bob"}'
68
+ mpx-api put https://api.example.com/users/1 --json '{"name":"Alice"}'
69
+ mpx-api delete https://api.example.com/users/1
70
+ ```
71
+
72
+ ### Collections
73
+
74
+ Collections are YAML files for repeatable API test suites:
56
75
 
57
76
  ```bash
58
77
  # Initialize collection in your project
@@ -66,9 +85,7 @@ mpx-api collection add create-user POST /users --json '{"name":"Bob"}'
66
85
  mpx-api collection run
67
86
  ```
68
87
 
69
- ### Collection File Format
70
-
71
- Collections are simple YAML files (`.mpx-api/collection.yaml`):
88
+ Collection file format (`.mpx-api/collection.yaml`):
72
89
 
73
90
  ```yaml
74
91
  name: My API Tests
@@ -90,111 +107,68 @@ requests:
90
107
  url: /users/{{get-users.response.body[0].id}}
91
108
  assert:
92
109
  status: 200
93
- body.email: { exists: true }
94
-
95
- - name: create-post
96
- method: POST
97
- url: /posts
98
- json:
99
- title: New Post
100
- userId: {{get-users.response.body[0].id}}
101
- assert:
102
- status: 201
103
- body.title: New Post
104
110
  ```
105
111
 
106
- **Key features:**
107
-
108
- - **Request chaining**: `{{get-users.response.body[0].id}}` uses the response from a previous request
109
- - **Environment variables**: `{{env.API_TOKEN}}` pulls from environment files
110
- - **Assertions**: Test status codes, response times, body fields, headers
111
- - **Operators**: `gt`, `lt`, `gte`, `lte`, `eq`, `ne`, `contains`, `exists`
112
+ Features: request chaining (`{{request.response.body.id}}`), environment variables (`{{env.VAR}}`), built-in assertions.
112
113
 
113
114
  ### Environments
114
115
 
115
116
  ```bash
116
- # Initialize environments (creates dev, staging, production)
117
- mpx-api env init
118
-
119
- # Set variables
120
- mpx-api env set staging API_URL=https://staging.example.com
121
- mpx-api env set staging API_TOKEN=abc123
122
-
123
- # List environments
124
- mpx-api env list
125
-
126
- # List variables in environment
127
- mpx-api env list staging
128
-
129
- # Run collection with environment
130
- mpx-api collection run --env staging
131
- ```
132
-
133
- Environment files (`.mpx-api/environments/staging.yaml`):
134
-
135
- ```yaml
136
- name: staging
137
- variables:
138
- API_URL: https://staging.example.com
139
- API_TOKEN: secret-token-here
117
+ mpx-api env init # Create dev, staging, production
118
+ mpx-api env set staging API_URL=https://... # Set variables
119
+ mpx-api env list # List environments
120
+ mpx-api collection run --env staging # Run with environment
140
121
  ```
141
122
 
142
123
  ### Testing & Assertions
143
124
 
144
125
  ```bash
145
- # Run tests from collection
146
126
  mpx-api test ./collection.yaml
147
-
148
- # With environment
149
127
  mpx-api test ./collection.yaml --env production
150
-
151
- # JSON output for CI/CD
152
128
  mpx-api test ./collection.yaml --json
129
+ mpx-api test ./collection.yaml --pdf report.pdf # Export PDF report
153
130
  ```
154
131
 
155
- Assertions support:
156
-
157
- - **Status codes**: `status: 200`
158
- - **Response time**: `responseTime: { lt: 500 }`
159
- - **Headers**: `headers.content-type: application/json`
160
- - **Body fields**: `body.users[0].name: Alice`
161
- - **Operators**:
162
- - `{ gt: 10 }` - greater than
163
- - `{ lt: 100 }` - less than
164
- - `{ gte: 5 }` - greater than or equal
165
- - `{ lte: 50 }` - less than or equal
166
- - `{ eq: "value" }` - equals
167
- - `{ ne: "value" }` - not equals
168
- - `{ contains: "text" }` - contains substring
169
- - `{ exists: true }` - field exists
132
+ Assertion operators: `gt`, `lt`, `gte`, `lte`, `eq`, `ne`, `contains`, `exists`
170
133
 
171
134
  ### Request History
172
135
 
173
136
  ```bash
174
- # View recent requests
175
- mpx-api history
137
+ mpx-api history # View recent requests
138
+ mpx-api history -n 50 # Last 50
139
+ ```
176
140
 
177
- # Limit to last 50
178
- mpx-api history -n 50
141
+ ### Mock Server (Pro)
142
+
143
+ ```bash
144
+ mpx-api mock ./openapi.yaml --port 4000
179
145
  ```
180
146
 
181
- ### Cookie Management
147
+ ### Load Testing (Pro)
182
148
 
183
- Cookies are automatically saved and sent with subsequent requests. Cookie jar is stored at `~/.mpx-api/cookies.json`.
149
+ ```bash
150
+ mpx-api load https://api.example.com/health --rps 100 --duration 30s
151
+ ```
184
152
 
185
- ## AI Agent Usage 🤖
153
+ ### Documentation Generation (Pro)
186
154
 
187
- **mpx-api is AI-native!** Every command supports structured JSON output, schema discovery, and MCP (Model Context Protocol) integration for seamless AI agent automation.
155
+ ```bash
156
+ mpx-api docs ./collection.yaml --output API.md
157
+ ```
158
+
159
+ ## AI Agent Usage
160
+
161
+ mpx-api is designed to be used by AI agents as well as humans.
188
162
 
189
163
  ### JSON Output
190
164
 
191
- Add `--json` to any command for machine-readable output:
165
+ Add `--json` to any command for structured, machine-readable output:
192
166
 
193
167
  ```bash
194
- # HTTP request with JSON output
195
168
  mpx-api get https://api.github.com/users/octocat --json
169
+ ```
196
170
 
197
- # Output structure
171
+ ```json
198
172
  {
199
173
  "request": {
200
174
  "method": "GET",
@@ -206,8 +180,7 @@ mpx-api get https://api.github.com/users/octocat --json
206
180
  "status": 200,
207
181
  "statusText": "OK",
208
182
  "headers": { "content-type": "application/json" },
209
- "body": { "login": "octocat", ... },
210
- "rawBody": "...",
183
+ "body": { "login": "octocat" },
211
184
  "responseTime": 145,
212
185
  "size": 1234
213
186
  }
@@ -216,30 +189,15 @@ mpx-api get https://api.github.com/users/octocat --json
216
189
 
217
190
  ### Schema Discovery
218
191
 
219
- AI agents can discover all available commands, flags, and output formats:
220
-
221
192
  ```bash
222
193
  mpx-api --schema
223
194
  ```
224
195
 
225
- Returns a complete JSON schema describing:
226
- - All commands and subcommands
227
- - Available flags and their types
228
- - Input/output schemas
229
- - Usage examples
230
- - Exit codes
231
-
232
- Perfect for dynamic tool discovery by AI assistants!
233
-
234
- ### MCP Server Mode
196
+ Returns a complete JSON schema describing all commands, flags, inputs, outputs, and examples.
235
197
 
236
- Start mpx-api as an MCP (Model Context Protocol) server for AI agent integration:
237
-
238
- ```bash
239
- mpx-api mcp
240
- ```
198
+ ### MCP Integration
241
199
 
242
- Add to your MCP client configuration (e.g., Claude Desktop, Cline):
200
+ Add to your MCP client configuration (Claude Desktop, Cursor, Windsurf, etc.):
243
201
 
244
202
  ```json
245
203
  {
@@ -252,222 +210,65 @@ Add to your MCP client configuration (e.g., Claude Desktop, Cline):
252
210
  }
253
211
  ```
254
212
 
255
- **Available MCP tools:**
256
-
257
- - `http_request` - Send HTTP requests with full control over method, headers, body
258
- - `get_schema` - Get the complete tool schema for dynamic discovery
259
-
260
- **Example MCP usage:**
261
-
262
- AI agents can now make API requests on your behalf:
263
- - "Make a GET request to https://api.github.com/users/octocat"
264
- - "POST to https://api.example.com/users with JSON body {name: 'Alice'}"
265
- - "What commands does mpx-api support?" (via get_schema)
266
-
267
- ### Quiet Mode
268
-
269
- Suppress non-essential output with `--quiet` or `-q`:
270
-
271
- ```bash
272
- mpx-api get https://api.example.com/data --quiet --json
273
- ```
274
-
275
- Perfect for scripting and automation where you only want the result data.
276
-
277
- ### Composability
278
-
279
- All commands are designed for Unix-style composition:
280
-
281
- ```bash
282
- # Pipe output to jq
283
- mpx-api get https://api.github.com/users/octocat --json | jq '.response.body.login'
284
-
285
- # Use in scripts
286
- STATUS=$(mpx-api get https://api.example.com/health --json | jq -r '.response.status')
287
- if [ "$STATUS" -eq 200 ]; then
288
- echo "API is healthy"
289
- fi
290
-
291
- # Batch processing
292
- cat urls.txt | while read url; do
293
- mpx-api get "$url" --json >> results.jsonl
294
- done
295
- ```
213
+ The MCP server exposes these tools:
214
+ - **`http_request`** — Send HTTP requests with full control over method, headers, body
215
+ - **`get_schema`** Get the complete tool schema for dynamic discovery
296
216
 
297
217
  ### Exit Codes
298
218
 
299
- Predictable exit codes for automation:
219
+ | Code | Meaning |
220
+ |------|---------|
221
+ | 0 | Success (2xx or 3xx HTTP status) |
222
+ | 1 | Request failed or 4xx/5xx HTTP status |
300
223
 
301
- - `0` - Success (2xx or 3xx HTTP status)
302
- - `1` - Request failed or 4xx/5xx HTTP status
224
+ ### Automation Tips
303
225
 
304
- ```bash
305
- # Check if request succeeded
306
- if mpx-api get https://api.example.com/endpoint --quiet; then
307
- echo "Success!"
308
- else
309
- echo "Request failed"
310
- fi
311
- ```
312
-
313
- ## Pro Features 💎
314
-
315
- Upgrade to **mpx-api Pro** ($12/mo) for advanced features:
316
-
317
- ### Mock Server
318
-
319
- Start a mock API server from an OpenAPI spec:
320
-
321
- ```bash
322
- mpx-api mock ./openapi.yaml --port 4000
323
- ```
324
-
325
- Supports:
326
- - OpenAPI 3.0 specs (YAML or JSON)
327
- - Automatic response generation from schemas
328
- - Configurable response delay: `--delay 200`
329
- - CORS support: `--cors`
330
-
331
- ### Load Testing
332
-
333
- ```bash
334
- mpx-api load https://api.example.com/health --rps 100 --duration 30s
335
- ```
336
-
337
- Features:
338
- - Requests per second (RPS) control
339
- - Response time percentiles (P50, P95, P99)
340
- - Status code distribution
341
- - Error tracking
342
-
343
- ### Documentation Generation
344
-
345
- Generate beautiful API docs from your collections:
346
-
347
- ```bash
348
- mpx-api docs ./collection.yaml --output API.md
349
- ```
350
-
351
- Creates Markdown documentation with:
352
- - Table of contents
353
- - Request/response examples
354
- - Expected responses from assertions
355
- - Auto-generated from your test collections
356
-
357
- ### Request Chaining
358
-
359
- Already included in free tier! Use response data from previous requests:
360
-
361
- ```yaml
362
- requests:
363
- - name: login
364
- method: POST
365
- url: /auth/login
366
- json:
367
- username: test
368
- password: secret
369
-
370
- - name: get-profile
371
- method: GET
372
- url: /users/me
373
- headers:
374
- Authorization: Bearer {{login.response.body.token}}
375
- ```
376
-
377
- ## Examples
378
-
379
- See the `examples/` directory for real-world collections:
380
-
381
- - `jsonplaceholder.yaml` - CRUD operations with JSONPlaceholder API
382
- - `github-api.yaml` - GitHub API with request chaining
383
- - `openapi-petstore.yaml` - OpenAPI spec for mock server testing
384
-
385
- Run examples:
386
-
387
- ```bash
388
- mpx-api test examples/jsonplaceholder.yaml
389
- mpx-api collection run examples/github-api.yaml
390
- ```
226
+ - Use `--json` for machine-parseable output
227
+ - Use `--quiet` to suppress banners and progress info
228
+ - Pipe output to `jq` for filtering
229
+ - Check exit codes for pass/fail in CI/CD
391
230
 
392
231
  ## CI/CD Integration
393
232
 
394
- Use exit codes for CI/CD pipelines:
395
-
396
- ```bash
397
- # In your CI script
398
- mpx-api test ./api-tests.yaml --env production
399
-
400
- # Exit code 0 = all tests passed
401
- # Exit code 1 = tests failed
402
- ```
403
-
404
- GitHub Actions example:
405
-
406
233
  ```yaml
234
+ # .github/workflows/api-tests.yml
407
235
  - name: Run API Tests
408
236
  run: npx mpx-api test ./tests/api-collection.yaml --env staging
409
237
  ```
410
238
 
411
- ## Error Handling
412
-
413
- mpx-api gracefully handles:
414
-
415
- - **DNS failures**: Clear error messages
416
- - **Timeouts**: Configurable with `--timeout <ms>`
417
- - **SSL errors**: Skip verification with `--no-verify`
418
- - **Invalid JSON**: Parse errors with helpful messages
419
- - **Large responses**: Automatic truncation in terminal (full body still accessible)
420
-
421
- ## Configuration
422
-
423
- Global config: `~/.mpx-api/config.json`
424
- Project config: `.mpx-api/config.json`
425
- Cookie jar: `~/.mpx-api/cookies.json`
426
- History: `~/.mpx-api/history.jsonl`
427
-
428
- ## Comparison
239
+ ## Free vs Pro
429
240
 
430
- | Feature | mpx-api | Postman | HTTPie Pro | curl |
431
- |---------|---------|---------|------------|------|
432
- | **Price** | Free / $12 Pro | $49/user | $9/mo | Free |
433
- | **Collections** | YAML files | ✅ Proprietary | | ❌ |
434
- | **Request chaining** | ✅ | ✅ | ❌ | ❌ |
435
- | **Assertions** | Built-in | ✅ Scripts | | ❌ |
436
- | **Mock server** | ✅ Pro | ✅ Pro | ❌ | ❌ |
437
- | **Load testing** | ✅ Pro | ✅ Pro | ❌ | ❌ |
438
- | **Git-friendly** | | ⚠️ Export needed | N/A | N/A |
439
- | **No GUI needed** | ✅ | ❌ | ✅ | ✅ |
440
- | **Syntax highlighting** | | ✅ | ✅ | ❌ |
241
+ | Feature | Free | Pro |
242
+ |---------|------|-----|
243
+ | HTTP requests | | |
244
+ | Collections & chaining | ✅ | |
245
+ | Environments | ✅ | ✅ |
246
+ | Assertions & testing | ✅ | |
247
+ | JSON output | ✅ | ✅ |
248
+ | MCP server | ✅ | ✅ |
249
+ | Mock server | | |
250
+ | Load testing | ❌ | ✅ |
251
+ | Doc generation | | ✅ |
441
252
 
442
- ## Development
253
+ **Upgrade to Pro:** [https://mesaplex.com/mpx-api](https://mesaplex.com/mpx-api)
443
254
 
444
- ```bash
445
- # Clone and install
446
- git clone https://github.com/mesaplex/mpx-api.git
447
- cd mpx-api
448
- npm install
449
-
450
- # Run tests
451
- npm test
452
-
453
- # Link for local development
454
- npm link
455
- ```
456
-
457
- ## Contributing
255
+ ## License
458
256
 
459
- Contributions welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) first.
257
+ Dual License Free tier for personal use, Pro license for commercial use and advanced features. See [LICENSE](LICENSE) for full terms.
460
258
 
461
- ## License
259
+ ## Links
462
260
 
463
- MIT © Mesaplex
261
+ - **Website:** [https://mesaplex.com](https://mesaplex.com)
262
+ - **npm:** [https://www.npmjs.com/package/mpx-api](https://www.npmjs.com/package/mpx-api)
263
+ - **GitHub:** [https://github.com/mesaplexdev/mpx-api](https://github.com/mesaplexdev/mpx-api)
264
+ - **Support:** support@mesaplex.com
464
265
 
465
- ## Support
266
+ ### Related Tools
466
267
 
467
- - **Issues**: [GitHub Issues](https://github.com/mesaplex/mpx-api/issues)
468
- - **Discussions**: [GitHub Discussions](https://github.com/mesaplex/mpx-api/discussions)
469
- - **Email**: support@mpx-api.dev
268
+ - **[mpx-scan](https://www.npmjs.com/package/mpx-scan)** — Website security scanner
269
+ - **[mpx-db](https://www.npmjs.com/package/mpx-db)** — Database management CLI
270
+ - **[mpx-secrets-audit](https://www.npmjs.com/package/mpx-secrets-audit)** — Secret lifecycle tracking and audit
470
271
 
471
272
  ---
472
273
 
473
- **Built with ❤️ by developers, for developers.**
274
+ **Made with ❤️ by [Mesaplex](https://mesaplex.com)**
package/bin/mpx-api.js CHANGED
@@ -32,14 +32,34 @@ if (process.argv.includes('--schema')) {
32
32
  process.exit(0);
33
33
  }
34
34
 
35
+ // Handle --no-color early
36
+ if (process.argv.includes('--no-color') || !process.stdout.isTTY) {
37
+ process.env.FORCE_COLOR = '0';
38
+ }
39
+
40
+ // Handle --json as alias for --output json
41
+ if (process.argv.includes('--json') && !process.argv.includes('--output')) {
42
+ const idx = process.argv.indexOf('--json');
43
+ process.argv.splice(idx, 1, '--output', 'json');
44
+ }
45
+
35
46
  program
36
47
  .name('mpx-api')
37
48
  .description('Developer-first API testing, mocking, and documentation CLI')
38
49
  .version(pkg.version)
39
50
  .enablePositionalOptions()
40
51
  .passThroughOptions()
52
+ .option('--json', 'Output as JSON (machine-readable)')
41
53
  .option('-o, --output <format>', 'Output format: json for structured JSON (machine-readable)')
42
- .option('-q, --quiet', 'Suppress non-essential output');
54
+ .option('-q, --quiet', 'Suppress non-essential output')
55
+ .option('--no-color', 'Disable colored output')
56
+ .option('--schema', 'Output JSON schema describing all commands and flags');
57
+
58
+ // Error handling — must be set BEFORE .command() so subcommands inherit exitOverride
59
+ program.exitOverride();
60
+ program.configureOutput({
61
+ writeErr: () => {} // Suppress Commander's own error output; we handle it in the catch below
62
+ });
43
63
 
44
64
  // Register HTTP method commands (get, post, put, patch, delete, head, options)
45
65
  registerRequestCommands(program);
@@ -129,7 +149,7 @@ program
129
149
  if (jsonMode) {
130
150
  console.log(JSON.stringify({ error: err.message, code: 'ERR_UPDATE' }, null, 2));
131
151
  } else {
132
- console.error(chalk.red.bold('\n❌ Update check failed:'), err.message);
152
+ console.error(chalk.red('Error:'), err.message);
133
153
  console.error('');
134
154
  }
135
155
  process.exit(1);
@@ -149,4 +169,22 @@ program
149
169
  }
150
170
  });
151
171
 
152
- program.parse();
172
+ // Error handling
173
+ program.exitOverride();
174
+ program.configureOutput({
175
+ writeErr: () => {} // Suppress Commander's own error output; we handle it below
176
+ });
177
+
178
+ try {
179
+ await program.parseAsync(process.argv);
180
+ } catch (err) {
181
+ if (err.code === 'commander.version') {
182
+ process.exit(0);
183
+ }
184
+ if (err.code !== 'commander.help' && err.code !== 'commander.helpDisplayed') {
185
+ const chalk = (await import('chalk')).default;
186
+ const msg = err.message.startsWith('error:') ? `Error: ${err.message.slice(7)}` : `Error: ${err.message}`;
187
+ console.error(chalk.red(msg));
188
+ process.exit(2);
189
+ }
190
+ }
package/package.json CHANGED
@@ -1,26 +1,29 @@
1
1
  {
2
2
  "name": "mpx-api",
3
- "version": "1.2.2",
4
- "description": "Developer-first API testing, mocking, and documentation CLI with AI-native features (JSON output, MCP server)",
3
+ "version": "1.2.3",
4
+ "description": "API testing, mocking, and documentation CLI. Developer-first HTTP workflows. AI-native with JSON output and MCP server.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "mpx-api": "bin/mpx-api.js"
8
8
  },
9
9
  "keywords": [
10
+ "cli",
11
+ "devtools",
12
+ "mesaplex",
13
+ "ai-native",
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "automation",
17
+ "json-output",
10
18
  "api",
11
19
  "testing",
12
20
  "http",
13
21
  "rest",
14
22
  "mock",
15
- "cli",
16
23
  "postman",
17
24
  "httpie",
18
25
  "openapi",
19
- "swagger",
20
- "mcp",
21
- "ai-native",
22
- "model-context-protocol",
23
- "automation"
26
+ "swagger"
24
27
  ],
25
28
  "author": "Mesaplex <support@mesaplex.com>",
26
29
  "license": "SEE LICENSE IN LICENSE",
@@ -42,22 +45,22 @@
42
45
  "lint": "echo 'TODO: Add eslint'",
43
46
  "prepublishOnly": "npm test"
44
47
  },
48
+ "funding": "https://mesaplex.com/pricing",
45
49
  "dependencies": {
46
50
  "@modelcontextprotocol/sdk": "^1.26.0",
47
51
  "chalk": "^5.3.0",
48
- "commander": "^12.0.0",
49
- "yaml": "^2.3.4",
50
- "undici": "^6.6.0",
51
52
  "cli-highlight": "^2.1.11",
53
+ "commander": "^12.0.0",
54
+ "filenamify": "^6.0.0",
55
+ "pdfkit": "^0.17.2",
52
56
  "tough-cookie": "^4.1.3",
53
- "filenamify": "^6.0.0"
57
+ "undici": "^6.6.0",
58
+ "yaml": "^2.3.4"
54
59
  },
55
- "devDependencies": {},
56
60
  "files": [
57
61
  "src/",
58
62
  "bin/",
59
63
  "README.md",
60
- "LICENSE",
61
- "package.json"
64
+ "LICENSE"
62
65
  ]
63
66
  }
@@ -1,9 +1,8 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { parse, stringify } from 'yaml';
3
3
  import { ensureLocalDir } from '../lib/config.js';
4
- import { formatSuccess, formatError, formatInfo, formatWarning } from '../lib/output.js';
4
+ import { formatSuccess, formatError, formatInfo, formatWarning, formatTestResults } from '../lib/output.js';
5
5
  import { runCollection } from '../lib/collection-runner.js';
6
- import { formatTestResults } from '../lib/output.js';
7
6
  import { join } from 'path';
8
7
 
9
8
  export function registerCollectionCommands(program) {
@@ -96,6 +95,7 @@ export function registerCollectionCommands(program) {
96
95
  .option('-e, --env <name>', 'Environment to use')
97
96
  .option('--base-url <url>', 'Override base URL')
98
97
  .option('--json', 'Output results as JSON')
98
+ .option('--pdf <filename>', 'Export results as a PDF report')
99
99
  .action(async (file, options, command) => {
100
100
  try {
101
101
  const collectionPath = file || join('.mpx-api', 'collection.yaml');
@@ -122,7 +122,21 @@ export function registerCollectionCommands(program) {
122
122
 
123
123
  const baseUrl = options.baseUrl || collection.baseUrl || '';
124
124
 
125
+ const startTime = Date.now();
125
126
  const results = await runCollection(collection, { env, baseUrl });
127
+ const totalTime = Date.now() - startTime;
128
+
129
+ // Generate PDF if requested
130
+ if (options.pdf) {
131
+ const { generatePDFReport } = await import('../lib/pdf-report.js');
132
+ const pdfPath = options.pdf.endsWith('.pdf') ? options.pdf : `${options.pdf}.pdf`;
133
+ await generatePDFReport(results, {
134
+ target: baseUrl || collection.baseUrl || 'API Tests',
135
+ collectionName: collection.name,
136
+ totalTime,
137
+ }, pdfPath);
138
+ formatSuccess(`PDF report saved to ${pdfPath}`);
139
+ }
126
140
 
127
141
  const globalOpts = command.optsWithGlobals();
128
142
  const jsonOutput = options.json || globalOpts.output === 'json';
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
2
  import { parse } from 'yaml';
3
- import { formatError } from '../lib/output.js';
3
+ import { formatError, formatSuccess } from '../lib/output.js';
4
4
  import { runCollection } from '../lib/collection-runner.js';
5
5
  import { formatTestResults } from '../lib/output.js';
6
6
  import { join } from 'path';
@@ -12,6 +12,7 @@ export function registerTestCommand(program) {
12
12
  .option('-e, --env <name>', 'Environment to use')
13
13
  .option('--base-url <url>', 'Override base URL')
14
14
  .option('--json', 'Output results as JSON')
15
+ .option('--pdf <filename>', 'Export results as a PDF report')
15
16
  .action(async (file, options) => {
16
17
  try {
17
18
  const collectionPath = file || join('.mpx-api', 'collection.yaml');
@@ -37,7 +38,21 @@ export function registerTestCommand(program) {
37
38
 
38
39
  const baseUrl = options.baseUrl || collection.baseUrl || '';
39
40
 
41
+ const startTime = Date.now();
40
42
  const results = await runCollection(collection, { env, baseUrl });
43
+ const totalTime = Date.now() - startTime;
44
+
45
+ // Generate PDF if requested
46
+ if (options.pdf) {
47
+ const { generatePDFReport } = await import('../lib/pdf-report.js');
48
+ const pdfPath = options.pdf.endsWith('.pdf') ? options.pdf : `${options.pdf}.pdf`;
49
+ await generatePDFReport(results, {
50
+ target: baseUrl || collection.baseUrl || 'API Tests',
51
+ collectionName: collection.name,
52
+ totalTime,
53
+ }, pdfPath);
54
+ formatSuccess(`PDF report saved to ${pdfPath}`);
55
+ }
41
56
 
42
57
  if (options.json) {
43
58
  console.log(JSON.stringify(results, null, 2));
@@ -63,12 +63,10 @@ export class HttpClient {
63
63
  requestOptions.maxRedirections = 0;
64
64
  }
65
65
 
66
- // Handle timeout
66
+ // Handle timeout via undici's built-in options
67
67
  if (this.timeout) {
68
- const controller = new AbortController();
69
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
70
- requestOptions.signal = controller.signal;
71
- requestOptions._timeoutId = timeoutId;
68
+ requestOptions.headersTimeout = this.timeout;
69
+ requestOptions.bodyTimeout = this.timeout;
72
70
  }
73
71
 
74
72
  // Add cookies to request
@@ -89,10 +87,7 @@ export class HttpClient {
89
87
  }
90
88
 
91
89
  try {
92
- const timeoutId = requestOptions._timeoutId;
93
- delete requestOptions._timeoutId;
94
90
  const response = await request(url, requestOptions);
95
- if (timeoutId) clearTimeout(timeoutId);
96
91
 
97
92
  // Store cookies from response
98
93
  const setCookieHeaders = response.headers['set-cookie'];
@@ -139,7 +134,7 @@ export class HttpClient {
139
134
  };
140
135
  } catch (err) {
141
136
  // Handle network errors gracefully
142
- if (err.name === 'AbortError' || err.code === 'UND_ERR_ABORTED') {
137
+ if (err.name === 'AbortError' || err.code === 'UND_ERR_ABORTED' || err.code === 'UND_ERR_HEADERS_TIMEOUT' || err.code === 'UND_ERR_BODY_TIMEOUT') {
143
138
  throw new Error(`Request timeout after ${this.timeout}ms`);
144
139
  } else if (err.code === 'ENOTFOUND') {
145
140
  throw new Error(`DNS lookup failed for ${url}`);
@@ -0,0 +1,217 @@
1
+ /**
2
+ * PDF Report Generator for mpx-api
3
+ *
4
+ * Generates professional API test result reports using PDFKit.
5
+ * Style consistent with mpx-scan PDF reports.
6
+ */
7
+
8
+ import PDFDocument from 'pdfkit';
9
+ import { createWriteStream, readFileSync } from 'fs';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname, join } from 'path';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
16
+
17
+ // Color palette (consistent with mpx-scan)
18
+ const COLORS = {
19
+ primary: '#1a56db',
20
+ dark: '#1f2937',
21
+ gray: '#6b7280',
22
+ lightGray: '#e5e7eb',
23
+ white: '#ffffff',
24
+ pass: '#16a34a',
25
+ warn: '#ea580c',
26
+ fail: '#dc2626',
27
+ headerBg: '#1e3a5f',
28
+ sectionBg: '#f3f4f6',
29
+ };
30
+
31
+ /**
32
+ * Generate a PDF report from test/collection results
33
+ * @param {Array} results - Array of test result objects from collection-runner
34
+ * @param {object} meta - Metadata: { target, collectionName, totalTime }
35
+ * @param {string} outputPath - Path to write the PDF
36
+ * @returns {Promise<string>} - Resolved path of the generated PDF
37
+ */
38
+ export function generatePDFReport(results, meta, outputPath) {
39
+ return new Promise((resolve, reject) => {
40
+ try {
41
+ const now = new Date().toLocaleDateString('en-US', {
42
+ year: 'numeric', month: 'long', day: 'numeric',
43
+ hour: '2-digit', minute: '2-digit',
44
+ });
45
+
46
+ const passed = results.filter(r => r.passed).length;
47
+ const failed = results.filter(r => !r.passed).length;
48
+ const total = results.length;
49
+
50
+ const doc = new PDFDocument({
51
+ size: 'A4',
52
+ margins: { top: 50, bottom: 50, left: 50, right: 50 },
53
+ info: {
54
+ Title: `mpx-api Test Report — ${meta.target || 'API Tests'}`,
55
+ Author: 'mpx-api',
56
+ Subject: 'API Test Results',
57
+ Creator: `mpx-api v${pkg.version}`,
58
+ },
59
+ bufferPages: true,
60
+ });
61
+
62
+ const stream = createWriteStream(outputPath);
63
+ doc.pipe(stream);
64
+
65
+ const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
66
+
67
+ // ─── Header ───
68
+ doc.rect(0, 0, doc.page.width, 100).fill(COLORS.headerBg);
69
+ doc.fontSize(22).fillColor(COLORS.white).font('Helvetica-Bold')
70
+ .text('mpx-api Test Report', 50, 30);
71
+ doc.fontSize(10).fillColor('#a0b4cc').font('Helvetica')
72
+ .text(`v${pkg.version} • ${now} • ${meta.target || 'Collection Run'}`, 50, 60);
73
+
74
+ doc.y = 120;
75
+
76
+ // ─── Summary Box ───
77
+ const passRate = total > 0 ? Math.round((passed / total) * 100) : 0;
78
+ const gradeColor = passRate === 100 ? COLORS.pass : passRate >= 50 ? COLORS.warn : COLORS.fail;
79
+
80
+ doc.roundedRect(50, doc.y, pageWidth, 80, 6).fill(COLORS.sectionBg);
81
+ const summaryTop = doc.y + 12;
82
+
83
+ // Pass rate circle
84
+ doc.circle(100, summaryTop + 28, 26).fill(gradeColor);
85
+ doc.fontSize(20).fillColor(COLORS.white).font('Helvetica-Bold')
86
+ .text(`${passRate}%`, 100 - 20, summaryTop + 16, { width: 40, align: 'center' });
87
+
88
+ // Summary text
89
+ doc.fontSize(14).fillColor(COLORS.dark).font('Helvetica-Bold')
90
+ .text(`${passed}/${total} tests passed`, 145, summaryTop + 5);
91
+
92
+ const timingText = meta.totalTime ? `Total time: ${meta.totalTime}ms` : '';
93
+ const collName = meta.collectionName ? `Collection: ${meta.collectionName}` : '';
94
+ const subtext = [collName, timingText].filter(Boolean).join(' • ');
95
+ if (subtext) {
96
+ doc.fontSize(10).fillColor(COLORS.gray).font('Helvetica')
97
+ .text(subtext, 145, summaryTop + 25);
98
+ }
99
+
100
+ // Counts on right side
101
+ const countsX = 370;
102
+ const counts = [
103
+ { label: 'Passed', count: passed, color: COLORS.pass },
104
+ { label: 'Failed', count: failed, color: COLORS.fail },
105
+ { label: 'Total', count: total, color: COLORS.primary },
106
+ ];
107
+ counts.forEach((c, i) => {
108
+ const cx = countsX + i * 60;
109
+ doc.fontSize(18).fillColor(c.color).font('Helvetica-Bold')
110
+ .text(String(c.count), cx, summaryTop + 5, { width: 50, align: 'center' });
111
+ doc.fontSize(7).fillColor(COLORS.gray).font('Helvetica')
112
+ .text(c.label, cx, summaryTop + 28, { width: 50, align: 'center' });
113
+ });
114
+
115
+ doc.y = summaryTop + 68;
116
+
117
+ // ─── Detailed Results ───
118
+ for (const result of results) {
119
+ // Check page space
120
+ if (doc.y > doc.page.height - 160) {
121
+ doc.addPage();
122
+ doc.y = 50;
123
+ }
124
+
125
+ doc.y += 8;
126
+
127
+ // Result header bar
128
+ const statusColor = result.passed ? COLORS.pass : COLORS.fail;
129
+ const statusLabel = result.passed ? '✓ PASS' : '✗ FAIL';
130
+
131
+ doc.roundedRect(50, doc.y, pageWidth, 26, 4).fill(statusColor);
132
+ doc.fontSize(10).fillColor(COLORS.white).font('Helvetica-Bold')
133
+ .text(statusLabel, 60, doc.y + 7);
134
+ doc.fontSize(10).fillColor(COLORS.white).font('Helvetica-Bold')
135
+ .text(result.name, 120, doc.y + 7);
136
+
137
+ // Response info on right
138
+ if (result.response) {
139
+ const info = `${result.response.method || 'GET'} ${result.response.status} • ${result.response.responseTime}ms`;
140
+ doc.fontSize(8).fillColor('#ffffffcc').font('Helvetica')
141
+ .text(info, 60, doc.y + 8, { width: pageWidth - 20, align: 'right' });
142
+ }
143
+
144
+ doc.y += 32;
145
+
146
+ // URL
147
+ if (result.response?.url) {
148
+ doc.fontSize(8).fillColor(COLORS.gray).font('Helvetica')
149
+ .text(result.response.url, 60, doc.y, { width: pageWidth - 20 });
150
+ doc.y += 14;
151
+ }
152
+
153
+ // Error message
154
+ if (result.error) {
155
+ doc.fontSize(9).fillColor(COLORS.fail).font('Helvetica')
156
+ .text(`Error: ${result.error}`, 60, doc.y, { width: pageWidth - 20 });
157
+ doc.y += doc.heightOfString(`Error: ${result.error}`, { width: pageWidth - 20, fontSize: 9 }) + 6;
158
+ }
159
+
160
+ // Assertions
161
+ if (result.assertions && result.assertions.length > 0) {
162
+ for (const assertion of result.assertions) {
163
+ if (doc.y > doc.page.height - 80) {
164
+ doc.addPage();
165
+ doc.y = 50;
166
+ }
167
+
168
+ const aColor = assertion.passed ? COLORS.pass : COLORS.fail;
169
+ const aIcon = assertion.passed ? '✓' : '✗';
170
+
171
+ doc.fontSize(8).fillColor(aColor).font('Helvetica-Bold')
172
+ .text(aIcon, 65, doc.y);
173
+ doc.fontSize(8).fillColor(COLORS.dark).font('Helvetica')
174
+ .text(assertion.description, 80, doc.y, { width: pageWidth - 45 });
175
+ doc.y += 14;
176
+
177
+ if (!assertion.passed) {
178
+ doc.fontSize(7).fillColor(COLORS.gray).font('Helvetica')
179
+ .text(`Expected: ${JSON.stringify(assertion.expected)} | Got: ${JSON.stringify(assertion.actual)}`, 80, doc.y, { width: pageWidth - 45 });
180
+ doc.y += 12;
181
+ }
182
+ }
183
+ }
184
+
185
+ doc.y += 4;
186
+
187
+ // Separator line
188
+ doc.moveTo(50, doc.y).lineTo(50 + pageWidth, doc.y)
189
+ .strokeColor(COLORS.lightGray).lineWidth(0.5).stroke();
190
+ doc.y += 4;
191
+ }
192
+
193
+ // ─── Footer on every page ───
194
+ const range = doc.bufferedPageRange();
195
+ for (let i = range.start; i < range.start + range.count; i++) {
196
+ doc.switchToPage(i);
197
+ const footerY = doc.page.height - 35;
198
+ doc.fontSize(7).fillColor(COLORS.gray).font('Helvetica')
199
+ .text(
200
+ `Generated by mpx-api v${pkg.version} on ${now}`,
201
+ 50, footerY, { width: pageWidth, align: 'center' }
202
+ );
203
+ doc.text(
204
+ `Page ${i + 1} of ${range.count}`,
205
+ 50, footerY + 12, { width: pageWidth, align: 'center' }
206
+ );
207
+ }
208
+
209
+ doc.end();
210
+
211
+ stream.on('finish', () => resolve(outputPath));
212
+ stream.on('error', reject);
213
+ } catch (err) {
214
+ reject(err);
215
+ }
216
+ });
217
+ }
package/src/mcp.js CHANGED
@@ -166,8 +166,7 @@ export async function startMCPServer() {
166
166
  type: 'text',
167
167
  text: JSON.stringify({
168
168
  error: err.message,
169
- code: err.code || 'ERR_REQUEST',
170
- stack: err.stack
169
+ code: err.code || 'ERR_REQUEST'
171
170
  }, null, 2)
172
171
  }],
173
172
  isError: true
package/src/schema.js CHANGED
@@ -160,7 +160,8 @@ export function getSchema() {
160
160
  flags: {
161
161
  '-e, --env': { type: 'string', description: 'Environment name to use' },
162
162
  '--base-url': { type: 'string', description: 'Override base URL' },
163
- '--json': { type: 'boolean', description: 'Output results as JSON' }
163
+ '--json': { type: 'boolean', description: 'Output results as JSON' },
164
+ '--pdf': { type: 'string', description: 'Export results as a PDF report to the specified file' }
164
165
  }
165
166
  },
166
167
  list: {
@@ -212,7 +213,8 @@ export function getSchema() {
212
213
  },
213
214
  flags: {
214
215
  '-e, --env': { type: 'string', description: 'Environment name' },
215
- '--json': { type: 'boolean', description: 'Output results as JSON' }
216
+ '--json': { type: 'boolean', description: 'Output results as JSON' },
217
+ '--pdf': { type: 'string', description: 'Export results as a PDF report to the specified file' }
216
218
  }
217
219
  },
218
220
  history: {
@@ -267,6 +269,10 @@ export function getSchema() {
267
269
  ]
268
270
  }
269
271
  },
272
+ exitCodes: {
273
+ 0: 'Success',
274
+ 1: 'Error (request failed, validation error, etc.)'
275
+ },
270
276
  globalFlags: {
271
277
  '-o, --output': {
272
278
  type: 'string',