@vibecheckai/cli 3.0.10 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,807 @@
1
+ /**
2
+ * Vibecheck MCP Server
3
+ *
4
+ * Secure, tier-gated access to Vibecheck's truth analysis
5
+ * for IDE agents and AI assistants.
6
+ */
7
+
1
8
  const path = require("path");
2
- const { execSync } = require("child_process");
9
+ const http = require("http");
10
+ const { URL } = require("url");
11
+
12
+ // Import Vibecheck modules
13
+ const { buildTruthpack, writeTruthpack } = require("./lib/truth");
14
+ const { shipCore } = require("./runShip");
15
+ const { generateRunId } = require("./lib/cli-output");
16
+ const { enforceLimit, enforceFeature, trackUsage } = require("./lib/entitlements");
17
+
18
+ // MCP Server class
19
+ class VibecheckMCPServer {
20
+ constructor(options = {}) {
21
+ this.host = options.host || "127.0.0.1";
22
+ this.port = options.port || 3000;
23
+ this.allowRemote = options.allowRemote || false;
24
+ this.requireApiKey = options.requireApiKey !== false;
25
+ this.apiKey = options.apiKey || process.env.VIBECHECK_API_KEY;
26
+ this.logLevel = options.logLevel || "info";
27
+ this.auditLog = options.auditLog || path.join(process.cwd(), ".vibecheck", "mcp-audit.log");
28
+
29
+ // Rate limiting (in-memory for now)
30
+ this.rateLimits = new Map();
31
+ this.tiers = {
32
+ free: { rpm: 10, burst: 20, daily: 100 },
33
+ starter: { rpm: 30, burst: 60, daily: 500 },
34
+ pro: { rpm: 100, burst: 200, daily: 2000 },
35
+ enterprise: { rpm: 500, burst: 1000, daily: Infinity }
36
+ };
37
+
38
+ // Tool definitions
39
+ this.tools = this.defineTools();
40
+
41
+ // Ensure audit directory exists
42
+ require("fs").mkdirSync(path.dirname(this.auditLog), { recursive: true });
43
+ }
44
+
45
+ defineTools() {
46
+ return {
47
+ // Free tier tools
48
+ repo_map: {
49
+ description: "Map repository structure and detect project type",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ path: { type: "string", default: ".", description: "Repository path" },
54
+ includeTests: { type: "boolean", default: false, description: "Include test files" }
55
+ }
56
+ },
57
+ tier: "free"
58
+ },
59
+
60
+ routes_list: {
61
+ description: "List server and client routes with metadata",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ path: { type: "string", default: ".", description: "Project path" },
66
+ source: { type: "string", enum: ["server", "client", "both"], default: "both" }
67
+ }
68
+ },
69
+ tier: "free"
70
+ },
71
+
72
+ truthpack_get: {
73
+ description: "Retrieve the latest truthpack for the project",
74
+ inputSchema: {
75
+ type: "object",
76
+ properties: {
77
+ path: { type: "string", default: ".", description: "Project path" },
78
+ refresh: { type: "boolean", default: false, description: "Force refresh" }
79
+ }
80
+ },
81
+ tier: "free"
82
+ },
83
+
84
+ findings_latest: {
85
+ description: "Retrieve findings from the latest ship check",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ path: { type: "string", default: ".", description: "Project path" },
90
+ severity: { type: "string", enum: ["all", "BLOCK", "WARN", "INFO"], default: "all" }
91
+ }
92
+ },
93
+ tier: "free"
94
+ },
95
+
96
+ // Pro tier tools
97
+ run_ship: {
98
+ description: "Execute a comprehensive ship check",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: {
102
+ path: { type: "string", default: ".", description: "Project path" },
103
+ strict: { type: "boolean", default: false, description: "Fail on warnings" },
104
+ saveReport: { type: "boolean", default: true, description: "Save report" }
105
+ }
106
+ },
107
+ tier: "pro"
108
+ },
109
+
110
+ // Enterprise tier tools
111
+ policy_check: {
112
+ description: "Validate project against custom compliance policies",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: {
116
+ path: { type: "string", default: ".", description: "Project path" },
117
+ policyFile: { type: "string", default: ".vibecheck/policy.yml", description: "Policy file" },
118
+ reportFormat: { type: "string", enum: ["json", "sarif", "markdown"], default: "json" }
119
+ }
120
+ },
121
+ tier: "enterprise"
122
+ }
123
+ };
124
+ }
125
+
126
+ // Authentication and tier checking
127
+ async authenticate(req) {
128
+ const authHeader = req.headers["authorization"];
129
+ const apiKey = authHeader?.replace("Bearer ", "");
130
+
131
+ // Check if API key is required for paid tools
132
+ if (this.requireApiKey && !apiKey) {
133
+ return { tier: "free", error: "API key required for paid features" };
134
+ }
135
+
136
+ // TODO: Implement actual API key validation against Vibecheck API
137
+ // For now, return pro tier if API key provided
138
+ const tier = apiKey ? "pro" : "free";
139
+
140
+ return { tier };
141
+ }
142
+
143
+ // Rate limiting check
144
+ checkRateLimit(clientId, tier) {
145
+ const now = Date.now();
146
+ const limits = this.tiers[tier];
147
+
148
+ if (!this.rateLimits.has(clientId)) {
149
+ this.rateLimits.set(clientId, {
150
+ requests: [],
151
+ dailyCount: 0,
152
+ dailyReset: new Date().setHours(0, 0, 0, 0) + 24 * 60 * 60 * 1000
153
+ });
154
+ }
155
+
156
+ const client = this.rateLimits.get(clientId);
157
+
158
+ // Reset daily counter if needed
159
+ if (now > client.dailyReset) {
160
+ client.dailyCount = 0;
161
+ client.dailyReset = new Date().setHours(0, 0, 0, 0) + 24 * 60 * 60 * 1000;
162
+ }
163
+
164
+ // Clean old requests (older than 1 minute)
165
+ client.requests = client.requests.filter(time => now - time < 60000);
166
+
167
+ // Check limits
168
+ if (client.requests.length >= limits.rpm) {
169
+ return { allowed: false, error: "Rate limit exceeded", resetAt: now + 60000 };
170
+ }
171
+
172
+ if (client.dailyCount >= limits.daily) {
173
+ return { allowed: false, error: "Daily quota exceeded" };
174
+ }
175
+
176
+ // Record request
177
+ client.requests.push(now);
178
+ client.dailyCount++;
179
+
180
+ return { allowed: true, remaining: limits.rpm - client.requests.length };
181
+ }
182
+
183
+ // Audit logging
184
+ async logAudit(event) {
185
+ const logEntry = {
186
+ timestamp: new Date().toISOString(),
187
+ ...event
188
+ };
189
+
190
+ try {
191
+ require("fs").appendFileSync(this.auditLog, JSON.stringify(logEntry) + "\n");
192
+ } catch (err) {
193
+ console.error("Failed to write audit log:", err);
194
+ }
195
+ }
3
196
 
4
- const c = {
5
- reset: "\x1b[0m",
6
- bold: "\x1b[1m",
7
- dim: "\x1b[2m",
8
- cyan: "\x1b[36m",
9
- };
197
+ // Tool implementations
198
+ async handleRepoMap(args) {
199
+ const projectPath = path.resolve(args.path || ".");
200
+
201
+ // Detect project type
202
+ const pkgPath = path.join(projectPath, "package.json");
203
+ let projectType = "unknown";
204
+ let structure = { routes: [], components: [], config: [] };
205
+ let metadata = {};
206
+
207
+ if (require("fs").existsSync(pkgPath)) {
208
+ const pkg = JSON.parse(require("fs").readFileSync(pkgPath, "utf8"));
209
+ metadata.packageManager = this.detectPackageManager(projectPath);
210
+ metadata.hasTypeScript = require("fs").existsSync(path.join(projectPath, "tsconfig.json"));
211
+ metadata.frameworks = [];
212
+
213
+ if (pkg.dependencies?.next) {
214
+ projectType = "nextjs";
215
+ metadata.frameworks.push("nextjs");
216
+ structure = this.scanNextProject(projectPath, args.includeTests);
217
+ } else if (pkg.dependencies?.fastify) {
218
+ projectType = "fastify";
219
+ metadata.frameworks.push("fastify");
220
+ structure = this.scanFastifyProject(projectPath, args.includeTests);
221
+ } else {
222
+ projectType = "mixed";
223
+ structure = this.scanGenericProject(projectPath, args.includeTests);
224
+ }
225
+ }
226
+
227
+ return {
228
+ type: projectType,
229
+ structure,
230
+ metadata
231
+ };
232
+ }
233
+
234
+ async handleRoutesList(args) {
235
+ const projectPath = path.resolve(args.path || ".");
236
+ const source = args.source || "both";
237
+
238
+ // Get truthpack
239
+ const truthpack = await buildTruthpack({ repoRoot: projectPath });
240
+
241
+ const result = {};
242
+
243
+ if (source === "server" || source === "both") {
244
+ result.server = truthpack.routes?.server || [];
245
+ }
246
+
247
+ if (source === "client" || source === "both") {
248
+ result.client = truthpack.routes?.clientRefs || [];
249
+ }
250
+
251
+ result.gaps = truthpack.routes?.gaps || [];
252
+
253
+ return result;
254
+ }
255
+
256
+ async handleTruthpackGet(args) {
257
+ const projectPath = path.resolve(args.path || ".");
258
+ let truthpack;
259
+
260
+ if (args.refresh) {
261
+ truthpack = await buildTruthpack({ repoRoot: projectPath });
262
+ writeTruthpack(projectPath, truthpack);
263
+ } else {
264
+ // Try to read existing truthpack
265
+ const truthpackPath = path.join(projectPath, ".vibecheck", "truth", "truthpack.json");
266
+ if (require("fs").existsSync(truthpackPath)) {
267
+ truthpack = JSON.parse(require("fs").readFileSync(truthpackPath, "utf8"));
268
+ } else {
269
+ truthpack = await buildTruthpack({ repoRoot: projectPath });
270
+ writeTruthpack(projectPath, truthpack);
271
+ }
272
+ }
273
+
274
+ // Sanitize sensitive data
275
+ const sanitized = {
276
+ ...truthpack,
277
+ env: truthpack.env ? {
278
+ ...truthpack.env,
279
+ vars: truthpack.env.vars?.map(v => ({
280
+ ...v,
281
+ value: undefined // Never expose actual values
282
+ }))
283
+ } : undefined
284
+ };
285
+
286
+ return sanitized;
287
+ }
288
+
289
+ async handleFindingsLatest(args) {
290
+ const projectPath = path.resolve(args.path || ".");
291
+ const severity = args.severity || "all";
292
+
293
+ // Try to read latest ship report
294
+ const reportPath = path.join(projectPath, ".vibecheck", "last_ship.json");
295
+
296
+ if (!require("fs").existsSync(reportPath)) {
297
+ throw new Error("No ship report found. Run 'vibecheck ship' first.");
298
+ }
299
+
300
+ const report = JSON.parse(require("fs").readFileSync(reportPath, "utf8"));
301
+
302
+ let findings = report.findings || [];
303
+
304
+ if (severity !== "all") {
305
+ findings = findings.filter(f => f.severity === severity);
306
+ }
307
+
308
+ const summary = {
309
+ total: findings.length,
310
+ blockers: findings.filter(f => f.severity === "BLOCK").length,
311
+ warnings: findings.filter(f => f.severity === "WARN").length,
312
+ info: findings.filter(f => f.severity === "INFO").length
313
+ };
314
+
315
+ return {
316
+ verdict: report.verdict,
317
+ score: report.score || 0,
318
+ findings,
319
+ summary,
320
+ runId: report.runId,
321
+ timestamp: report.timestamp
322
+ };
323
+ }
10
324
 
325
+ async handleRunShip(args) {
326
+ const projectPath = path.resolve(args.path || ".");
327
+
328
+ // Check entitlements
329
+ try {
330
+ await enforceLimit("scans");
331
+ await enforceFeature("ship");
332
+ } catch (err) {
333
+ throw new Error(`Access denied: ${err.message}`);
334
+ }
335
+
336
+ // Run ship check
337
+ const result = await shipCore({
338
+ repoRoot: projectPath,
339
+ noWrite: !args.saveReport,
340
+ strict: args.strict
341
+ });
342
+
343
+ // Track usage
344
+ await trackUsage("scans");
345
+
346
+ return result;
347
+ }
348
+
349
+ async handlePolicyCheck(args) {
350
+ const projectPath = path.resolve(args.path || ".");
351
+ const policyFile = path.resolve(args.policyFile || ".vibecheck/policy.yml");
352
+
353
+ if (!require("fs").existsSync(policyFile)) {
354
+ throw new Error("Policy file not found");
355
+ }
356
+
357
+ // TODO: Implement actual policy checking
358
+ // For now, return a mock response
359
+ return {
360
+ compliant: true,
361
+ violations: [],
362
+ score: 100,
363
+ report: "Policy check passed"
364
+ };
365
+ }
366
+
367
+ // Helper methods
368
+ detectPackageManager(projectPath) {
369
+ if (require("fs").existsSync(path.join(projectPath, "pnpm-lock.yaml"))) return "pnpm";
370
+ if (require("fs").existsSync(path.join(projectPath, "yarn.lock"))) return "yarn";
371
+ if (require("fs").existsSync(path.join(projectPath, "package-lock.json"))) return "npm";
372
+ return "unknown";
373
+ }
374
+
375
+ scanNextProject(projectPath, includeTests = false) {
376
+ const structure = { routes: [], components: [], config: [] };
377
+
378
+ // Scan app directory
379
+ const appDir = path.join(projectPath, "app");
380
+ if (require("fs").existsSync(appDir)) {
381
+ this.scanDirectory(appDir, structure, includeTests, "app");
382
+ }
383
+
384
+ // Scan pages directory
385
+ const pagesDir = path.join(projectPath, "pages");
386
+ if (require("fs").existsSync(pagesDir)) {
387
+ this.scanDirectory(pagesDir, structure, includeTests, "pages");
388
+ }
389
+
390
+ // Scan components
391
+ const componentsDir = path.join(projectPath, "components");
392
+ if (require("fs").existsSync(componentsDir)) {
393
+ this.scanDirectory(componentsDir, structure, includeTests, "components");
394
+ }
395
+
396
+ return structure;
397
+ }
398
+
399
+ scanFastifyProject(projectPath, includeTests = false) {
400
+ const structure = { routes: [], components: [], config: [] };
401
+
402
+ // Find main entry file
403
+ const possibleEntries = ["index.js", "server.js", "app.js", "main.js"];
404
+ for (const entry of possibleEntries) {
405
+ const entryPath = path.join(projectPath, entry);
406
+ if (require("fs").existsSync(entryPath)) {
407
+ this.scanFile(entryPath, structure, "server");
408
+ break;
409
+ }
410
+ }
411
+
412
+ return structure;
413
+ }
414
+
415
+ scanGenericProject(projectPath, includeTests = false) {
416
+ const structure = { routes: [], components: [], config: [] };
417
+ this.scanDirectory(projectPath, structure, includeTests, "root");
418
+ return structure;
419
+ }
420
+
421
+ scanDirectory(dir, structure, includeTests, type) {
422
+ if (!require("fs").existsSync(dir)) return;
423
+
424
+ const items = require("fs").readdirSync(dir);
425
+
426
+ for (const item of items) {
427
+ if (item.startsWith(".") || item === "node_modules") continue;
428
+ if (!includeTests && (item.includes(".test.") || item.includes(".spec."))) continue;
429
+
430
+ const itemPath = path.join(dir, item);
431
+ const stat = require("fs").statSync(itemPath);
432
+
433
+ if (stat.isDirectory()) {
434
+ this.scanDirectory(itemPath, structure, includeTests, type);
435
+ } else if (stat.isFile() && (item.endsWith(".js") || item.endsWith(".ts") || item.endsWith(".jsx") || item.endsWith(".tsx"))) {
436
+ this.scanFile(itemPath, structure, type);
437
+ }
438
+ }
439
+ }
440
+
441
+ scanFile(filePath, structure, type) {
442
+ const relative = path.relative(process.cwd(), filePath);
443
+
444
+ if (type === "components" || relative.includes("components")) {
445
+ structure.components.push(relative);
446
+ } else if (type === "server" || relative.includes("routes") || relative.includes("api")) {
447
+ structure.routes.push(relative);
448
+ } else if (relative.includes("config") || relative.endsWith(".config.js") || relative.endsWith(".config.ts")) {
449
+ structure.config.push(relative);
450
+ }
451
+ }
452
+
453
+ // HTTP request handler
454
+ async handleRequest(req, res) {
455
+ const startTime = Date.now();
456
+ let clientId = req.headers["x-client-id"] || "unknown";
457
+
458
+ try {
459
+ // Parse URL
460
+ const url = new URL(req.url, `http://${req.headers.host}`);
461
+
462
+ // Handle different endpoints
463
+ if (url.pathname === "/tools") {
464
+ // List available tools
465
+ const auth = await this.authenticate(req);
466
+ const availableTools = Object.entries(this.tools)
467
+ .filter(([_, tool]) => this.checkTierAccess(tool.tier, auth.tier))
468
+ .map(([name, tool]) => ({
469
+ name,
470
+ description: tool.description,
471
+ inputSchema: tool.inputSchema
472
+ }));
473
+
474
+ this.sendJson(res, { tools: availableTools });
475
+ return;
476
+ }
477
+
478
+ if (url.pathname === "/call" && req.method === "POST") {
479
+ // Tool execution
480
+ const body = await this.parseBody(req);
481
+ const { tool, arguments: args } = body;
482
+
483
+ if (!tool || !this.tools[tool]) {
484
+ this.sendError(res, 400, "Unknown tool");
485
+ return;
486
+ }
487
+
488
+ // Authenticate
489
+ const auth = await this.authenticate(req);
490
+ if (auth.error) {
491
+ this.sendError(res, 401, auth.error);
492
+ return;
493
+ }
494
+
495
+ // Check tier access
496
+ if (!this.checkTierAccess(this.tools[tool].tier, auth.tier)) {
497
+ this.sendError(res, 403, `Tool '${tool}' requires ${this.tools[tool].tier} tier`);
498
+ return;
499
+ }
500
+
501
+ // Rate limit
502
+ const rateLimit = this.checkRateLimit(clientId, auth.tier);
503
+ if (!rateLimit.allowed) {
504
+ this.sendError(res, 429, rateLimit.error);
505
+ return;
506
+ }
507
+
508
+ // Execute tool
509
+ const runId = generateRunId();
510
+ const result = await this.executeTool(tool, args);
511
+ const duration = Date.now() - startTime;
512
+
513
+ // Log audit
514
+ await this.logAudit({
515
+ clientId,
516
+ tier: auth.tier,
517
+ tool,
518
+ duration,
519
+ success: true,
520
+ runId
521
+ });
522
+
523
+ // Send response
524
+ this.sendJson(res, {
525
+ success: true,
526
+ data: result,
527
+ meta: {
528
+ runId,
529
+ duration,
530
+ tier: auth.tier,
531
+ rateLimit: {
532
+ remaining: rateLimit.remaining
533
+ }
534
+ }
535
+ });
536
+
537
+ return;
538
+ }
539
+
540
+ if (url.pathname === "/status") {
541
+ // Server status
542
+ this.sendJson(res, {
543
+ status: "running",
544
+ version: require("../../package.json").version,
545
+ uptime: process.uptime(),
546
+ activeClients: this.rateLimits.size
547
+ });
548
+ return;
549
+ }
550
+
551
+ // Unknown endpoint
552
+ this.sendError(res, 404, "Not found");
553
+
554
+ } catch (error) {
555
+ console.error("MCP Server Error:", error);
556
+
557
+ // Log audit
558
+ await this.logAudit({
559
+ clientId,
560
+ error: error.message,
561
+ success: false
562
+ });
563
+
564
+ this.sendError(res, 500, "Internal server error");
565
+ }
566
+ }
567
+
568
+ async executeTool(toolName, args) {
569
+ switch (toolName) {
570
+ case "repo_map":
571
+ return await this.handleRepoMap(args);
572
+ case "routes_list":
573
+ return await this.handleRoutesList(args);
574
+ case "truthpack_get":
575
+ return await this.handleTruthpackGet(args);
576
+ case "findings_latest":
577
+ return await this.handleFindingsLatest(args);
578
+ case "run_ship":
579
+ return await this.handleRunShip(args);
580
+ case "policy_check":
581
+ return await this.handlePolicyCheck(args);
582
+ default:
583
+ throw new Error(`Unknown tool: ${toolName}`);
584
+ }
585
+ }
586
+
587
+ checkTierAccess(requiredTier, userTier) {
588
+ const tierOrder = ["free", "starter", "pro", "enterprise"];
589
+ const requiredIndex = tierOrder.indexOf(requiredTier);
590
+ const userIndex = tierOrder.indexOf(userTier);
591
+
592
+ return userIndex >= requiredIndex;
593
+ }
594
+
595
+ parseBody(req) {
596
+ return new Promise((resolve, reject) => {
597
+ let body = "";
598
+ req.on("data", chunk => body += chunk);
599
+ req.on("end", () => {
600
+ try {
601
+ resolve(JSON.parse(body));
602
+ } catch (err) {
603
+ reject(err);
604
+ }
605
+ });
606
+ req.on("error", reject);
607
+ });
608
+ }
609
+
610
+ sendJson(res, data) {
611
+ res.writeHead(200, { "Content-Type": "application/json" });
612
+ res.end(JSON.stringify(data, null, 2));
613
+ }
614
+
615
+ sendError(res, code, message) {
616
+ res.writeHead(code, { "Content-Type": "application/json" });
617
+ res.end(JSON.stringify({
618
+ error: {
619
+ code,
620
+ message
621
+ }
622
+ }, null, 2));
623
+ }
624
+
625
+ // Start server
626
+ start() {
627
+ this.server = http.createServer((req, res) => {
628
+ // Check remote access
629
+ if (!this.allowRemote && req.socket.remoteAddress !== "127.0.0.1" && req.socket.remoteAddress !== "::1") {
630
+ this.sendError(res, 403, "Remote access not allowed");
631
+ return;
632
+ }
633
+
634
+ this.handleRequest(req, res);
635
+ });
636
+
637
+ this.server.listen(this.port, this.host, () => {
638
+ console.log(`🚀 Vibecheck MCP Server running on http://${this.host}:${this.port}`);
639
+ console.log(`📋 Tools available: ${Object.keys(this.tools).length}`);
640
+ console.log(`🔐 Remote access: ${this.allowRemote ? "enabled" : "disabled"}`);
641
+ console.log(`🔑 API key required: ${this.requireApiKey ? "yes" : "no"}`);
642
+ });
643
+
644
+ this.server.on("error", err => {
645
+ console.error("MCP Server failed to start:", err);
646
+ process.exit(1);
647
+ });
648
+ }
649
+
650
+ stop() {
651
+ if (this.server) {
652
+ this.server.close();
653
+ console.log("MCP Server stopped");
654
+ }
655
+ }
656
+ }
657
+
658
+ // CLI handler
659
+ async function runMcp(args) {
660
+ const opts = parseArgs(args);
661
+
662
+ // Check if we're in free tier and only showing help/config
663
+ const isFreeTier = process.env.VIBECHECK_TIER === "free" || !process.env.VIBECHECK_API_KEY;
664
+ const isHelpOrConfig = opts.help || opts.printConfig || opts.status || opts.test;
665
+
666
+ if (isFreeTier && !isHelpOrConfig) {
667
+ // This will be handled by entitlements system
668
+ // But we can show a helpful message
669
+ console.log("\n🔌 MCP Server requires STARTER plan or higher");
670
+ console.log("Use --help to see available options or");
671
+ console.log("Upgrade: https://vibecheckai.dev/pricing\n");
672
+ return 3; // EXIT_FEATURE_NOT_ALLOWED
673
+ }
674
+
675
+ if (opts.help) {
676
+ printHelp();
677
+ return 0;
678
+ }
679
+
680
+ if (opts.printConfig) {
681
+ printClientConfig(opts);
682
+ return 0;
683
+ }
684
+
685
+ if (opts.status) {
686
+ // Check server status
687
+ try {
688
+ const response = await fetch(`http://${opts.host}:${opts.port}/status`);
689
+ const status = await response.json();
690
+ console.log(JSON.stringify(status, null, 2));
691
+ return 0;
692
+ } catch (err) {
693
+ console.error("Server not running or not reachable");
694
+ return 1;
695
+ }
696
+ }
697
+
698
+ if (opts.test) {
699
+ // Test connection
700
+ try {
701
+ const response = await fetch(`http://${opts.host}:${opts.port}/tools`);
702
+ const tools = await response.json();
703
+ console.log("✅ Connection successful");
704
+ console.log(`📋 Available tools: ${tools.tools.length}`);
705
+ return 0;
706
+ } catch (err) {
707
+ console.error("❌ Connection failed:", err.message);
708
+ return 1;
709
+ }
710
+ }
711
+
712
+ // Load config if provided
713
+ let config = {};
714
+ if (opts.config) {
715
+ const configPath = path.resolve(opts.config);
716
+ if (require("fs").existsSync(configPath)) {
717
+ config = JSON.parse(require("fs").readFileSync(configPath, "utf8"));
718
+ } else {
719
+ console.error(`Config file not found: ${configPath}`);
720
+ return 1;
721
+ }
722
+ }
723
+
724
+ // Create and start server
725
+ const server = new VibecheckMCPServer({
726
+ host: opts.host || config.server?.host || "127.0.0.1",
727
+ port: opts.port || config.server?.port || 3000,
728
+ allowRemote: opts.allowRemote || config.server?.allowRemote || false,
729
+ requireApiKey: opts.requireApiKey !== false,
730
+ apiKey: opts.apiKey || config.auth?.apiKey,
731
+ logLevel: opts.logLevel || config.logging?.level || "info",
732
+ auditLog: opts.auditLog || config.logging?.auditFile
733
+ });
734
+
735
+ server.start();
736
+
737
+ // Handle shutdown
738
+ process.on("SIGINT", () => {
739
+ console.log("\nShutting down MCP server...");
740
+ server.stop();
741
+ process.exit(0);
742
+ });
743
+
744
+ process.on("SIGTERM", () => {
745
+ console.log("\nShutting down MCP server...");
746
+ server.stop();
747
+ process.exit(0);
748
+ });
749
+
750
+ // Keep process alive
751
+ return new Promise(() => {});
752
+ }
753
+
754
+ // Argument parsing
755
+ function parseArgs(args) {
756
+ const opts = {
757
+ host: null,
758
+ port: null,
759
+ allowRemote: false,
760
+ requireApiKey: true,
761
+ apiKey: null,
762
+ config: null,
763
+ logLevel: null,
764
+ auditLog: null,
765
+ printConfig: false,
766
+ status: false,
767
+ test: false,
768
+ help: false
769
+ };
770
+
771
+ for (let i = 0; i < args.length; i++) {
772
+ const a = args[i];
773
+ if (a === "--host") opts.host = args[++i];
774
+ else if (a.startsWith("--host=")) opts.host = a.split("=")[1];
775
+ else if (a === "--port") opts.port = parseInt(args[++i]);
776
+ else if (a.startsWith("--port=")) opts.port = parseInt(a.split("=")[1]);
777
+ else if (a === "--allow-remote") opts.allowRemote = true;
778
+ else if (a === "--no-api-key") opts.requireApiKey = false;
779
+ else if (a === "--api-key") opts.apiKey = args[++i];
780
+ else if (a.startsWith("--api-key=")) opts.apiKey = a.split("=")[1];
781
+ else if (a === "--config") opts.config = args[++i];
782
+ else if (a.startsWith("--config=")) opts.config = a.split("=")[1];
783
+ else if (a === "--log-level") opts.logLevel = args[++i];
784
+ else if (a.startsWith("--log-level=")) opts.logLevel = a.split("=")[1];
785
+ else if (a === "--audit-log") opts.auditLog = args[++i];
786
+ else if (a.startsWith("--audit-log=")) opts.auditLog = a.split("=")[1];
787
+ else if (a === "--print-config") opts.printConfig = true;
788
+ else if (a === "--status") opts.status = true;
789
+ else if (a === "--test") opts.test = true;
790
+ else if (a === "--help" || a === "-h") opts.help = true;
791
+ }
792
+
793
+ return opts;
794
+ }
795
+
796
+ // Help text
11
797
  function printHelp() {
798
+ const c = {
799
+ reset: "\x1b[0m",
800
+ bold: "\x1b[1m",
801
+ dim: "\x1b[2m",
802
+ cyan: "\x1b[36m",
803
+ };
804
+
12
805
  console.log(`
13
806
  ${c.cyan}${c.bold}vibecheck mcp${c.reset} - MCP Server for AI Agents
14
807
 
@@ -16,62 +809,92 @@ ${c.bold}USAGE${c.reset}
16
809
  vibecheck mcp [options]
17
810
 
18
811
  ${c.bold}OPTIONS${c.reset}
19
- --help, -h Show this help
20
-
21
- ${c.bold}WHAT IT DOES${c.reset}
22
- Starts a Model Context Protocol (MCP) server that allows AI agents
23
- (like Claude, Cursor, Windsurf) to interact with vibecheck tools.
812
+ --host <host> Server host (default: 127.0.0.1)
813
+ --port <port> Server port (default: 3000)
814
+ --allow-remote Allow remote connections
815
+ --no-api-key Don't require API key for paid features
816
+ --api-key <key> API key for authentication
817
+ --config <file> Configuration file path
818
+ --log-level <level> Log level (debug, info, warn, error)
819
+ --audit-log <file> Audit log file path
820
+ --print-config Print client configuration
821
+ --status Check server status
822
+ --test Test connection
823
+ --help, -h Show this help
24
824
 
25
825
  ${c.bold}MCP TOOLS AVAILABLE${c.reset}
26
- vibecheck.scan Run static analysis
27
- vibecheck.ship Get ship verdict
28
- vibecheck.validate_claim Verify code claims
29
- vibecheck.get_truthpack Get ground truth
30
- vibecheck.list_findings List current findings
826
+ ${c.dim}Free Tier:${c.reset}
827
+ repo_map Map repository structure
828
+ routes_list List server/client routes
829
+ truthpack_get Get project truthpack
830
+ findings_latest Get latest scan findings
831
+
832
+ ${c.dim}Pro Tier:${c.reset}
833
+ • run_ship Execute ship check
834
+
835
+ ${c.dim}Enterprise Tier:${c.reset}
836
+ • policy_check Validate against policies
31
837
 
32
838
  ${c.bold}CONFIGURATION${c.reset}
33
839
  Add to your AI IDE's MCP config:
34
840
 
35
- ${c.dim}// cursor/windsurf settings${c.reset}
841
+ ${c.dim}// Claude Desktop config${c.reset}
36
842
  {
37
843
  "mcpServers": {
38
844
  "vibecheck": {
39
- "command": "npx",
40
- "args": ["vibecheck", "mcp"]
845
+ "command": "node",
846
+ "args": ["./bin/vibecheck.js", "mcp"],
847
+ "env": {
848
+ "VIBECHECK_API_KEY": "your-key-here"
849
+ }
41
850
  }
42
851
  }
43
852
  }
44
853
 
854
+ ${c.bold}SECURITY${c.reset}
855
+ • Local-only by default (127.0.0.1)
856
+ • API key authentication for paid tiers
857
+ • Rate limiting per client
858
+ • Full audit logging
859
+ • No secret exposure
860
+
861
+ ${c.bold}EXAMPLES${c.reset}
862
+ vibecheck mcp # Start server with defaults
863
+ vibecheck mcp --port 8080 # Custom port
864
+ vibecheck mcp --allow-remote # Allow remote connections
865
+ vibecheck mcp --print-config # Print client config
866
+ vibecheck mcp --status # Check if running
867
+ vibecheck mcp --test # Test connection
868
+
45
869
  ${c.bold}NOTE${c.reset}
46
870
  This command starts a long-running server process.
47
871
  Press Ctrl+C to stop.
48
-
49
- ${c.bold}EXAMPLES${c.reset}
50
- vibecheck mcp # Start MCP server
51
872
  `);
52
873
  }
53
874
 
54
- function runMcp(args) {
55
- // Handle array args from CLI
56
- if (Array.isArray(args)) {
57
- if (args.includes("--help") || args.includes("-h")) {
58
- printHelp();
59
- return 0;
875
+ // Print client configuration
876
+ function printClientConfig(opts) {
877
+ const config = {
878
+ mcpServers: {
879
+ vibecheck: {
880
+ command: "node",
881
+ args: ["./bin/vibecheck.js", "mcp"],
882
+ env: {}
883
+ }
60
884
  }
885
+ };
886
+
887
+ if (opts.apiKey) {
888
+ config.mcpServers.vibecheck.env.VIBECHECK_API_KEY = opts.apiKey;
61
889
  }
62
-
63
- console.log("\n 🔌 Starting vibecheck MCP Server...\n");
64
-
65
- const mcpServer = path.join(__dirname, "../../mcp-server/index.js");
66
-
67
- try {
68
- execSync(`node "${mcpServer}"`, { stdio: "inherit" });
69
- } catch (error) {
70
- console.error(" ❌ MCP Server failed to start");
71
- return 1;
890
+
891
+ if (opts.port && opts.port !== 3000) {
892
+ config.mcpServers.vibecheck.args.push("--port", opts.port.toString());
72
893
  }
73
-
74
- return 0;
894
+
895
+ console.log("Client configuration for Claude Desktop:");
896
+ console.log(JSON.stringify(config, null, 2));
897
+ console.log("\nAdd this to your claude_desktop_config.json file");
75
898
  }
76
899
 
77
- module.exports = { runMcp };
900
+ module.exports = { runMcp, VibecheckMCPServer };