mpx-api 1.0.2 → 1.2.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
@@ -182,6 +182,134 @@ mpx-api history -n 50
182
182
 
183
183
  Cookies are automatically saved and sent with subsequent requests. Cookie jar is stored at `~/.mpx-api/cookies.json`.
184
184
 
185
+ ## AI Agent Usage 🤖
186
+
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.
188
+
189
+ ### JSON Output
190
+
191
+ Add `--json` to any command for machine-readable output:
192
+
193
+ ```bash
194
+ # HTTP request with JSON output
195
+ mpx-api get https://api.github.com/users/octocat --json
196
+
197
+ # Output structure
198
+ {
199
+ "request": {
200
+ "method": "GET",
201
+ "url": "https://api.github.com/users/octocat",
202
+ "headers": {},
203
+ "body": null
204
+ },
205
+ "response": {
206
+ "status": 200,
207
+ "statusText": "OK",
208
+ "headers": { "content-type": "application/json" },
209
+ "body": { "login": "octocat", ... },
210
+ "rawBody": "...",
211
+ "responseTime": 145,
212
+ "size": 1234
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### Schema Discovery
218
+
219
+ AI agents can discover all available commands, flags, and output formats:
220
+
221
+ ```bash
222
+ mpx-api --schema
223
+ ```
224
+
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
235
+
236
+ Start mpx-api as an MCP (Model Context Protocol) server for AI agent integration:
237
+
238
+ ```bash
239
+ mpx-api mcp
240
+ ```
241
+
242
+ Add to your MCP client configuration (e.g., Claude Desktop, Cline):
243
+
244
+ ```json
245
+ {
246
+ "mcpServers": {
247
+ "mpx-api": {
248
+ "command": "npx",
249
+ "args": ["mpx-api", "mcp"]
250
+ }
251
+ }
252
+ }
253
+ ```
254
+
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
+ ```
296
+
297
+ ### Exit Codes
298
+
299
+ Predictable exit codes for automation:
300
+
301
+ - `0` - Success (2xx or 3xx HTTP status)
302
+ - `1` - Request failed or 4xx/5xx HTTP status
303
+
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
+
185
313
  ## Pro Features 💎
186
314
 
187
315
  Upgrade to **mpx-api Pro** ($12/mo) for advanced features:
package/bin/mpx-api.js CHANGED
@@ -15,6 +15,10 @@ import { registerHistoryCommand } from '../src/commands/history.js';
15
15
  import { registerLoadCommand } from '../src/commands/load.js';
16
16
  import { registerDocsCommand } from '../src/commands/docs.js';
17
17
 
18
+ // AI-native features
19
+ import { getSchema } from '../src/schema.js';
20
+ import { startMCPServer } from '../src/mcp.js';
21
+
18
22
  const __filename = fileURLToPath(import.meta.url);
19
23
  const __dirname = dirname(__filename);
20
24
 
@@ -22,10 +26,20 @@ const pkg = JSON.parse(
22
26
  readFileSync(join(__dirname, '../package.json'), 'utf8')
23
27
  );
24
28
 
29
+ // Handle --schema flag early (before commander parsing)
30
+ if (process.argv.includes('--schema')) {
31
+ console.log(JSON.stringify(getSchema(), null, 2));
32
+ process.exit(0);
33
+ }
34
+
25
35
  program
26
36
  .name('mpx-api')
27
37
  .description('Developer-first API testing, mocking, and documentation CLI')
28
- .version(pkg.version);
38
+ .version(pkg.version)
39
+ .enablePositionalOptions()
40
+ .passThroughOptions()
41
+ .option('-o, --output <format>', 'Output format: json for structured JSON (machine-readable)')
42
+ .option('-q, --quiet', 'Suppress non-essential output');
29
43
 
30
44
  // Register HTTP method commands (get, post, put, patch, delete, head, options)
31
45
  registerRequestCommands(program);
@@ -49,4 +63,17 @@ registerHistoryCommand(program);
49
63
  registerLoadCommand(program);
50
64
  registerDocsCommand(program);
51
65
 
66
+ // MCP subcommand
67
+ program
68
+ .command('mcp')
69
+ .description('Start MCP (Model Context Protocol) stdio server')
70
+ .action(async () => {
71
+ try {
72
+ await startMCPServer();
73
+ } catch (err) {
74
+ console.error(JSON.stringify({ error: err.message, code: 'ERR_MCP_START' }));
75
+ process.exit(1);
76
+ }
77
+ });
78
+
52
79
  program.parse();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mpx-api",
3
- "version": "1.0.2",
4
- "description": "Developer-first API testing, mocking, and documentation CLI",
3
+ "version": "1.2.0",
4
+ "description": "Developer-first API testing, mocking, and documentation CLI with AI-native features (JSON output, MCP server)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "mpx-api": "bin/mpx-api.js"
@@ -16,7 +16,11 @@
16
16
  "postman",
17
17
  "httpie",
18
18
  "openapi",
19
- "swagger"
19
+ "swagger",
20
+ "mcp",
21
+ "ai-native",
22
+ "model-context-protocol",
23
+ "automation"
20
24
  ],
21
25
  "author": "Mesaplex <support@mesaplex.com>",
22
26
  "license": "MIT",
@@ -33,12 +37,13 @@
33
37
  },
34
38
  "type": "module",
35
39
  "scripts": {
36
- "test": "node --test test/**/*.test.js",
40
+ "test": "node --test test/*.test.js",
37
41
  "test:watch": "node --test --watch test/**/*.test.js",
38
42
  "lint": "echo 'TODO: Add eslint'",
39
43
  "prepublishOnly": "npm test"
40
44
  },
41
45
  "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.26.0",
42
47
  "chalk": "^5.3.0",
43
48
  "commander": "^12.0.0",
44
49
  "yaml": "^2.3.4",
@@ -1,7 +1,7 @@
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 } from '../lib/output.js';
4
+ import { formatSuccess, formatError, formatInfo, formatWarning } from '../lib/output.js';
5
5
  import { runCollection } from '../lib/collection-runner.js';
6
6
  import { formatTestResults } from '../lib/output.js';
7
7
  import { join } from 'path';
@@ -95,7 +95,8 @@ export function registerCollectionCommands(program) {
95
95
  .description('Run a collection')
96
96
  .option('-e, --env <name>', 'Environment to use')
97
97
  .option('--base-url <url>', 'Override base URL')
98
- .action(async (file, options) => {
98
+ .option('--json', 'Output results as JSON')
99
+ .action(async (file, options, command) => {
99
100
  try {
100
101
  const collectionPath = file || join('.mpx-api', 'collection.yaml');
101
102
 
@@ -123,9 +124,17 @@ export function registerCollectionCommands(program) {
123
124
 
124
125
  const results = await runCollection(collection, { env, baseUrl });
125
126
 
126
- const allPassed = formatTestResults(results);
127
+ const globalOpts = command.optsWithGlobals();
128
+ const jsonOutput = options.json || globalOpts.output === 'json';
127
129
 
128
- process.exit(allPassed ? 0 : 1);
130
+ if (jsonOutput) {
131
+ console.log(JSON.stringify(results, null, 2));
132
+ const allPassed = results.every(r => r.passed);
133
+ process.exit(allPassed ? 0 : 1);
134
+ } else {
135
+ const allPassed = formatTestResults(results);
136
+ process.exit(allPassed ? 0 : 1);
137
+ }
129
138
  } catch (err) {
130
139
  formatError(err);
131
140
  process.exit(1);
@@ -170,6 +179,4 @@ export function registerCollectionCommands(program) {
170
179
  });
171
180
  }
172
181
 
173
- function formatWarning(message) {
174
- console.log(chalk.yellow('⚠'), message);
175
- }
182
+ // formatWarning imported from output.js
@@ -17,8 +17,14 @@ export function registerRequestCommands(program) {
17
17
  .option('--no-follow', 'Do not follow redirects')
18
18
  .option('--no-verify', 'Skip SSL certificate verification')
19
19
  .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
20
- .action(async (url, options) => {
20
+ .option('-o, --output <format>', 'Output format (json)')
21
+ .action(async (url, options, command) => {
21
22
  try {
23
+ // Get global options
24
+ const globalOpts = command.optsWithGlobals();
25
+ const jsonOutput = options.output === 'json' || globalOpts.output === 'json';
26
+ const quiet = options.quiet || globalOpts.quiet || false;
27
+
22
28
  const client = new HttpClient({
23
29
  followRedirects: options.follow,
24
30
  verifySsl: options.verify,
@@ -34,7 +40,11 @@ export function registerRequestCommands(program) {
34
40
  try {
35
41
  requestOptions.json = JSON.parse(options.json);
36
42
  } catch (err) {
37
- formatError(new Error(`Invalid JSON: ${err.message}`));
43
+ if (jsonOutput) {
44
+ console.log(JSON.stringify({ error: `Invalid JSON: ${err.message}` }));
45
+ } else {
46
+ formatError(new Error(`Invalid JSON: ${err.message}`));
47
+ }
38
48
  process.exit(1);
39
49
  }
40
50
  } else if (options.data) {
@@ -57,7 +67,14 @@ export function registerRequestCommands(program) {
57
67
  // Format and display response
58
68
  formatResponse(response, {
59
69
  verbose: options.verbose,
60
- quiet: options.quiet,
70
+ quiet: quiet,
71
+ jsonOutput: jsonOutput,
72
+ request: {
73
+ method: method.toUpperCase(),
74
+ url,
75
+ headers: requestOptions.headers,
76
+ body: requestOptions.json || requestOptions.body,
77
+ }
61
78
  });
62
79
 
63
80
  // Exit with non-zero code for 4xx/5xx errors
@@ -65,7 +82,12 @@ export function registerRequestCommands(program) {
65
82
  process.exit(1);
66
83
  }
67
84
  } catch (err) {
68
- formatError(err);
85
+ const globalOpts = command.optsWithGlobals();
86
+ if (options.output === 'json' || globalOpts.output === 'json') {
87
+ console.log(JSON.stringify({ error: err.message, code: err.code || 'ERR_REQUEST' }));
88
+ } else {
89
+ formatError(err);
90
+ }
69
91
  process.exit(1);
70
92
  }
71
93
  });
@@ -1,3 +1,5 @@
1
+ import { getNestedValue } from './utils.js';
2
+
1
3
  // Assertion engine for testing
2
4
 
3
5
  export function runAssertions(response, assertions) {
@@ -75,27 +77,7 @@ export function runAssertions(response, assertions) {
75
77
  return results;
76
78
  }
77
79
 
78
- function getNestedValue(obj, path) {
79
- const parts = path.split('.');
80
- let current = obj;
81
-
82
- for (const part of parts) {
83
- if (current === null || current === undefined) return undefined;
84
-
85
- // Handle array indexing
86
- const arrayMatch = part.match(/^(.+?)\[(\d+)\]$/);
87
- if (arrayMatch) {
88
- const [, key, index] = arrayMatch;
89
- current = current[key];
90
- if (!Array.isArray(current)) return undefined;
91
- current = current[parseInt(index)];
92
- } else {
93
- current = current[part];
94
- }
95
- }
96
-
97
- return current;
98
- }
80
+ // getNestedValue imported from utils.js
99
81
 
100
82
  function evaluateOperators(actual, expected) {
101
83
  for (const [op, value] of Object.entries(expected)) {
@@ -1,8 +1,14 @@
1
1
  import { request } from 'undici';
2
2
  import { CookieJar } from 'tough-cookie';
3
3
  import { readFileSync, writeFileSync, existsSync } from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
4
6
  import { getCookieJarPath } from './config.js';
5
7
 
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const pkgVersion = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8')).version;
11
+
6
12
  export class HttpClient {
7
13
  constructor(options = {}) {
8
14
  this.followRedirects = options.followRedirects !== false;
@@ -40,7 +46,7 @@ export class HttpClient {
40
46
 
41
47
  // Set default User-Agent, allow override via options.headers
42
48
  const defaultHeaders = {
43
- 'user-agent': 'mpx-api/1.0.1',
49
+ 'user-agent': `mpx-api/${pkgVersion}`,
44
50
  };
45
51
 
46
52
  // Merge headers, with user headers taking precedence
@@ -52,7 +58,18 @@ export class HttpClient {
52
58
  body: options.body,
53
59
  };
54
60
 
55
- // Note: undici follows redirects by default, maxRedirections removed in newer versions
61
+ // Handle redirects
62
+ if (!this.followRedirects) {
63
+ requestOptions.maxRedirections = 0;
64
+ }
65
+
66
+ // Handle timeout
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;
72
+ }
56
73
 
57
74
  // Add cookies to request
58
75
  const cookies = await this.cookieJar.getCookies(url);
@@ -72,7 +89,10 @@ export class HttpClient {
72
89
  }
73
90
 
74
91
  try {
92
+ const timeoutId = requestOptions._timeoutId;
93
+ delete requestOptions._timeoutId;
75
94
  const response = await request(url, requestOptions);
95
+ if (timeoutId) clearTimeout(timeoutId);
76
96
 
77
97
  // Store cookies from response
78
98
  const setCookieHeaders = response.headers['set-cookie'];
@@ -119,7 +139,9 @@ export class HttpClient {
119
139
  };
120
140
  } catch (err) {
121
141
  // Handle network errors gracefully
122
- if (err.code === 'ENOTFOUND') {
142
+ if (err.name === 'AbortError' || err.code === 'UND_ERR_ABORTED') {
143
+ throw new Error(`Request timeout after ${this.timeout}ms`);
144
+ } else if (err.code === 'ENOTFOUND') {
123
145
  throw new Error(`DNS lookup failed for ${url}`);
124
146
  } else if (err.code === 'ECONNREFUSED') {
125
147
  throw new Error(`Connection refused to ${url}`);
package/src/lib/output.js CHANGED
@@ -3,8 +3,34 @@ import { highlight } from 'cli-highlight';
3
3
 
4
4
  const MAX_BODY_SIZE = 50 * 1024; // 50KB max for terminal display
5
5
 
6
+ export function formatResponseJSON(response, request = {}) {
7
+ return JSON.stringify({
8
+ request: {
9
+ method: request.method || response.method,
10
+ url: request.url || response.url,
11
+ headers: request.headers || {},
12
+ body: request.body || null
13
+ },
14
+ response: {
15
+ status: response.status,
16
+ statusText: response.statusText,
17
+ headers: response.headers,
18
+ body: response.body,
19
+ rawBody: response.rawBody,
20
+ responseTime: response.responseTime,
21
+ size: response.size
22
+ }
23
+ }, null, 2);
24
+ }
25
+
6
26
  export function formatResponse(response, options = {}) {
7
- const { verbose = false, quiet = false } = options;
27
+ const { verbose = false, quiet = false, jsonOutput = false } = options;
28
+
29
+ // Handle JSON output mode
30
+ if (jsonOutput) {
31
+ console.log(formatResponseJSON(response, options.request || {}));
32
+ return;
33
+ }
8
34
 
9
35
  if (quiet) {
10
36
  // Only output body
@@ -1,3 +1,5 @@
1
+ import { getNestedValue } from './utils.js';
2
+
1
3
  // Template variable interpolation
2
4
  // Supports: {{varName}}, {{env.VAR}}, {{request-name.response.body.field}}
3
5
 
@@ -39,25 +41,4 @@ export function interpolateObject(obj, context = {}) {
39
41
  return obj;
40
42
  }
41
43
 
42
- function getNestedValue(obj, path) {
43
- // Handle array indexing: users[0].name
44
- const parts = path.split('.');
45
- let current = obj;
46
-
47
- for (const part of parts) {
48
- if (!current) return undefined;
49
-
50
- // Check for array indexing
51
- const arrayMatch = part.match(/^(.+?)\[(\d+)\]$/);
52
- if (arrayMatch) {
53
- const [, key, index] = arrayMatch;
54
- current = current[key];
55
- if (!Array.isArray(current)) return undefined;
56
- current = current[parseInt(index)];
57
- } else {
58
- current = current[part];
59
- }
60
- }
61
-
62
- return current;
63
- }
44
+ // getNestedValue imported from utils.js
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Get a nested value from an object using dot notation.
3
+ * Supports array indexing: users[0].name
4
+ */
5
+ export function getNestedValue(obj, path) {
6
+ const parts = path.split('.');
7
+ let current = obj;
8
+
9
+ for (const part of parts) {
10
+ if (current === null || current === undefined) return undefined;
11
+
12
+ // Handle array indexing
13
+ const arrayMatch = part.match(/^(.+?)\[(\d+)\]$/);
14
+ if (arrayMatch) {
15
+ const [, key, index] = arrayMatch;
16
+ current = current[key];
17
+ if (!Array.isArray(current)) return undefined;
18
+ current = current[parseInt(index)];
19
+ } else {
20
+ current = current[part];
21
+ }
22
+ }
23
+
24
+ return current;
25
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * MCP (Model Context Protocol) Server
3
+ *
4
+ * Exposes mpx-api capabilities as MCP tools for AI agent integration.
5
+ * Runs over stdio transport.
6
+ */
7
+
8
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import {
11
+ ListToolsRequestSchema,
12
+ CallToolRequestSchema
13
+ } from '@modelcontextprotocol/sdk/types.js';
14
+
15
+ import { HttpClient } from './lib/http-client.js';
16
+ import { getSchema } from './schema.js';
17
+ import { readFileSync } from 'fs';
18
+ import { fileURLToPath } from 'url';
19
+ import { dirname, join } from 'path';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
24
+
25
+ export async function startMCPServer() {
26
+ const server = new Server(
27
+ { name: 'mpx-api', version: pkg.version },
28
+ { capabilities: { tools: {} } }
29
+ );
30
+
31
+ // List available tools
32
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
33
+ return {
34
+ tools: [
35
+ {
36
+ name: 'http_request',
37
+ description: 'Send an HTTP request (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). Returns structured response with status, headers, body, and timing.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ method: {
42
+ type: 'string',
43
+ enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
44
+ description: 'HTTP method'
45
+ },
46
+ url: {
47
+ type: 'string',
48
+ description: 'Target URL'
49
+ },
50
+ headers: {
51
+ type: 'object',
52
+ description: 'Request headers as key-value pairs',
53
+ additionalProperties: { type: 'string' }
54
+ },
55
+ json: {
56
+ type: 'object',
57
+ description: 'JSON body (automatically sets Content-Type: application/json)'
58
+ },
59
+ body: {
60
+ type: 'string',
61
+ description: 'Raw request body (use either json or body, not both)'
62
+ },
63
+ followRedirects: {
64
+ type: 'boolean',
65
+ default: true,
66
+ description: 'Follow HTTP redirects'
67
+ },
68
+ verifySsl: {
69
+ type: 'boolean',
70
+ default: true,
71
+ description: 'Verify SSL certificates'
72
+ },
73
+ timeout: {
74
+ type: 'number',
75
+ default: 30000,
76
+ description: 'Request timeout in milliseconds'
77
+ }
78
+ },
79
+ required: ['method', 'url']
80
+ }
81
+ },
82
+ {
83
+ name: 'get_schema',
84
+ description: 'Get the full JSON schema describing all mpx-api commands, flags, and output formats.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {}
88
+ }
89
+ }
90
+ ]
91
+ };
92
+ });
93
+
94
+ // Handle tool calls
95
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
96
+ const { name, arguments: args } = request.params;
97
+
98
+ try {
99
+ switch (name) {
100
+ case 'http_request': {
101
+ const client = new HttpClient({
102
+ followRedirects: args.followRedirects !== false,
103
+ verifySsl: args.verifySsl !== false,
104
+ timeout: args.timeout || 30000,
105
+ });
106
+
107
+ const requestOptions = {
108
+ headers: args.headers || {},
109
+ };
110
+
111
+ // Handle JSON or raw body
112
+ if (args.json) {
113
+ requestOptions.json = args.json;
114
+ } else if (args.body) {
115
+ requestOptions.body = args.body;
116
+ }
117
+
118
+ const method = (args.method || 'GET').toLowerCase();
119
+ const response = await client.request(method, args.url, requestOptions);
120
+
121
+ // Return structured response
122
+ const result = {
123
+ request: {
124
+ method: method.toUpperCase(),
125
+ url: args.url,
126
+ headers: requestOptions.headers,
127
+ body: args.json || args.body || null
128
+ },
129
+ response: {
130
+ status: response.status,
131
+ statusText: response.statusText,
132
+ headers: response.headers,
133
+ body: response.body,
134
+ rawBody: response.rawBody,
135
+ responseTime: response.responseTime,
136
+ size: response.size
137
+ }
138
+ };
139
+
140
+ return {
141
+ content: [{
142
+ type: 'text',
143
+ text: JSON.stringify(result, null, 2)
144
+ }]
145
+ };
146
+ }
147
+
148
+ case 'get_schema': {
149
+ return {
150
+ content: [{
151
+ type: 'text',
152
+ text: JSON.stringify(getSchema(), null, 2)
153
+ }]
154
+ };
155
+ }
156
+
157
+ default:
158
+ return {
159
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
160
+ isError: true
161
+ };
162
+ }
163
+ } catch (err) {
164
+ return {
165
+ content: [{
166
+ type: 'text',
167
+ text: JSON.stringify({
168
+ error: err.message,
169
+ code: err.code || 'ERR_REQUEST',
170
+ stack: err.stack
171
+ }, null, 2)
172
+ }],
173
+ isError: true
174
+ };
175
+ }
176
+ });
177
+
178
+ const transport = new StdioServerTransport();
179
+ await server.connect(transport);
180
+ }
package/src/schema.js ADDED
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Schema Module
3
+ *
4
+ * Returns a machine-readable JSON schema describing all commands,
5
+ * flags, inputs, and outputs for AI agent discovery.
6
+ */
7
+
8
+ import { readFileSync } from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
15
+
16
+ export function getSchema() {
17
+ return {
18
+ tool: 'mpx-api',
19
+ version: pkg.version,
20
+ description: pkg.description,
21
+ homepage: pkg.homepage,
22
+ commands: {
23
+ 'http-methods': {
24
+ description: 'Send HTTP requests (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)',
25
+ usage: 'mpx-api <method> <url> [options]',
26
+ methods: ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'],
27
+ arguments: {
28
+ url: {
29
+ type: 'string',
30
+ required: true,
31
+ description: 'Target URL for the HTTP request'
32
+ }
33
+ },
34
+ flags: {
35
+ '-H, --header': {
36
+ type: 'array',
37
+ description: 'Add request headers (format: "key:value" or "key: value")',
38
+ repeatable: true
39
+ },
40
+ '-j, --json': {
41
+ type: 'string',
42
+ description: 'Send JSON data (automatically sets Content-Type: application/json)'
43
+ },
44
+ '-d, --data': {
45
+ type: 'string',
46
+ description: 'Send raw request body'
47
+ },
48
+ '-v, --verbose': {
49
+ type: 'boolean',
50
+ default: false,
51
+ description: 'Show response headers in output'
52
+ },
53
+ '-q, --quiet': {
54
+ type: 'boolean',
55
+ default: false,
56
+ description: 'Only output response body (no headers, no formatting)'
57
+ },
58
+ '--no-follow': {
59
+ type: 'boolean',
60
+ default: false,
61
+ description: 'Do not follow HTTP redirects'
62
+ },
63
+ '--no-verify': {
64
+ type: 'boolean',
65
+ default: false,
66
+ description: 'Skip SSL certificate verification'
67
+ },
68
+ '--timeout': {
69
+ type: 'number',
70
+ default: 30000,
71
+ description: 'Request timeout in milliseconds'
72
+ }
73
+ },
74
+ output: {
75
+ json: {
76
+ description: 'Structured response data when --output json is used',
77
+ schema: {
78
+ type: 'object',
79
+ properties: {
80
+ request: {
81
+ type: 'object',
82
+ properties: {
83
+ method: { type: 'string' },
84
+ url: { type: 'string' },
85
+ headers: { type: 'object' },
86
+ body: { type: ['string', 'object', 'null'] }
87
+ }
88
+ },
89
+ response: {
90
+ type: 'object',
91
+ properties: {
92
+ status: { type: 'number' },
93
+ statusText: { type: 'string' },
94
+ headers: { type: 'object' },
95
+ body: { type: ['string', 'object', 'null'] },
96
+ rawBody: { type: 'string' },
97
+ responseTime: { type: 'number', description: 'Response time in milliseconds' },
98
+ size: { type: 'number', description: 'Response size in bytes' }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ },
104
+ error: {
105
+ description: 'Error response when request fails',
106
+ schema: {
107
+ type: 'object',
108
+ properties: {
109
+ error: { type: 'string' },
110
+ message: { type: 'string' },
111
+ code: { type: 'string' }
112
+ }
113
+ }
114
+ }
115
+ },
116
+ exitCodes: {
117
+ 0: 'Success (2xx or 3xx status)',
118
+ 1: 'Request error or 4xx/5xx status'
119
+ },
120
+ examples: [
121
+ { command: 'mpx-api get https://api.example.com/users --output json', description: 'GET request with JSON output' },
122
+ { command: 'mpx-api post https://api.example.com/users -j \'{"name":"John"}\' --output json', description: 'POST JSON data with structured output' },
123
+ { command: 'mpx-api get https://api.example.com/data -H "Authorization: Bearer token"', description: 'GET with custom headers' }
124
+ ]
125
+ },
126
+ collection: {
127
+ description: 'Manage and run request collections',
128
+ subcommands: {
129
+ init: {
130
+ usage: 'mpx-api collection init [options]',
131
+ description: 'Initialize a new collection in current directory',
132
+ flags: {
133
+ '-n, --name': {
134
+ type: 'string',
135
+ default: 'API Collection',
136
+ description: 'Collection name'
137
+ }
138
+ }
139
+ },
140
+ add: {
141
+ usage: 'mpx-api collection add <name> <method> <url> [options]',
142
+ description: 'Add a request to the collection',
143
+ arguments: {
144
+ name: { type: 'string', required: true, description: 'Request name' },
145
+ method: { type: 'string', required: true, description: 'HTTP method' },
146
+ url: { type: 'string', required: true, description: 'Request URL' }
147
+ },
148
+ flags: {
149
+ '-H, --header': { type: 'array', description: 'Request headers' },
150
+ '-j, --json': { type: 'string', description: 'JSON body' },
151
+ '-d, --data': { type: 'string', description: 'Request body' }
152
+ }
153
+ },
154
+ run: {
155
+ usage: 'mpx-api collection run [file] [options]',
156
+ description: 'Run a collection',
157
+ arguments: {
158
+ file: { type: 'string', required: false, description: 'Collection file (default: .mpx-api/collection.yaml)' }
159
+ },
160
+ flags: {
161
+ '-e, --env': { type: 'string', description: 'Environment name to use' },
162
+ '--base-url': { type: 'string', description: 'Override base URL' },
163
+ '--json': { type: 'boolean', description: 'Output results as JSON' }
164
+ }
165
+ },
166
+ list: {
167
+ usage: 'mpx-api collection list [file]',
168
+ description: 'List requests in a collection'
169
+ }
170
+ }
171
+ },
172
+ env: {
173
+ description: 'Manage environments',
174
+ subcommands: {
175
+ init: {
176
+ usage: 'mpx-api env init',
177
+ description: 'Initialize environments directory with dev, staging, production'
178
+ },
179
+ set: {
180
+ usage: 'mpx-api env set <environment> <variable>',
181
+ description: 'Set an environment variable (KEY=value format)',
182
+ arguments: {
183
+ environment: { type: 'string', required: true, description: 'Environment name' },
184
+ variable: { type: 'string', required: true, description: 'Variable in KEY=value format' }
185
+ }
186
+ },
187
+ list: {
188
+ usage: 'mpx-api env list [environment]',
189
+ description: 'List all environments or variables in a specific environment',
190
+ arguments: {
191
+ environment: { type: 'string', required: false, description: 'Environment name (optional)' }
192
+ }
193
+ }
194
+ }
195
+ },
196
+ mock: {
197
+ description: 'Start a mock API server',
198
+ usage: 'mpx-api mock [file] [options]',
199
+ arguments: {
200
+ file: { type: 'string', required: false, description: 'Mock definition file' }
201
+ },
202
+ flags: {
203
+ '-p, --port': { type: 'number', default: 3000, description: 'Server port' },
204
+ '--watch': { type: 'boolean', description: 'Watch file for changes' }
205
+ }
206
+ },
207
+ test: {
208
+ description: 'Run API tests',
209
+ usage: 'mpx-api test <file> [options]',
210
+ arguments: {
211
+ file: { type: 'string', required: false, description: 'Test file to run (default: .mpx-api/collection.yaml)' }
212
+ },
213
+ flags: {
214
+ '-e, --env': { type: 'string', description: 'Environment name' },
215
+ '--json': { type: 'boolean', description: 'Output results as JSON' }
216
+ }
217
+ },
218
+ history: {
219
+ description: 'View request history',
220
+ usage: 'mpx-api history [options]',
221
+ flags: {
222
+ '-n, --limit': { type: 'number', default: 10, description: 'Number of entries to show' },
223
+ '--clear': { type: 'boolean', description: 'Clear history' }
224
+ }
225
+ },
226
+ load: {
227
+ description: 'Load test API endpoint (Pro)',
228
+ usage: 'mpx-api load <url> [options]',
229
+ arguments: {
230
+ url: { type: 'string', required: true, description: 'Target URL' }
231
+ },
232
+ flags: {
233
+ '--rps': { type: 'number', default: 10, description: 'Requests per second' },
234
+ '-d, --duration': { type: 'number', default: 10, description: 'Test duration in seconds' },
235
+ '-H, --header': { type: 'array', description: 'Add request headers' },
236
+ '--method': { type: 'string', default: 'GET', description: 'HTTP method' }
237
+ }
238
+ },
239
+ docs: {
240
+ description: 'Generate API documentation (Pro)',
241
+ usage: 'mpx-api docs <file> [options]',
242
+ arguments: {
243
+ file: { type: 'string', required: true, description: 'Collection or OpenAPI file' }
244
+ },
245
+ flags: {
246
+ '-o, --output': { type: 'string', description: 'Output directory' },
247
+ '--format': { type: 'string', enum: ['html', 'markdown'], default: 'html', description: 'Output format' }
248
+ }
249
+ },
250
+ mcp: {
251
+ description: 'Start MCP (Model Context Protocol) stdio server for AI agent integration',
252
+ usage: 'mpx-api mcp',
253
+ examples: [
254
+ { command: 'mpx-api mcp', description: 'Start MCP stdio server' }
255
+ ]
256
+ }
257
+ },
258
+ globalFlags: {
259
+ '-o, --output': {
260
+ type: 'string',
261
+ description: 'Output format: "json" for structured JSON for machine consumption'
262
+ },
263
+ '-q, --quiet': {
264
+ type: 'boolean',
265
+ default: false,
266
+ description: 'Suppress non-essential output'
267
+ },
268
+ '--schema': {
269
+ type: 'boolean',
270
+ default: false,
271
+ description: 'Output this schema as JSON'
272
+ },
273
+ '--version': {
274
+ type: 'boolean',
275
+ description: 'Show version number'
276
+ },
277
+ '--help': {
278
+ type: 'boolean',
279
+ description: 'Show help information'
280
+ }
281
+ },
282
+ mcpConfig: {
283
+ description: 'Add to your MCP client configuration to use mpx-api as an AI tool',
284
+ config: {
285
+ mcpServers: {
286
+ 'mpx-api': {
287
+ command: 'npx',
288
+ args: ['mpx-api', 'mcp']
289
+ }
290
+ }
291
+ }
292
+ }
293
+ };
294
+ }