mpx-scan 1.0.2 → 1.1.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
@@ -1,6 +1,6 @@
1
1
  # mpx-scan 🔍
2
2
 
3
- **Professional website security scanner for developers**
3
+ **Professional website security scanner for developers and AI agents**
4
4
 
5
5
  Check your site's security headers, SSL/TLS configuration, DNS settings, and get actionable fix suggestions — all from your terminal.
6
6
 
@@ -13,10 +13,14 @@ Part of the [Mesaplex](https://mesaplex.com) developer toolchain.
13
13
 
14
14
  - **Zero-config security scanning** — just point it at a URL
15
15
  - **Beautiful terminal output** with color-coded results
16
+ - **Structured JSON output** — `--json` for CI/CD and AI agent consumption
17
+ - **MCP server** — integrates with any MCP-compatible AI agent (Claude, GPT, Cursor, etc.)
16
18
  - **Actionable fix suggestions** — copy-paste config for nginx, Apache, Caddy, Cloudflare
19
+ - **Batch scanning** — pipe URLs from stdin
20
+ - **Self-documenting** — `--schema` returns machine-readable tool description
17
21
  - **Fast** — scans complete in seconds
18
22
  - **Zero native dependencies** — installs cleanly everywhere
19
- - **CI/CD ready** — JSON output and exit codes for automated testing
23
+ - **CI/CD ready** — predictable exit codes and JSON output
20
24
 
21
25
  ### Security Checks
22
26
 
@@ -50,40 +54,20 @@ mpx-scan https://example.com
50
54
  mpx-scan https://example.com
51
55
  ```
52
56
 
53
- ![Example output](https://example.com/mpx-scan-demo.gif)
54
-
55
- ### Get Fix Suggestions
56
-
57
- ```bash
58
- mpx-scan https://example.com --fix nginx
59
- mpx-scan https://example.com --fix apache
60
- mpx-scan https://example.com --fix caddy
61
- mpx-scan https://example.com --fix cloudflare
62
- ```
63
-
64
- Generates copy-paste configuration snippets for your platform.
65
-
66
- ### Deep Scan (Pro)
67
-
68
- ```bash
69
- mpx-scan https://example.com --full
70
- ```
71
-
72
- Runs all security checks including DNS, cookies, SRI, exposed files.
73
-
74
- ### JSON Output (Pro)
57
+ ### JSON Output
75
58
 
76
59
  ```bash
77
60
  mpx-scan https://example.com --json
78
61
  ```
79
62
 
80
- Perfect for CI/CD pipelines:
63
+ Returns structured JSON to stdout (progress/status goes to stderr):
81
64
 
82
65
  ```json
83
66
  {
84
67
  "mpxScan": {
85
- "version": "1.0.0",
86
- "scannedAt": "2026-02-15T22:00:00.000Z"
68
+ "version": "1.1.0",
69
+ "scannedAt": "2026-02-16T22:00:00.000Z",
70
+ "scanDuration": 350
87
71
  },
88
72
  "target": {
89
73
  "url": "https://example.com",
@@ -98,28 +82,123 @@ Perfect for CI/CD pipelines:
98
82
  "summary": {
99
83
  "passed": 12,
100
84
  "warnings": 3,
101
- "failed": 2
102
- }
85
+ "failed": 2,
86
+ "info": 0
87
+ },
88
+ "sections": { ... },
89
+ "tier": "free"
103
90
  }
104
91
  ```
105
92
 
93
+ ### Get Fix Suggestions
94
+
95
+ ```bash
96
+ mpx-scan https://example.com --fix nginx
97
+ mpx-scan https://example.com --fix apache
98
+ mpx-scan https://example.com --fix caddy
99
+ mpx-scan https://example.com --fix cloudflare
100
+ ```
101
+
102
+ ### Deep Scan (Pro)
103
+
104
+ ```bash
105
+ mpx-scan https://example.com --full
106
+ ```
107
+
106
108
  ### Brief Output
107
109
 
108
110
  ```bash
109
111
  mpx-scan https://example.com --brief
110
112
  ```
111
113
 
112
- One-line summary — great for monitoring multiple sites.
114
+ ### Batch Scanning
113
115
 
114
- ## 🎯 Use Cases
116
+ ```bash
117
+ cat urls.txt | mpx-scan --batch --json
118
+ ```
119
+
120
+ Reads one URL per line from stdin, outputs one JSON result per line (JSONL format). Lines starting with `#` are ignored.
121
+
122
+ ### Tool Schema
123
+
124
+ ```bash
125
+ mpx-scan --schema
126
+ ```
127
+
128
+ Returns a JSON schema describing all commands, flags, inputs, and outputs — designed for AI agent tool discovery.
129
+
130
+ ## 🤖 AI Agent Usage
131
+
132
+ mpx-scan is designed to be used by AI agents as well as humans.
133
+
134
+ ### MCP Integration
135
+
136
+ Add to your MCP client configuration (Claude Desktop, Cursor, Windsurf, etc.):
115
137
 
116
- ### Local Development
138
+ ```json
139
+ {
140
+ "mcpServers": {
141
+ "mpx-scan": {
142
+ "command": "npx",
143
+ "args": ["mpx-scan", "mcp"]
144
+ }
145
+ }
146
+ }
147
+ ```
148
+
149
+ The MCP server exposes these tools:
150
+ - **`scan`** — Scan a URL and return structured results
151
+ - **`generate_fixes`** — Scan and generate platform-specific fix config
152
+ - **`get_schema`** — Get full tool schema
153
+
154
+ ### Programmatic Usage
117
155
 
118
156
  ```bash
119
- mpx-scan http://localhost:3000 --fix nginx
157
+ # JSON output for parsing
158
+ mpx-scan https://example.com --json
159
+
160
+ # Batch processing
161
+ cat urls.txt | mpx-scan --batch --json
162
+
163
+ # Schema discovery
164
+ mpx-scan --schema
165
+
166
+ # Quiet mode (no banners, progress goes to stderr)
167
+ mpx-scan https://example.com --json --quiet
168
+ ```
169
+
170
+ ### Exit Codes
171
+
172
+ | Code | Meaning |
173
+ |------|---------|
174
+ | 0 | Scan complete, no security issues found |
175
+ | 1 | Scan complete, security issues found |
176
+ | 2 | Invalid arguments |
177
+ | 3 | Configuration error (license, rate limit) |
178
+ | 4 | Network/connectivity error |
179
+
180
+ ### Error Responses (JSON mode)
181
+
182
+ When `--json` is used, errors return structured JSON:
183
+
184
+ ```json
185
+ {
186
+ "error": "Description of what went wrong",
187
+ "code": "ERR_NETWORK"
188
+ }
120
189
  ```
121
190
 
122
- Check your security before deploying.
191
+ Error codes: `ERR_NETWORK`, `ERR_SCAN`, `ERR_RATE_LIMIT`, `ERR_PRO_REQUIRED`, `ERR_NO_INPUT`
192
+
193
+ ### Automation Tips
194
+
195
+ - Use `--json` for machine-parseable output (stdout only, no ANSI)
196
+ - Use `--no-color` to strip ANSI codes from human-readable output
197
+ - Use `--quiet` to suppress banners and progress info
198
+ - Pipe `--batch --json` for JSONL (one result per line) processing
199
+ - Check exit codes for pass/fail decisions in CI/CD
200
+
201
+ ## 🎯 Use Cases
123
202
 
124
203
  ### CI/CD Integration
125
204
 
@@ -131,14 +210,17 @@ jobs:
131
210
  scan:
132
211
  runs-on: ubuntu-latest
133
212
  steps:
134
- - run: npx mpx-scan https://mysite.com --json
213
+ - run: npx mpx-scan https://mysite.com --ci --min-score 70 --json
135
214
  ```
136
215
 
137
- ### Batch Scanning (Pro)
216
+ ### Monitoring Script
138
217
 
139
218
  ```bash
219
+ #!/bin/bash
140
220
  for site in site1.com site2.com site3.com; do
141
- mpx-scan $site --json >> security-report.jsonl
221
+ result=$(npx mpx-scan "$site" --json 2>/dev/null)
222
+ grade=$(echo "$result" | jq -r '.score.grade')
223
+ echo "$site: $grade"
142
224
  done
143
225
  ```
144
226
 
@@ -150,38 +232,27 @@ done
150
232
  | **Security headers** | ✅ | ✅ |
151
233
  | **SSL/TLS checks** | ✅ | ✅ |
152
234
  | **Server info checks** | ✅ | ✅ |
235
+ | **JSON output** | ✅ | ✅ |
236
+ | **Batch scanning** | ✅ | ✅ |
237
+ | **MCP server** | ✅ | ✅ |
153
238
  | **DNS security** | ❌ | ✅ |
154
239
  | **Cookie security** | ❌ | ✅ |
155
240
  | **SRI checks** | ❌ | ✅ |
156
241
  | **Exposed files** | ❌ | ✅ |
157
242
  | **Mixed content** | ❌ | ✅ |
158
- | **JSON export** | ❌ | ✅ |
159
- | **Batch scanning** | ❌ | ✅ |
160
- | **CI/CD integration** | ❌ | ✅ |
243
+ | **Full scan (--full)** | ❌ | ✅ |
161
244
 
162
245
  **Upgrade to Pro:** [https://mesaplex.com/mpx-scan](https://mesaplex.com/mpx-scan)
163
246
 
164
247
  ## 🔐 License Management
165
248
 
166
- ### Check License Status
167
-
168
- ```bash
169
- mpx-scan license
170
- ```
171
-
172
- ### Activate Pro License
173
-
174
- ```bash
175
- mpx-scan activate MPX-PRO-XXXXXXXXXXXXXXXX
176
- ```
177
-
178
- ### Deactivate
179
-
180
249
  ```bash
181
- mpx-scan deactivate
250
+ mpx-scan license # Check status
251
+ mpx-scan activate MPX-PRO-XXXXXXXX # Activate Pro
252
+ mpx-scan deactivate # Return to free tier
182
253
  ```
183
254
 
184
- ## 🛠️ CLI Options
255
+ ## 🛠️ CLI Reference
185
256
 
186
257
  ```
187
258
  Usage: mpx-scan [url] [options]
@@ -190,48 +261,44 @@ Arguments:
190
261
  url URL to scan
191
262
 
192
263
  Options:
193
- -V, --version output the version number
264
+ -V, --version Output version number
265
+ --json Output as structured JSON
194
266
  --full Run all checks (Pro only)
195
- --json Output as JSON (Pro only)
196
- --brief Brief output (one-line summary)
267
+ --brief Brief one-line output
268
+ --quiet, -q Minimal output (no banners)
269
+ --no-color Disable ANSI color codes
270
+ --batch Read URLs from stdin (one per line)
271
+ --schema Output JSON schema for tool discovery
197
272
  --fix <platform> Generate fix config (nginx, apache, caddy, cloudflare)
198
- --timeout <seconds> Connection timeout (default: "10")
199
- -h, --help display help for command
273
+ --timeout <seconds> Connection timeout (default: 10)
274
+ --ci CI mode: exit 1 if below --min-score
275
+ --min-score <score> Minimum score for CI mode (default: 70)
276
+ -h, --help Display help
200
277
 
201
278
  Commands:
202
- license Manage your mpx-scan license
203
- activate <key> Activate a Pro license
204
- deactivate Deactivate license
279
+ license Show license status
280
+ activate <key> Activate Pro license
281
+ deactivate Return to free tier
282
+ mcp Start MCP stdio server
205
283
  ```
206
284
 
207
285
  ## 📦 Installation
208
286
 
209
- ### Global Install
210
-
211
287
  ```bash
288
+ # Global
212
289
  npm install -g mpx-scan
213
- ```
214
-
215
- ### Project Dependency
216
290
 
217
- ```bash
291
+ # Project dependency
218
292
  npm install --save-dev mpx-scan
219
- ```
220
-
221
- Add to `package.json`:
222
293
 
223
- ```json
224
- {
225
- "scripts": {
226
- "security": "mpx-scan https://mysite.com"
227
- }
228
- }
294
+ # One-off with npx
295
+ npx mpx-scan https://example.com
229
296
  ```
230
297
 
231
298
  ### Requirements
232
299
 
233
300
  - Node.js 18.0.0 or higher
234
- - No other dependencies required for scanning
301
+ - No native dependencies
235
302
  - Works on macOS, Linux, Windows
236
303
 
237
304
  ## 🧪 Testing
@@ -240,11 +307,9 @@ Add to `package.json`:
240
307
  npm test
241
308
  ```
242
309
 
243
- Runs the built-in test suite for core scanning logic.
244
-
245
310
  ## 🤝 Contributing
246
311
 
247
- This is a commercial product with a free tier. Security improvements and bug fixes are welcome!
312
+ Security improvements and bug fixes are welcome!
248
313
 
249
314
  ## 📄 License
250
315
 
@@ -255,22 +320,15 @@ See [LICENSE](LICENSE) for full terms.
255
320
  ## 🔗 Links
256
321
 
257
322
  - **Website:** [https://mesaplex.com/mpx-scan](https://mesaplex.com/mpx-scan)
258
- - **Documentation:** [https://docs.mesaplex.com/mpx-scan](https://docs.mesaplex.com/mpx-scan)
323
+ - **npm:** [https://www.npmjs.com/package/mpx-scan](https://www.npmjs.com/package/mpx-scan)
324
+ - **GitHub:** [https://github.com/mesaplexdev/mpx-scan](https://github.com/mesaplexdev/mpx-scan)
259
325
  - **Support:** support@mesaplex.com
260
- - **Twitter:** [@mesaplex](https://twitter.com/mesaplex)
261
-
262
- ## 🐛 Known Issues
263
-
264
- None currently! Report issues via email: support@mesaplex.com
265
326
 
266
327
  ## 📚 Related Tools
267
328
 
268
- Part of the Mesaplex developer toolchain:
269
-
270
329
  - **mpx-scan** — Security scanner (you are here)
271
- - **mpx-api** — API testing toolkit *(coming soon)*
272
- - **mpx-perf** — Performance profiler *(coming soon)*
273
- - **mpx-deploy** — Deployment automation *(coming soon)*
330
+ - **[mpx-api](https://www.npmjs.com/package/mpx-api)** — API testing toolkit
331
+ - **[mpx-db](https://www.npmjs.com/package/mpx-db)** — Database toolkit
274
332
 
275
333
  ---
276
334
 
package/bin/cli.js CHANGED
@@ -13,6 +13,7 @@ const { scan } = require('../src/index');
13
13
  const { formatReport, formatBrief } = require('../src/reporters/terminal');
14
14
  const { formatJSON } = require('../src/reporters/json');
15
15
  const { generateFixes, PLATFORMS } = require('../src/generators/fixes');
16
+ const { getSchema } = require('../src/schema');
16
17
  const {
17
18
  getLicense,
18
19
  activateLicense,
@@ -24,6 +25,18 @@ const {
24
25
 
25
26
  const pkg = require('../package.json');
26
27
 
28
+ // Exit codes per AI-native spec
29
+ const EXIT = {
30
+ SUCCESS: 0, // Success, no issues found
31
+ ISSUES_FOUND: 1, // Success, issues found
32
+ BAD_ARGS: 2, // Invalid arguments
33
+ CONFIG_ERROR: 3, // Configuration error
34
+ NETWORK_ERROR: 4 // Network/connectivity error
35
+ };
36
+
37
+ // Auto-detect non-interactive mode
38
+ const isInteractive = process.stdout.isTTY && !process.env.CI;
39
+
27
40
  const program = new Command();
28
41
 
29
42
  program
@@ -32,105 +45,252 @@ program
32
45
  .version(pkg.version)
33
46
  .argument('[url]', 'URL to scan')
34
47
  .option('--full', 'Run all checks (Pro only)')
35
- .option('--json', 'Output as JSON')
48
+ .option('--json', 'Output as JSON (machine-readable)')
36
49
  .option('--brief', 'Brief output (one-line summary)')
50
+ .option('--quiet, -q', 'Minimal output (results only, no banners)')
51
+ .option('--no-color', 'Disable colored output')
52
+ .option('--batch', 'Batch mode: read URLs from stdin (one per line)')
53
+ .option('--schema', 'Output JSON schema describing all commands and flags')
37
54
  .option('--fix <platform>', `Generate fix config for platform (${PLATFORMS.join(', ')})`)
38
55
  .option('--timeout <seconds>', 'Connection timeout', '10')
39
56
  .option('--ci', 'CI/CD mode: exit 1 if score below threshold')
40
57
  .option('--min-score <score>', 'Minimum score for CI mode (default: 70)', '70')
41
58
  .action(async (url, options) => {
59
+ // Handle --schema flag
60
+ if (options.schema) {
61
+ console.log(JSON.stringify(getSchema(), null, 2));
62
+ process.exit(EXIT.SUCCESS);
63
+ return;
64
+ }
65
+
66
+ // Handle --batch mode (read URLs from stdin)
67
+ if (options.batch) {
68
+ await runBatchMode(options);
69
+ return;
70
+ }
71
+
42
72
  // Show help if no URL provided
43
73
  if (!url) {
44
74
  program.help();
45
75
  return;
46
76
  }
47
77
 
48
- try {
49
- // Check license and rate limits
50
- const license = getLicense();
51
- const rateLimit = checkRateLimit();
52
-
53
- // Handle rate limiting
54
- if (!rateLimit.allowed) {
78
+ const exitCode = await runSingleScan(url, options);
79
+ process.exit(exitCode);
80
+ });
81
+
82
+ async function runSingleScan(url, options) {
83
+ const jsonMode = options.json;
84
+ const quietMode = options.quiet || options.Q;
85
+
86
+ // Disable chalk if --no-color or non-TTY
87
+ if (options.color === false || !process.stdout.isTTY) {
88
+ chalk.level = 0;
89
+ }
90
+
91
+ try {
92
+ // Check license and rate limits
93
+ const license = getLicense();
94
+ const rateLimit = checkRateLimit();
95
+
96
+ // Handle rate limiting
97
+ if (!rateLimit.allowed) {
98
+ if (jsonMode) {
99
+ console.log(JSON.stringify({
100
+ error: 'Daily scan limit reached',
101
+ code: 'ERR_RATE_LIMIT',
102
+ resetsAt: new Date(rateLimit.resetsAt).toISOString(),
103
+ limit: FREE_DAILY_LIMIT,
104
+ upgrade: 'https://mesaplex.com/mpx-scan'
105
+ }, null, 2));
106
+ } else {
55
107
  console.error(chalk.red.bold('\n❌ Daily scan limit reached'));
56
108
  console.error(chalk.yellow(`Free tier: ${FREE_DAILY_LIMIT} scans/day`));
57
109
  console.error(chalk.gray(`Resets: ${new Date(rateLimit.resetsAt).toLocaleString()}\n`));
58
110
  console.error(chalk.blue('Upgrade to Pro for unlimited scans:'));
59
111
  console.error(chalk.blue(' https://mesaplex.com/mpx-scan\n'));
60
- process.exit(1);
61
112
  }
62
-
63
- // Check for Pro-only features
64
- if (options.full && license.tier !== 'pro') {
113
+ return EXIT.CONFIG_ERROR;
114
+ }
115
+
116
+ // Check for Pro-only features
117
+ if (options.full && license.tier !== 'pro') {
118
+ if (jsonMode) {
119
+ console.log(JSON.stringify({
120
+ error: '--full flag requires Pro license',
121
+ code: 'ERR_PRO_REQUIRED',
122
+ upgrade: 'https://mesaplex.com/mpx-scan'
123
+ }, null, 2));
124
+ } else {
65
125
  console.error(chalk.red.bold('\n❌ --full flag requires Pro license'));
66
126
  console.error(chalk.yellow('Free tier includes: headers, SSL, server checks'));
67
127
  console.error(chalk.yellow('Pro includes: all checks (DNS, cookies, SRI, exposed files, etc.)\n'));
68
128
  console.error(chalk.blue('Upgrade: https://mesaplex.com/mpx-scan\n'));
69
- process.exit(1);
70
129
  }
71
-
72
- if (options.json && license.tier !== 'pro') {
73
- console.error(chalk.red.bold('\n❌ --json output requires Pro license\n'));
74
- console.error(chalk.blue('Upgrade: https://mesaplex.com/mpx-scan\n'));
75
- process.exit(1);
130
+ return EXIT.CONFIG_ERROR;
131
+ }
132
+
133
+ // Show scan info (unless quiet/json/brief)
134
+ if (!jsonMode && !options.brief && !quietMode) {
135
+ console.error(chalk.bold.cyan('🔍 Scanning...'));
136
+ if (license.tier === 'free') {
137
+ console.error(chalk.gray(`Free tier: ${rateLimit.remaining} scan(s) remaining today`));
76
138
  }
77
-
78
- // Show scan info
79
- if (!options.json && !options.brief) {
80
- console.log('');
81
- console.log(chalk.bold.cyan('🔍 Scanning...'));
82
- if (license.tier === 'free') {
83
- console.log(chalk.gray(`Free tier: ${rateLimit.remaining} scan(s) remaining today\n`));
139
+ }
140
+
141
+ // Run scan
142
+ const results = await scan(url, {
143
+ timeout: parseInt(options.timeout) * 1000,
144
+ tier: license.tier,
145
+ full: options.full
146
+ });
147
+
148
+ // Record scan for rate limiting
149
+ recordScan();
150
+
151
+ // Output results
152
+ if (options.fix) {
153
+ console.log(generateFixes(options.fix, results));
154
+ } else if (jsonMode) {
155
+ console.log(formatJSON(results, true));
156
+ } else if (options.brief) {
157
+ console.log(formatBrief(results));
158
+ } else {
159
+ console.log(formatReport(results, { ...options, quiet: quietMode }));
160
+ }
161
+
162
+ // Determine exit code based on findings
163
+ if (options.ci) {
164
+ const minScore = parseInt(options.minScore);
165
+ const percentage = Math.round((results.score / results.maxScore) * 100);
166
+ if (percentage < minScore) {
167
+ if (!jsonMode && !options.brief && !quietMode) {
168
+ console.error(chalk.yellow(`\n⚠️ CI mode: Score ${percentage}/100 below minimum ${minScore}`));
84
169
  }
170
+ return EXIT.ISSUES_FOUND;
85
171
  }
172
+ }
173
+
174
+ // Exit 1 if there are failures, 0 if clean
175
+ if (results.summary.failed > 0) {
176
+ return EXIT.ISSUES_FOUND;
177
+ }
178
+ return EXIT.SUCCESS;
179
+
180
+ } catch (err) {
181
+ if (jsonMode) {
182
+ const code = isNetworkError(err) ? 'ERR_NETWORK' : 'ERR_SCAN';
183
+ console.log(JSON.stringify({ error: err.message, code }, null, 2));
184
+ } else {
185
+ console.error(chalk.red.bold('\n❌ Error:'), err.message);
186
+ console.error('');
187
+ }
188
+ return isNetworkError(err) ? EXIT.NETWORK_ERROR : EXIT.ISSUES_FOUND;
189
+ }
190
+ }
191
+
192
+ async function runBatchMode(options) {
193
+ const jsonMode = options.json;
194
+
195
+ // Read URLs from stdin
196
+ const input = await readStdin();
197
+ if (!input.trim()) {
198
+ if (jsonMode) {
199
+ console.log(JSON.stringify({ error: 'No URLs provided on stdin', code: 'ERR_NO_INPUT' }, null, 2));
200
+ } else {
201
+ console.error(chalk.red('No URLs provided. Pipe URLs via stdin:'));
202
+ console.error(chalk.gray(' cat urls.txt | mpx-scan --batch --json'));
203
+ }
204
+ process.exit(EXIT.BAD_ARGS);
205
+ return;
206
+ }
207
+
208
+ const urls = input.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
209
+
210
+ if (urls.length === 0) {
211
+ if (jsonMode) {
212
+ console.log(JSON.stringify({ error: 'No valid URLs found in input', code: 'ERR_NO_INPUT' }, null, 2));
213
+ } else {
214
+ console.error(chalk.red('No valid URLs found in input.'));
215
+ }
216
+ process.exit(EXIT.BAD_ARGS);
217
+ return;
218
+ }
219
+
220
+ let hasIssues = false;
221
+ let hasErrors = false;
222
+
223
+ for (const url of urls) {
224
+ try {
225
+ const license = getLicense();
226
+ const rateLimit = checkRateLimit();
86
227
 
87
- // Run scan
228
+ if (!rateLimit.allowed) {
229
+ if (jsonMode) {
230
+ console.log(JSON.stringify({
231
+ url,
232
+ error: 'Rate limit reached',
233
+ code: 'ERR_RATE_LIMIT'
234
+ }));
235
+ }
236
+ hasErrors = true;
237
+ continue;
238
+ }
239
+
88
240
  const results = await scan(url, {
89
241
  timeout: parseInt(options.timeout) * 1000,
90
242
  tier: license.tier,
91
243
  full: options.full
92
244
  });
93
245
 
94
- // Record scan for rate limiting
95
246
  recordScan();
96
247
 
97
- // Output results
98
- if (options.fix) {
99
- console.log(generateFixes(options.fix, results));
100
- } else if (options.json) {
101
- console.log(formatJSON(results, true));
248
+ if (results.summary.failed > 0) hasIssues = true;
249
+
250
+ if (jsonMode) {
251
+ // JSONL: one JSON object per line
252
+ console.log(formatJSON(results, false));
102
253
  } else if (options.brief) {
103
254
  console.log(formatBrief(results));
104
255
  } else {
105
256
  console.log(formatReport(results, options));
106
257
  }
107
-
108
- // Exit code logic:
109
- // - Exit 0: scan completed successfully (default)
110
- // - Exit 1: only in --ci mode if score below threshold
111
- if (options.ci) {
112
- const minScore = parseInt(options.minScore);
113
- const percentage = Math.round((results.score / results.maxScore) * 100);
114
- if (percentage < minScore) {
115
- if (!options.json && !options.brief) {
116
- console.error(chalk.yellow(`\n⚠️ CI mode: Score ${percentage}/100 below minimum ${minScore}`));
117
- }
118
- process.exit(1);
119
- }
120
- }
121
-
122
- process.exit(0);
123
-
124
258
  } catch (err) {
125
- if (options.json) {
126
- console.log(JSON.stringify({ error: err.message }, null, 2));
259
+ hasErrors = true;
260
+ if (jsonMode) {
261
+ console.log(JSON.stringify({ url, error: err.message, code: isNetworkError(err) ? 'ERR_NETWORK' : 'ERR_SCAN' }));
127
262
  } else {
128
- console.error(chalk.red.bold('\n❌ Error:'), err.message);
129
- console.error('');
263
+ console.error(chalk.red(`Error scanning ${url}: ${err.message}`));
130
264
  }
131
- process.exit(1);
132
265
  }
266
+ }
267
+
268
+ if (hasErrors) process.exit(EXIT.NETWORK_ERROR);
269
+ if (hasIssues) process.exit(EXIT.ISSUES_FOUND);
270
+ process.exit(EXIT.SUCCESS);
271
+ }
272
+
273
+ function readStdin() {
274
+ return new Promise((resolve) => {
275
+ if (process.stdin.isTTY) {
276
+ resolve('');
277
+ return;
278
+ }
279
+ let data = '';
280
+ process.stdin.setEncoding('utf8');
281
+ process.stdin.on('data', chunk => data += chunk);
282
+ process.stdin.on('end', () => resolve(data));
133
283
  });
284
+ }
285
+
286
+ function isNetworkError(err) {
287
+ const msg = (err.message || '').toLowerCase();
288
+ return msg.includes('econnrefused') || msg.includes('enotfound') ||
289
+ msg.includes('timeout') || msg.includes('network') ||
290
+ msg.includes('dns') || msg.includes('econnreset') ||
291
+ err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' ||
292
+ err.code === 'ETIMEDOUT';
293
+ }
134
294
 
135
295
  // License management subcommands
136
296
  program
@@ -187,7 +347,7 @@ program
187
347
  } catch (err) {
188
348
  console.error(chalk.red.bold('\n❌ Activation failed:'), err.message);
189
349
  console.error('');
190
- process.exit(1);
350
+ process.exit(EXIT.CONFIG_ERROR);
191
351
  }
192
352
  });
193
353
 
@@ -202,20 +362,43 @@ program
202
362
  console.log('');
203
363
  });
204
364
 
365
+ // MCP subcommand
366
+ program
367
+ .command('mcp')
368
+ .description('Start MCP (Model Context Protocol) stdio server')
369
+ .action(async () => {
370
+ try {
371
+ const { startMCPServer } = require('../src/mcp');
372
+ await startMCPServer();
373
+ } catch (err) {
374
+ console.error(JSON.stringify({ error: err.message, code: 'ERR_MCP_START' }));
375
+ process.exit(EXIT.CONFIG_ERROR);
376
+ }
377
+ });
378
+
205
379
  // Examples
206
380
  program.addHelpText('after', `
207
381
  ${chalk.bold('Examples:')}
208
382
  ${chalk.cyan('mpx-scan https://example.com')} Quick security scan
209
383
  ${chalk.cyan('mpx-scan example.com --full')} Deep scan (Pro only)
210
- ${chalk.cyan('mpx-scan example.com --json')} JSON output (Pro only)
384
+ ${chalk.cyan('mpx-scan example.com --json')} JSON output
211
385
  ${chalk.cyan('mpx-scan example.com --fix nginx')} Generate nginx config
212
386
  ${chalk.cyan('mpx-scan example.com --brief')} One-line summary
387
+ ${chalk.cyan('mpx-scan --schema')} Show tool schema (JSON)
388
+ ${chalk.cyan('cat urls.txt | mpx-scan --batch --json')} Batch scan from stdin
389
+ ${chalk.cyan('mpx-scan mcp')} Start MCP server
213
390
  ${chalk.cyan('mpx-scan license')} Check license status
214
- ${chalk.cyan('mpx-scan activate MPX-PRO-XXX')} Activate Pro license
391
+
392
+ ${chalk.bold('Exit Codes:')}
393
+ 0 Success, no issues found
394
+ 1 Success, issues found
395
+ 2 Invalid arguments
396
+ 3 Configuration error (license, rate limit)
397
+ 4 Network/connectivity error
215
398
 
216
399
  ${chalk.bold('Free vs Pro:')}
217
400
  ${chalk.yellow('Free:')} 3 scans/day, basic checks (headers, SSL, server)
218
- ${chalk.green('Pro:')} Unlimited scans, all checks, JSON export, CI/CD integration
401
+ ${chalk.green('Pro:')} Unlimited scans, all checks, batch mode, CI/CD integration
219
402
 
220
403
  ${chalk.blue('Upgrade: https://mesaplex.com/mpx-scan')}
221
404
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpx-scan",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Professional website security scanner CLI. Check headers, SSL, cookies, DNS, and get actionable fix suggestions. Part of the Mesaplex developer toolchain.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -25,7 +25,11 @@
25
25
  "security-headers",
26
26
  "ssl-check",
27
27
  "dns-security",
28
- "cors"
28
+ "cors",
29
+ "mcp",
30
+ "ai-native",
31
+ "model-context-protocol",
32
+ "automation"
29
33
  ],
30
34
  "author": "Mesaplex <support@mesaplex.com>",
31
35
  "license": "SEE LICENSE IN LICENSE",
@@ -39,6 +43,7 @@
39
43
  "node": ">=18.0.0"
40
44
  },
41
45
  "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.26.0",
42
47
  "chalk": "^4.1.2",
43
48
  "commander": "^12.0.0"
44
49
  },
package/src/mcp.js ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * MCP (Model Context Protocol) Server
3
+ *
4
+ * Exposes mpx-scan capabilities as MCP tools for AI agent integration.
5
+ * Runs over stdio transport.
6
+ */
7
+
8
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
9
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
10
+ const {
11
+ ListToolsRequestSchema,
12
+ CallToolRequestSchema
13
+ } = require('@modelcontextprotocol/sdk/types.js');
14
+
15
+ const { scan } = require('./index');
16
+ const { formatJSON } = require('./reporters/json');
17
+ const { getSchema } = require('./schema');
18
+ const { getLicense, checkRateLimit, recordScan } = require('./license');
19
+ const { generateFixes, PLATFORMS } = require('./generators/fixes');
20
+ const pkg = require('../package.json');
21
+
22
+ async function startMCPServer() {
23
+ const server = new Server(
24
+ { name: 'mpx-scan', version: pkg.version },
25
+ { capabilities: { tools: {} } }
26
+ );
27
+
28
+ // List available tools
29
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
30
+ return {
31
+ tools: [
32
+ {
33
+ name: 'scan',
34
+ description: 'Scan a website for security issues. Returns structured results with grade, score, and per-check details.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ url: {
39
+ type: 'string',
40
+ description: 'URL to scan (https:// added automatically if missing)'
41
+ },
42
+ full: {
43
+ type: 'boolean',
44
+ description: 'Run all security checks (Pro license required)',
45
+ default: false
46
+ },
47
+ timeout: {
48
+ type: 'number',
49
+ description: 'Connection timeout in seconds',
50
+ default: 10
51
+ }
52
+ },
53
+ required: ['url']
54
+ }
55
+ },
56
+ {
57
+ name: 'generate_fixes',
58
+ description: 'Generate platform-specific configuration to fix security issues found by a scan.',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: {
62
+ url: {
63
+ type: 'string',
64
+ description: 'URL to scan and generate fixes for'
65
+ },
66
+ platform: {
67
+ type: 'string',
68
+ enum: PLATFORMS,
69
+ description: 'Target platform for fix configuration'
70
+ },
71
+ timeout: {
72
+ type: 'number',
73
+ description: 'Connection timeout in seconds',
74
+ default: 10
75
+ }
76
+ },
77
+ required: ['url', 'platform']
78
+ }
79
+ },
80
+ {
81
+ name: 'get_schema',
82
+ description: 'Get the full JSON schema describing all mpx-scan commands, flags, and output formats.',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {}
86
+ }
87
+ }
88
+ ]
89
+ };
90
+ });
91
+
92
+ // Handle tool calls
93
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
94
+ const { name, arguments: args } = request.params;
95
+
96
+ try {
97
+ switch (name) {
98
+ case 'scan': {
99
+ const license = getLicense();
100
+ const rateLimit = checkRateLimit();
101
+
102
+ if (!rateLimit.allowed) {
103
+ return {
104
+ content: [{
105
+ type: 'text',
106
+ text: JSON.stringify({
107
+ error: 'Daily scan limit reached',
108
+ code: 'ERR_RATE_LIMIT',
109
+ resetsAt: new Date(rateLimit.resetsAt).toISOString()
110
+ }, null, 2)
111
+ }],
112
+ isError: true
113
+ };
114
+ }
115
+
116
+ const results = await scan(args.url, {
117
+ timeout: (args.timeout || 10) * 1000,
118
+ tier: license.tier,
119
+ full: args.full || false
120
+ });
121
+
122
+ recordScan();
123
+
124
+ return {
125
+ content: [{
126
+ type: 'text',
127
+ text: formatJSON(results, true)
128
+ }]
129
+ };
130
+ }
131
+
132
+ case 'generate_fixes': {
133
+ const license = getLicense();
134
+ const rateLimit2 = checkRateLimit();
135
+
136
+ if (!rateLimit2.allowed) {
137
+ return {
138
+ content: [{
139
+ type: 'text',
140
+ text: JSON.stringify({
141
+ error: 'Daily scan limit reached',
142
+ code: 'ERR_RATE_LIMIT',
143
+ resetsAt: new Date(rateLimit2.resetsAt).toISOString()
144
+ }, null, 2)
145
+ }],
146
+ isError: true
147
+ };
148
+ }
149
+
150
+ const fixResults = await scan(args.url, {
151
+ timeout: (args.timeout || 10) * 1000,
152
+ tier: license.tier,
153
+ full: false
154
+ });
155
+
156
+ recordScan();
157
+
158
+ // Collect issues and generate structured fix data
159
+ const issues = [];
160
+ for (const [sectionName, section] of Object.entries(fixResults.sections)) {
161
+ for (const check of section.checks) {
162
+ if ((check.status === 'fail' || check.status === 'warn') && check.recommendation) {
163
+ issues.push({
164
+ section: sectionName,
165
+ name: check.name,
166
+ status: check.status,
167
+ recommendation: check.recommendation
168
+ });
169
+ }
170
+ }
171
+ }
172
+
173
+ // Extract headers from issues
174
+ const headers = {};
175
+ for (const issue of issues) {
176
+ const rec = issue.recommendation;
177
+ if (rec.includes('Strict-Transport-Security')) headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload';
178
+ if (rec.includes('Content-Security-Policy')) headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'";
179
+ if (rec.includes('X-Content-Type-Options')) headers['X-Content-Type-Options'] = 'nosniff';
180
+ if (rec.includes('X-Frame-Options')) headers['X-Frame-Options'] = 'DENY';
181
+ if (rec.includes('Referrer-Policy')) headers['Referrer-Policy'] = 'strict-origin-when-cross-origin';
182
+ if (rec.includes('Permissions-Policy')) headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()';
183
+ if (rec.includes('Cross-Origin-Opener-Policy')) headers['Cross-Origin-Opener-Policy'] = 'same-origin';
184
+ if (rec.includes('Cross-Origin-Resource-Policy')) headers['Cross-Origin-Resource-Policy'] = 'same-origin';
185
+ }
186
+
187
+ const hasSSL = issues.some(i => i.section === 'ssl');
188
+
189
+ // Build platform-specific config snippet
190
+ let configSnippet = '';
191
+ const p = args.platform;
192
+ if (p === 'nginx') {
193
+ const headerLines = Object.entries(headers).map(([h, v]) => ` add_header ${h} "${v}" always;`).join('\n');
194
+ configSnippet = `server {\n # ... your existing config ...\n\n${headerLines}\n}`;
195
+ if (hasSSL) configSnippet += '\n\n# SSL/TLS\nssl_protocols TLSv1.2 TLSv1.3;\nssl_prefer_server_ciphers on;';
196
+ } else if (p === 'apache') {
197
+ const headerLines = Object.entries(headers).map(([h, v]) => ` Header always set ${h} "${v}"`).join('\n');
198
+ configSnippet = `<IfModule mod_headers.c>\n${headerLines}\n</IfModule>`;
199
+ } else if (p === 'caddy') {
200
+ const headerLines = Object.entries(headers).map(([h, v]) => ` ${h} "${v}"`).join('\n');
201
+ configSnippet = `header {\n${headerLines}\n}`;
202
+ } else if (p === 'cloudflare') {
203
+ configSnippet = Object.entries(headers).map(([h, v]) => `Set "${h}" to "${v}"`).join('\n');
204
+ }
205
+
206
+ const fixData = {
207
+ url: args.url,
208
+ platform: args.platform,
209
+ issueCount: issues.length,
210
+ issues: issues.map(i => ({ section: i.section, name: i.name, status: i.status, recommendation: i.recommendation })),
211
+ headers,
212
+ hasSSLIssues: hasSSL,
213
+ configSnippet,
214
+ instructions: {
215
+ nginx: 'Add to server {} block, then: sudo nginx -t && sudo systemctl reload nginx',
216
+ apache: 'Add to .htaccess or site config, then: sudo apachectl configtest && sudo systemctl reload apache2',
217
+ caddy: 'Add to Caddyfile site block, then: sudo systemctl reload caddy',
218
+ cloudflare: 'Dashboard → Rules → Transform Rules → Modify Response Header'
219
+ }[args.platform]
220
+ };
221
+
222
+ return {
223
+ content: [{
224
+ type: 'text',
225
+ text: JSON.stringify(fixData, null, 2)
226
+ }]
227
+ };
228
+ }
229
+
230
+ case 'get_schema': {
231
+ return {
232
+ content: [{
233
+ type: 'text',
234
+ text: JSON.stringify(getSchema(), null, 2)
235
+ }]
236
+ };
237
+ }
238
+
239
+ default:
240
+ return {
241
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
242
+ isError: true
243
+ };
244
+ }
245
+ } catch (err) {
246
+ return {
247
+ content: [{
248
+ type: 'text',
249
+ text: JSON.stringify({ error: err.message, code: 'ERR_SCAN' }, null, 2)
250
+ }],
251
+ isError: true
252
+ };
253
+ }
254
+ });
255
+
256
+ const transport = new StdioServerTransport();
257
+ await server.connect(transport);
258
+ }
259
+
260
+ module.exports = { startMCPServer };
package/src/schema.js ADDED
@@ -0,0 +1,198 @@
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
+ const pkg = require('../package.json');
9
+ const { PLATFORMS } = require('./generators/fixes');
10
+ const { SCANNER_TIERS } = require('./index');
11
+
12
+ function getSchema() {
13
+ return {
14
+ tool: 'mpx-scan',
15
+ version: pkg.version,
16
+ description: pkg.description,
17
+ homepage: pkg.homepage,
18
+ commands: {
19
+ scan: {
20
+ description: 'Scan a URL for security issues',
21
+ usage: 'mpx-scan <url> [options]',
22
+ arguments: {
23
+ url: {
24
+ type: 'string',
25
+ required: true,
26
+ description: 'URL to scan (https:// prefix added automatically if missing)'
27
+ }
28
+ },
29
+ flags: {
30
+ '--json': {
31
+ type: 'boolean',
32
+ default: false,
33
+ description: 'Output results as structured JSON'
34
+ },
35
+ '--full': {
36
+ type: 'boolean',
37
+ default: false,
38
+ description: 'Run all security checks (Pro license required)'
39
+ },
40
+ '--brief': {
41
+ type: 'boolean',
42
+ default: false,
43
+ description: 'One-line summary output'
44
+ },
45
+ '--quiet': {
46
+ type: 'boolean',
47
+ default: false,
48
+ description: 'Minimal output (no banners or progress)'
49
+ },
50
+ '--no-color': {
51
+ type: 'boolean',
52
+ default: false,
53
+ description: 'Disable ANSI color codes in output'
54
+ },
55
+ '--batch': {
56
+ type: 'boolean',
57
+ default: false,
58
+ description: 'Read URLs from stdin (one per line), output JSONL with --json'
59
+ },
60
+ '--fix': {
61
+ type: 'string',
62
+ enum: PLATFORMS,
63
+ description: 'Generate fix configuration for specified platform'
64
+ },
65
+ '--timeout': {
66
+ type: 'number',
67
+ default: 10,
68
+ description: 'Connection timeout in seconds'
69
+ },
70
+ '--ci': {
71
+ type: 'boolean',
72
+ default: false,
73
+ description: 'CI/CD mode: exit 1 if score below --min-score'
74
+ },
75
+ '--min-score': {
76
+ type: 'number',
77
+ default: 70,
78
+ description: 'Minimum score threshold for --ci mode (0-100)'
79
+ },
80
+ '--schema': {
81
+ type: 'boolean',
82
+ default: false,
83
+ description: 'Output this schema as JSON'
84
+ }
85
+ },
86
+ output: {
87
+ json: {
88
+ description: 'Structured scan results when --json is used',
89
+ schema: {
90
+ type: 'object',
91
+ properties: {
92
+ mpxScan: {
93
+ type: 'object',
94
+ properties: {
95
+ version: { type: 'string' },
96
+ scannedAt: { type: 'string', format: 'date-time' },
97
+ scanDuration: { type: 'number', description: 'Duration in milliseconds' }
98
+ }
99
+ },
100
+ target: {
101
+ type: 'object',
102
+ properties: {
103
+ url: { type: 'string' },
104
+ hostname: { type: 'string' }
105
+ }
106
+ },
107
+ score: {
108
+ type: 'object',
109
+ properties: {
110
+ grade: { type: 'string', enum: ['A+', 'A', 'B', 'C', 'D', 'F'] },
111
+ numeric: { type: 'number' },
112
+ maxScore: { type: 'number' },
113
+ percentage: { type: 'number', minimum: 0, maximum: 100 }
114
+ }
115
+ },
116
+ summary: {
117
+ type: 'object',
118
+ properties: {
119
+ passed: { type: 'number' },
120
+ warnings: { type: 'number' },
121
+ failed: { type: 'number' },
122
+ info: { type: 'number' }
123
+ }
124
+ },
125
+ sections: { type: 'object', description: 'Per-scanner results keyed by scanner name' },
126
+ tier: { type: 'string', enum: ['free', 'pro'] }
127
+ }
128
+ }
129
+ },
130
+ error: {
131
+ description: 'Error response when scan fails',
132
+ schema: {
133
+ type: 'object',
134
+ properties: {
135
+ error: { type: 'string' },
136
+ code: { type: 'string', enum: ['ERR_NETWORK', 'ERR_SCAN', 'ERR_RATE_LIMIT', 'ERR_PRO_REQUIRED', 'ERR_NO_INPUT'] }
137
+ }
138
+ }
139
+ }
140
+ },
141
+ exitCodes: {
142
+ 0: 'Success, no security issues found',
143
+ 1: 'Success, security issues found',
144
+ 2: 'Invalid arguments',
145
+ 3: 'Configuration error (license, rate limit)',
146
+ 4: 'Network/connectivity error'
147
+ },
148
+ examples: [
149
+ { command: 'mpx-scan https://example.com --json', description: 'Scan with JSON output' },
150
+ { command: 'mpx-scan https://example.com --json --full', description: 'Full scan with JSON (Pro)' },
151
+ { command: 'cat urls.txt | mpx-scan --batch --json', description: 'Batch scan from stdin' },
152
+ { command: 'mpx-scan https://example.com --fix nginx', description: 'Get nginx fix config' }
153
+ ]
154
+ },
155
+ mcp: {
156
+ description: 'Start MCP (Model Context Protocol) stdio server for AI agent integration',
157
+ usage: 'mpx-scan mcp',
158
+ arguments: {},
159
+ flags: {},
160
+ examples: [
161
+ { command: 'mpx-scan mcp', description: 'Start MCP stdio server' }
162
+ ]
163
+ },
164
+ license: {
165
+ description: 'Show current license status',
166
+ usage: 'mpx-scan license'
167
+ },
168
+ activate: {
169
+ description: 'Activate a Pro license key',
170
+ usage: 'mpx-scan activate <key>',
171
+ arguments: {
172
+ key: { type: 'string', required: true, description: 'License key (MPX-PRO-...)' }
173
+ }
174
+ },
175
+ deactivate: {
176
+ description: 'Deactivate Pro license and return to free tier',
177
+ usage: 'mpx-scan deactivate'
178
+ }
179
+ },
180
+ scanners: {
181
+ free: SCANNER_TIERS.free,
182
+ pro: SCANNER_TIERS.pro
183
+ },
184
+ mcpConfig: {
185
+ description: 'Add to your MCP client configuration to use mpx-scan as an AI tool',
186
+ config: {
187
+ mcpServers: {
188
+ 'mpx-scan': {
189
+ command: 'npx',
190
+ args: ['mpx-scan', 'mcp']
191
+ }
192
+ }
193
+ }
194
+ }
195
+ };
196
+ }
197
+
198
+ module.exports = { getSchema };