mpx-scan 1.0.2 → 1.2.1

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/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.2.1",
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/index.js CHANGED
@@ -58,7 +58,7 @@ async function scan(url, options = {}) {
58
58
  };
59
59
 
60
60
  const allScanners = [
61
- { name: 'headers', fn: scanHeaders, weight: 25 },
61
+ { name: 'headers', fn: scanHeaders, weight: 15 },
62
62
  { name: 'ssl', fn: scanSSL, weight: 20 },
63
63
  { name: 'cookies', fn: scanCookies, weight: 10 },
64
64
  { name: 'server', fn: scanServer, weight: 8 },
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 };