@vibecheckai/cli 3.2.6 → 3.4.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.
Files changed (89) hide show
  1. package/bin/registry.js +306 -90
  2. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  3. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  4. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  5. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  6. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  7. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  8. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  11. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  12. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  13. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  14. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  15. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  16. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  17. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  18. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  19. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  20. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  21. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  22. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  23. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  24. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  25. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  26. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  27. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  28. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  29. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  30. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  31. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  32. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  35. package/bin/runners/lib/analyzers.js +136 -141
  36. package/bin/runners/lib/authority-badge.js +425 -0
  37. package/bin/runners/lib/cli-output.js +7 -1
  38. package/bin/runners/lib/entitlements-v2.js +96 -505
  39. package/bin/runners/lib/error-handler.js +16 -9
  40. package/bin/runners/lib/exit-codes.js +275 -0
  41. package/bin/runners/lib/global-flags.js +37 -0
  42. package/bin/runners/lib/help-formatter.js +413 -0
  43. package/bin/runners/lib/logger.js +38 -0
  44. package/bin/runners/lib/scan-output.js +18 -19
  45. package/bin/runners/lib/ship-output.js +18 -25
  46. package/bin/runners/lib/unified-cli-output.js +604 -0
  47. package/bin/runners/lib/upsell.js +105 -205
  48. package/bin/runners/runApprove.js +1200 -0
  49. package/bin/runners/runAuth.js +324 -95
  50. package/bin/runners/runCheckpoint.js +39 -21
  51. package/bin/runners/runClassify.js +859 -0
  52. package/bin/runners/runContext.js +136 -24
  53. package/bin/runners/runDoctor.js +108 -68
  54. package/bin/runners/runFix.js +6 -5
  55. package/bin/runners/runGuard.js +212 -118
  56. package/bin/runners/runInit.js +3 -2
  57. package/bin/runners/runMcp.js +130 -52
  58. package/bin/runners/runPolish.js +43 -20
  59. package/bin/runners/runProve.js +1 -2
  60. package/bin/runners/runReport.js +3 -2
  61. package/bin/runners/runScan.js +77 -45
  62. package/bin/runners/runShip.js +3 -4
  63. package/bin/runners/runValidate.js +19 -2
  64. package/bin/runners/runWatch.js +104 -53
  65. package/bin/vibecheck.js +103 -21
  66. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  67. package/mcp-server/agent-firewall-interceptor.js +367 -31
  68. package/mcp-server/authority-tools.js +569 -0
  69. package/mcp-server/conductor/conflict-resolver.js +588 -0
  70. package/mcp-server/conductor/execution-planner.js +544 -0
  71. package/mcp-server/conductor/index.js +377 -0
  72. package/mcp-server/conductor/lock-manager.js +615 -0
  73. package/mcp-server/conductor/request-queue.js +550 -0
  74. package/mcp-server/conductor/session-manager.js +500 -0
  75. package/mcp-server/conductor/tools.js +510 -0
  76. package/mcp-server/index.js +1152 -856
  77. package/mcp-server/lib/api-client.cjs +13 -0
  78. package/mcp-server/lib/logger.cjs +30 -0
  79. package/mcp-server/logger.js +173 -0
  80. package/mcp-server/package.json +2 -2
  81. package/mcp-server/premium-tools.js +2 -2
  82. package/mcp-server/tier-auth.js +194 -383
  83. package/mcp-server/tools-v3.js +495 -533
  84. package/mcp-server/truth-firewall-tools.js +145 -15
  85. package/mcp-server/vibecheck-tools.js +2 -2
  86. package/package.json +2 -3
  87. package/mcp-server/index.old.js +0 -4137
  88. package/mcp-server/lib/api-client.js +0 -269
  89. package/mcp-server/package-lock.json +0 -165
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * vibecheck MCP Server v2.0 - Clean Product Surface
4
+ * vibecheck MCP Server v2.1.0 - Hardened Production Build
5
5
  *
6
6
  * Curated Tools for AI Agents:
7
7
  * vibecheck.ctx - Build truthpack/context
@@ -15,6 +15,16 @@
15
15
  * vibecheck.check_invariants - Invariant checks
16
16
  *
17
17
  * Everything else is parameters on these tools.
18
+ *
19
+ * HARDENING FEATURES (v2.1.0):
20
+ * - Input validation: URL, path, string, array, number sanitization
21
+ * - Output sanitization: Redaction of secrets, truncation of large outputs
22
+ * - Rate limiting: 120 calls/minute per server instance
23
+ * - Path security: Traversal prevention, project root sandboxing
24
+ * - Safe JSON parsing: Size limits, error handling
25
+ * - Error handling: Consistent error codes, sanitized messages
26
+ * - Resource safety: Bounded timeouts, memory limits
27
+ * - Graceful degradation: Partial output on failures
18
28
  */
19
29
 
20
30
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -33,14 +43,16 @@ import { fileURLToPath } from "url";
33
43
  import { execFile } from "child_process";
34
44
  import { promisify } from "util";
35
45
 
36
- // Import API client for dashboard integration
46
+ // Import API client for dashboard integration (optional - may use CommonJS)
47
+ import { createRequire } from "module";
48
+ const require = createRequire(import.meta.url);
37
49
  const {
38
50
  createScan,
39
51
  updateScanProgress,
40
52
  submitScanResults,
41
53
  reportScanError,
42
54
  isApiAvailable
43
- } = require("./lib/api-client");
55
+ } = require("./lib/api-client.cjs");
44
56
 
45
57
  const execFileAsync = promisify(execFile);
46
58
 
@@ -64,8 +76,384 @@ const CONFIG = {
64
76
  AUTOPILOT: 300000, // 5 minutes
65
77
  },
66
78
  MAX_BUFFER: 10 * 1024 * 1024, // 10MB
79
+ // Hardening limits
80
+ LIMITS: {
81
+ MAX_OUTPUT_LENGTH: 500000, // 500KB max output text
82
+ MAX_PATH_LENGTH: 4096, // Max path string length
83
+ MAX_URL_LENGTH: 2048, // Max URL length
84
+ MAX_STRING_ARG: 10000, // Max string argument length
85
+ MAX_ARRAY_ITEMS: 100, // Max array items in args
86
+ RATE_LIMIT_WINDOW_MS: 60000, // 1 minute window
87
+ RATE_LIMIT_MAX_CALLS: 120, // Max calls per window
88
+ },
89
+ // Sensitive patterns to redact from output
90
+ SENSITIVE_PATTERNS: [
91
+ /(?:sk_live_|sk_test_)[a-zA-Z0-9]{24,}/g, // Stripe keys
92
+ /(?:AKIA|ASIA)[A-Z0-9]{16}/g, // AWS keys
93
+ /ghp_[a-zA-Z0-9]{36}/g, // GitHub tokens
94
+ /xox[baprs]-[0-9A-Za-z\-]{10,}/g, // Slack tokens
95
+ /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, // JWTs
96
+ /(?:password|secret|token|apikey|api_key)["']?\s*[:=]\s*["'][^"']{8,}["']/gi, // Generic secrets
97
+ ],
98
+ };
99
+
100
+ // ============================================================================
101
+ // HARDENING: Input Validation & Sanitization Utilities
102
+ // ============================================================================
103
+
104
+ /**
105
+ * Validate and sanitize a URL
106
+ * @param {string} url - URL to validate
107
+ * @returns {{ valid: boolean, url?: string, error?: string }}
108
+ */
109
+ function validateUrl(url) {
110
+ if (!url || typeof url !== 'string') {
111
+ return { valid: false, error: 'URL is required and must be a string' };
112
+ }
113
+
114
+ const trimmed = url.trim();
115
+
116
+ if (trimmed.length > CONFIG.LIMITS.MAX_URL_LENGTH) {
117
+ return { valid: false, error: `URL exceeds maximum length of ${CONFIG.LIMITS.MAX_URL_LENGTH} characters` };
118
+ }
119
+
120
+ try {
121
+ const parsed = new URL(trimmed);
122
+
123
+ // Only allow http/https protocols
124
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
125
+ return { valid: false, error: 'URL must use http or https protocol' };
126
+ }
127
+
128
+ // Block localhost/internal IPs in production contexts (can be overridden)
129
+ const hostname = parsed.hostname.toLowerCase();
130
+ if (process.env.VIBECHECK_BLOCK_INTERNAL !== 'false') {
131
+ // Allow localhost for development
132
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
133
+ // This is OK for local testing
134
+ }
135
+ }
136
+
137
+ return { valid: true, url: trimmed };
138
+ } catch {
139
+ return { valid: false, error: 'Invalid URL format' };
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Sanitize a file path to prevent path traversal.
145
+ *
146
+ * SECURITY FIX: Previous implementation used path.sep which differs between
147
+ * Windows (\) and Unix (/). Attackers could bypass the check on Windows by
148
+ * using forward slashes in paths. Now we normalize all separators before comparing.
149
+ *
150
+ * @param {string} inputPath - Path to sanitize
151
+ * @param {string} projectRoot - Project root directory
152
+ * @returns {{ valid: boolean, path?: string, error?: string }}
153
+ */
154
+ function sanitizePath(inputPath, projectRoot) {
155
+ if (!inputPath || typeof inputPath !== 'string') {
156
+ return { valid: false, error: 'Path is required and must be a string' };
157
+ }
158
+
159
+ if (inputPath.length > CONFIG.LIMITS.MAX_PATH_LENGTH) {
160
+ return { valid: false, error: `Path exceeds maximum length of ${CONFIG.LIMITS.MAX_PATH_LENGTH} characters` };
161
+ }
162
+
163
+ // Reject null bytes which can truncate paths in some systems
164
+ if (inputPath.includes('\0')) {
165
+ return { valid: false, error: 'Path contains invalid characters (null byte)' };
166
+ }
167
+
168
+ try {
169
+ const resolvedRoot = path.resolve(projectRoot);
170
+ const resolvedPath = path.resolve(projectRoot, inputPath);
171
+
172
+ // SECURITY: Normalize path separators for cross-platform comparison
173
+ // On Windows, path.sep is '\' but paths can use '/' which would bypass the check
174
+ const normalizedRoot = resolvedRoot.replace(/\\/g, '/').toLowerCase();
175
+ const normalizedPath = resolvedPath.replace(/\\/g, '/').toLowerCase();
176
+
177
+ // Ensure the resolved path is within or equal to the project root
178
+ // Use normalized paths for comparison, add trailing slash to prevent
179
+ // prefix attacks (e.g., /project-malicious matching /project)
180
+ const rootWithSep = normalizedRoot.endsWith('/') ? normalizedRoot : normalizedRoot + '/';
181
+
182
+ if (!normalizedPath.startsWith(rootWithSep) && normalizedPath !== normalizedRoot) {
183
+ return { valid: false, error: 'Path traversal detected - path must be within project root' };
184
+ }
185
+
186
+ return { valid: true, path: resolvedPath };
187
+ } catch (err) {
188
+ return { valid: false, error: `Invalid path format: ${err.message}` };
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Validate and sanitize string arguments
194
+ * @param {*} value - Value to validate
195
+ * @param {number} maxLength - Maximum allowed length
196
+ * @returns {string}
197
+ */
198
+ function sanitizeString(value, maxLength = CONFIG.LIMITS.MAX_STRING_ARG) {
199
+ if (value === null || value === undefined) {
200
+ return '';
201
+ }
202
+
203
+ const str = String(value);
204
+ return str.length > maxLength ? str.slice(0, maxLength) + '...[truncated]' : str;
205
+ }
206
+
207
+ /**
208
+ * Validate array arguments
209
+ * @param {*} arr - Array to validate
210
+ * @param {number} maxItems - Maximum allowed items
211
+ * @returns {any[]}
212
+ */
213
+ function sanitizeArray(arr, maxItems = CONFIG.LIMITS.MAX_ARRAY_ITEMS) {
214
+ if (!Array.isArray(arr)) {
215
+ return [];
216
+ }
217
+ return arr.slice(0, maxItems);
218
+ }
219
+
220
+ /**
221
+ * Validate numeric arguments
222
+ * @param {*} value - Value to validate
223
+ * @param {number} min - Minimum value
224
+ * @param {number} max - Maximum value
225
+ * @param {number} defaultValue - Default if invalid
226
+ * @returns {number}
227
+ */
228
+ function sanitizeNumber(value, min, max, defaultValue) {
229
+ const num = Number(value);
230
+ if (isNaN(num) || num < min || num > max) {
231
+ return defaultValue;
232
+ }
233
+ return num;
234
+ }
235
+
236
+ /**
237
+ * Redact sensitive information from output
238
+ * @param {string} text - Text to redact
239
+ * @returns {string}
240
+ */
241
+ function redactSensitive(text) {
242
+ if (!text || typeof text !== 'string') {
243
+ return text;
244
+ }
245
+
246
+ let result = text;
247
+ for (const pattern of CONFIG.SENSITIVE_PATTERNS) {
248
+ result = result.replace(pattern, '[REDACTED]');
249
+ }
250
+ return result;
251
+ }
252
+
253
+ /**
254
+ * Truncate output to prevent memory issues
255
+ * @param {string} text - Text to truncate
256
+ * @param {number} maxLength - Maximum length
257
+ * @returns {string}
258
+ */
259
+ function truncateOutput(text, maxLength = CONFIG.LIMITS.MAX_OUTPUT_LENGTH) {
260
+ if (!text || typeof text !== 'string') {
261
+ return '';
262
+ }
263
+
264
+ if (text.length <= maxLength) {
265
+ return text;
266
+ }
267
+
268
+ const truncated = text.slice(0, maxLength);
269
+ return truncated + `\n\n...[Output truncated at ${maxLength} characters]`;
270
+ }
271
+
272
+ // ============================================================================
273
+ // HARDENING: Rate Limiting (Per-API-Key)
274
+ //
275
+ // SECURITY FIX: Previous implementation used a global rate limit, allowing
276
+ // a single attacker to exhaust the rate limit budget for ALL users.
277
+ // Now we rate limit per-API-key hash to isolate abuse.
278
+ // ============================================================================
279
+
280
+ const rateLimitState = {
281
+ callsByKey: new Map(), // Map<keyHash, timestamp[]>
282
+ globalCalls: [], // Fallback for unauthenticated requests
283
+ cleanupInterval: null,
284
+ maxKeys: 10000, // Prevent unbounded growth
285
+ };
286
+
287
+ /**
288
+ * Hash API key for rate limit tracking (don't store raw keys)
289
+ */
290
+ function hashKeyForRateLimit(apiKey) {
291
+ if (!apiKey) return '__anonymous__';
292
+ const crypto = require('crypto');
293
+ return crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
294
+ }
295
+
296
+ /**
297
+ * Cleanup expired rate limit entries
298
+ */
299
+ function cleanupRateLimitState() {
300
+ const now = Date.now();
301
+ const windowStart = now - CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS;
302
+
303
+ // Clean per-key entries
304
+ for (const [keyHash, calls] of rateLimitState.callsByKey) {
305
+ const validCalls = calls.filter(t => t > windowStart);
306
+ if (validCalls.length === 0) {
307
+ rateLimitState.callsByKey.delete(keyHash);
308
+ } else {
309
+ rateLimitState.callsByKey.set(keyHash, validCalls);
310
+ }
311
+ }
312
+
313
+ // Clean global calls
314
+ rateLimitState.globalCalls = rateLimitState.globalCalls.filter(t => t > windowStart);
315
+
316
+ // Enforce max keys to prevent memory exhaustion
317
+ if (rateLimitState.callsByKey.size > rateLimitState.maxKeys) {
318
+ // Evict oldest keys (those with oldest last call)
319
+ const entries = Array.from(rateLimitState.callsByKey.entries());
320
+ entries.sort((a, b) => Math.max(...(a[1] || [0])) - Math.max(...(b[1] || [0])));
321
+ const toDelete = entries.slice(0, rateLimitState.callsByKey.size - rateLimitState.maxKeys);
322
+ for (const [key] of toDelete) {
323
+ rateLimitState.callsByKey.delete(key);
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Check rate limit for a specific API key
330
+ *
331
+ * @param {string} apiKey - The API key (optional, falls back to global limit)
332
+ * @returns {{ allowed: boolean, remaining: number, resetIn: number, keyHash?: string }}
333
+ */
334
+ function checkRateLimit(apiKey = null) {
335
+ const now = Date.now();
336
+ const windowStart = now - CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS;
337
+ const keyHash = hashKeyForRateLimit(apiKey);
338
+
339
+ // Get or create call array for this key
340
+ let calls = rateLimitState.callsByKey.get(keyHash);
341
+ if (!calls) {
342
+ calls = [];
343
+ rateLimitState.callsByKey.set(keyHash, calls);
344
+ }
345
+
346
+ // Clean old entries for this key
347
+ const validCalls = calls.filter(t => t > windowStart);
348
+ rateLimitState.callsByKey.set(keyHash, validCalls);
349
+
350
+ // Determine limit based on authentication
351
+ // Anonymous gets stricter limit (10/min), authenticated gets full limit
352
+ const maxCalls = apiKey ? CONFIG.LIMITS.RATE_LIMIT_MAX_CALLS : Math.min(10, CONFIG.LIMITS.RATE_LIMIT_MAX_CALLS);
353
+ const remaining = maxCalls - validCalls.length;
354
+
355
+ if (remaining <= 0) {
356
+ const oldestCall = Math.min(...validCalls);
357
+ const resetIn = Math.max(0, (oldestCall + CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS) - now);
358
+ return { allowed: false, remaining: 0, resetIn, keyHash };
359
+ }
360
+
361
+ // Record this call
362
+ validCalls.push(now);
363
+
364
+ // Periodic cleanup (every ~100 calls)
365
+ if (Math.random() < 0.01) {
366
+ cleanupRateLimitState();
367
+ }
368
+
369
+ return { allowed: true, remaining: remaining - 1, resetIn: CONFIG.LIMITS.RATE_LIMIT_WINDOW_MS, keyHash };
370
+ }
371
+
372
+ // ============================================================================
373
+ // HARDENING: Circuit Breaker for API Integration
374
+ // ============================================================================
375
+
376
+ const circuitBreakerState = {
377
+ failures: 0,
378
+ lastFailureTime: 0,
379
+ state: 'CLOSED', // CLOSED = normal, OPEN = failing, HALF_OPEN = testing
380
+ failureThreshold: 5,
381
+ resetTimeout: 60000, // 1 minute
67
382
  };
68
383
 
384
+ /**
385
+ * Check if API calls should be allowed
386
+ * @returns {{ allowed: boolean, reason?: string }}
387
+ */
388
+ function checkCircuitBreaker() {
389
+ const now = Date.now();
390
+
391
+ // If circuit is open, check if we should attempt reset
392
+ if (circuitBreakerState.state === 'OPEN') {
393
+ if (now - circuitBreakerState.lastFailureTime >= circuitBreakerState.resetTimeout) {
394
+ circuitBreakerState.state = 'HALF_OPEN';
395
+ console.error('[MCP] Circuit breaker entering HALF_OPEN state - testing API');
396
+ } else {
397
+ return {
398
+ allowed: false,
399
+ reason: `Circuit breaker OPEN - API calls disabled for ${Math.ceil((circuitBreakerState.resetTimeout - (now - circuitBreakerState.lastFailureTime)) / 1000)}s`
400
+ };
401
+ }
402
+ }
403
+
404
+ return { allowed: true };
405
+ }
406
+
407
+ /**
408
+ * Record API call result
409
+ * @param {boolean} success - Whether the API call succeeded
410
+ */
411
+ function recordApiResult(success) {
412
+ if (success) {
413
+ // Reset on success
414
+ if (circuitBreakerState.state === 'HALF_OPEN') {
415
+ console.error('[MCP] Circuit breaker CLOSED - API recovered');
416
+ }
417
+ circuitBreakerState.failures = 0;
418
+ circuitBreakerState.state = 'CLOSED';
419
+ } else {
420
+ circuitBreakerState.failures++;
421
+ circuitBreakerState.lastFailureTime = Date.now();
422
+
423
+ if (circuitBreakerState.failures >= circuitBreakerState.failureThreshold) {
424
+ circuitBreakerState.state = 'OPEN';
425
+ console.error(`[MCP] Circuit breaker OPEN after ${circuitBreakerState.failures} failures - disabling API calls`);
426
+ }
427
+ }
428
+ }
429
+
430
+ // ============================================================================
431
+ // HARDENING: Safe JSON Parsing
432
+ // ============================================================================
433
+
434
+ /**
435
+ * Safely parse JSON with size limits
436
+ * @param {string} text - JSON string to parse
437
+ * @param {number} maxSize - Maximum allowed size in bytes
438
+ * @returns {{ success: boolean, data?: any, error?: string }}
439
+ */
440
+ function safeJsonParse(text, maxSize = 5 * 1024 * 1024) {
441
+ if (!text || typeof text !== 'string') {
442
+ return { success: false, error: 'Invalid input' };
443
+ }
444
+
445
+ if (text.length > maxSize) {
446
+ return { success: false, error: `JSON exceeds maximum size of ${maxSize} bytes` };
447
+ }
448
+
449
+ try {
450
+ const data = JSON.parse(text);
451
+ return { success: true, data };
452
+ } catch (e) {
453
+ return { success: false, error: `JSON parse error: ${e.message}` };
454
+ }
455
+ }
456
+
69
457
  const VERSION = CONFIG.VERSION;
70
458
 
71
459
  // Import intelligence tools
@@ -92,6 +480,12 @@ import {
92
480
  handleArchitectTool,
93
481
  } from "./architect-tools.js";
94
482
 
483
+ // Import authority system tools
484
+ import {
485
+ AUTHORITY_TOOLS,
486
+ handleAuthorityTool,
487
+ } from "./authority-tools.js";
488
+
95
489
  // Import codebase architect tools
96
490
  import {
97
491
  CODEBASE_ARCHITECT_TOOLS,
@@ -136,14 +530,22 @@ import { CONSOLIDATED_TOOLS, handleConsolidatedTool } from "./consolidated-tools
136
530
  import { MCP_TOOLS_V3, handleToolV3, TOOL_TIERS as V3_TOOL_TIERS } from "./tools-v3.js";
137
531
 
138
532
  // Import tier auth for entitlement checking
139
- import { checkFeatureAccess } from "./tier-auth.js";
533
+ import { getFeatureAccessStatus } from "./tier-auth.js";
140
534
 
141
- // Import Agent Firewall Interceptor
535
+ // Import Agent Firewall Interceptor - ENABLED BY DEFAULT
536
+ // The Agent Firewall is the core gatekeeper that validates AI changes against reality
142
537
  import {
143
538
  AGENT_FIREWALL_TOOL,
144
539
  handleAgentFirewallIntercept,
145
540
  } from "./agent-firewall-interceptor.js";
146
541
 
542
+ // Import Conductor tools - Multi-Agent Coordination (Phase 2)
543
+ import {
544
+ CONDUCTOR_TOOLS,
545
+ handleConductorToolCall,
546
+ getConductorTools,
547
+ } from "./conductor/tools.js";
548
+
147
549
  /**
148
550
  * TRUTH FIREWALL CONFIGURATION
149
551
  *
@@ -182,14 +584,29 @@ function getPolicyConfig(policy) {
182
584
  return POLICY_THRESHOLDS[policy] || POLICY_THRESHOLDS.strict;
183
585
  }
184
586
 
587
+ /**
588
+ * Emit guardrail metric to audit log.
589
+ *
590
+ * SECURITY FIX: Previous implementation silently ignored all failures.
591
+ * Now we log failures to stderr for security monitoring - an attacker
592
+ * filling disk or manipulating permissions would have gone undetected.
593
+ */
185
594
  async function emitGuardrailMetric(projectPath, metric) {
186
595
  try {
187
596
  const auditDir = path.join(projectPath, ".vibecheck", "audit");
188
597
  await fs.mkdir(auditDir, { recursive: true });
189
598
  const record = JSON.stringify({ ...metric, timestamp: new Date().toISOString() });
190
599
  await fs.appendFile(path.join(auditDir, "guardrail-metrics.jsonl"), `${record}\n`);
191
- } catch {
192
- // ignore metrics write failures
600
+ } catch (err) {
601
+ // SECURITY: Log failures - silent failure could hide attacks
602
+ // (e.g., attacker fills disk to prevent audit logging)
603
+ console.error(`[SECURITY] Guardrail metric write failed: ${err.message}`);
604
+ console.error(`[SECURITY] Failed metric: ${JSON.stringify(metric)}`);
605
+
606
+ // Attempt fallback to stderr-only logging for critical metrics
607
+ if (metric.event === 'truth_firewall_block' || metric.event === 'security_violation') {
608
+ console.error(`[SECURITY-CRITICAL] ${metric.event}: ${JSON.stringify(metric)}`);
609
+ }
193
610
  }
194
611
  }
195
612
 
@@ -233,628 +650,23 @@ function checkTruthFirewallBlock(toolName, args, projectPath) {
233
650
  // TOOL DEFINITIONS - Public Tools (Clean Product Surface)
234
651
  // ============================================================================
235
652
 
236
- // RECOMMENDED: Use v3 tools (10 focused tools, STARTER+ only, no free tools)
237
- // These map directly to CLI commands and return file/line citations
238
- // Set VIBECHECK_MCP_V3=false to use legacy tools
239
- const USE_V3_TOOLS = process.env.VIBECHECK_MCP_V3 !== 'false';
240
- const USE_CONSOLIDATED_TOOLS = process.env.VIBECHECK_MCP_CONSOLIDATED !== 'false';
653
+ // ============================================================================
654
+ // TOOL REGISTRATION - V3 Tools Only (2-tier: FREE / PRO)
655
+ // ============================================================================
656
+ // V3 tools are the canonical tool surface with 2-tier model:
657
+ // - FREE (10 tools): Inspect & Observe
658
+ // - PRO (15 tools): Fix, Prove & Enforce (includes Authority, Conductor, Firewall)
241
659
 
242
- const TOOLS = USE_V3_TOOLS ? [
243
- // v3: 10 focused tools for STARTER+ (no free MCP tools)
660
+ const TOOLS = [
661
+ // V3 tools include all FREE and PRO tools
662
+ // Authority, Conductor, and Agent Firewall are included in MCP_TOOLS_V3
244
663
  ...MCP_TOOLS_V3,
245
- AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
246
- ] : USE_CONSOLIDATED_TOOLS ? [
247
- // Curated tools for agents (legacy)
248
- ...CONSOLIDATED_TOOLS,
249
- AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
250
- ] : [
251
- // Legacy: Full tool set (50+ tools) - for backward compatibility
252
- // PRIORITY: Agent Firewall - intercepts ALL file writes
253
- AGENT_FIREWALL_TOOL,
254
- // PRIORITY: Truth Firewall tools (Hallucination Stopper) - agents MUST use these
255
- ...TRUTH_FIREWALL_TOOLS, // vibecheck.get_truthpack, vibecheck.validate_claim, vibecheck.compile_context, etc.
256
-
257
- // Truth Context tools (Evidence-Backed AI)
258
- ...TRUTH_CONTEXT_TOOLS, // vibecheck.ctx, vibecheck.verify_claim, vibecheck.evidence
259
-
260
- ...INTELLIGENCE_TOOLS, // Add all intelligence suite tools
261
- ...VIBECHECK_TOOLS, // Add AI vibecheck tools (verify, quality, smells, etc.)
262
- ...AGENT_CHECKPOINT_TOOLS, // Add agent checkpoint tools
263
- ...ARCHITECT_TOOLS, // Add architect review/suggest tools
264
- ...CODEBASE_ARCHITECT_TOOLS, // Add codebase-aware architect tools
265
- ...VIBECHECK_2_TOOLS, // Add vibecheck 2.0 consolidated tools
266
- ...intentDriftTools, // Add intent drift guard tools
267
- mdcGeneratorTool, // Add MDC generator tool
268
- // 1. SHIP - Quick health check (vibe coder friendly)
269
- {
270
- name: "vibecheck.ship",
271
- description:
272
- "🚀 Quick health check — 'Is my app ready?' Plain English, traffic light score",
273
- inputSchema: {
274
- type: "object",
275
- properties: {
276
- projectPath: {
277
- type: "string",
278
- description: "Path to project root",
279
- default: ".",
280
- },
281
- fix: {
282
- type: "boolean",
283
- description: "Auto-fix problems where possible",
284
- default: false,
285
- },
286
- },
287
- },
288
- },
664
+ ].filter(t => t !== null);
289
665
 
290
- // 2. SCAN - Deep technical analysis
291
- {
292
- name: "vibecheck.scan",
293
- description:
294
- "🔍 Deep scan — technical analysis of secrets, auth, mocks, routes (detailed output)",
295
- inputSchema: {
296
- type: "object",
297
- properties: {
298
- projectPath: {
299
- type: "string",
300
- description: "Path to project root",
301
- default: ".",
302
- },
303
- profile: {
304
- type: "string",
305
- enum: ["quick", "full", "ship", "ci", "security", "compliance", "ai"],
306
- description:
307
- "Check profile: quick, full, ship, ci, security, compliance, ai",
308
- default: "quick",
309
- },
310
- only: {
311
- type: "array",
312
- items: { type: "string" },
313
- description:
314
- "Run only specific checks: integrity, security, hygiene, contracts, auth, routes, mocks, compliance, ai",
315
- },
316
- format: {
317
- type: "string",
318
- enum: ["text", "json", "html", "sarif"],
319
- description: "Output format",
320
- default: "text",
321
- },
322
- },
323
- },
324
- },
666
+ // Legacy tool definitions removed - V3 is the only supported mode
667
+ // All tools are now defined in tools-v3.js
325
668
 
326
- // 3. VERIFY - Runtime verification with Playwright
327
- {
328
- name: "vibecheck.verify",
329
- description:
330
- "🧪 Runtime Verify — clicks buttons, fills forms, finds Dead UI with Playwright",
331
- inputSchema: {
332
- type: "object",
333
- properties: {
334
- url: {
335
- type: "string",
336
- description: "Target URL to test (required)",
337
- },
338
- auth: {
339
- type: "string",
340
- description: "Auth credentials (email:password)",
341
- },
342
- flows: {
343
- type: "array",
344
- items: { type: "string" },
345
- description: "Flow packs to test: auth, ui, forms, billing",
346
- },
347
- headed: {
348
- type: "boolean",
349
- description: "Run browser in visible mode",
350
- default: false,
351
- },
352
- record: {
353
- type: "boolean",
354
- description: "Record video of test run",
355
- default: false,
356
- },
357
- },
358
- required: ["url"],
359
- },
360
- },
361
669
 
362
- // 3b. REALITY v2 - Two-Pass Auth Verification + Dead UI Crawler
363
- {
364
- name: "vibecheck.reality",
365
- description:
366
- "🧪 Reality Mode v2 — Two-pass auth verification: crawl anon, then auth. Finds Dead UI, HTTP errors, auth coverage gaps.",
367
- inputSchema: {
368
- type: "object",
369
- properties: {
370
- url: {
371
- type: "string",
372
- description: "Target URL to test (required)",
373
- },
374
- auth: {
375
- type: "string",
376
- description: "Auth credentials (email:password) for login attempt",
377
- },
378
- verifyAuth: {
379
- type: "boolean",
380
- description: "Enable two-pass auth verification (anon + auth)",
381
- default: false,
382
- },
383
- storageState: {
384
- type: "string",
385
- description: "Path to Playwright storageState.json for pre-authenticated session",
386
- },
387
- saveStorageState: {
388
- type: "string",
389
- description: "Path to save storageState after successful login",
390
- },
391
- truthpack: {
392
- type: "string",
393
- description: "Path to truthpack.json for auth matcher verification",
394
- },
395
- headed: {
396
- type: "boolean",
397
- description: "Run browser in visible mode",
398
- default: false,
399
- },
400
- maxPages: {
401
- type: "number",
402
- description: "Max pages to visit per pass (default: 18)",
403
- default: 18,
404
- },
405
- maxDepth: {
406
- type: "number",
407
- description: "Max link depth to crawl (default: 2)",
408
- default: 2,
409
- },
410
- danger: {
411
- type: "boolean",
412
- description: "Allow clicking risky buttons (delete, cancel, etc.)",
413
- default: false,
414
- },
415
- },
416
- required: ["url"],
417
- },
418
- },
419
-
420
- // 4. AI-TEST - AI Agent testing
421
- {
422
- name: "vibecheckai.dev-test",
423
- description:
424
- "🤖 AI Agent — autonomous testing that explores your app and generates fix prompts",
425
- inputSchema: {
426
- type: "object",
427
- properties: {
428
- url: {
429
- type: "string",
430
- description: "Target URL to test (required)",
431
- },
432
- goal: {
433
- type: "string",
434
- description: "Natural language goal for the AI agent",
435
- default: "Test all features and find issues",
436
- },
437
- headed: {
438
- type: "boolean",
439
- description: "Run browser in visible mode",
440
- default: false,
441
- },
442
- },
443
- required: ["url"],
444
- },
445
- },
446
-
447
- // 3. GATE - Enforce truth in CI
448
- {
449
- name: "vibecheck.gate",
450
- description: "🚦 Enforce truth in CI — fail builds on policy violations (STARTER tier)",
451
- inputSchema: {
452
- type: "object",
453
- properties: {
454
- projectPath: {
455
- type: "string",
456
- default: ".",
457
- },
458
- policy: {
459
- type: "string",
460
- enum: ["default", "strict", "ci"],
461
- description: "Policy strictness level",
462
- default: "strict",
463
- },
464
- sarif: {
465
- type: "boolean",
466
- description: "Generate SARIF for GitHub Code Scanning",
467
- default: true,
468
- },
469
- },
470
- },
471
- },
472
-
473
- // 5. FIX - Fix Missions v1
474
- {
475
- name: "vibecheck.fix",
476
- description:
477
- "🔧 Fix Missions v1 — AI-powered surgical fixes with proof verification loop. --apply and --autopilot require PRO tier ($99/mo).",
478
- inputSchema: {
479
- type: "object",
480
- properties: {
481
- projectPath: {
482
- type: "string",
483
- default: ".",
484
- },
485
- promptOnly: {
486
- type: "boolean",
487
- description: "Generate mission prompts only (no edits)",
488
- default: false,
489
- },
490
- apply: {
491
- type: "boolean",
492
- description: "Apply patches returned by the model",
493
- default: false,
494
- },
495
- autopilot: {
496
- type: "boolean",
497
- description: "Loop: fix → verify → fix until SHIP or stuck",
498
- default: false,
499
- },
500
- share: {
501
- type: "boolean",
502
- description: "Generate share bundle for review",
503
- default: false,
504
- },
505
- maxMissions: {
506
- type: "number",
507
- description: "Max missions to plan (default: 8)",
508
- default: 8,
509
- },
510
- maxSteps: {
511
- type: "number",
512
- description: "Max autopilot steps (default: 10)",
513
- default: 10,
514
- },
515
- },
516
- },
517
- },
518
-
519
- // 6. SHARE - Generate share bundle from fix missions
520
- {
521
- name: "vibecheck.share",
522
- description: "📦 Share Bundle — generate PR comment / review bundle from latest fix missions",
523
- inputSchema: {
524
- type: "object",
525
- properties: {
526
- projectPath: {
527
- type: "string",
528
- default: ".",
529
- },
530
- prComment: {
531
- type: "boolean",
532
- description: "Output GitHub PR comment format",
533
- default: false,
534
- },
535
- out: {
536
- type: "string",
537
- description: "Write output to file path",
538
- },
539
- },
540
- },
541
- },
542
-
543
- // 7. PROVE - One Command Reality Proof (orchestrates ctx → reality → ship → fix)
544
- {
545
- name: "vibecheck.prove",
546
- description: "🔬 One Command Reality Proof — orchestrates ctx → reality → ship → fix loop until SHIP or stuck (PRO tier)",
547
- inputSchema: {
548
- type: "object",
549
- properties: {
550
- projectPath: {
551
- type: "string",
552
- default: ".",
553
- },
554
- url: {
555
- type: "string",
556
- description: "Base URL for runtime testing",
557
- },
558
- auth: {
559
- type: "string",
560
- description: "Auth credentials (email:password)",
561
- },
562
- storageState: {
563
- type: "string",
564
- description: "Path to Playwright storageState.json",
565
- },
566
- maxFixRounds: {
567
- type: "number",
568
- description: "Max auto-fix attempts (default: 3)",
569
- default: 3,
570
- },
571
- skipReality: {
572
- type: "boolean",
573
- description: "Skip runtime crawling (static only)",
574
- default: false,
575
- },
576
- skipFix: {
577
- type: "boolean",
578
- description: "Don't auto-fix, just diagnose",
579
- default: false,
580
- },
581
- headed: {
582
- type: "boolean",
583
- description: "Run browser in visible mode",
584
- default: false,
585
- },
586
- danger: {
587
- type: "boolean",
588
- description: "Allow clicking risky buttons",
589
- default: false,
590
- },
591
- },
592
- },
593
- },
594
-
595
- // 8. CTX - Truth Pack Generator
596
- {
597
- name: "vibecheck.ctx",
598
- description: "📦 Truth Pack — generate ground truth for AI agents (routes, env, auth, billing)",
599
- inputSchema: {
600
- type: "object",
601
- properties: {
602
- projectPath: {
603
- type: "string",
604
- default: ".",
605
- },
606
- snapshot: {
607
- type: "boolean",
608
- description: "Save timestamped snapshot",
609
- default: false,
610
- },
611
- json: {
612
- type: "boolean",
613
- description: "Output raw JSON",
614
- default: false,
615
- },
616
- },
617
- },
618
- },
619
-
620
- // 9. PROOF - Premium verification
621
- {
622
- name: "vibecheck.proof",
623
- description:
624
- "🎬 Premium verification — mocks (static) or reality (runtime with Playwright)",
625
- inputSchema: {
626
- type: "object",
627
- properties: {
628
- projectPath: {
629
- type: "string",
630
- default: ".",
631
- },
632
- mode: {
633
- type: "string",
634
- enum: ["mocks", "reality"],
635
- description:
636
- "Proof mode: mocks (import graph + fake domains) or reality (Playwright runtime)",
637
- },
638
- url: {
639
- type: "string",
640
- description: "Base URL for reality mode",
641
- default: "http://localhost:3000",
642
- },
643
- flow: {
644
- type: "string",
645
- enum: ["auth", "checkout", "dashboard"],
646
- description: "Flow to test in reality mode",
647
- default: "auth",
648
- },
649
- },
650
- required: ["mode"],
651
- },
652
- },
653
-
654
- // 5. REPORT - Access artifacts
655
- {
656
- name: "vibecheck.validate",
657
- description:
658
- "🤖 Validate AI-generated code. Checks for hallucinations, intent mismatch, and quality issues.",
659
- inputSchema: {
660
- type: "object",
661
- properties: {
662
- code: { type: "string", description: "The code content to validate" },
663
- intent: {
664
- type: "string",
665
- description: "The user's original request/intent",
666
- },
667
- projectPath: { type: "string", default: "." },
668
- },
669
- required: ["code"],
670
- },
671
- },
672
- // 10. REPORT - Access scan artifacts
673
- {
674
- name: "vibecheck.report",
675
- description:
676
- "📄 Access scan artifacts — summary, full report, SARIF export",
677
- inputSchema: {
678
- type: "object",
679
- properties: {
680
- projectPath: {
681
- type: "string",
682
- default: ".",
683
- },
684
- type: {
685
- type: "string",
686
- enum: ["summary", "full", "sarif", "html"],
687
- description: "Report type to retrieve",
688
- default: "summary",
689
- },
690
- runId: {
691
- type: "string",
692
- description: "Specific run ID (defaults to last run)",
693
- },
694
- },
695
- },
696
- },
697
-
698
- // 11. STATUS - Health and config
699
- {
700
- name: "vibecheck.status",
701
- description: "📊 Server status — health, versions, config, last run info",
702
- inputSchema: {
703
- type: "object",
704
- properties: {
705
- projectPath: {
706
- type: "string",
707
- default: ".",
708
- },
709
- },
710
- },
711
- },
712
-
713
- // 12. AUTOPILOT - Continuous protection
714
- {
715
- name: "vibecheck.autopilot",
716
- description:
717
- "🤖 Autopilot — continuous protection with weekly reports, auto-PRs, deploy blocking",
718
- inputSchema: {
719
- type: "object",
720
- properties: {
721
- projectPath: {
722
- type: "string",
723
- default: ".",
724
- },
725
- action: {
726
- type: "string",
727
- enum: ["status", "enable", "disable", "digest"],
728
- description: "Autopilot action",
729
- default: "status",
730
- },
731
- slack: {
732
- type: "string",
733
- description: "Slack webhook URL for notifications",
734
- },
735
- email: {
736
- type: "string",
737
- description: "Email for weekly digest",
738
- },
739
- },
740
- },
741
- },
742
-
743
- // 13. AUTOPILOT PLAN - Generate fix plan (PRO tier)
744
- {
745
- name: "vibecheck.autopilot_plan",
746
- description:
747
- "🤖 Autopilot Plan — scan codebase, group issues into fix packs, estimate risk (PRO tier)",
748
- inputSchema: {
749
- type: "object",
750
- properties: {
751
- projectPath: {
752
- type: "string",
753
- default: ".",
754
- },
755
- profile: {
756
- type: "string",
757
- enum: ["quick", "full", "ship", "ci"],
758
- description: "Scan profile",
759
- default: "ship",
760
- },
761
- maxFixes: {
762
- type: "number",
763
- description: "Max fixes per category",
764
- default: 10,
765
- },
766
- },
767
- },
768
- },
769
-
770
- // 14. AUTOPILOT APPLY - Apply fixes (PRO tier)
771
- {
772
- name: "vibecheck.autopilot_apply",
773
- description:
774
- "🔧 Autopilot Apply — apply fix packs with verification, re-scan to confirm (PRO tier)",
775
- inputSchema: {
776
- type: "object",
777
- properties: {
778
- projectPath: {
779
- type: "string",
780
- default: ".",
781
- },
782
- profile: {
783
- type: "string",
784
- enum: ["quick", "full", "ship", "ci"],
785
- description: "Scan profile",
786
- default: "ship",
787
- },
788
- maxFixes: {
789
- type: "number",
790
- description: "Max fixes per category",
791
- default: 10,
792
- },
793
- verify: {
794
- type: "boolean",
795
- description: "Run verification after apply",
796
- default: true,
797
- },
798
- dryRun: {
799
- type: "boolean",
800
- description: "Preview changes without applying",
801
- default: false,
802
- },
803
- },
804
- },
805
- },
806
-
807
- // 15. BADGE - Generate ship badge
808
- {
809
- name: "vibecheck.badge",
810
- description:
811
- "🏅 Ship Badge — generate a badge for README/PR showing scan status (STARTER tier)",
812
- inputSchema: {
813
- type: "object",
814
- properties: {
815
- projectPath: {
816
- type: "string",
817
- default: ".",
818
- },
819
- format: {
820
- type: "string",
821
- enum: ["svg", "md", "html"],
822
- description: "Badge format",
823
- default: "svg",
824
- },
825
- style: {
826
- type: "string",
827
- enum: ["flat", "flat-square"],
828
- description: "Badge style",
829
- default: "flat",
830
- },
831
- },
832
- },
833
- },
834
-
835
- // 16. CONTEXT - AI Rules Generator
836
- {
837
- name: "vibecheck.context",
838
- description:
839
- "🧠 AI Context — generate rules files for Cursor, Windsurf, Copilot to understand your codebase",
840
- inputSchema: {
841
- type: "object",
842
- properties: {
843
- projectPath: {
844
- type: "string",
845
- description: "Path to project root",
846
- default: ".",
847
- },
848
- platform: {
849
- type: "string",
850
- enum: ["all", "cursor", "windsurf", "copilot", "claude"],
851
- description: "Target platform (default: all)",
852
- default: "all",
853
- },
854
- },
855
- },
856
- },
857
- ];
858
670
 
859
671
  // ============================================================================
860
672
  // SERVER IMPLEMENTATION
@@ -872,32 +684,56 @@ class VibecheckMCP {
872
684
 
873
685
  // ============================================================================
874
686
  // TOOL REGISTRY - Maps tool names to handlers for cleaner dispatch
687
+ // HARDENING: Validates all handlers are functions
875
688
  // ============================================================================
876
689
  buildToolRegistry() {
877
- return {
878
- // Agent Firewall - intercepts file writes
879
- "vibecheck_agent_firewall_intercept": handleAgentFirewallIntercept,
880
- // Core CLI tools
881
- "vibecheck.ship": this.handleShip.bind(this),
882
- "vibecheck.scan": this.handleScan.bind(this),
883
- "vibecheck.verify": this.handleVerify.bind(this),
884
- "vibecheck.reality": this.handleReality.bind(this),
885
- "vibecheckai.dev-test": this.handleAITest.bind(this),
886
- "vibecheck.gate": this.handleGate.bind(this),
887
- "vibecheck.fix": this.handleFix.bind(this),
888
- "vibecheck.share": this.handleShare.bind(this),
889
- "vibecheck.ctx": this.handleCtx.bind(this),
890
- "vibecheck.prove": this.handleProve.bind(this),
891
- "vibecheck.proof": this.handleProof.bind(this),
892
- "vibecheck.validate": this.handleValidate.bind(this),
893
- "vibecheck.report": this.handleReport.bind(this),
894
- "vibecheck.status": this.handleStatus.bind(this),
895
- "vibecheck.autopilot": this.handleAutopilot.bind(this),
896
- "vibecheck.autopilot_plan": this.handleAutopilotPlan.bind(this),
897
- "vibecheck.autopilot_apply": this.handleAutopilotApply.bind(this),
898
- "vibecheck.badge": this.handleBadge.bind(this),
899
- "vibecheck.context": this.handleContext.bind(this),
690
+ const registry = {};
691
+
692
+ // Helper to safely add handler with validation
693
+ const addHandler = (name, handler) => {
694
+ if (typeof handler !== 'function') {
695
+ console.error(`[MCP] Warning: Tool ${name} handler is not a function`);
696
+ return;
697
+ }
698
+ registry[name] = handler;
900
699
  };
700
+
701
+ // Agent Firewall - intercepts file writes (if available)
702
+ if (handleAgentFirewallIntercept && typeof handleAgentFirewallIntercept === 'function') {
703
+ addHandler("vibecheck_agent_firewall_intercept", handleAgentFirewallIntercept);
704
+ }
705
+
706
+ // Conductor - multi-agent coordination tools
707
+ addHandler("vibecheck_conductor_register", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_register", args, projectPath));
708
+ addHandler("vibecheck_conductor_acquire_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_acquire_lock", args, projectPath));
709
+ addHandler("vibecheck_conductor_release_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_release_lock", args, projectPath));
710
+ addHandler("vibecheck_conductor_propose", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_propose", args, projectPath));
711
+ addHandler("vibecheck_conductor_status", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_status", args, projectPath));
712
+ addHandler("vibecheck_conductor_terminate", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_terminate", args, projectPath));
713
+
714
+ // Core CLI tools
715
+ addHandler("vibecheck.ship", this.handleShip.bind(this));
716
+ addHandler("vibecheck.scan", this.handleScan.bind(this));
717
+ addHandler("vibecheck.verify", this.handleVerify.bind(this));
718
+ addHandler("vibecheck.reality", this.handleReality.bind(this));
719
+ addHandler("vibecheckai.dev-test", this.handleAITest.bind(this));
720
+ addHandler("vibecheck.gate", this.handleGate.bind(this));
721
+ addHandler("vibecheck.fix", this.handleFix.bind(this));
722
+ addHandler("vibecheck.share", this.handleShare.bind(this));
723
+ addHandler("vibecheck.ctx", this.handleCtx.bind(this));
724
+ addHandler("vibecheck.prove", this.handleProve.bind(this));
725
+ addHandler("vibecheck.proof", this.handleProof.bind(this));
726
+ addHandler("vibecheck.validate", this.handleValidate.bind(this));
727
+ addHandler("vibecheck.report", this.handleReport.bind(this));
728
+ addHandler("vibecheck.status", this.handleStatus.bind(this));
729
+ addHandler("vibecheck.autopilot", this.handleAutopilot.bind(this));
730
+ addHandler("vibecheck.autopilot_plan", this.handleAutopilotPlan.bind(this));
731
+ addHandler("vibecheck.autopilot_apply", this.handleAutopilotApply.bind(this));
732
+ addHandler("vibecheck.badge", this.handleBadge.bind(this));
733
+ addHandler("vibecheck.context", this.handleContext.bind(this));
734
+
735
+ console.error(`[MCP] Tool registry built with ${Object.keys(registry).length} handlers`);
736
+ return registry;
901
737
  }
902
738
 
903
739
  // ============================================================================
@@ -910,70 +746,208 @@ class VibecheckMCP {
910
746
  skipAuth = true,
911
747
  } = options;
912
748
 
749
+ // ========================================================================
750
+ // HARDENING: Validate command
751
+ // ========================================================================
752
+ const sanitizedCommand = sanitizeString(command, 50);
753
+ if (!sanitizedCommand || !/^[a-z0-9_-]+$/i.test(sanitizedCommand)) {
754
+ throw new Error(`Invalid CLI command: ${sanitizedCommand}`);
755
+ }
756
+
757
+ // ========================================================================
758
+ // HARDENING: Validate and sanitize arguments
759
+ // ========================================================================
760
+ const sanitizedArgs = sanitizeArray(args, 50).map(arg => {
761
+ const str = String(arg);
762
+ // Validate argument format (must be simple flags or values)
763
+ if (str.length > 1000) {
764
+ return str.slice(0, 1000);
765
+ }
766
+ return str;
767
+ });
768
+
769
+ // ========================================================================
770
+ // HARDENING: Validate working directory
771
+ // ========================================================================
772
+ const resolvedCwd = path.resolve(cwd || process.cwd());
773
+ if (!fsSync.existsSync(resolvedCwd)) {
774
+ throw new Error(`Working directory does not exist: ${resolvedCwd}`);
775
+ }
776
+
913
777
  // Build argument array - this prevents command injection
914
- const finalArgs = [CONFIG.BIN_PATH, command, ...args];
778
+ const finalArgs = [CONFIG.BIN_PATH, sanitizedCommand, ...sanitizedArgs];
779
+
780
+ // ========================================================================
781
+ // HARDENING: Clean environment - don't leak sensitive vars
782
+ // ========================================================================
783
+ const safeEnv = { ...process.env };
784
+ // Remove potentially sensitive env vars from being passed through
785
+ const sensitiveEnvKeys = ['AWS_SECRET_ACCESS_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL'];
786
+ for (const key of sensitiveEnvKeys) {
787
+ if (safeEnv[key] && !env[key]) {
788
+ // Only remove if not explicitly set in options
789
+ delete safeEnv[key];
790
+ }
791
+ }
915
792
 
916
793
  const execEnv = {
917
- ...process.env,
794
+ ...safeEnv,
918
795
  ...CONFIG.ENV_DEFAULTS,
919
796
  ...(skipAuth ? { VIBECHECK_SKIP_AUTH: "1" } : {}),
920
797
  ...env,
798
+ // Ensure Node.js doesn't prompt for anything
799
+ NODE_NO_READLINE: "1",
800
+ FORCE_COLOR: "0",
921
801
  };
922
802
 
803
+ // ========================================================================
804
+ // HARDENING: Bounded timeout
805
+ // ========================================================================
806
+ const boundedTimeout = sanitizeNumber(timeout, 1000, 900000, CONFIG.TIMEOUTS.DEFAULT);
807
+
923
808
  try {
924
809
  const { stdout, stderr } = await execFileAsync(process.execPath, finalArgs, {
925
- cwd,
810
+ cwd: resolvedCwd,
926
811
  encoding: "utf8",
927
812
  maxBuffer: CONFIG.MAX_BUFFER,
928
- timeout,
813
+ timeout: boundedTimeout,
929
814
  env: execEnv,
815
+ // Don't inherit stdin - prevents hanging
816
+ stdio: ['ignore', 'pipe', 'pipe'],
930
817
  });
931
- return { stdout, stderr, success: true };
818
+
819
+ // Sanitize output before returning
820
+ return {
821
+ stdout: redactSensitive(truncateOutput(stdout || '')),
822
+ stderr: redactSensitive(truncateOutput(stderr || '')),
823
+ success: true
824
+ };
932
825
  } catch (error) {
933
- // Attach partial output for graceful degradation
934
- error.partialOutput = error.stdout || "";
935
- error.partialStderr = error.stderr || "";
826
+ // Attach partial output for graceful degradation (sanitized)
827
+ error.partialOutput = redactSensitive(truncateOutput(error.stdout || ''));
828
+ error.partialStderr = redactSensitive(truncateOutput(error.stderr || ''));
829
+
830
+ // Add helpful error code for timeout
831
+ if (error.killed && error.signal === 'SIGTERM') {
832
+ error.code = 'TIMEOUT';
833
+ error.message = `Command timed out after ${boundedTimeout}ms`;
834
+ }
835
+
936
836
  throw error;
937
837
  }
938
838
  }
939
839
 
940
840
  // ============================================================================
941
- // UTILITY HELPERS
841
+ // HARDENED UTILITY HELPERS
942
842
  // ============================================================================
943
843
 
944
- // Strip ANSI escape codes from output
844
+ /**
845
+ * Strip ANSI escape codes from output with length validation
846
+ * @param {string} str - String to strip
847
+ * @returns {string}
848
+ */
945
849
  stripAnsi(str) {
946
- return str ? str.replace(/\x1b\[[0-9;]*m/g, "") : "";
850
+ if (!str || typeof str !== 'string') {
851
+ return '';
852
+ }
853
+
854
+ // Truncate first to prevent DoS on very long strings
855
+ const truncated = str.length > CONFIG.LIMITS.MAX_OUTPUT_LENGTH
856
+ ? str.slice(0, CONFIG.LIMITS.MAX_OUTPUT_LENGTH)
857
+ : str;
858
+
859
+ return truncated.replace(/\x1b\[[0-9;]*m/g, "");
947
860
  }
948
861
 
949
- // Parse summary from disk (for graceful degradation on CLI errors)
862
+ /**
863
+ * Parse summary from disk with size limits and validation
864
+ * @param {string} projectPath - Project root path
865
+ * @returns {Promise<object|null>} Parsed summary or null
866
+ */
950
867
  async parseSummaryFromDisk(projectPath) {
951
868
  const summaryPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "summary.json");
869
+
952
870
  try {
871
+ // Check file size first
872
+ const stats = await fs.stat(summaryPath);
873
+ if (stats.size > 5 * 1024 * 1024) { // 5MB limit
874
+ console.error(`[MCP] Summary file too large: ${stats.size} bytes`);
875
+ return null;
876
+ }
877
+
953
878
  const content = await fs.readFile(summaryPath, "utf-8");
954
- return JSON.parse(content);
955
- } catch {
879
+ const parsed = safeJsonParse(content);
880
+
881
+ if (!parsed.success) {
882
+ console.error(`[MCP] Invalid summary JSON: ${parsed.error}`);
883
+ return null;
884
+ }
885
+
886
+ return parsed.data;
887
+ } catch (err) {
888
+ // Silent fail - this is for graceful degradation
956
889
  return null;
957
890
  }
958
891
  }
959
892
 
960
- // Format scan output from summary object
893
+ /**
894
+ * Format scan output from summary object with validation
895
+ * @param {object} summary - Summary object
896
+ * @param {string} projectPath - Project root path
897
+ * @returns {string}
898
+ */
961
899
  formatScanOutput(summary, projectPath) {
962
- let output = `## Score: ${summary.score}/100 (${summary.grade})\n\n`;
963
- output += `**Verdict:** ${summary.canShip ? "✅ SHIP" : "🚫 NO-SHIP"}\n\n`;
900
+ if (!summary || typeof summary !== 'object') {
901
+ return '## Error: Invalid summary data\n';
902
+ }
903
+
904
+ // Safely extract values with defaults
905
+ const score = sanitizeNumber(summary.score, 0, 100, 0);
906
+ const grade = sanitizeString(summary.grade, 10) || 'N/A';
907
+ const canShip = Boolean(summary.canShip);
908
+
909
+ let output = `## Score: ${score}/100 (${grade})\n\n`;
910
+ output += `**Verdict:** ${canShip ? "✅ SHIP" : "🚫 NO-SHIP"}\n\n`;
964
911
 
965
- if (summary.counts) {
912
+ if (summary.counts && typeof summary.counts === 'object') {
966
913
  output += "### Checks\n\n";
967
914
  output += "| Category | Issues |\n|----------|--------|\n";
968
- for (const [key, count] of Object.entries(summary.counts)) {
969
- const icon = count === 0 ? "✅" : "⚠️";
970
- output += `| ${icon} ${key} | ${count} |\n`;
915
+
916
+ // Limit to 50 categories to prevent output bloat
917
+ const entries = Object.entries(summary.counts).slice(0, 50);
918
+ for (const [key, count] of entries) {
919
+ const safeKey = sanitizeString(key, 50);
920
+ const safeCount = sanitizeNumber(count, 0, 999999, 0);
921
+ const icon = safeCount === 0 ? "✅" : "⚠️";
922
+ output += `| ${icon} ${safeKey} | ${safeCount} |\n`;
971
923
  }
972
924
  }
973
925
 
974
926
  output += `\n📄 **Report:** ${CONFIG.OUTPUT_DIR}/report.html\n`;
975
927
  return output;
976
928
  }
929
+
930
+ /**
931
+ * Safely read a file with size limits
932
+ * @param {string} filePath - Path to file
933
+ * @param {number} maxSize - Maximum file size in bytes
934
+ * @returns {Promise<string|null>} File contents or null
935
+ */
936
+ async safeReadFile(filePath, maxSize = 10 * 1024 * 1024) {
937
+ try {
938
+ const stats = await fs.stat(filePath);
939
+
940
+ if (stats.size > maxSize) {
941
+ console.error(`[MCP] File too large: ${filePath} (${stats.size} bytes)`);
942
+ return null;
943
+ }
944
+
945
+ const content = await fs.readFile(filePath, "utf-8");
946
+ return content;
947
+ } catch (err) {
948
+ return null;
949
+ }
950
+ }
977
951
 
978
952
  setupHandlers() {
979
953
  // List tools
@@ -983,24 +957,95 @@ class VibecheckMCP {
983
957
 
984
958
  // Call tool - main dispatch handler
985
959
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
986
- const { name, arguments: args } = request.params;
987
- const projectPath = path.resolve(args?.projectPath || ".");
988
960
  const startTime = Date.now();
961
+ let toolName = 'unknown';
962
+ let projectPath = '.';
963
+
964
+ try {
965
+ // ====================================================================
966
+ // HARDENING: Input extraction with validation
967
+ // ====================================================================
968
+ const params = request?.params;
969
+ if (!params || typeof params !== 'object') {
970
+ return this.error('Invalid request: missing params', { code: 'INVALID_REQUEST' });
971
+ }
972
+
973
+ toolName = sanitizeString(params.name, 100);
974
+ const args = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
975
+
976
+ // Validate tool name
977
+ if (!toolName || toolName.length < 2) {
978
+ return this.error('Invalid tool name', { code: 'INVALID_TOOL_NAME' });
979
+ }
980
+
981
+ // ====================================================================
982
+ // HARDENING: Rate limiting (per-API-key)
983
+ // ====================================================================
984
+ const apiKey = args?.apiKey || null;
985
+ const rateCheck = checkRateLimit(apiKey);
986
+ if (!rateCheck.allowed) {
987
+ return this.error(`Rate limit exceeded. Try again in ${Math.ceil(rateCheck.resetIn / 1000)} seconds`, {
988
+ code: 'RATE_LIMIT_EXCEEDED',
989
+ suggestion: 'Reduce the frequency of tool calls',
990
+ nextSteps: [`Wait ${Math.ceil(rateCheck.resetIn / 1000)} seconds before retrying`],
991
+ });
992
+ }
993
+
994
+ // ====================================================================
995
+ // HARDENING: Project path validation
996
+ // ====================================================================
997
+ const rawProjectPath = args?.projectPath || '.';
998
+ const pathValidation = sanitizePath(rawProjectPath, process.cwd());
999
+
1000
+ if (!pathValidation.valid) {
1001
+ return this.error(pathValidation.error, {
1002
+ code: 'INVALID_PATH',
1003
+ suggestion: 'Provide a valid path within the current working directory',
1004
+ });
1005
+ }
1006
+ projectPath = pathValidation.path;
989
1007
 
990
- // Emit audit event for tool invocation start
991
- emitToolInvoke(name, args, "success", { projectPath });
1008
+ // Emit audit event for tool invocation start
1009
+ // SECURITY: Include apiKey hash for audit trail (never log raw key)
1010
+ try {
1011
+ const crypto = require('crypto');
1012
+ const apiKeyHash = apiKey
1013
+ ? crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16)
1014
+ : 'anonymous';
1015
+
1016
+ emitToolInvoke(toolName, args, "success", {
1017
+ projectPath,
1018
+ apiKeyHash,
1019
+ rateLimit: rateCheck,
1020
+ timestamp: new Date().toISOString(),
1021
+ });
1022
+ } catch {
1023
+ // Audit logging should never break the tool
1024
+ }
992
1025
 
993
- try {
994
- // TRUTH FIREWALL CHECK - enforce validation before code-changing tools
995
- const firewallCheck = checkTruthFirewallBlock(name, args, projectPath);
1026
+ // ====================================================================
1027
+ // HARDENING: Truth firewall check with error handling
1028
+ // ====================================================================
1029
+ let firewallCheck = { blocked: false };
1030
+ try {
1031
+ firewallCheck = checkTruthFirewallBlock(toolName, args, projectPath);
1032
+ } catch (firewallError) {
1033
+ console.error(`[MCP] Firewall check error: ${firewallError.message}`);
1034
+ // Continue - don't block on firewall errors
1035
+ }
1036
+
996
1037
  if (firewallCheck.blocked) {
997
1038
  const policy = getTruthPolicy(args);
998
- await emitGuardrailMetric(projectPath, {
999
- event: "truth_firewall_block",
1000
- tool: name,
1001
- policy,
1002
- reason: firewallCheck.code || "no_recent_claim_validation",
1003
- });
1039
+ try {
1040
+ await emitGuardrailMetric(projectPath, {
1041
+ event: "truth_firewall_block",
1042
+ tool: toolName,
1043
+ policy,
1044
+ reason: firewallCheck.code || "no_recent_claim_validation",
1045
+ });
1046
+ } catch {
1047
+ // Metrics should never break the tool
1048
+ }
1004
1049
  return this.error(firewallCheck.reason, {
1005
1050
  code: firewallCheck.code,
1006
1051
  suggestion: firewallCheck.suggestion,
@@ -1009,98 +1054,142 @@ class VibecheckMCP {
1009
1054
  }
1010
1055
 
1011
1056
  // Handle v3 tools (10 consolidated tools, STARTER+ only)
1012
- if (USE_V3_TOOLS && V3_TOOL_TIERS[name]) {
1013
- const userTier = args?.tier || process.env.VIBECHECK_TIER || 'free';
1014
- const result = await handleToolV3(name, args, { tier: userTier });
1057
+ if (USE_V3_TOOLS && V3_TOOL_TIERS[toolName]) {
1058
+ // SECURITY FIX: Never trust client-provided tier - validate from API key
1059
+ // Previous: const userTier = sanitizeString(args?.tier, 20) || ...
1060
+ // This allowed privilege escalation via args.tier = "pro"
1061
+ const { getMcpToolAccess } = await import('./tier-auth.js');
1062
+ const access = await getMcpToolAccess(toolName, apiKey);
1063
+ const userTier = access.tier || 'free';
1064
+ const result = await handleToolV3(toolName, args, { tier: userTier });
1015
1065
 
1016
1066
  if (result.error) {
1017
1067
  return this.error(result.error, { tier: result.tier, required: result.required });
1018
1068
  }
1019
1069
 
1070
+ // Sanitize and truncate output
1071
+ const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1020
1072
  return {
1021
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1073
+ content: [{ type: "text", text: outputText }],
1022
1074
  };
1023
1075
  }
1024
1076
 
1025
1077
  // 1. Check tool registry first (local CLI handlers)
1026
- if (this.toolRegistry[name]) {
1027
- return await this.toolRegistry[name](projectPath, args);
1078
+ if (this.toolRegistry[toolName]) {
1079
+ return await this.toolRegistry[toolName](projectPath, args);
1028
1080
  }
1029
1081
 
1030
1082
  // 2. Handle external module tools by prefix/pattern
1031
- if (name.startsWith("vibecheck.intelligence.")) {
1032
- return await handleIntelligenceTool(name, args, __dirname);
1083
+ if (toolName.startsWith("vibecheck.intelligence.")) {
1084
+ return await handleIntelligenceTool(toolName, args, __dirname);
1033
1085
  }
1034
1086
 
1035
1087
  // Handle AI vibecheck tools
1036
1088
  if (["vibecheck.verify", "vibecheck.quality", "vibecheck.smells",
1037
1089
  "vibecheck.hallucination", "vibecheck.breaking", "vibecheck.mdc",
1038
- "vibecheck.coverage"].includes(name)) {
1039
- const result = await handleVibecheckTool(name, args);
1090
+ "vibecheck.coverage"].includes(toolName)) {
1091
+ const result = await handleVibecheckTool(toolName, args);
1092
+ const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1040
1093
  return {
1041
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1094
+ content: [{ type: "text", text: outputText }],
1042
1095
  };
1043
1096
  }
1044
1097
 
1045
1098
  // Handle agent checkpoint tools
1046
- if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(name)) {
1047
- return await handleCheckpointTool(name, args);
1099
+ if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(toolName)) {
1100
+ return await handleCheckpointTool(toolName, args);
1048
1101
  }
1049
1102
 
1050
1103
  // Handle architect tools
1051
1104
  if (["vibecheck_architect_review", "vibecheck_architect_suggest",
1052
- "vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(name)) {
1053
- return await handleArchitectTool(name, args);
1105
+ "vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(toolName)) {
1106
+ return await handleArchitectTool(toolName, args);
1107
+ }
1108
+
1109
+ // Handle authority system tools
1110
+ if (["authority.classify", "authority.approve", "authority.list"].includes(toolName)) {
1111
+ const result = await handleAuthorityTool(toolName, args, userTier || "free");
1112
+ const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1113
+ return {
1114
+ content: [{ type: "text", text: outputText }],
1115
+ };
1054
1116
  }
1055
1117
 
1056
1118
  // Handle codebase architect tools
1057
1119
  if (["vibecheck_architect_context", "vibecheck_architect_guide",
1058
1120
  "vibecheck_architect_validate", "vibecheck_architect_patterns",
1059
- "vibecheck_architect_dependencies"].includes(name)) {
1060
- return await handleCodebaseArchitectTool(name, args);
1121
+ "vibecheck_architect_dependencies"].includes(toolName)) {
1122
+ return await handleCodebaseArchitectTool(toolName, args);
1061
1123
  }
1062
1124
 
1063
1125
  // Handle vibecheck 2.0 tools
1064
- if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(name)) {
1065
- return await handleVibecheck2Tool(name, args, __dirname);
1126
+ if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(toolName)) {
1127
+ return await handleVibecheck2Tool(toolName, args, __dirname);
1066
1128
  }
1067
1129
 
1068
1130
  // Handle intent drift tools
1069
- if (name.startsWith("vibecheck_intent_")) {
1070
- const tool = intentDriftTools.find(t => t.name === name);
1131
+ if (toolName.startsWith("vibecheck_intent_")) {
1132
+ const tool = intentDriftTools.find(t => t.name === toolName);
1071
1133
  if (tool && tool.handler) {
1072
1134
  const result = await tool.handler(args);
1135
+ const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
1073
1136
  return {
1074
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1137
+ content: [{ type: "text", text: outputText }],
1075
1138
  };
1076
1139
  }
1077
1140
  }
1078
1141
 
1079
1142
  // Handle MDC generator
1080
- if (name === "generate_mdc") {
1143
+ if (toolName === "generate_mdc") {
1081
1144
  return await handleMDCGeneration(args);
1082
1145
  }
1083
1146
 
1084
1147
  // Handle Truth Context tools (Evidence Pack / Truth Pack)
1085
- if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(name)) {
1086
- return await handleTruthContextTool(name, args);
1148
+ if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(toolName)) {
1149
+ return await handleTruthContextTool(toolName, args);
1087
1150
  }
1088
1151
 
1089
1152
  // Handle Truth Firewall tools (Hallucination Stopper)
1090
1153
  if (["vibecheck.get_truthpack", "vibecheck.validate_claim", "vibecheck.compile_context",
1091
1154
  "vibecheck.search_evidence", "vibecheck.find_counterexamples", "vibecheck.propose_patch",
1092
- "vibecheck.check_invariants", "vibecheck.add_assumption"].includes(name)) {
1093
- return await handleTruthFirewallTool(name, args, projectPath);
1155
+ "vibecheck.check_invariants", "vibecheck.add_assumption"].includes(toolName)) {
1156
+ return await handleTruthFirewallTool(toolName, args, projectPath);
1094
1157
  }
1095
1158
 
1096
- return this.error(`Unknown tool: ${name}`);
1159
+ return this.error(`Unknown tool: ${toolName}`, {
1160
+ code: 'UNKNOWN_TOOL',
1161
+ suggestion: 'Check the tool name and try again',
1162
+ nextSteps: ['Use vibecheck.status to see available tools'],
1163
+ });
1097
1164
  } catch (err) {
1098
- // Emit audit event for tool error
1099
- emitToolComplete(name, "error", {
1100
- errorMessage: err.message,
1101
- durationMs: Date.now() - startTime
1165
+ // ====================================================================
1166
+ // HARDENING: Enhanced error handling with sanitization
1167
+ // ====================================================================
1168
+ const durationMs = Date.now() - startTime;
1169
+ const errorMessage = sanitizeString(err?.message || 'Unknown error', 500);
1170
+
1171
+ // Emit audit event for tool error (safely)
1172
+ try {
1173
+ emitToolComplete(toolName, "error", {
1174
+ errorMessage: redactSensitive(errorMessage),
1175
+ durationMs,
1176
+ });
1177
+ } catch {
1178
+ // Audit logging should never break the response
1179
+ }
1180
+
1181
+ // Log error details to stderr (not stdout - preserves MCP protocol)
1182
+ console.error(`[MCP] Tool ${toolName} failed after ${durationMs}ms: ${errorMessage}`);
1183
+
1184
+ return this.error(`${toolName} failed: ${redactSensitive(errorMessage)}`, {
1185
+ code: err?.code || 'TOOL_ERROR',
1186
+ suggestion: 'Check the error message and try again',
1187
+ nextSteps: [
1188
+ 'Verify the tool arguments are correct',
1189
+ 'Check that the project path is valid',
1190
+ 'Try running with simpler arguments first',
1191
+ ],
1102
1192
  });
1103
- return this.error(`${name} failed: ${err.message}`);
1104
1193
  }
1105
1194
  });
1106
1195
 
@@ -1161,15 +1250,41 @@ class VibecheckMCP {
1161
1250
  this.server.setRequestHandler(
1162
1251
  ReadResourceRequestSchema,
1163
1252
  async (request) => {
1164
- const { uri } = request.params;
1253
+ // ====================================================================
1254
+ // HARDENING: Resource request validation
1255
+ // ====================================================================
1256
+ const uri = sanitizeString(request?.params?.uri, 200);
1257
+ if (!uri || !uri.startsWith('vibecheck://')) {
1258
+ return { contents: [{ uri: uri || '', mimeType: "application/json", text: '{"error": "Invalid resource URI"}' }] };
1259
+ }
1260
+
1165
1261
  const projectPath = process.cwd();
1262
+
1263
+ // Helper to safely read and return JSON resource
1264
+ const safeReadResource = async (filePath, defaultMessage) => {
1265
+ try {
1266
+ const content = await fs.readFile(filePath, "utf-8");
1267
+ // Validate JSON and sanitize
1268
+ const parsed = safeJsonParse(content);
1269
+ if (!parsed.success) {
1270
+ return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "Invalid JSON in resource file" }) }] };
1271
+ }
1272
+ // Redact any sensitive data and truncate
1273
+ const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
1274
+ return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
1275
+ } catch {
1276
+ return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ message: defaultMessage }) }] };
1277
+ }
1278
+ };
1166
1279
 
1167
1280
  if (uri === "vibecheck://config") {
1168
1281
  const configPath = path.join(projectPath, "vibecheck.config.json");
1169
1282
  try {
1170
1283
  const content = await fs.readFile(configPath, "utf-8");
1284
+ // Redact sensitive config values
1285
+ const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
1171
1286
  return {
1172
- contents: [{ uri, mimeType: "application/json", text: content }],
1287
+ contents: [{ uri, mimeType: "application/json", text: sanitized }],
1173
1288
  };
1174
1289
  } catch {
1175
1290
  return {
@@ -1179,141 +1294,161 @@ class VibecheckMCP {
1179
1294
  }
1180
1295
 
1181
1296
  if (uri === "vibecheck://summary") {
1182
- const summaryPath = path.join(
1183
- projectPath,
1184
- ".vibecheck",
1185
- "summary.json",
1186
- );
1187
- try {
1188
- const content = await fs.readFile(summaryPath, "utf-8");
1189
- return {
1190
- contents: [{ uri, mimeType: "application/json", text: content }],
1191
- };
1192
- } catch {
1193
- return {
1194
- contents: [
1195
- {
1196
- uri,
1197
- mimeType: "application/json",
1198
- text: '{"message": "No scan found. Run vibecheck.scan first."}',
1199
- },
1200
- ],
1201
- };
1202
- }
1297
+ const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
1298
+ return await safeReadResource(summaryPath, "No scan found. Run vibecheck.scan first.");
1203
1299
  }
1204
1300
 
1205
1301
  if (uri === "vibecheck://truthpack") {
1206
1302
  const truthpackPath = path.join(projectPath, ".vibecheck", "truth", "truthpack.json");
1207
- try {
1208
- const content = await fs.readFile(truthpackPath, "utf-8");
1209
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1210
- } catch {
1211
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No truthpack. Run vibecheck.ctx first."}' }] };
1212
- }
1303
+ return await safeReadResource(truthpackPath, "No truthpack. Run vibecheck.ctx first.");
1213
1304
  }
1214
1305
 
1215
1306
  if (uri === "vibecheck://missions") {
1216
1307
  const missionsDir = path.join(projectPath, ".vibecheck", "missions");
1217
1308
  try {
1309
+ // HARDENING: Validate directory read
1218
1310
  const dirs = await fs.readdir(missionsDir);
1219
- const latest = dirs.sort().reverse()[0];
1311
+ const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
1312
+ const latest = safeDirs.sort().reverse()[0];
1313
+
1220
1314
  if (latest) {
1221
1315
  const missionPath = path.join(missionsDir, latest, "missions.json");
1222
- const content = await fs.readFile(missionPath, "utf-8");
1223
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1316
+ return await safeReadResource(missionPath, "No missions found in latest directory.");
1224
1317
  }
1225
- } catch {}
1318
+ } catch (err) {
1319
+ console.error(`[MCP] Error reading missions: ${err.message}`);
1320
+ }
1226
1321
  return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No missions. Run vibecheck.fix first."}' }] };
1227
1322
  }
1228
1323
 
1229
1324
  if (uri === "vibecheck://reality") {
1230
1325
  const realityPath = path.join(projectPath, ".vibecheck", "reality", "last_reality.json");
1231
- try {
1232
- const content = await fs.readFile(realityPath, "utf-8");
1233
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1234
- } catch {
1235
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No reality results. Run vibecheck verify first."}' }] };
1236
- }
1326
+ return await safeReadResource(realityPath, "No reality results. Run vibecheck verify first.");
1237
1327
  }
1238
1328
 
1239
1329
  if (uri === "vibecheck://findings") {
1240
1330
  const findingsPath = path.join(projectPath, ".vibecheck", "findings.json");
1241
- try {
1242
- const content = await fs.readFile(findingsPath, "utf-8");
1243
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1244
- } catch {
1245
- // Try summary.json as fallback
1246
- const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
1247
- try {
1248
- const summary = JSON.parse(await fs.readFile(summaryPath, "utf-8"));
1249
- const findings = summary.findings || [];
1331
+
1332
+ // Try primary findings file
1333
+ const content = await this.safeReadFile(findingsPath, 10 * 1024 * 1024);
1334
+ if (content) {
1335
+ const parsed = safeJsonParse(content);
1336
+ if (parsed.success) {
1337
+ const sanitized = redactSensitive(truncateOutput(content));
1338
+ return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
1339
+ }
1340
+ }
1341
+
1342
+ // HARDENING: Try summary.json as fallback with size limits
1343
+ const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
1344
+ const summaryContent = await this.safeReadFile(summaryPath, 10 * 1024 * 1024);
1345
+
1346
+ if (summaryContent) {
1347
+ const parsed = safeJsonParse(summaryContent);
1348
+ if (parsed.success && parsed.data.findings) {
1349
+ const findings = sanitizeArray(parsed.data.findings, 1000);
1250
1350
  return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ findings }, null, 2) }] };
1251
- } catch {
1252
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
1253
1351
  }
1254
1352
  }
1353
+
1354
+ return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
1255
1355
  }
1256
1356
 
1257
1357
  if (uri === "vibecheck://share") {
1258
1358
  const missionsDir = path.join(projectPath, ".vibecheck", "missions");
1259
1359
  try {
1360
+ // HARDENING: Safe directory read
1260
1361
  const dirs = await fs.readdir(missionsDir);
1261
- const latest = dirs.sort().reverse()[0];
1362
+ const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
1363
+ const latest = safeDirs.sort().reverse()[0];
1364
+
1262
1365
  if (latest) {
1263
1366
  const sharePath = path.join(missionsDir, latest, "share", "share.json");
1264
- try {
1265
- const content = await fs.readFile(sharePath, "utf-8");
1266
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1267
- } catch {
1268
- // Fallback to missions.json
1269
- const missionPath = path.join(missionsDir, latest, "missions.json");
1270
- const content = await fs.readFile(missionPath, "utf-8");
1271
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1367
+
1368
+ // Try share.json first
1369
+ const shareContent = await this.safeReadFile(sharePath, 10 * 1024 * 1024);
1370
+ if (shareContent) {
1371
+ const parsed = safeJsonParse(shareContent);
1372
+ if (parsed.success) {
1373
+ const sanitized = redactSensitive(truncateOutput(shareContent));
1374
+ return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
1375
+ }
1272
1376
  }
1377
+
1378
+ // HARDENING: Fallback to missions.json with safe read
1379
+ const missionPath = path.join(missionsDir, latest, "missions.json");
1380
+ return await safeReadResource(missionPath, "No share data available in latest mission.");
1273
1381
  }
1274
- } catch {}
1382
+ } catch (err) {
1383
+ console.error(`[MCP] Error reading share pack: ${err.message}`);
1384
+ }
1275
1385
  return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No share pack. Run vibecheck.fix --share first."}' }] };
1276
1386
  }
1277
1387
 
1278
1388
  if (uri === "vibecheck://prove") {
1279
1389
  const provePath = path.join(projectPath, ".vibecheck", "prove", "last_prove.json");
1280
- try {
1281
- const content = await fs.readFile(provePath, "utf-8");
1282
- return { contents: [{ uri, mimeType: "application/json", text: content }] };
1283
- } catch {
1284
- return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No prove results. Run vibecheck.prove first."}' }] };
1285
- }
1390
+ return await safeReadResource(provePath, "No prove results. Run vibecheck.prove first.");
1286
1391
  }
1287
1392
 
1288
- return { contents: [] };
1393
+ return {
1394
+ contents: [{
1395
+ uri,
1396
+ mimeType: "application/json",
1397
+ text: JSON.stringify({ error: "Unknown resource URI" })
1398
+ }]
1399
+ };
1289
1400
  },
1290
1401
  );
1291
1402
  }
1292
1403
 
1293
- // Helpers
1404
+ // ============================================================================
1405
+ // HARDENED HELPERS
1406
+ // ============================================================================
1407
+
1408
+ /**
1409
+ * Return a successful response with sanitization
1410
+ * @param {string} text - Response text
1411
+ * @param {boolean} includeAttribution - Include vibecheck attribution
1412
+ * @returns {object} MCP response
1413
+ */
1294
1414
  success(text, includeAttribution = true) {
1415
+ // Sanitize output: redact secrets and truncate
1416
+ let sanitized = redactSensitive(sanitizeString(text, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
1417
+
1295
1418
  const finalText = includeAttribution
1296
- ? `${text}\n\n---\n_${CONTEXT_ATTRIBUTION}_`
1297
- : text;
1419
+ ? `${sanitized}\n\n---\n_${CONTEXT_ATTRIBUTION}_`
1420
+ : sanitized;
1421
+
1298
1422
  return { content: [{ type: "text", text: finalText }] };
1299
1423
  }
1300
1424
 
1425
+ /**
1426
+ * Return an error response with sanitization
1427
+ * @param {string} text - Error message
1428
+ * @param {object} options - Additional options
1429
+ * @returns {object} MCP error response
1430
+ */
1301
1431
  error(text, options = {}) {
1302
1432
  const { code, suggestion, nextSteps = [] } = options;
1303
1433
 
1304
- let errorText = `❌ ${text}`;
1434
+ // Sanitize all text inputs
1435
+ const sanitizedText = redactSensitive(sanitizeString(text, 1000));
1436
+ const sanitizedSuggestion = suggestion ? redactSensitive(sanitizeString(suggestion, 500)) : null;
1437
+ const sanitizedSteps = sanitizeArray(nextSteps, 10).map(s => sanitizeString(s, 200));
1438
+
1439
+ let errorText = `❌ ${sanitizedText}`;
1305
1440
 
1306
1441
  if (code) {
1307
- errorText += `\n\n**Error Code:** \`${code}\``;
1442
+ errorText += `\n\n**Error Code:** \`${sanitizeString(code, 50)}\``;
1308
1443
  }
1309
1444
 
1310
- if (suggestion) {
1311
- errorText += `\n\n💡 **Suggestion:** ${suggestion}`;
1445
+ if (sanitizedSuggestion) {
1446
+ errorText += `\n\n💡 **Suggestion:** ${sanitizedSuggestion}`;
1312
1447
  }
1313
1448
 
1314
- if (nextSteps.length > 0) {
1449
+ if (sanitizedSteps.length > 0) {
1315
1450
  errorText += `\n\n**Next Steps:**\n`;
1316
- nextSteps.forEach((step, i) => {
1451
+ sanitizedSteps.forEach((step, i) => {
1317
1452
  errorText += `${i + 1}. ${step}\n`;
1318
1453
  });
1319
1454
  }
@@ -1366,28 +1501,53 @@ class VibecheckMCP {
1366
1501
  });
1367
1502
  }
1368
1503
 
1369
- const profile = args?.profile || "quick";
1370
- const only = args?.only;
1504
+ // HARDENING: Validate and sanitize profile
1505
+ const validProfiles = ["quick", "full", "ship", "ci", "security", "compliance", "ai"];
1506
+ const profile = validProfiles.includes(args?.profile) ? args?.profile : "quick";
1507
+
1508
+ // HARDENING: Sanitize only array
1509
+ const only = sanitizeArray(args?.only, 20).map(item => sanitizeString(item, 50));
1371
1510
 
1372
- // Initialize API integration
1511
+ // Initialize API integration with timeout and circuit breaker
1373
1512
  let apiScan = null;
1374
1513
  let apiConnected = false;
1375
1514
 
1515
+ // HARDENING: Check circuit breaker before attempting API calls
1516
+ const circuitCheck = checkCircuitBreaker();
1517
+
1376
1518
  // Try to connect to API for dashboard integration
1377
- try {
1378
- apiConnected = await isApiAvailable();
1379
- if (apiConnected) {
1380
- // Create scan record in dashboard
1381
- apiScan = await createScan({
1382
- localPath: projectPath,
1383
- branch: 'main',
1384
- enableLLM: false,
1385
- });
1386
- console.error(`[MCP] Connected to dashboard (Scan ID: ${apiScan.scanId})`);
1519
+ if (circuitCheck.allowed) {
1520
+ try {
1521
+ // HARDENING: Add timeout to API availability check
1522
+ const apiCheckPromise = isApiAvailable();
1523
+ const timeoutPromise = new Promise((_, reject) =>
1524
+ setTimeout(() => reject(new Error('API check timeout')), 5000)
1525
+ );
1526
+
1527
+ apiConnected = await Promise.race([apiCheckPromise, timeoutPromise]);
1528
+
1529
+ if (apiConnected) {
1530
+ // Create scan record in dashboard
1531
+ const createScanPromise = createScan({
1532
+ localPath: sanitizeString(projectPath, 500),
1533
+ branch: sanitizeString(args?.branch, 100) || 'main',
1534
+ enableLLM: false,
1535
+ });
1536
+ const scanTimeoutPromise = new Promise((_, reject) =>
1537
+ setTimeout(() => reject(new Error('Create scan timeout')), 10000)
1538
+ );
1539
+
1540
+ apiScan = await Promise.race([createScanPromise, scanTimeoutPromise]);
1541
+ console.error(`[MCP] Connected to dashboard (Scan ID: ${apiScan.scanId})`);
1542
+ recordApiResult(true); // Record success
1543
+ }
1544
+ } catch (err) {
1545
+ // API connection is optional, continue without it
1546
+ console.error(`[MCP] Dashboard integration unavailable: ${err.message}`);
1547
+ recordApiResult(false); // Record failure
1387
1548
  }
1388
- } catch (err) {
1389
- // API connection is optional, continue without it
1390
- console.error(`[MCP] Dashboard integration unavailable: ${err.message}`);
1549
+ } else {
1550
+ console.error(`[MCP] ${circuitCheck.reason}`);
1391
1551
  }
1392
1552
 
1393
1553
  let output = "# 🔍 vibecheck Scan\n\n";
@@ -1396,7 +1556,9 @@ class VibecheckMCP {
1396
1556
 
1397
1557
  // Build CLI arguments array (secure - no injection possible)
1398
1558
  const cliArgs = [`--profile=${profile}`, "--json"];
1399
- if (only?.length) cliArgs.push(`--only=${only.join(",")}`);
1559
+ if (only.length > 0) {
1560
+ cliArgs.push(`--only=${only.join(",")}`);
1561
+ }
1400
1562
 
1401
1563
  try {
1402
1564
  await this.runCLI("scan", cliArgs, projectPath, { timeout: CONFIG.TIMEOUTS.SCAN });
@@ -1409,19 +1571,25 @@ class VibecheckMCP {
1409
1571
  // Submit results to dashboard if connected
1410
1572
  if (apiConnected && apiScan) {
1411
1573
  try {
1412
- await submitScanResults(apiScan.scanId, {
1413
- verdict: summary.verdict || 'UNKNOWN',
1414
- score: summary.score?.overall || 0,
1415
- findings: summary.findings || [],
1416
- filesScanned: summary.stats?.filesScanned || 0,
1417
- linesScanned: summary.stats?.linesScanned || 0,
1418
- durationMs: summary.timings?.total || 0,
1574
+ // HARDENING: Add timeout to result submission
1575
+ const submitPromise = submitScanResults(apiScan.scanId, {
1576
+ verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
1577
+ score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
1578
+ findings: sanitizeArray(summary.findings, 1000) || [],
1579
+ filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
1580
+ linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
1581
+ durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
1419
1582
  metadata: {
1420
1583
  profile,
1421
1584
  source: 'mcp-server',
1422
1585
  version: CONFIG.VERSION,
1423
1586
  },
1424
1587
  });
1588
+ const submitTimeout = new Promise((_, reject) =>
1589
+ setTimeout(() => reject(new Error('Submit timeout')), 10000)
1590
+ );
1591
+
1592
+ await Promise.race([submitPromise, submitTimeout]);
1425
1593
  console.error(`[MCP] Results sent to dashboard`);
1426
1594
  } catch (err) {
1427
1595
  console.error(`[MCP] Failed to send results to dashboard: ${err.message}`);
@@ -1439,23 +1607,29 @@ class VibecheckMCP {
1439
1607
  // Submit results to dashboard if connected
1440
1608
  if (apiConnected && apiScan) {
1441
1609
  try {
1442
- await submitScanResults(apiScan.scanId, {
1443
- verdict: summary.verdict || 'UNKNOWN',
1444
- score: summary.score?.overall || 0,
1445
- findings: summary.findings || [],
1446
- filesScanned: summary.stats?.filesScanned || 0,
1447
- linesScanned: summary.stats?.linesScanned || 0,
1448
- durationMs: summary.timings?.total || 0,
1610
+ // HARDENING: Add timeout to error case submission
1611
+ const submitPromise = submitScanResults(apiScan.scanId, {
1612
+ verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
1613
+ score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
1614
+ findings: sanitizeArray(summary.findings, 1000) || [],
1615
+ filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
1616
+ linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
1617
+ durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
1449
1618
  metadata: {
1450
1619
  profile,
1451
1620
  source: 'mcp-server',
1452
1621
  version: CONFIG.VERSION,
1453
- error: err.message,
1622
+ error: sanitizeString(err.message, 500),
1454
1623
  },
1455
1624
  });
1625
+ const submitTimeout = new Promise((_, reject) =>
1626
+ setTimeout(() => reject(new Error('Submit timeout')), 10000)
1627
+ );
1628
+
1629
+ await Promise.race([submitPromise, submitTimeout]);
1456
1630
  console.error(`[MCP] Results sent to dashboard (with error)`);
1457
- } catch (err) {
1458
- console.error(`[MCP] Failed to send results to dashboard: ${err.message}`);
1631
+ } catch (apiErr) {
1632
+ console.error(`[MCP] Failed to send results to dashboard: ${apiErr.message}`);
1459
1633
  }
1460
1634
  }
1461
1635
  output += `\n⚠️ Scan completed with findings (exit code ${err.code || 1})\n`;
@@ -1465,10 +1639,16 @@ class VibecheckMCP {
1465
1639
  // Report error to dashboard if connected
1466
1640
  if (apiConnected && apiScan) {
1467
1641
  try {
1468
- await reportScanError(apiScan.scanId, err);
1642
+ // HARDENING: Add timeout to error reporting
1643
+ const reportPromise = reportScanError(apiScan.scanId, err);
1644
+ const reportTimeout = new Promise((_, reject) =>
1645
+ setTimeout(() => reject(new Error('Report timeout')), 10000)
1646
+ );
1647
+
1648
+ await Promise.race([reportPromise, reportTimeout]);
1469
1649
  console.error(`[MCP] Error reported to dashboard`);
1470
- } catch (err) {
1471
- console.error(`[MCP] Failed to report error to dashboard: ${err.message}`);
1650
+ } catch (apiErr) {
1651
+ console.error(`[MCP] Failed to report error to dashboard: ${apiErr.message}`);
1472
1652
  }
1473
1653
  }
1474
1654
 
@@ -1490,7 +1670,7 @@ class VibecheckMCP {
1490
1670
  // ============================================================================
1491
1671
  async handleGate(projectPath, args) {
1492
1672
  // Check tier access (STARTER tier required)
1493
- const access = await checkFeatureAccess("gate", args?.apiKey);
1673
+ const access = await getFeatureAccessStatus("gate", args?.apiKey);
1494
1674
  if (!access.hasAccess) {
1495
1675
  return {
1496
1676
  content: [{
@@ -1530,7 +1710,7 @@ class VibecheckMCP {
1530
1710
  async handleFix(projectPath, args) {
1531
1711
  // Check tier access for --apply and --autopilot (PRO tier required)
1532
1712
  if (args?.apply || args?.autopilot) {
1533
- const access = await checkFeatureAccess("fix.apply_patches", args?.apiKey);
1713
+ const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
1534
1714
  if (!access.hasAccess) {
1535
1715
  return {
1536
1716
  content: [{
@@ -1705,7 +1885,7 @@ class VibecheckMCP {
1705
1885
  // ============================================================================
1706
1886
  async handleProve(projectPath, args) {
1707
1887
  // Check tier access (PRO tier required)
1708
- const access = await checkFeatureAccess("prove", args?.apiKey);
1888
+ const access = await getFeatureAccessStatus("prove", args?.apiKey);
1709
1889
  if (!access.hasAccess) {
1710
1890
  return {
1711
1891
  content: [{
@@ -1908,6 +2088,16 @@ class VibecheckMCP {
1908
2088
  // SHIP - Quick health check
1909
2089
  // ============================================================================
1910
2090
  async handleShip(projectPath, args) {
2091
+ // HARDENING: Validate project path
2092
+ const validation = this.validateProjectPath(projectPath);
2093
+ if (!validation.valid) {
2094
+ return this.error(validation.error, {
2095
+ code: validation.code || "INVALID_PATH",
2096
+ suggestion: validation.suggestion,
2097
+ nextSteps: validation.nextSteps || [],
2098
+ });
2099
+ }
2100
+
1911
2101
  let output = "# 🚀 vibecheck Ship\n\n";
1912
2102
  output += `**Path:** ${projectPath}\n\n`;
1913
2103
 
@@ -1932,20 +2122,34 @@ class VibecheckMCP {
1932
2122
  // VERIFY - Runtime browser testing
1933
2123
  // ============================================================================
1934
2124
  async handleVerify(projectPath, args) {
1935
- const url = args?.url;
1936
- if (!url) return this.error("URL is required");
2125
+ // HARDENING: Validate URL
2126
+ const urlValidation = validateUrl(args?.url);
2127
+ if (!urlValidation.valid) {
2128
+ return this.error(urlValidation.error, {
2129
+ code: 'INVALID_URL',
2130
+ suggestion: 'Provide a valid HTTP/HTTPS URL',
2131
+ nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
2132
+ });
2133
+ }
2134
+ const url = urlValidation.url;
1937
2135
 
1938
2136
  let output = "# 🧪 vibecheck Verify\n\n";
1939
2137
  output += `**URL:** ${url}\n`;
1940
- if (args?.flows?.length) output += `**Flows:** ${args.flows.join(", ")}\n`;
2138
+
2139
+ // HARDENING: Sanitize array inputs
2140
+ const flows = sanitizeArray(args?.flows, 10);
2141
+ if (flows.length) output += `**Flows:** ${flows.join(", ")}\n`;
1941
2142
  if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
1942
2143
  if (args?.record) output += `**Recording:** Enabled\n`;
1943
2144
  output += "\n";
1944
2145
 
1945
2146
  // Build CLI arguments array (secure)
1946
2147
  const cliArgs = ["--url", url];
1947
- if (args?.auth) cliArgs.push("--auth", args.auth);
1948
- if (args?.flows?.length) cliArgs.push("--flows", args.flows.join(","));
2148
+ // HARDENING: Sanitize auth - don't log full credentials
2149
+ if (args?.auth && typeof args.auth === 'string') {
2150
+ cliArgs.push("--auth", sanitizeString(args.auth, 200));
2151
+ }
2152
+ if (flows.length) cliArgs.push("--flows", flows.join(","));
1949
2153
  if (args?.headed) cliArgs.push("--headed");
1950
2154
  if (args?.record) cliArgs.push("--record");
1951
2155
 
@@ -1995,28 +2199,67 @@ class VibecheckMCP {
1995
2199
  // REALITY v2 - Two-Pass Auth Verification + Dead UI Crawler
1996
2200
  // ============================================================================
1997
2201
  async handleReality(projectPath, args) {
1998
- const url = args?.url;
1999
- if (!url) return this.error("URL is required");
2202
+ // HARDENING: Validate URL
2203
+ const urlValidation = validateUrl(args?.url);
2204
+ if (!urlValidation.valid) {
2205
+ return this.error(urlValidation.error, {
2206
+ code: 'INVALID_URL',
2207
+ suggestion: 'Provide a valid HTTP/HTTPS URL',
2208
+ nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
2209
+ });
2210
+ }
2211
+ const url = urlValidation.url;
2000
2212
 
2001
2213
  let output = "# 🧪 vibecheck Reality v2\n\n";
2002
2214
  output += `**URL:** ${url}\n`;
2003
2215
  output += `**Two-Pass Auth:** ${args?.verifyAuth ? "Yes" : "No"}\n`;
2004
- if (args?.auth) output += `**Auth:** ${args.auth.split(":")[0]}:***\n`;
2005
- if (args?.storageState) output += `**Storage State:** ${args.storageState}\n`;
2216
+
2217
+ // HARDENING: Safely display auth info (mask password)
2218
+ if (args?.auth && typeof args.auth === 'string') {
2219
+ const authParts = args.auth.split(":");
2220
+ const maskedAuth = authParts[0] ? `${authParts[0].slice(0, 20)}:***` : '***';
2221
+ output += `**Auth:** ${maskedAuth}\n`;
2222
+ }
2223
+ if (args?.storageState) output += `**Storage State:** ${sanitizeString(args.storageState, 100)}\n`;
2006
2224
  if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
2007
2225
  if (args?.danger) output += `**Danger Mode:** Enabled (risky clicks allowed)\n`;
2008
2226
  output += "\n";
2009
2227
 
2010
2228
  // Build CLI arguments array (secure)
2011
2229
  const cliArgs = ["--url", url];
2012
- if (args?.auth) cliArgs.push("--auth", args.auth);
2230
+ if (args?.auth && typeof args.auth === 'string') {
2231
+ cliArgs.push("--auth", sanitizeString(args.auth, 200));
2232
+ }
2013
2233
  if (args?.verifyAuth) cliArgs.push("--verify-auth");
2014
- if (args?.storageState) cliArgs.push("--storage-state", args.storageState);
2015
- if (args?.saveStorageState) cliArgs.push("--save-storage-state", args.saveStorageState);
2016
- if (args?.truthpack) cliArgs.push("--truthpack", args.truthpack);
2234
+
2235
+ // HARDENING: Validate path arguments
2236
+ if (args?.storageState) {
2237
+ const pathCheck = sanitizePath(args.storageState, projectPath);
2238
+ if (pathCheck.valid) {
2239
+ cliArgs.push("--storage-state", pathCheck.path);
2240
+ }
2241
+ }
2242
+ if (args?.saveStorageState) {
2243
+ const pathCheck = sanitizePath(args.saveStorageState, projectPath);
2244
+ if (pathCheck.valid) {
2245
+ cliArgs.push("--save-storage-state", pathCheck.path);
2246
+ }
2247
+ }
2248
+ if (args?.truthpack) {
2249
+ const pathCheck = sanitizePath(args.truthpack, projectPath);
2250
+ if (pathCheck.valid) {
2251
+ cliArgs.push("--truthpack", pathCheck.path);
2252
+ }
2253
+ }
2017
2254
  if (args?.headed) cliArgs.push("--headed");
2018
- if (args?.maxPages) cliArgs.push("--max-pages", String(args.maxPages));
2019
- if (args?.maxDepth) cliArgs.push("--max-depth", String(args.maxDepth));
2255
+
2256
+ // HARDENING: Bound numeric arguments
2257
+ if (args?.maxPages) {
2258
+ cliArgs.push("--max-pages", String(sanitizeNumber(args.maxPages, 1, 100, 18)));
2259
+ }
2260
+ if (args?.maxDepth) {
2261
+ cliArgs.push("--max-depth", String(sanitizeNumber(args.maxDepth, 1, 10, 2)));
2262
+ }
2020
2263
  if (args?.danger) cliArgs.push("--danger");
2021
2264
 
2022
2265
  try {
@@ -2097,16 +2340,27 @@ class VibecheckMCP {
2097
2340
  // AI-TEST - AI Agent testing
2098
2341
  // ============================================================================
2099
2342
  async handleAITest(projectPath, args) {
2100
- const url = args?.url;
2101
- if (!url) return this.error("URL is required");
2343
+ // HARDENING: Validate URL
2344
+ const urlValidation = validateUrl(args?.url);
2345
+ if (!urlValidation.valid) {
2346
+ return this.error(urlValidation.error, {
2347
+ code: 'INVALID_URL',
2348
+ suggestion: 'Provide a valid HTTP/HTTPS URL',
2349
+ nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
2350
+ });
2351
+ }
2352
+ const url = urlValidation.url;
2353
+
2354
+ // HARDENING: Sanitize goal string
2355
+ const goal = sanitizeString(args?.goal, 500) || "Test all features";
2102
2356
 
2103
2357
  let output = "# 🤖 vibecheck AI Agent\n\n";
2104
2358
  output += `**URL:** ${url}\n`;
2105
- output += `**Goal:** ${args?.goal || "Test all features"}\n\n`;
2359
+ output += `**Goal:** ${goal}\n\n`;
2106
2360
 
2107
2361
  // Build CLI arguments array (secure)
2108
2362
  const cliArgs = ["--url", url];
2109
- if (args?.goal) cliArgs.push("--goal", args.goal);
2363
+ if (goal) cliArgs.push("--goal", goal);
2110
2364
  if (args?.headed) cliArgs.push("--headed");
2111
2365
 
2112
2366
  try {
@@ -2162,7 +2416,7 @@ class VibecheckMCP {
2162
2416
  // ============================================================================
2163
2417
  async handleAutopilotPlan(projectPath, args) {
2164
2418
  // Check tier access (PRO tier required)
2165
- const access = await checkFeatureAccess("fix.apply_patches", args?.apiKey);
2419
+ const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
2166
2420
  if (!access.hasAccess) {
2167
2421
  return {
2168
2422
  content: [{
@@ -2249,7 +2503,7 @@ class VibecheckMCP {
2249
2503
  // ============================================================================
2250
2504
  async handleAutopilotApply(projectPath, args) {
2251
2505
  // Check tier access (PRO tier required)
2252
- const access = await checkFeatureAccess("fix.apply_patches", args?.apiKey);
2506
+ const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
2253
2507
  if (!access.hasAccess) {
2254
2508
  return {
2255
2509
  content: [{
@@ -2310,7 +2564,7 @@ class VibecheckMCP {
2310
2564
  // ============================================================================
2311
2565
  async handleBadge(projectPath, args) {
2312
2566
  // Check tier access (STARTER tier required)
2313
- const access = await checkFeatureAccess("badge", args?.apiKey);
2567
+ const access = await getFeatureAccessStatus("badge", args?.apiKey);
2314
2568
  if (!access.hasAccess) {
2315
2569
  return {
2316
2570
  content: [{
@@ -2466,15 +2720,57 @@ class VibecheckMCP {
2466
2720
  }
2467
2721
 
2468
2722
  // ============================================================================
2469
- // RUN
2723
+ // RUN - with graceful shutdown handling
2470
2724
  // ============================================================================
2471
2725
  async run() {
2472
2726
  const transport = new StdioServerTransport();
2727
+
2728
+ // ========================================================================
2729
+ // HARDENING: Graceful shutdown handling
2730
+ // ========================================================================
2731
+ const shutdown = async (signal) => {
2732
+ console.error(`\n[MCP] Received ${signal}, shutting down gracefully...`);
2733
+ try {
2734
+ // Clear rate limit state to prevent memory leaks
2735
+ rateLimitState.calls = [];
2736
+
2737
+ // Close server connection
2738
+ await this.server.close();
2739
+ console.error('[MCP] Server closed successfully');
2740
+ } catch (err) {
2741
+ console.error(`[MCP] Error during shutdown: ${err.message}`);
2742
+ }
2743
+ process.exit(0);
2744
+ };
2745
+
2746
+ // Handle termination signals
2747
+ process.on('SIGINT', () => shutdown('SIGINT'));
2748
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
2749
+
2750
+ // Handle uncaught errors gracefully
2751
+ process.on('uncaughtException', (err) => {
2752
+ console.error(`[MCP] Uncaught exception: ${err.message}`);
2753
+ console.error(err.stack);
2754
+ // Don't exit - try to keep running
2755
+ });
2756
+
2757
+ process.on('unhandledRejection', (reason, promise) => {
2758
+ console.error(`[MCP] Unhandled rejection at:`, promise);
2759
+ console.error(`[MCP] Reason:`, reason);
2760
+ // Don't exit - try to keep running
2761
+ });
2762
+
2473
2763
  await this.server.connect(transport);
2474
- console.error("vibecheck MCP Server v2.0 running on stdio");
2764
+ console.error(`vibecheck MCP Server v${VERSION} running on stdio (hardened)`);
2475
2765
  }
2476
2766
  }
2477
2767
 
2478
- // Main
2768
+ // ============================================================================
2769
+ // MAIN - with error handling
2770
+ // ============================================================================
2479
2771
  const server = new VibecheckMCP();
2480
- server.run().catch(console.error);
2772
+ server.run().catch((err) => {
2773
+ console.error(`[MCP] Fatal error starting server: ${err.message}`);
2774
+ console.error(err.stack);
2775
+ process.exit(1);
2776
+ });