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 +107 -306
- package/bin/mpx-api.js +41 -3
- package/package.json +18 -15
- package/src/commands/collection.js +16 -2
- package/src/commands/test.js +16 -1
- package/src/lib/http-client.js +4 -9
- package/src/lib/pdf-report.js +217 -0
- package/src/mcp.js +1 -2
- package/src/schema.js +8 -2
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
|
[](https://www.npmjs.com/package/mpx-api)
|
|
8
|
-
[](LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
9
12
|
|
|
10
|
-
##
|
|
13
|
+
## Features
|
|
11
14
|
|
|
12
|
-
- **Git-friendly
|
|
13
|
-
- **CI/CD ready
|
|
14
|
-
- **
|
|
15
|
-
- **Request chaining
|
|
16
|
-
- **Built-in mock server
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
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
|
|
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
|
-
|
|
37
|
+
**Requirements:** Node.js 18+ · No native dependencies · macOS, Linux, Windows
|
|
33
38
|
|
|
34
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
mpx-api env
|
|
118
|
-
|
|
119
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
141
|
+
### Mock Server (Pro)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
mpx-api mock ./openapi.yaml --port 4000
|
|
179
145
|
```
|
|
180
146
|
|
|
181
|
-
###
|
|
147
|
+
### Load Testing (Pro)
|
|
182
148
|
|
|
183
|
-
|
|
149
|
+
```bash
|
|
150
|
+
mpx-api load https://api.example.com/health --rps 100 --duration 30s
|
|
151
|
+
```
|
|
184
152
|
|
|
185
|
-
|
|
153
|
+
### Documentation Generation (Pro)
|
|
186
154
|
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
mpx-api mcp
|
|
240
|
-
```
|
|
198
|
+
### MCP Integration
|
|
241
199
|
|
|
242
|
-
Add to your MCP client configuration (
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
-
|
|
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
|
-
|
|
219
|
+
| Code | Meaning |
|
|
220
|
+
|------|---------|
|
|
221
|
+
| 0 | Success (2xx or 3xx HTTP status) |
|
|
222
|
+
| 1 | Request failed or 4xx/5xx HTTP status |
|
|
300
223
|
|
|
301
|
-
|
|
302
|
-
- `1` - Request failed or 4xx/5xx HTTP status
|
|
224
|
+
### Automation Tips
|
|
303
225
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
##
|
|
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 |
|
|
431
|
-
|
|
432
|
-
|
|
|
433
|
-
|
|
|
434
|
-
|
|
|
435
|
-
|
|
|
436
|
-
|
|
|
437
|
-
|
|
|
438
|
-
|
|
|
439
|
-
|
|
|
440
|
-
|
|
|
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
|
-
|
|
253
|
+
**Upgrade to Pro:** [https://mesaplex.com/mpx-api](https://mesaplex.com/mpx-api)
|
|
443
254
|
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
259
|
+
## Links
|
|
462
260
|
|
|
463
|
-
|
|
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
|
-
|
|
266
|
+
### Related Tools
|
|
466
267
|
|
|
467
|
-
- **
|
|
468
|
-
- **
|
|
469
|
-
- **
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
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';
|
package/src/commands/test.js
CHANGED
|
@@ -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));
|
package/src/lib/http-client.js
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
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
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',
|