@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.
- package/bin/.generated +25 -0
- package/bin/registry.js +105 -0
- package/bin/runners/lib/cli-output.js +368 -0
- package/bin/runners/lib/entitlements-v2.js +26 -30
- package/bin/runners/lib/receipts.js +179 -0
- package/bin/runners/lib/upsell.js +510 -0
- package/bin/runners/lib/usage.js +153 -0
- package/bin/runners/runBadge.js +31 -4
- package/bin/runners/runDoctor.js +72 -3
- package/bin/runners/runFix.js +13 -0
- package/bin/runners/runGraph.js +14 -0
- package/bin/runners/runMcp.js +865 -42
- package/bin/runners/runPermissions.js +14 -0
- package/bin/runners/runPreflight.js +553 -0
- package/bin/runners/runProve.js +100 -41
- package/bin/runners/runShip.js +98 -19
- package/bin/runners/runVerify.js +272 -0
- package/bin/vibecheck.js +108 -94
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
package/bin/runners/runMcp.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
--
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
•
|
|
28
|
-
•
|
|
29
|
-
•
|
|
30
|
-
•
|
|
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}//
|
|
841
|
+
${c.dim}// Claude Desktop config${c.reset}
|
|
36
842
|
{
|
|
37
843
|
"mcpServers": {
|
|
38
844
|
"vibecheck": {
|
|
39
|
-
"command": "
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|