@vibecheckai/cli 3.2.5 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/registry.js +192 -5
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/analyzers.js +81 -18
- package/bin/runners/lib/api-client.js +269 -0
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-output.js +7 -1
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/error-handler.js +16 -9
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/upsell.js +658 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runAgent.d.ts +5 -0
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +324 -95
- package/bin/runners/runCheckpoint.js +39 -21
- package/bin/runners/runClassify.js +859 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +108 -68
- package/bin/runners/runFirewall.d.ts +5 -0
- package/bin/runners/runFirewallHook.d.ts +5 -0
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +262 -168
- package/bin/runners/runInit.js +3 -2
- package/bin/runners/runMcp.js +130 -52
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProve.js +1 -2
- package/bin/runners/runReport.js +3 -2
- package/bin/runners/runScan.js +145 -44
- package/bin/runners/runShip.js +3 -4
- package/bin/runners/runTruth.d.ts +5 -0
- package/bin/runners/runValidate.js +19 -2
- package/bin/runners/runWatch.js +104 -53
- package/bin/vibecheck.js +106 -19
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +367 -31
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/index.js +1199 -208
- package/mcp-server/lib/api-client.cjs +305 -0
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +2 -2
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/tier-auth.js +351 -136
- package/mcp-server/tools/index.js +72 -72
- package/mcp-server/truth-firewall-tools.js +145 -15
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +2 -3
- package/mcp-server/index.old.js +0 -4137
- package/mcp-server/package-lock.json +0 -165
package/mcp-server/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* vibecheck MCP Server v2.0 -
|
|
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,6 +43,17 @@ import { fileURLToPath } from "url";
|
|
|
33
43
|
import { execFile } from "child_process";
|
|
34
44
|
import { promisify } from "util";
|
|
35
45
|
|
|
46
|
+
// Import API client for dashboard integration (optional - may use CommonJS)
|
|
47
|
+
import { createRequire } from "module";
|
|
48
|
+
const require = createRequire(import.meta.url);
|
|
49
|
+
const {
|
|
50
|
+
createScan,
|
|
51
|
+
updateScanProgress,
|
|
52
|
+
submitScanResults,
|
|
53
|
+
reportScanError,
|
|
54
|
+
isApiAvailable
|
|
55
|
+
} = require("./lib/api-client.cjs");
|
|
56
|
+
|
|
36
57
|
const execFileAsync = promisify(execFile);
|
|
37
58
|
|
|
38
59
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -55,8 +76,384 @@ const CONFIG = {
|
|
|
55
76
|
AUTOPILOT: 300000, // 5 minutes
|
|
56
77
|
},
|
|
57
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
|
|
58
382
|
};
|
|
59
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
|
+
|
|
60
457
|
const VERSION = CONFIG.VERSION;
|
|
61
458
|
|
|
62
459
|
// Import intelligence tools
|
|
@@ -83,6 +480,12 @@ import {
|
|
|
83
480
|
handleArchitectTool,
|
|
84
481
|
} from "./architect-tools.js";
|
|
85
482
|
|
|
483
|
+
// Import authority system tools
|
|
484
|
+
import {
|
|
485
|
+
AUTHORITY_TOOLS,
|
|
486
|
+
handleAuthorityTool,
|
|
487
|
+
} from "./authority-tools.js";
|
|
488
|
+
|
|
86
489
|
// Import codebase architect tools
|
|
87
490
|
import {
|
|
88
491
|
CODEBASE_ARCHITECT_TOOLS,
|
|
@@ -127,14 +530,22 @@ import { CONSOLIDATED_TOOLS, handleConsolidatedTool } from "./consolidated-tools
|
|
|
127
530
|
import { MCP_TOOLS_V3, handleToolV3, TOOL_TIERS as V3_TOOL_TIERS } from "./tools-v3.js";
|
|
128
531
|
|
|
129
532
|
// Import tier auth for entitlement checking
|
|
130
|
-
import {
|
|
533
|
+
import { getFeatureAccessStatus } from "./tier-auth.js";
|
|
131
534
|
|
|
132
|
-
// 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
|
|
133
537
|
import {
|
|
134
538
|
AGENT_FIREWALL_TOOL,
|
|
135
539
|
handleAgentFirewallIntercept,
|
|
136
540
|
} from "./agent-firewall-interceptor.js";
|
|
137
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
|
+
|
|
138
549
|
/**
|
|
139
550
|
* TRUTH FIREWALL CONFIGURATION
|
|
140
551
|
*
|
|
@@ -173,14 +584,29 @@ function getPolicyConfig(policy) {
|
|
|
173
584
|
return POLICY_THRESHOLDS[policy] || POLICY_THRESHOLDS.strict;
|
|
174
585
|
}
|
|
175
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
|
+
*/
|
|
176
594
|
async function emitGuardrailMetric(projectPath, metric) {
|
|
177
595
|
try {
|
|
178
596
|
const auditDir = path.join(projectPath, ".vibecheck", "audit");
|
|
179
597
|
await fs.mkdir(auditDir, { recursive: true });
|
|
180
598
|
const record = JSON.stringify({ ...metric, timestamp: new Date().toISOString() });
|
|
181
599
|
await fs.appendFile(path.join(auditDir, "guardrail-metrics.jsonl"), `${record}\n`);
|
|
182
|
-
} catch {
|
|
183
|
-
//
|
|
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
|
+
}
|
|
184
610
|
}
|
|
185
611
|
}
|
|
186
612
|
|
|
@@ -230,18 +656,22 @@ function checkTruthFirewallBlock(toolName, args, projectPath) {
|
|
|
230
656
|
const USE_V3_TOOLS = process.env.VIBECHECK_MCP_V3 !== 'false';
|
|
231
657
|
const USE_CONSOLIDATED_TOOLS = process.env.VIBECHECK_MCP_CONSOLIDATED !== 'false';
|
|
232
658
|
|
|
233
|
-
const TOOLS = USE_V3_TOOLS ? [
|
|
659
|
+
const TOOLS = (USE_V3_TOOLS ? [
|
|
234
660
|
// v3: 10 focused tools for STARTER+ (no free MCP tools)
|
|
235
661
|
...MCP_TOOLS_V3,
|
|
236
662
|
AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
|
|
237
|
-
|
|
663
|
+
...getConductorTools(), // Conductor - multi-agent coordination
|
|
664
|
+
].filter(t => t !== null) : USE_CONSOLIDATED_TOOLS ? [
|
|
238
665
|
// Curated tools for agents (legacy)
|
|
239
666
|
...CONSOLIDATED_TOOLS,
|
|
240
667
|
AGENT_FIREWALL_TOOL, // Agent Firewall - intercepts file writes
|
|
241
|
-
|
|
668
|
+
...getConductorTools(), // Conductor - multi-agent coordination
|
|
669
|
+
].filter(t => t !== null) : [
|
|
242
670
|
// Legacy: Full tool set (50+ tools) - for backward compatibility
|
|
243
671
|
// PRIORITY: Agent Firewall - intercepts ALL file writes
|
|
244
672
|
AGENT_FIREWALL_TOOL,
|
|
673
|
+
// PRIORITY: Conductor - multi-agent coordination
|
|
674
|
+
...getConductorTools(),
|
|
245
675
|
// PRIORITY: Truth Firewall tools (Hallucination Stopper) - agents MUST use these
|
|
246
676
|
...TRUTH_FIREWALL_TOOLS, // vibecheck.get_truthpack, vibecheck.validate_claim, vibecheck.compile_context, etc.
|
|
247
677
|
|
|
@@ -250,6 +680,7 @@ const TOOLS = USE_V3_TOOLS ? [
|
|
|
250
680
|
|
|
251
681
|
...INTELLIGENCE_TOOLS, // Add all intelligence suite tools
|
|
252
682
|
...VIBECHECK_TOOLS, // Add AI vibecheck tools (verify, quality, smells, etc.)
|
|
683
|
+
...AUTHORITY_TOOLS, // Add authority system tools (classify, approve, list)
|
|
253
684
|
...AGENT_CHECKPOINT_TOOLS, // Add agent checkpoint tools
|
|
254
685
|
...ARCHITECT_TOOLS, // Add architect review/suggest tools
|
|
255
686
|
...CODEBASE_ARCHITECT_TOOLS, // Add codebase-aware architect tools
|
|
@@ -845,7 +1276,7 @@ const TOOLS = USE_V3_TOOLS ? [
|
|
|
845
1276
|
},
|
|
846
1277
|
},
|
|
847
1278
|
},
|
|
848
|
-
];
|
|
1279
|
+
]).filter(t => t !== null);
|
|
849
1280
|
|
|
850
1281
|
// ============================================================================
|
|
851
1282
|
// SERVER IMPLEMENTATION
|
|
@@ -863,32 +1294,56 @@ class VibecheckMCP {
|
|
|
863
1294
|
|
|
864
1295
|
// ============================================================================
|
|
865
1296
|
// TOOL REGISTRY - Maps tool names to handlers for cleaner dispatch
|
|
1297
|
+
// HARDENING: Validates all handlers are functions
|
|
866
1298
|
// ============================================================================
|
|
867
1299
|
buildToolRegistry() {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
"vibecheck.gate": this.handleGate.bind(this),
|
|
878
|
-
"vibecheck.fix": this.handleFix.bind(this),
|
|
879
|
-
"vibecheck.share": this.handleShare.bind(this),
|
|
880
|
-
"vibecheck.ctx": this.handleCtx.bind(this),
|
|
881
|
-
"vibecheck.prove": this.handleProve.bind(this),
|
|
882
|
-
"vibecheck.proof": this.handleProof.bind(this),
|
|
883
|
-
"vibecheck.validate": this.handleValidate.bind(this),
|
|
884
|
-
"vibecheck.report": this.handleReport.bind(this),
|
|
885
|
-
"vibecheck.status": this.handleStatus.bind(this),
|
|
886
|
-
"vibecheck.autopilot": this.handleAutopilot.bind(this),
|
|
887
|
-
"vibecheck.autopilot_plan": this.handleAutopilotPlan.bind(this),
|
|
888
|
-
"vibecheck.autopilot_apply": this.handleAutopilotApply.bind(this),
|
|
889
|
-
"vibecheck.badge": this.handleBadge.bind(this),
|
|
890
|
-
"vibecheck.context": this.handleContext.bind(this),
|
|
1300
|
+
const registry = {};
|
|
1301
|
+
|
|
1302
|
+
// Helper to safely add handler with validation
|
|
1303
|
+
const addHandler = (name, handler) => {
|
|
1304
|
+
if (typeof handler !== 'function') {
|
|
1305
|
+
console.error(`[MCP] Warning: Tool ${name} handler is not a function`);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
registry[name] = handler;
|
|
891
1309
|
};
|
|
1310
|
+
|
|
1311
|
+
// Agent Firewall - intercepts file writes (if available)
|
|
1312
|
+
if (handleAgentFirewallIntercept && typeof handleAgentFirewallIntercept === 'function') {
|
|
1313
|
+
addHandler("vibecheck_agent_firewall_intercept", handleAgentFirewallIntercept);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Conductor - multi-agent coordination tools
|
|
1317
|
+
addHandler("vibecheck_conductor_register", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_register", args, projectPath));
|
|
1318
|
+
addHandler("vibecheck_conductor_acquire_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_acquire_lock", args, projectPath));
|
|
1319
|
+
addHandler("vibecheck_conductor_release_lock", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_release_lock", args, projectPath));
|
|
1320
|
+
addHandler("vibecheck_conductor_propose", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_propose", args, projectPath));
|
|
1321
|
+
addHandler("vibecheck_conductor_status", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_status", args, projectPath));
|
|
1322
|
+
addHandler("vibecheck_conductor_terminate", (projectPath, args) => handleConductorToolCall("vibecheck_conductor_terminate", args, projectPath));
|
|
1323
|
+
|
|
1324
|
+
// Core CLI tools
|
|
1325
|
+
addHandler("vibecheck.ship", this.handleShip.bind(this));
|
|
1326
|
+
addHandler("vibecheck.scan", this.handleScan.bind(this));
|
|
1327
|
+
addHandler("vibecheck.verify", this.handleVerify.bind(this));
|
|
1328
|
+
addHandler("vibecheck.reality", this.handleReality.bind(this));
|
|
1329
|
+
addHandler("vibecheckai.dev-test", this.handleAITest.bind(this));
|
|
1330
|
+
addHandler("vibecheck.gate", this.handleGate.bind(this));
|
|
1331
|
+
addHandler("vibecheck.fix", this.handleFix.bind(this));
|
|
1332
|
+
addHandler("vibecheck.share", this.handleShare.bind(this));
|
|
1333
|
+
addHandler("vibecheck.ctx", this.handleCtx.bind(this));
|
|
1334
|
+
addHandler("vibecheck.prove", this.handleProve.bind(this));
|
|
1335
|
+
addHandler("vibecheck.proof", this.handleProof.bind(this));
|
|
1336
|
+
addHandler("vibecheck.validate", this.handleValidate.bind(this));
|
|
1337
|
+
addHandler("vibecheck.report", this.handleReport.bind(this));
|
|
1338
|
+
addHandler("vibecheck.status", this.handleStatus.bind(this));
|
|
1339
|
+
addHandler("vibecheck.autopilot", this.handleAutopilot.bind(this));
|
|
1340
|
+
addHandler("vibecheck.autopilot_plan", this.handleAutopilotPlan.bind(this));
|
|
1341
|
+
addHandler("vibecheck.autopilot_apply", this.handleAutopilotApply.bind(this));
|
|
1342
|
+
addHandler("vibecheck.badge", this.handleBadge.bind(this));
|
|
1343
|
+
addHandler("vibecheck.context", this.handleContext.bind(this));
|
|
1344
|
+
|
|
1345
|
+
console.error(`[MCP] Tool registry built with ${Object.keys(registry).length} handlers`);
|
|
1346
|
+
return registry;
|
|
892
1347
|
}
|
|
893
1348
|
|
|
894
1349
|
// ============================================================================
|
|
@@ -901,70 +1356,208 @@ class VibecheckMCP {
|
|
|
901
1356
|
skipAuth = true,
|
|
902
1357
|
} = options;
|
|
903
1358
|
|
|
1359
|
+
// ========================================================================
|
|
1360
|
+
// HARDENING: Validate command
|
|
1361
|
+
// ========================================================================
|
|
1362
|
+
const sanitizedCommand = sanitizeString(command, 50);
|
|
1363
|
+
if (!sanitizedCommand || !/^[a-z0-9_-]+$/i.test(sanitizedCommand)) {
|
|
1364
|
+
throw new Error(`Invalid CLI command: ${sanitizedCommand}`);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// ========================================================================
|
|
1368
|
+
// HARDENING: Validate and sanitize arguments
|
|
1369
|
+
// ========================================================================
|
|
1370
|
+
const sanitizedArgs = sanitizeArray(args, 50).map(arg => {
|
|
1371
|
+
const str = String(arg);
|
|
1372
|
+
// Validate argument format (must be simple flags or values)
|
|
1373
|
+
if (str.length > 1000) {
|
|
1374
|
+
return str.slice(0, 1000);
|
|
1375
|
+
}
|
|
1376
|
+
return str;
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// ========================================================================
|
|
1380
|
+
// HARDENING: Validate working directory
|
|
1381
|
+
// ========================================================================
|
|
1382
|
+
const resolvedCwd = path.resolve(cwd || process.cwd());
|
|
1383
|
+
if (!fsSync.existsSync(resolvedCwd)) {
|
|
1384
|
+
throw new Error(`Working directory does not exist: ${resolvedCwd}`);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
904
1387
|
// Build argument array - this prevents command injection
|
|
905
|
-
const finalArgs = [CONFIG.BIN_PATH,
|
|
1388
|
+
const finalArgs = [CONFIG.BIN_PATH, sanitizedCommand, ...sanitizedArgs];
|
|
1389
|
+
|
|
1390
|
+
// ========================================================================
|
|
1391
|
+
// HARDENING: Clean environment - don't leak sensitive vars
|
|
1392
|
+
// ========================================================================
|
|
1393
|
+
const safeEnv = { ...process.env };
|
|
1394
|
+
// Remove potentially sensitive env vars from being passed through
|
|
1395
|
+
const sensitiveEnvKeys = ['AWS_SECRET_ACCESS_KEY', 'STRIPE_SECRET_KEY', 'DATABASE_URL'];
|
|
1396
|
+
for (const key of sensitiveEnvKeys) {
|
|
1397
|
+
if (safeEnv[key] && !env[key]) {
|
|
1398
|
+
// Only remove if not explicitly set in options
|
|
1399
|
+
delete safeEnv[key];
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
906
1402
|
|
|
907
1403
|
const execEnv = {
|
|
908
|
-
...
|
|
1404
|
+
...safeEnv,
|
|
909
1405
|
...CONFIG.ENV_DEFAULTS,
|
|
910
1406
|
...(skipAuth ? { VIBECHECK_SKIP_AUTH: "1" } : {}),
|
|
911
1407
|
...env,
|
|
1408
|
+
// Ensure Node.js doesn't prompt for anything
|
|
1409
|
+
NODE_NO_READLINE: "1",
|
|
1410
|
+
FORCE_COLOR: "0",
|
|
912
1411
|
};
|
|
913
1412
|
|
|
1413
|
+
// ========================================================================
|
|
1414
|
+
// HARDENING: Bounded timeout
|
|
1415
|
+
// ========================================================================
|
|
1416
|
+
const boundedTimeout = sanitizeNumber(timeout, 1000, 900000, CONFIG.TIMEOUTS.DEFAULT);
|
|
1417
|
+
|
|
914
1418
|
try {
|
|
915
1419
|
const { stdout, stderr } = await execFileAsync(process.execPath, finalArgs, {
|
|
916
|
-
cwd,
|
|
1420
|
+
cwd: resolvedCwd,
|
|
917
1421
|
encoding: "utf8",
|
|
918
1422
|
maxBuffer: CONFIG.MAX_BUFFER,
|
|
919
|
-
timeout,
|
|
1423
|
+
timeout: boundedTimeout,
|
|
920
1424
|
env: execEnv,
|
|
1425
|
+
// Don't inherit stdin - prevents hanging
|
|
1426
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
921
1427
|
});
|
|
922
|
-
|
|
1428
|
+
|
|
1429
|
+
// Sanitize output before returning
|
|
1430
|
+
return {
|
|
1431
|
+
stdout: redactSensitive(truncateOutput(stdout || '')),
|
|
1432
|
+
stderr: redactSensitive(truncateOutput(stderr || '')),
|
|
1433
|
+
success: true
|
|
1434
|
+
};
|
|
923
1435
|
} catch (error) {
|
|
924
|
-
// Attach partial output for graceful degradation
|
|
925
|
-
error.partialOutput = error.stdout ||
|
|
926
|
-
error.partialStderr = error.stderr ||
|
|
1436
|
+
// Attach partial output for graceful degradation (sanitized)
|
|
1437
|
+
error.partialOutput = redactSensitive(truncateOutput(error.stdout || ''));
|
|
1438
|
+
error.partialStderr = redactSensitive(truncateOutput(error.stderr || ''));
|
|
1439
|
+
|
|
1440
|
+
// Add helpful error code for timeout
|
|
1441
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
1442
|
+
error.code = 'TIMEOUT';
|
|
1443
|
+
error.message = `Command timed out after ${boundedTimeout}ms`;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
927
1446
|
throw error;
|
|
928
1447
|
}
|
|
929
1448
|
}
|
|
930
1449
|
|
|
931
1450
|
// ============================================================================
|
|
932
|
-
// UTILITY HELPERS
|
|
1451
|
+
// HARDENED UTILITY HELPERS
|
|
933
1452
|
// ============================================================================
|
|
934
1453
|
|
|
935
|
-
|
|
1454
|
+
/**
|
|
1455
|
+
* Strip ANSI escape codes from output with length validation
|
|
1456
|
+
* @param {string} str - String to strip
|
|
1457
|
+
* @returns {string}
|
|
1458
|
+
*/
|
|
936
1459
|
stripAnsi(str) {
|
|
937
|
-
|
|
1460
|
+
if (!str || typeof str !== 'string') {
|
|
1461
|
+
return '';
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Truncate first to prevent DoS on very long strings
|
|
1465
|
+
const truncated = str.length > CONFIG.LIMITS.MAX_OUTPUT_LENGTH
|
|
1466
|
+
? str.slice(0, CONFIG.LIMITS.MAX_OUTPUT_LENGTH)
|
|
1467
|
+
: str;
|
|
1468
|
+
|
|
1469
|
+
return truncated.replace(/\x1b\[[0-9;]*m/g, "");
|
|
938
1470
|
}
|
|
939
1471
|
|
|
940
|
-
|
|
1472
|
+
/**
|
|
1473
|
+
* Parse summary from disk with size limits and validation
|
|
1474
|
+
* @param {string} projectPath - Project root path
|
|
1475
|
+
* @returns {Promise<object|null>} Parsed summary or null
|
|
1476
|
+
*/
|
|
941
1477
|
async parseSummaryFromDisk(projectPath) {
|
|
942
1478
|
const summaryPath = path.join(projectPath, CONFIG.OUTPUT_DIR, "summary.json");
|
|
1479
|
+
|
|
943
1480
|
try {
|
|
1481
|
+
// Check file size first
|
|
1482
|
+
const stats = await fs.stat(summaryPath);
|
|
1483
|
+
if (stats.size > 5 * 1024 * 1024) { // 5MB limit
|
|
1484
|
+
console.error(`[MCP] Summary file too large: ${stats.size} bytes`);
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
944
1488
|
const content = await fs.readFile(summaryPath, "utf-8");
|
|
945
|
-
|
|
946
|
-
|
|
1489
|
+
const parsed = safeJsonParse(content);
|
|
1490
|
+
|
|
1491
|
+
if (!parsed.success) {
|
|
1492
|
+
console.error(`[MCP] Invalid summary JSON: ${parsed.error}`);
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
return parsed.data;
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
// Silent fail - this is for graceful degradation
|
|
947
1499
|
return null;
|
|
948
1500
|
}
|
|
949
1501
|
}
|
|
950
1502
|
|
|
951
|
-
|
|
1503
|
+
/**
|
|
1504
|
+
* Format scan output from summary object with validation
|
|
1505
|
+
* @param {object} summary - Summary object
|
|
1506
|
+
* @param {string} projectPath - Project root path
|
|
1507
|
+
* @returns {string}
|
|
1508
|
+
*/
|
|
952
1509
|
formatScanOutput(summary, projectPath) {
|
|
953
|
-
|
|
954
|
-
|
|
1510
|
+
if (!summary || typeof summary !== 'object') {
|
|
1511
|
+
return '## Error: Invalid summary data\n';
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// Safely extract values with defaults
|
|
1515
|
+
const score = sanitizeNumber(summary.score, 0, 100, 0);
|
|
1516
|
+
const grade = sanitizeString(summary.grade, 10) || 'N/A';
|
|
1517
|
+
const canShip = Boolean(summary.canShip);
|
|
1518
|
+
|
|
1519
|
+
let output = `## Score: ${score}/100 (${grade})\n\n`;
|
|
1520
|
+
output += `**Verdict:** ${canShip ? "✅ SHIP" : "🚫 NO-SHIP"}\n\n`;
|
|
955
1521
|
|
|
956
|
-
if (summary.counts) {
|
|
1522
|
+
if (summary.counts && typeof summary.counts === 'object') {
|
|
957
1523
|
output += "### Checks\n\n";
|
|
958
1524
|
output += "| Category | Issues |\n|----------|--------|\n";
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1525
|
+
|
|
1526
|
+
// Limit to 50 categories to prevent output bloat
|
|
1527
|
+
const entries = Object.entries(summary.counts).slice(0, 50);
|
|
1528
|
+
for (const [key, count] of entries) {
|
|
1529
|
+
const safeKey = sanitizeString(key, 50);
|
|
1530
|
+
const safeCount = sanitizeNumber(count, 0, 999999, 0);
|
|
1531
|
+
const icon = safeCount === 0 ? "✅" : "⚠️";
|
|
1532
|
+
output += `| ${icon} ${safeKey} | ${safeCount} |\n`;
|
|
962
1533
|
}
|
|
963
1534
|
}
|
|
964
1535
|
|
|
965
1536
|
output += `\n📄 **Report:** ${CONFIG.OUTPUT_DIR}/report.html\n`;
|
|
966
1537
|
return output;
|
|
967
1538
|
}
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* Safely read a file with size limits
|
|
1542
|
+
* @param {string} filePath - Path to file
|
|
1543
|
+
* @param {number} maxSize - Maximum file size in bytes
|
|
1544
|
+
* @returns {Promise<string|null>} File contents or null
|
|
1545
|
+
*/
|
|
1546
|
+
async safeReadFile(filePath, maxSize = 10 * 1024 * 1024) {
|
|
1547
|
+
try {
|
|
1548
|
+
const stats = await fs.stat(filePath);
|
|
1549
|
+
|
|
1550
|
+
if (stats.size > maxSize) {
|
|
1551
|
+
console.error(`[MCP] File too large: ${filePath} (${stats.size} bytes)`);
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1556
|
+
return content;
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
968
1561
|
|
|
969
1562
|
setupHandlers() {
|
|
970
1563
|
// List tools
|
|
@@ -974,24 +1567,95 @@ class VibecheckMCP {
|
|
|
974
1567
|
|
|
975
1568
|
// Call tool - main dispatch handler
|
|
976
1569
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
977
|
-
const { name, arguments: args } = request.params;
|
|
978
|
-
const projectPath = path.resolve(args?.projectPath || ".");
|
|
979
1570
|
const startTime = Date.now();
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1571
|
+
let toolName = 'unknown';
|
|
1572
|
+
let projectPath = '.';
|
|
1573
|
+
|
|
984
1574
|
try {
|
|
985
|
-
//
|
|
986
|
-
|
|
1575
|
+
// ====================================================================
|
|
1576
|
+
// HARDENING: Input extraction with validation
|
|
1577
|
+
// ====================================================================
|
|
1578
|
+
const params = request?.params;
|
|
1579
|
+
if (!params || typeof params !== 'object') {
|
|
1580
|
+
return this.error('Invalid request: missing params', { code: 'INVALID_REQUEST' });
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
toolName = sanitizeString(params.name, 100);
|
|
1584
|
+
const args = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
|
|
1585
|
+
|
|
1586
|
+
// Validate tool name
|
|
1587
|
+
if (!toolName || toolName.length < 2) {
|
|
1588
|
+
return this.error('Invalid tool name', { code: 'INVALID_TOOL_NAME' });
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// ====================================================================
|
|
1592
|
+
// HARDENING: Rate limiting (per-API-key)
|
|
1593
|
+
// ====================================================================
|
|
1594
|
+
const apiKey = args?.apiKey || null;
|
|
1595
|
+
const rateCheck = checkRateLimit(apiKey);
|
|
1596
|
+
if (!rateCheck.allowed) {
|
|
1597
|
+
return this.error(`Rate limit exceeded. Try again in ${Math.ceil(rateCheck.resetIn / 1000)} seconds`, {
|
|
1598
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
1599
|
+
suggestion: 'Reduce the frequency of tool calls',
|
|
1600
|
+
nextSteps: [`Wait ${Math.ceil(rateCheck.resetIn / 1000)} seconds before retrying`],
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// ====================================================================
|
|
1605
|
+
// HARDENING: Project path validation
|
|
1606
|
+
// ====================================================================
|
|
1607
|
+
const rawProjectPath = args?.projectPath || '.';
|
|
1608
|
+
const pathValidation = sanitizePath(rawProjectPath, process.cwd());
|
|
1609
|
+
|
|
1610
|
+
if (!pathValidation.valid) {
|
|
1611
|
+
return this.error(pathValidation.error, {
|
|
1612
|
+
code: 'INVALID_PATH',
|
|
1613
|
+
suggestion: 'Provide a valid path within the current working directory',
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
projectPath = pathValidation.path;
|
|
1617
|
+
|
|
1618
|
+
// Emit audit event for tool invocation start
|
|
1619
|
+
// SECURITY: Include apiKey hash for audit trail (never log raw key)
|
|
1620
|
+
try {
|
|
1621
|
+
const crypto = require('crypto');
|
|
1622
|
+
const apiKeyHash = apiKey
|
|
1623
|
+
? crypto.createHash('sha256').update(apiKey).digest('hex').slice(0, 16)
|
|
1624
|
+
: 'anonymous';
|
|
1625
|
+
|
|
1626
|
+
emitToolInvoke(toolName, args, "success", {
|
|
1627
|
+
projectPath,
|
|
1628
|
+
apiKeyHash,
|
|
1629
|
+
rateLimit: rateCheck,
|
|
1630
|
+
timestamp: new Date().toISOString(),
|
|
1631
|
+
});
|
|
1632
|
+
} catch {
|
|
1633
|
+
// Audit logging should never break the tool
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// ====================================================================
|
|
1637
|
+
// HARDENING: Truth firewall check with error handling
|
|
1638
|
+
// ====================================================================
|
|
1639
|
+
let firewallCheck = { blocked: false };
|
|
1640
|
+
try {
|
|
1641
|
+
firewallCheck = checkTruthFirewallBlock(toolName, args, projectPath);
|
|
1642
|
+
} catch (firewallError) {
|
|
1643
|
+
console.error(`[MCP] Firewall check error: ${firewallError.message}`);
|
|
1644
|
+
// Continue - don't block on firewall errors
|
|
1645
|
+
}
|
|
1646
|
+
|
|
987
1647
|
if (firewallCheck.blocked) {
|
|
988
1648
|
const policy = getTruthPolicy(args);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1649
|
+
try {
|
|
1650
|
+
await emitGuardrailMetric(projectPath, {
|
|
1651
|
+
event: "truth_firewall_block",
|
|
1652
|
+
tool: toolName,
|
|
1653
|
+
policy,
|
|
1654
|
+
reason: firewallCheck.code || "no_recent_claim_validation",
|
|
1655
|
+
});
|
|
1656
|
+
} catch {
|
|
1657
|
+
// Metrics should never break the tool
|
|
1658
|
+
}
|
|
995
1659
|
return this.error(firewallCheck.reason, {
|
|
996
1660
|
code: firewallCheck.code,
|
|
997
1661
|
suggestion: firewallCheck.suggestion,
|
|
@@ -1000,98 +1664,142 @@ class VibecheckMCP {
|
|
|
1000
1664
|
}
|
|
1001
1665
|
|
|
1002
1666
|
// Handle v3 tools (10 consolidated tools, STARTER+ only)
|
|
1003
|
-
if (USE_V3_TOOLS && V3_TOOL_TIERS[
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1667
|
+
if (USE_V3_TOOLS && V3_TOOL_TIERS[toolName]) {
|
|
1668
|
+
// SECURITY FIX: Never trust client-provided tier - validate from API key
|
|
1669
|
+
// Previous: const userTier = sanitizeString(args?.tier, 20) || ...
|
|
1670
|
+
// This allowed privilege escalation via args.tier = "pro"
|
|
1671
|
+
const { getMcpToolAccess } = await import('./tier-auth.js');
|
|
1672
|
+
const access = await getMcpToolAccess(toolName, apiKey);
|
|
1673
|
+
const userTier = access.tier || 'free';
|
|
1674
|
+
const result = await handleToolV3(toolName, args, { tier: userTier });
|
|
1006
1675
|
|
|
1007
1676
|
if (result.error) {
|
|
1008
1677
|
return this.error(result.error, { tier: result.tier, required: result.required });
|
|
1009
1678
|
}
|
|
1010
1679
|
|
|
1680
|
+
// Sanitize and truncate output
|
|
1681
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1011
1682
|
return {
|
|
1012
|
-
content: [{ type: "text", text:
|
|
1683
|
+
content: [{ type: "text", text: outputText }],
|
|
1013
1684
|
};
|
|
1014
1685
|
}
|
|
1015
1686
|
|
|
1016
1687
|
// 1. Check tool registry first (local CLI handlers)
|
|
1017
|
-
if (this.toolRegistry[
|
|
1018
|
-
return await this.toolRegistry[
|
|
1688
|
+
if (this.toolRegistry[toolName]) {
|
|
1689
|
+
return await this.toolRegistry[toolName](projectPath, args);
|
|
1019
1690
|
}
|
|
1020
1691
|
|
|
1021
1692
|
// 2. Handle external module tools by prefix/pattern
|
|
1022
|
-
if (
|
|
1023
|
-
return await handleIntelligenceTool(
|
|
1693
|
+
if (toolName.startsWith("vibecheck.intelligence.")) {
|
|
1694
|
+
return await handleIntelligenceTool(toolName, args, __dirname);
|
|
1024
1695
|
}
|
|
1025
1696
|
|
|
1026
1697
|
// Handle AI vibecheck tools
|
|
1027
1698
|
if (["vibecheck.verify", "vibecheck.quality", "vibecheck.smells",
|
|
1028
1699
|
"vibecheck.hallucination", "vibecheck.breaking", "vibecheck.mdc",
|
|
1029
|
-
"vibecheck.coverage"].includes(
|
|
1030
|
-
const result = await handleVibecheckTool(
|
|
1700
|
+
"vibecheck.coverage"].includes(toolName)) {
|
|
1701
|
+
const result = await handleVibecheckTool(toolName, args);
|
|
1702
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1031
1703
|
return {
|
|
1032
|
-
content: [{ type: "text", text:
|
|
1704
|
+
content: [{ type: "text", text: outputText }],
|
|
1033
1705
|
};
|
|
1034
1706
|
}
|
|
1035
1707
|
|
|
1036
1708
|
// Handle agent checkpoint tools
|
|
1037
|
-
if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(
|
|
1038
|
-
return await handleCheckpointTool(
|
|
1709
|
+
if (["vibecheck_checkpoint", "vibecheck_set_strictness", "vibecheck_checkpoint_status"].includes(toolName)) {
|
|
1710
|
+
return await handleCheckpointTool(toolName, args);
|
|
1039
1711
|
}
|
|
1040
1712
|
|
|
1041
1713
|
// Handle architect tools
|
|
1042
1714
|
if (["vibecheck_architect_review", "vibecheck_architect_suggest",
|
|
1043
|
-
"vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(
|
|
1044
|
-
return await handleArchitectTool(
|
|
1715
|
+
"vibecheck_architect_patterns", "vibecheck_architect_set_strictness"].includes(toolName)) {
|
|
1716
|
+
return await handleArchitectTool(toolName, args);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// Handle authority system tools
|
|
1720
|
+
if (["authority.classify", "authority.approve", "authority.list"].includes(toolName)) {
|
|
1721
|
+
const result = await handleAuthorityTool(toolName, args, userTier || "free");
|
|
1722
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1723
|
+
return {
|
|
1724
|
+
content: [{ type: "text", text: outputText }],
|
|
1725
|
+
};
|
|
1045
1726
|
}
|
|
1046
1727
|
|
|
1047
1728
|
// Handle codebase architect tools
|
|
1048
1729
|
if (["vibecheck_architect_context", "vibecheck_architect_guide",
|
|
1049
1730
|
"vibecheck_architect_validate", "vibecheck_architect_patterns",
|
|
1050
|
-
"vibecheck_architect_dependencies"].includes(
|
|
1051
|
-
return await handleCodebaseArchitectTool(
|
|
1731
|
+
"vibecheck_architect_dependencies"].includes(toolName)) {
|
|
1732
|
+
return await handleCodebaseArchitectTool(toolName, args);
|
|
1052
1733
|
}
|
|
1053
1734
|
|
|
1054
1735
|
// Handle vibecheck 2.0 tools
|
|
1055
|
-
if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(
|
|
1056
|
-
return await handleVibecheck2Tool(
|
|
1736
|
+
if (["checkpoint", "check", "ship", "fix", "status", "set_strictness"].includes(toolName)) {
|
|
1737
|
+
return await handleVibecheck2Tool(toolName, args, __dirname);
|
|
1057
1738
|
}
|
|
1058
1739
|
|
|
1059
1740
|
// Handle intent drift tools
|
|
1060
|
-
if (
|
|
1061
|
-
const tool = intentDriftTools.find(t => t.name ===
|
|
1741
|
+
if (toolName.startsWith("vibecheck_intent_")) {
|
|
1742
|
+
const tool = intentDriftTools.find(t => t.name === toolName);
|
|
1062
1743
|
if (tool && tool.handler) {
|
|
1063
1744
|
const result = await tool.handler(args);
|
|
1745
|
+
const outputText = truncateOutput(redactSensitive(JSON.stringify(result, null, 2)));
|
|
1064
1746
|
return {
|
|
1065
|
-
content: [{ type: "text", text:
|
|
1747
|
+
content: [{ type: "text", text: outputText }],
|
|
1066
1748
|
};
|
|
1067
1749
|
}
|
|
1068
1750
|
}
|
|
1069
1751
|
|
|
1070
1752
|
// Handle MDC generator
|
|
1071
|
-
if (
|
|
1753
|
+
if (toolName === "generate_mdc") {
|
|
1072
1754
|
return await handleMDCGeneration(args);
|
|
1073
1755
|
}
|
|
1074
1756
|
|
|
1075
1757
|
// Handle Truth Context tools (Evidence Pack / Truth Pack)
|
|
1076
|
-
if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(
|
|
1077
|
-
return await handleTruthContextTool(
|
|
1758
|
+
if (["vibecheck.verify_claim", "vibecheck.evidence"].includes(toolName)) {
|
|
1759
|
+
return await handleTruthContextTool(toolName, args);
|
|
1078
1760
|
}
|
|
1079
1761
|
|
|
1080
1762
|
// Handle Truth Firewall tools (Hallucination Stopper)
|
|
1081
1763
|
if (["vibecheck.get_truthpack", "vibecheck.validate_claim", "vibecheck.compile_context",
|
|
1082
1764
|
"vibecheck.search_evidence", "vibecheck.find_counterexamples", "vibecheck.propose_patch",
|
|
1083
|
-
"vibecheck.check_invariants", "vibecheck.add_assumption"].includes(
|
|
1084
|
-
return await handleTruthFirewallTool(
|
|
1765
|
+
"vibecheck.check_invariants", "vibecheck.add_assumption"].includes(toolName)) {
|
|
1766
|
+
return await handleTruthFirewallTool(toolName, args, projectPath);
|
|
1085
1767
|
}
|
|
1086
1768
|
|
|
1087
|
-
return this.error(`Unknown tool: ${
|
|
1769
|
+
return this.error(`Unknown tool: ${toolName}`, {
|
|
1770
|
+
code: 'UNKNOWN_TOOL',
|
|
1771
|
+
suggestion: 'Check the tool name and try again',
|
|
1772
|
+
nextSteps: ['Use vibecheck.status to see available tools'],
|
|
1773
|
+
});
|
|
1088
1774
|
} catch (err) {
|
|
1089
|
-
//
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1775
|
+
// ====================================================================
|
|
1776
|
+
// HARDENING: Enhanced error handling with sanitization
|
|
1777
|
+
// ====================================================================
|
|
1778
|
+
const durationMs = Date.now() - startTime;
|
|
1779
|
+
const errorMessage = sanitizeString(err?.message || 'Unknown error', 500);
|
|
1780
|
+
|
|
1781
|
+
// Emit audit event for tool error (safely)
|
|
1782
|
+
try {
|
|
1783
|
+
emitToolComplete(toolName, "error", {
|
|
1784
|
+
errorMessage: redactSensitive(errorMessage),
|
|
1785
|
+
durationMs,
|
|
1786
|
+
});
|
|
1787
|
+
} catch {
|
|
1788
|
+
// Audit logging should never break the response
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Log error details to stderr (not stdout - preserves MCP protocol)
|
|
1792
|
+
console.error(`[MCP] Tool ${toolName} failed after ${durationMs}ms: ${errorMessage}`);
|
|
1793
|
+
|
|
1794
|
+
return this.error(`${toolName} failed: ${redactSensitive(errorMessage)}`, {
|
|
1795
|
+
code: err?.code || 'TOOL_ERROR',
|
|
1796
|
+
suggestion: 'Check the error message and try again',
|
|
1797
|
+
nextSteps: [
|
|
1798
|
+
'Verify the tool arguments are correct',
|
|
1799
|
+
'Check that the project path is valid',
|
|
1800
|
+
'Try running with simpler arguments first',
|
|
1801
|
+
],
|
|
1093
1802
|
});
|
|
1094
|
-
return this.error(`${name} failed: ${err.message}`);
|
|
1095
1803
|
}
|
|
1096
1804
|
});
|
|
1097
1805
|
|
|
@@ -1152,15 +1860,41 @@ class VibecheckMCP {
|
|
|
1152
1860
|
this.server.setRequestHandler(
|
|
1153
1861
|
ReadResourceRequestSchema,
|
|
1154
1862
|
async (request) => {
|
|
1155
|
-
|
|
1863
|
+
// ====================================================================
|
|
1864
|
+
// HARDENING: Resource request validation
|
|
1865
|
+
// ====================================================================
|
|
1866
|
+
const uri = sanitizeString(request?.params?.uri, 200);
|
|
1867
|
+
if (!uri || !uri.startsWith('vibecheck://')) {
|
|
1868
|
+
return { contents: [{ uri: uri || '', mimeType: "application/json", text: '{"error": "Invalid resource URI"}' }] };
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1156
1871
|
const projectPath = process.cwd();
|
|
1872
|
+
|
|
1873
|
+
// Helper to safely read and return JSON resource
|
|
1874
|
+
const safeReadResource = async (filePath, defaultMessage) => {
|
|
1875
|
+
try {
|
|
1876
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1877
|
+
// Validate JSON and sanitize
|
|
1878
|
+
const parsed = safeJsonParse(content);
|
|
1879
|
+
if (!parsed.success) {
|
|
1880
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "Invalid JSON in resource file" }) }] };
|
|
1881
|
+
}
|
|
1882
|
+
// Redact any sensitive data and truncate
|
|
1883
|
+
const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
1884
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1885
|
+
} catch {
|
|
1886
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ message: defaultMessage }) }] };
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1157
1889
|
|
|
1158
1890
|
if (uri === "vibecheck://config") {
|
|
1159
1891
|
const configPath = path.join(projectPath, "vibecheck.config.json");
|
|
1160
1892
|
try {
|
|
1161
1893
|
const content = await fs.readFile(configPath, "utf-8");
|
|
1894
|
+
// Redact sensitive config values
|
|
1895
|
+
const sanitized = redactSensitive(truncateOutput(content, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
1162
1896
|
return {
|
|
1163
|
-
contents: [{ uri, mimeType: "application/json", text:
|
|
1897
|
+
contents: [{ uri, mimeType: "application/json", text: sanitized }],
|
|
1164
1898
|
};
|
|
1165
1899
|
} catch {
|
|
1166
1900
|
return {
|
|
@@ -1170,141 +1904,161 @@ class VibecheckMCP {
|
|
|
1170
1904
|
}
|
|
1171
1905
|
|
|
1172
1906
|
if (uri === "vibecheck://summary") {
|
|
1173
|
-
const summaryPath = path.join(
|
|
1174
|
-
|
|
1175
|
-
".vibecheck",
|
|
1176
|
-
"summary.json",
|
|
1177
|
-
);
|
|
1178
|
-
try {
|
|
1179
|
-
const content = await fs.readFile(summaryPath, "utf-8");
|
|
1180
|
-
return {
|
|
1181
|
-
contents: [{ uri, mimeType: "application/json", text: content }],
|
|
1182
|
-
};
|
|
1183
|
-
} catch {
|
|
1184
|
-
return {
|
|
1185
|
-
contents: [
|
|
1186
|
-
{
|
|
1187
|
-
uri,
|
|
1188
|
-
mimeType: "application/json",
|
|
1189
|
-
text: '{"message": "No scan found. Run vibecheck.scan first."}',
|
|
1190
|
-
},
|
|
1191
|
-
],
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1907
|
+
const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
|
|
1908
|
+
return await safeReadResource(summaryPath, "No scan found. Run vibecheck.scan first.");
|
|
1194
1909
|
}
|
|
1195
1910
|
|
|
1196
1911
|
if (uri === "vibecheck://truthpack") {
|
|
1197
1912
|
const truthpackPath = path.join(projectPath, ".vibecheck", "truth", "truthpack.json");
|
|
1198
|
-
|
|
1199
|
-
const content = await fs.readFile(truthpackPath, "utf-8");
|
|
1200
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1201
|
-
} catch {
|
|
1202
|
-
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No truthpack. Run vibecheck.ctx first."}' }] };
|
|
1203
|
-
}
|
|
1913
|
+
return await safeReadResource(truthpackPath, "No truthpack. Run vibecheck.ctx first.");
|
|
1204
1914
|
}
|
|
1205
1915
|
|
|
1206
1916
|
if (uri === "vibecheck://missions") {
|
|
1207
1917
|
const missionsDir = path.join(projectPath, ".vibecheck", "missions");
|
|
1208
1918
|
try {
|
|
1919
|
+
// HARDENING: Validate directory read
|
|
1209
1920
|
const dirs = await fs.readdir(missionsDir);
|
|
1210
|
-
const
|
|
1921
|
+
const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
|
|
1922
|
+
const latest = safeDirs.sort().reverse()[0];
|
|
1923
|
+
|
|
1211
1924
|
if (latest) {
|
|
1212
1925
|
const missionPath = path.join(missionsDir, latest, "missions.json");
|
|
1213
|
-
|
|
1214
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1926
|
+
return await safeReadResource(missionPath, "No missions found in latest directory.");
|
|
1215
1927
|
}
|
|
1216
|
-
} catch {
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
console.error(`[MCP] Error reading missions: ${err.message}`);
|
|
1930
|
+
}
|
|
1217
1931
|
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No missions. Run vibecheck.fix first."}' }] };
|
|
1218
1932
|
}
|
|
1219
1933
|
|
|
1220
1934
|
if (uri === "vibecheck://reality") {
|
|
1221
1935
|
const realityPath = path.join(projectPath, ".vibecheck", "reality", "last_reality.json");
|
|
1222
|
-
|
|
1223
|
-
const content = await fs.readFile(realityPath, "utf-8");
|
|
1224
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1225
|
-
} catch {
|
|
1226
|
-
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No reality results. Run vibecheck verify first."}' }] };
|
|
1227
|
-
}
|
|
1936
|
+
return await safeReadResource(realityPath, "No reality results. Run vibecheck verify first.");
|
|
1228
1937
|
}
|
|
1229
1938
|
|
|
1230
1939
|
if (uri === "vibecheck://findings") {
|
|
1231
1940
|
const findingsPath = path.join(projectPath, ".vibecheck", "findings.json");
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1941
|
+
|
|
1942
|
+
// Try primary findings file
|
|
1943
|
+
const content = await this.safeReadFile(findingsPath, 10 * 1024 * 1024);
|
|
1944
|
+
if (content) {
|
|
1945
|
+
const parsed = safeJsonParse(content);
|
|
1946
|
+
if (parsed.success) {
|
|
1947
|
+
const sanitized = redactSensitive(truncateOutput(content));
|
|
1948
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// HARDENING: Try summary.json as fallback with size limits
|
|
1953
|
+
const summaryPath = path.join(projectPath, ".vibecheck", "summary.json");
|
|
1954
|
+
const summaryContent = await this.safeReadFile(summaryPath, 10 * 1024 * 1024);
|
|
1955
|
+
|
|
1956
|
+
if (summaryContent) {
|
|
1957
|
+
const parsed = safeJsonParse(summaryContent);
|
|
1958
|
+
if (parsed.success && parsed.data.findings) {
|
|
1959
|
+
const findings = sanitizeArray(parsed.data.findings, 1000);
|
|
1241
1960
|
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ findings }, null, 2) }] };
|
|
1242
|
-
} catch {
|
|
1243
|
-
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
|
|
1244
1961
|
}
|
|
1245
1962
|
}
|
|
1963
|
+
|
|
1964
|
+
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No findings. Run vibecheck.scan first."}' }] };
|
|
1246
1965
|
}
|
|
1247
1966
|
|
|
1248
1967
|
if (uri === "vibecheck://share") {
|
|
1249
1968
|
const missionsDir = path.join(projectPath, ".vibecheck", "missions");
|
|
1250
1969
|
try {
|
|
1970
|
+
// HARDENING: Safe directory read
|
|
1251
1971
|
const dirs = await fs.readdir(missionsDir);
|
|
1252
|
-
const
|
|
1972
|
+
const safeDirs = sanitizeArray(dirs, 100).filter(d => typeof d === 'string' && d.length > 0);
|
|
1973
|
+
const latest = safeDirs.sort().reverse()[0];
|
|
1974
|
+
|
|
1253
1975
|
if (latest) {
|
|
1254
1976
|
const sharePath = path.join(missionsDir, latest, "share", "share.json");
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1977
|
+
|
|
1978
|
+
// Try share.json first
|
|
1979
|
+
const shareContent = await this.safeReadFile(sharePath, 10 * 1024 * 1024);
|
|
1980
|
+
if (shareContent) {
|
|
1981
|
+
const parsed = safeJsonParse(shareContent);
|
|
1982
|
+
if (parsed.success) {
|
|
1983
|
+
const sanitized = redactSensitive(truncateOutput(shareContent));
|
|
1984
|
+
return { contents: [{ uri, mimeType: "application/json", text: sanitized }] };
|
|
1985
|
+
}
|
|
1263
1986
|
}
|
|
1987
|
+
|
|
1988
|
+
// HARDENING: Fallback to missions.json with safe read
|
|
1989
|
+
const missionPath = path.join(missionsDir, latest, "missions.json");
|
|
1990
|
+
return await safeReadResource(missionPath, "No share data available in latest mission.");
|
|
1264
1991
|
}
|
|
1265
|
-
} catch {
|
|
1992
|
+
} catch (err) {
|
|
1993
|
+
console.error(`[MCP] Error reading share pack: ${err.message}`);
|
|
1994
|
+
}
|
|
1266
1995
|
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No share pack. Run vibecheck.fix --share first."}' }] };
|
|
1267
1996
|
}
|
|
1268
1997
|
|
|
1269
1998
|
if (uri === "vibecheck://prove") {
|
|
1270
1999
|
const provePath = path.join(projectPath, ".vibecheck", "prove", "last_prove.json");
|
|
1271
|
-
|
|
1272
|
-
const content = await fs.readFile(provePath, "utf-8");
|
|
1273
|
-
return { contents: [{ uri, mimeType: "application/json", text: content }] };
|
|
1274
|
-
} catch {
|
|
1275
|
-
return { contents: [{ uri, mimeType: "application/json", text: '{"message": "No prove results. Run vibecheck.prove first."}' }] };
|
|
1276
|
-
}
|
|
2000
|
+
return await safeReadResource(provePath, "No prove results. Run vibecheck.prove first.");
|
|
1277
2001
|
}
|
|
1278
2002
|
|
|
1279
|
-
return {
|
|
2003
|
+
return {
|
|
2004
|
+
contents: [{
|
|
2005
|
+
uri,
|
|
2006
|
+
mimeType: "application/json",
|
|
2007
|
+
text: JSON.stringify({ error: "Unknown resource URI" })
|
|
2008
|
+
}]
|
|
2009
|
+
};
|
|
1280
2010
|
},
|
|
1281
2011
|
);
|
|
1282
2012
|
}
|
|
1283
2013
|
|
|
1284
|
-
//
|
|
2014
|
+
// ============================================================================
|
|
2015
|
+
// HARDENED HELPERS
|
|
2016
|
+
// ============================================================================
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* Return a successful response with sanitization
|
|
2020
|
+
* @param {string} text - Response text
|
|
2021
|
+
* @param {boolean} includeAttribution - Include vibecheck attribution
|
|
2022
|
+
* @returns {object} MCP response
|
|
2023
|
+
*/
|
|
1285
2024
|
success(text, includeAttribution = true) {
|
|
2025
|
+
// Sanitize output: redact secrets and truncate
|
|
2026
|
+
let sanitized = redactSensitive(sanitizeString(text, CONFIG.LIMITS.MAX_OUTPUT_LENGTH));
|
|
2027
|
+
|
|
1286
2028
|
const finalText = includeAttribution
|
|
1287
|
-
? `${
|
|
1288
|
-
:
|
|
2029
|
+
? `${sanitized}\n\n---\n_${CONTEXT_ATTRIBUTION}_`
|
|
2030
|
+
: sanitized;
|
|
2031
|
+
|
|
1289
2032
|
return { content: [{ type: "text", text: finalText }] };
|
|
1290
2033
|
}
|
|
1291
2034
|
|
|
2035
|
+
/**
|
|
2036
|
+
* Return an error response with sanitization
|
|
2037
|
+
* @param {string} text - Error message
|
|
2038
|
+
* @param {object} options - Additional options
|
|
2039
|
+
* @returns {object} MCP error response
|
|
2040
|
+
*/
|
|
1292
2041
|
error(text, options = {}) {
|
|
1293
2042
|
const { code, suggestion, nextSteps = [] } = options;
|
|
1294
2043
|
|
|
1295
|
-
|
|
2044
|
+
// Sanitize all text inputs
|
|
2045
|
+
const sanitizedText = redactSensitive(sanitizeString(text, 1000));
|
|
2046
|
+
const sanitizedSuggestion = suggestion ? redactSensitive(sanitizeString(suggestion, 500)) : null;
|
|
2047
|
+
const sanitizedSteps = sanitizeArray(nextSteps, 10).map(s => sanitizeString(s, 200));
|
|
2048
|
+
|
|
2049
|
+
let errorText = `❌ ${sanitizedText}`;
|
|
1296
2050
|
|
|
1297
2051
|
if (code) {
|
|
1298
|
-
errorText += `\n\n**Error Code:** \`${code}\``;
|
|
2052
|
+
errorText += `\n\n**Error Code:** \`${sanitizeString(code, 50)}\``;
|
|
1299
2053
|
}
|
|
1300
2054
|
|
|
1301
|
-
if (
|
|
1302
|
-
errorText += `\n\n💡 **Suggestion:** ${
|
|
2055
|
+
if (sanitizedSuggestion) {
|
|
2056
|
+
errorText += `\n\n💡 **Suggestion:** ${sanitizedSuggestion}`;
|
|
1303
2057
|
}
|
|
1304
2058
|
|
|
1305
|
-
if (
|
|
2059
|
+
if (sanitizedSteps.length > 0) {
|
|
1306
2060
|
errorText += `\n\n**Next Steps:**\n`;
|
|
1307
|
-
|
|
2061
|
+
sanitizedSteps.forEach((step, i) => {
|
|
1308
2062
|
errorText += `${i + 1}. ${step}\n`;
|
|
1309
2063
|
});
|
|
1310
2064
|
}
|
|
@@ -1357,8 +2111,54 @@ class VibecheckMCP {
|
|
|
1357
2111
|
});
|
|
1358
2112
|
}
|
|
1359
2113
|
|
|
1360
|
-
|
|
1361
|
-
const
|
|
2114
|
+
// HARDENING: Validate and sanitize profile
|
|
2115
|
+
const validProfiles = ["quick", "full", "ship", "ci", "security", "compliance", "ai"];
|
|
2116
|
+
const profile = validProfiles.includes(args?.profile) ? args?.profile : "quick";
|
|
2117
|
+
|
|
2118
|
+
// HARDENING: Sanitize only array
|
|
2119
|
+
const only = sanitizeArray(args?.only, 20).map(item => sanitizeString(item, 50));
|
|
2120
|
+
|
|
2121
|
+
// Initialize API integration with timeout and circuit breaker
|
|
2122
|
+
let apiScan = null;
|
|
2123
|
+
let apiConnected = false;
|
|
2124
|
+
|
|
2125
|
+
// HARDENING: Check circuit breaker before attempting API calls
|
|
2126
|
+
const circuitCheck = checkCircuitBreaker();
|
|
2127
|
+
|
|
2128
|
+
// Try to connect to API for dashboard integration
|
|
2129
|
+
if (circuitCheck.allowed) {
|
|
2130
|
+
try {
|
|
2131
|
+
// HARDENING: Add timeout to API availability check
|
|
2132
|
+
const apiCheckPromise = isApiAvailable();
|
|
2133
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
2134
|
+
setTimeout(() => reject(new Error('API check timeout')), 5000)
|
|
2135
|
+
);
|
|
2136
|
+
|
|
2137
|
+
apiConnected = await Promise.race([apiCheckPromise, timeoutPromise]);
|
|
2138
|
+
|
|
2139
|
+
if (apiConnected) {
|
|
2140
|
+
// Create scan record in dashboard
|
|
2141
|
+
const createScanPromise = createScan({
|
|
2142
|
+
localPath: sanitizeString(projectPath, 500),
|
|
2143
|
+
branch: sanitizeString(args?.branch, 100) || 'main',
|
|
2144
|
+
enableLLM: false,
|
|
2145
|
+
});
|
|
2146
|
+
const scanTimeoutPromise = new Promise((_, reject) =>
|
|
2147
|
+
setTimeout(() => reject(new Error('Create scan timeout')), 10000)
|
|
2148
|
+
);
|
|
2149
|
+
|
|
2150
|
+
apiScan = await Promise.race([createScanPromise, scanTimeoutPromise]);
|
|
2151
|
+
console.error(`[MCP] Connected to dashboard (Scan ID: ${apiScan.scanId})`);
|
|
2152
|
+
recordApiResult(true); // Record success
|
|
2153
|
+
}
|
|
2154
|
+
} catch (err) {
|
|
2155
|
+
// API connection is optional, continue without it
|
|
2156
|
+
console.error(`[MCP] Dashboard integration unavailable: ${err.message}`);
|
|
2157
|
+
recordApiResult(false); // Record failure
|
|
2158
|
+
}
|
|
2159
|
+
} else {
|
|
2160
|
+
console.error(`[MCP] ${circuitCheck.reason}`);
|
|
2161
|
+
}
|
|
1362
2162
|
|
|
1363
2163
|
let output = "# 🔍 vibecheck Scan\n\n";
|
|
1364
2164
|
output += `**Profile:** ${profile}\n`;
|
|
@@ -1366,7 +2166,9 @@ class VibecheckMCP {
|
|
|
1366
2166
|
|
|
1367
2167
|
// Build CLI arguments array (secure - no injection possible)
|
|
1368
2168
|
const cliArgs = [`--profile=${profile}`, "--json"];
|
|
1369
|
-
if (only
|
|
2169
|
+
if (only.length > 0) {
|
|
2170
|
+
cliArgs.push(`--only=${only.join(",")}`);
|
|
2171
|
+
}
|
|
1370
2172
|
|
|
1371
2173
|
try {
|
|
1372
2174
|
await this.runCLI("scan", cliArgs, projectPath, { timeout: CONFIG.TIMEOUTS.SCAN });
|
|
@@ -1375,6 +2177,34 @@ class VibecheckMCP {
|
|
|
1375
2177
|
const summary = await this.parseSummaryFromDisk(projectPath);
|
|
1376
2178
|
if (summary) {
|
|
1377
2179
|
output += this.formatScanOutput(summary, projectPath);
|
|
2180
|
+
|
|
2181
|
+
// Submit results to dashboard if connected
|
|
2182
|
+
if (apiConnected && apiScan) {
|
|
2183
|
+
try {
|
|
2184
|
+
// HARDENING: Add timeout to result submission
|
|
2185
|
+
const submitPromise = submitScanResults(apiScan.scanId, {
|
|
2186
|
+
verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
|
|
2187
|
+
score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
|
|
2188
|
+
findings: sanitizeArray(summary.findings, 1000) || [],
|
|
2189
|
+
filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
|
|
2190
|
+
linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
|
|
2191
|
+
durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
|
|
2192
|
+
metadata: {
|
|
2193
|
+
profile,
|
|
2194
|
+
source: 'mcp-server',
|
|
2195
|
+
version: CONFIG.VERSION,
|
|
2196
|
+
},
|
|
2197
|
+
});
|
|
2198
|
+
const submitTimeout = new Promise((_, reject) =>
|
|
2199
|
+
setTimeout(() => reject(new Error('Submit timeout')), 10000)
|
|
2200
|
+
);
|
|
2201
|
+
|
|
2202
|
+
await Promise.race([submitPromise, submitTimeout]);
|
|
2203
|
+
console.error(`[MCP] Results sent to dashboard`);
|
|
2204
|
+
} catch (err) {
|
|
2205
|
+
console.error(`[MCP] Failed to send results to dashboard: ${err.message}`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
1378
2208
|
}
|
|
1379
2209
|
output += "\n---\n_Context Enhanced by vibecheck AI_\n";
|
|
1380
2210
|
return this.success(output);
|
|
@@ -1383,10 +2213,55 @@ class VibecheckMCP {
|
|
|
1383
2213
|
const summary = await this.parseSummaryFromDisk(projectPath);
|
|
1384
2214
|
if (summary) {
|
|
1385
2215
|
output += this.formatScanOutput(summary, projectPath);
|
|
2216
|
+
|
|
2217
|
+
// Submit results to dashboard if connected
|
|
2218
|
+
if (apiConnected && apiScan) {
|
|
2219
|
+
try {
|
|
2220
|
+
// HARDENING: Add timeout to error case submission
|
|
2221
|
+
const submitPromise = submitScanResults(apiScan.scanId, {
|
|
2222
|
+
verdict: sanitizeString(summary.verdict, 50) || 'UNKNOWN',
|
|
2223
|
+
score: sanitizeNumber(summary.score?.overall, 0, 100, 0),
|
|
2224
|
+
findings: sanitizeArray(summary.findings, 1000) || [],
|
|
2225
|
+
filesScanned: sanitizeNumber(summary.stats?.filesScanned, 0, 1000000, 0),
|
|
2226
|
+
linesScanned: sanitizeNumber(summary.stats?.linesScanned, 0, 100000000, 0),
|
|
2227
|
+
durationMs: sanitizeNumber(summary.timings?.total, 0, 3600000, 0),
|
|
2228
|
+
metadata: {
|
|
2229
|
+
profile,
|
|
2230
|
+
source: 'mcp-server',
|
|
2231
|
+
version: CONFIG.VERSION,
|
|
2232
|
+
error: sanitizeString(err.message, 500),
|
|
2233
|
+
},
|
|
2234
|
+
});
|
|
2235
|
+
const submitTimeout = new Promise((_, reject) =>
|
|
2236
|
+
setTimeout(() => reject(new Error('Submit timeout')), 10000)
|
|
2237
|
+
);
|
|
2238
|
+
|
|
2239
|
+
await Promise.race([submitPromise, submitTimeout]);
|
|
2240
|
+
console.error(`[MCP] Results sent to dashboard (with error)`);
|
|
2241
|
+
} catch (apiErr) {
|
|
2242
|
+
console.error(`[MCP] Failed to send results to dashboard: ${apiErr.message}`);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
1386
2245
|
output += `\n⚠️ Scan completed with findings (exit code ${err.code || 1})\n`;
|
|
1387
2246
|
return this.success(output);
|
|
1388
2247
|
}
|
|
1389
2248
|
|
|
2249
|
+
// Report error to dashboard if connected
|
|
2250
|
+
if (apiConnected && apiScan) {
|
|
2251
|
+
try {
|
|
2252
|
+
// HARDENING: Add timeout to error reporting
|
|
2253
|
+
const reportPromise = reportScanError(apiScan.scanId, err);
|
|
2254
|
+
const reportTimeout = new Promise((_, reject) =>
|
|
2255
|
+
setTimeout(() => reject(new Error('Report timeout')), 10000)
|
|
2256
|
+
);
|
|
2257
|
+
|
|
2258
|
+
await Promise.race([reportPromise, reportTimeout]);
|
|
2259
|
+
console.error(`[MCP] Error reported to dashboard`);
|
|
2260
|
+
} catch (apiErr) {
|
|
2261
|
+
console.error(`[MCP] Failed to report error to dashboard: ${apiErr.message}`);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
1390
2265
|
return this.error(`Scan failed: ${err.message}`, {
|
|
1391
2266
|
code: "SCAN_ERROR",
|
|
1392
2267
|
suggestion: "Check that the project path is valid and contains scanable code",
|
|
@@ -1405,7 +2280,7 @@ class VibecheckMCP {
|
|
|
1405
2280
|
// ============================================================================
|
|
1406
2281
|
async handleGate(projectPath, args) {
|
|
1407
2282
|
// Check tier access (STARTER tier required)
|
|
1408
|
-
const access = await
|
|
2283
|
+
const access = await getFeatureAccessStatus("gate", args?.apiKey);
|
|
1409
2284
|
if (!access.hasAccess) {
|
|
1410
2285
|
return {
|
|
1411
2286
|
content: [{
|
|
@@ -1445,7 +2320,7 @@ class VibecheckMCP {
|
|
|
1445
2320
|
async handleFix(projectPath, args) {
|
|
1446
2321
|
// Check tier access for --apply and --autopilot (PRO tier required)
|
|
1447
2322
|
if (args?.apply || args?.autopilot) {
|
|
1448
|
-
const access = await
|
|
2323
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
1449
2324
|
if (!access.hasAccess) {
|
|
1450
2325
|
return {
|
|
1451
2326
|
content: [{
|
|
@@ -1620,7 +2495,7 @@ class VibecheckMCP {
|
|
|
1620
2495
|
// ============================================================================
|
|
1621
2496
|
async handleProve(projectPath, args) {
|
|
1622
2497
|
// Check tier access (PRO tier required)
|
|
1623
|
-
const access = await
|
|
2498
|
+
const access = await getFeatureAccessStatus("prove", args?.apiKey);
|
|
1624
2499
|
if (!access.hasAccess) {
|
|
1625
2500
|
return {
|
|
1626
2501
|
content: [{
|
|
@@ -1823,6 +2698,16 @@ class VibecheckMCP {
|
|
|
1823
2698
|
// SHIP - Quick health check
|
|
1824
2699
|
// ============================================================================
|
|
1825
2700
|
async handleShip(projectPath, args) {
|
|
2701
|
+
// HARDENING: Validate project path
|
|
2702
|
+
const validation = this.validateProjectPath(projectPath);
|
|
2703
|
+
if (!validation.valid) {
|
|
2704
|
+
return this.error(validation.error, {
|
|
2705
|
+
code: validation.code || "INVALID_PATH",
|
|
2706
|
+
suggestion: validation.suggestion,
|
|
2707
|
+
nextSteps: validation.nextSteps || [],
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
|
|
1826
2711
|
let output = "# 🚀 vibecheck Ship\n\n";
|
|
1827
2712
|
output += `**Path:** ${projectPath}\n\n`;
|
|
1828
2713
|
|
|
@@ -1847,20 +2732,34 @@ class VibecheckMCP {
|
|
|
1847
2732
|
// VERIFY - Runtime browser testing
|
|
1848
2733
|
// ============================================================================
|
|
1849
2734
|
async handleVerify(projectPath, args) {
|
|
1850
|
-
|
|
1851
|
-
|
|
2735
|
+
// HARDENING: Validate URL
|
|
2736
|
+
const urlValidation = validateUrl(args?.url);
|
|
2737
|
+
if (!urlValidation.valid) {
|
|
2738
|
+
return this.error(urlValidation.error, {
|
|
2739
|
+
code: 'INVALID_URL',
|
|
2740
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2741
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
const url = urlValidation.url;
|
|
1852
2745
|
|
|
1853
2746
|
let output = "# 🧪 vibecheck Verify\n\n";
|
|
1854
2747
|
output += `**URL:** ${url}\n`;
|
|
1855
|
-
|
|
2748
|
+
|
|
2749
|
+
// HARDENING: Sanitize array inputs
|
|
2750
|
+
const flows = sanitizeArray(args?.flows, 10);
|
|
2751
|
+
if (flows.length) output += `**Flows:** ${flows.join(", ")}\n`;
|
|
1856
2752
|
if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
|
|
1857
2753
|
if (args?.record) output += `**Recording:** Enabled\n`;
|
|
1858
2754
|
output += "\n";
|
|
1859
2755
|
|
|
1860
2756
|
// Build CLI arguments array (secure)
|
|
1861
2757
|
const cliArgs = ["--url", url];
|
|
1862
|
-
|
|
1863
|
-
if (args?.
|
|
2758
|
+
// HARDENING: Sanitize auth - don't log full credentials
|
|
2759
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2760
|
+
cliArgs.push("--auth", sanitizeString(args.auth, 200));
|
|
2761
|
+
}
|
|
2762
|
+
if (flows.length) cliArgs.push("--flows", flows.join(","));
|
|
1864
2763
|
if (args?.headed) cliArgs.push("--headed");
|
|
1865
2764
|
if (args?.record) cliArgs.push("--record");
|
|
1866
2765
|
|
|
@@ -1910,28 +2809,67 @@ class VibecheckMCP {
|
|
|
1910
2809
|
// REALITY v2 - Two-Pass Auth Verification + Dead UI Crawler
|
|
1911
2810
|
// ============================================================================
|
|
1912
2811
|
async handleReality(projectPath, args) {
|
|
1913
|
-
|
|
1914
|
-
|
|
2812
|
+
// HARDENING: Validate URL
|
|
2813
|
+
const urlValidation = validateUrl(args?.url);
|
|
2814
|
+
if (!urlValidation.valid) {
|
|
2815
|
+
return this.error(urlValidation.error, {
|
|
2816
|
+
code: 'INVALID_URL',
|
|
2817
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2818
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
2821
|
+
const url = urlValidation.url;
|
|
1915
2822
|
|
|
1916
2823
|
let output = "# 🧪 vibecheck Reality v2\n\n";
|
|
1917
2824
|
output += `**URL:** ${url}\n`;
|
|
1918
2825
|
output += `**Two-Pass Auth:** ${args?.verifyAuth ? "Yes" : "No"}\n`;
|
|
1919
|
-
|
|
1920
|
-
|
|
2826
|
+
|
|
2827
|
+
// HARDENING: Safely display auth info (mask password)
|
|
2828
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2829
|
+
const authParts = args.auth.split(":");
|
|
2830
|
+
const maskedAuth = authParts[0] ? `${authParts[0].slice(0, 20)}:***` : '***';
|
|
2831
|
+
output += `**Auth:** ${maskedAuth}\n`;
|
|
2832
|
+
}
|
|
2833
|
+
if (args?.storageState) output += `**Storage State:** ${sanitizeString(args.storageState, 100)}\n`;
|
|
1921
2834
|
if (args?.headed) output += `**Mode:** Headed (visible browser)\n`;
|
|
1922
2835
|
if (args?.danger) output += `**Danger Mode:** Enabled (risky clicks allowed)\n`;
|
|
1923
2836
|
output += "\n";
|
|
1924
2837
|
|
|
1925
2838
|
// Build CLI arguments array (secure)
|
|
1926
2839
|
const cliArgs = ["--url", url];
|
|
1927
|
-
if (args?.auth
|
|
2840
|
+
if (args?.auth && typeof args.auth === 'string') {
|
|
2841
|
+
cliArgs.push("--auth", sanitizeString(args.auth, 200));
|
|
2842
|
+
}
|
|
1928
2843
|
if (args?.verifyAuth) cliArgs.push("--verify-auth");
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
if (args?.
|
|
2844
|
+
|
|
2845
|
+
// HARDENING: Validate path arguments
|
|
2846
|
+
if (args?.storageState) {
|
|
2847
|
+
const pathCheck = sanitizePath(args.storageState, projectPath);
|
|
2848
|
+
if (pathCheck.valid) {
|
|
2849
|
+
cliArgs.push("--storage-state", pathCheck.path);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
if (args?.saveStorageState) {
|
|
2853
|
+
const pathCheck = sanitizePath(args.saveStorageState, projectPath);
|
|
2854
|
+
if (pathCheck.valid) {
|
|
2855
|
+
cliArgs.push("--save-storage-state", pathCheck.path);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
if (args?.truthpack) {
|
|
2859
|
+
const pathCheck = sanitizePath(args.truthpack, projectPath);
|
|
2860
|
+
if (pathCheck.valid) {
|
|
2861
|
+
cliArgs.push("--truthpack", pathCheck.path);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
1932
2864
|
if (args?.headed) cliArgs.push("--headed");
|
|
1933
|
-
|
|
1934
|
-
|
|
2865
|
+
|
|
2866
|
+
// HARDENING: Bound numeric arguments
|
|
2867
|
+
if (args?.maxPages) {
|
|
2868
|
+
cliArgs.push("--max-pages", String(sanitizeNumber(args.maxPages, 1, 100, 18)));
|
|
2869
|
+
}
|
|
2870
|
+
if (args?.maxDepth) {
|
|
2871
|
+
cliArgs.push("--max-depth", String(sanitizeNumber(args.maxDepth, 1, 10, 2)));
|
|
2872
|
+
}
|
|
1935
2873
|
if (args?.danger) cliArgs.push("--danger");
|
|
1936
2874
|
|
|
1937
2875
|
try {
|
|
@@ -2012,16 +2950,27 @@ class VibecheckMCP {
|
|
|
2012
2950
|
// AI-TEST - AI Agent testing
|
|
2013
2951
|
// ============================================================================
|
|
2014
2952
|
async handleAITest(projectPath, args) {
|
|
2015
|
-
|
|
2016
|
-
|
|
2953
|
+
// HARDENING: Validate URL
|
|
2954
|
+
const urlValidation = validateUrl(args?.url);
|
|
2955
|
+
if (!urlValidation.valid) {
|
|
2956
|
+
return this.error(urlValidation.error, {
|
|
2957
|
+
code: 'INVALID_URL',
|
|
2958
|
+
suggestion: 'Provide a valid HTTP/HTTPS URL',
|
|
2959
|
+
nextSteps: ['Check the URL format', 'Ensure the URL is accessible'],
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
const url = urlValidation.url;
|
|
2963
|
+
|
|
2964
|
+
// HARDENING: Sanitize goal string
|
|
2965
|
+
const goal = sanitizeString(args?.goal, 500) || "Test all features";
|
|
2017
2966
|
|
|
2018
2967
|
let output = "# 🤖 vibecheck AI Agent\n\n";
|
|
2019
2968
|
output += `**URL:** ${url}\n`;
|
|
2020
|
-
output += `**Goal:** ${
|
|
2969
|
+
output += `**Goal:** ${goal}\n\n`;
|
|
2021
2970
|
|
|
2022
2971
|
// Build CLI arguments array (secure)
|
|
2023
2972
|
const cliArgs = ["--url", url];
|
|
2024
|
-
if (
|
|
2973
|
+
if (goal) cliArgs.push("--goal", goal);
|
|
2025
2974
|
if (args?.headed) cliArgs.push("--headed");
|
|
2026
2975
|
|
|
2027
2976
|
try {
|
|
@@ -2077,7 +3026,7 @@ class VibecheckMCP {
|
|
|
2077
3026
|
// ============================================================================
|
|
2078
3027
|
async handleAutopilotPlan(projectPath, args) {
|
|
2079
3028
|
// Check tier access (PRO tier required)
|
|
2080
|
-
const access = await
|
|
3029
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
2081
3030
|
if (!access.hasAccess) {
|
|
2082
3031
|
return {
|
|
2083
3032
|
content: [{
|
|
@@ -2164,7 +3113,7 @@ class VibecheckMCP {
|
|
|
2164
3113
|
// ============================================================================
|
|
2165
3114
|
async handleAutopilotApply(projectPath, args) {
|
|
2166
3115
|
// Check tier access (PRO tier required)
|
|
2167
|
-
const access = await
|
|
3116
|
+
const access = await getFeatureAccessStatus("fix.apply_patches", args?.apiKey);
|
|
2168
3117
|
if (!access.hasAccess) {
|
|
2169
3118
|
return {
|
|
2170
3119
|
content: [{
|
|
@@ -2225,7 +3174,7 @@ class VibecheckMCP {
|
|
|
2225
3174
|
// ============================================================================
|
|
2226
3175
|
async handleBadge(projectPath, args) {
|
|
2227
3176
|
// Check tier access (STARTER tier required)
|
|
2228
|
-
const access = await
|
|
3177
|
+
const access = await getFeatureAccessStatus("badge", args?.apiKey);
|
|
2229
3178
|
if (!access.hasAccess) {
|
|
2230
3179
|
return {
|
|
2231
3180
|
content: [{
|
|
@@ -2381,15 +3330,57 @@ class VibecheckMCP {
|
|
|
2381
3330
|
}
|
|
2382
3331
|
|
|
2383
3332
|
// ============================================================================
|
|
2384
|
-
// RUN
|
|
3333
|
+
// RUN - with graceful shutdown handling
|
|
2385
3334
|
// ============================================================================
|
|
2386
3335
|
async run() {
|
|
2387
3336
|
const transport = new StdioServerTransport();
|
|
3337
|
+
|
|
3338
|
+
// ========================================================================
|
|
3339
|
+
// HARDENING: Graceful shutdown handling
|
|
3340
|
+
// ========================================================================
|
|
3341
|
+
const shutdown = async (signal) => {
|
|
3342
|
+
console.error(`\n[MCP] Received ${signal}, shutting down gracefully...`);
|
|
3343
|
+
try {
|
|
3344
|
+
// Clear rate limit state to prevent memory leaks
|
|
3345
|
+
rateLimitState.calls = [];
|
|
3346
|
+
|
|
3347
|
+
// Close server connection
|
|
3348
|
+
await this.server.close();
|
|
3349
|
+
console.error('[MCP] Server closed successfully');
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
console.error(`[MCP] Error during shutdown: ${err.message}`);
|
|
3352
|
+
}
|
|
3353
|
+
process.exit(0);
|
|
3354
|
+
};
|
|
3355
|
+
|
|
3356
|
+
// Handle termination signals
|
|
3357
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
3358
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
3359
|
+
|
|
3360
|
+
// Handle uncaught errors gracefully
|
|
3361
|
+
process.on('uncaughtException', (err) => {
|
|
3362
|
+
console.error(`[MCP] Uncaught exception: ${err.message}`);
|
|
3363
|
+
console.error(err.stack);
|
|
3364
|
+
// Don't exit - try to keep running
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
3368
|
+
console.error(`[MCP] Unhandled rejection at:`, promise);
|
|
3369
|
+
console.error(`[MCP] Reason:`, reason);
|
|
3370
|
+
// Don't exit - try to keep running
|
|
3371
|
+
});
|
|
3372
|
+
|
|
2388
3373
|
await this.server.connect(transport);
|
|
2389
|
-
console.error(
|
|
3374
|
+
console.error(`vibecheck MCP Server v${VERSION} running on stdio (hardened)`);
|
|
2390
3375
|
}
|
|
2391
3376
|
}
|
|
2392
3377
|
|
|
2393
|
-
//
|
|
3378
|
+
// ============================================================================
|
|
3379
|
+
// MAIN - with error handling
|
|
3380
|
+
// ============================================================================
|
|
2394
3381
|
const server = new VibecheckMCP();
|
|
2395
|
-
server.run().catch(
|
|
3382
|
+
server.run().catch((err) => {
|
|
3383
|
+
console.error(`[MCP] Fatal error starting server: ${err.message}`);
|
|
3384
|
+
console.error(err.stack);
|
|
3385
|
+
process.exit(1);
|
|
3386
|
+
});
|