@saiteja1123/mcp-server 1.1.3 → 1.1.5

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/src/server.js CHANGED
@@ -10,29 +10,44 @@ import {
10
10
  } from './rule-engine/index.js';
11
11
  import {
12
12
  DEFAULT_INCLUDE, DEFAULT_EXCLUDE,
13
- inferLang, normalizeRootPath, ensureDirectory,
13
+ normalizeRootPath, ensureDirectory,
14
14
  gatherRepoScan, readGitignoreChecks,
15
15
  detectWorkspacePath, isHomePath,
16
16
  } from './repo-scan.mjs';
17
- import { postRemoteLocalScan } from './api-scan.mjs';
18
- import { validateScanPath, diagnosticLock } from './lock.mjs';
17
+ import { diagnosticLock } from './lock.mjs';
18
+ import { persistRepoScanLog } from './api-scan.mjs';
19
+ import { createBindingGuard, formatGuardError } from './security/pathGuard.js';
20
+ import { registerLocalScanTool } from './tools/localScan.js';
21
+ import { registerScanFileTool } from './tools/scanFile.js';
22
+ import { registerDeepScanTools } from './tools/deepScan.js';
23
+ import { registerProjectTools } from './tools/projects.js';
19
24
 
20
25
  const require = createRequire(import.meta.url);
21
26
  const mcpPkg = require('../package.json');
22
27
 
23
28
  const INSTALL_TOKEN = process.env.VIBESECUR_INSTALL_TOKEN || null;
29
+ const AUTH_TOKEN = process.env.VIBESECUR_AUTH_TOKEN || process.env.VIBESECUR_TOKEN || null;
24
30
  const BOUND_ROOT = process.env.VIBESECUR_BOUND_ROOT
25
31
  ? path.resolve(process.env.VIBESECUR_BOUND_ROOT)
26
32
  : null;
33
+ const API_BASE = process.env.VIBESECUR_API_BASE || process.env.VIBESECUR_API_URL || '';
34
+ const UNIVERSAL_MODE = !!AUTH_TOKEN && !BOUND_ROOT;
27
35
 
28
- if (!INSTALL_TOKEN || !BOUND_ROOT) {
29
- process.stderr.write(
30
- '[vibesecur] WARNING: VIBESECUR_INSTALL_TOKEN and VIBESECUR_BOUND_ROOT are not set.\n' +
31
- '[vibesecur] Run: vibesecur-mcp bind <folder> then vibesecur-mcp config <folder>\n' +
32
- '[vibesecur] Scans will be restricted to process.cwd() as fallback.\n',
33
- );
36
+ function debugLog(message, payload) {
37
+ if (process.env.VIBESECUR_DEBUG !== '1') return;
38
+ process.stderr.write(`[vibesecur-debug] ${message}: ${JSON.stringify(payload)}\n`);
34
39
  }
35
40
 
41
+ const guardPath = createBindingGuard({
42
+ boundRoot: BOUND_ROOT,
43
+ installToken: INSTALL_TOKEN,
44
+ authToken: AUTH_TOKEN,
45
+ apiBase: API_BASE,
46
+ universalMode: UNIVERSAL_MODE,
47
+ normalizePath: normalizeRootPath,
48
+ debugLog,
49
+ });
50
+
36
51
  const server = new McpServer({
37
52
  name: 'vibesecur-mcp-server',
38
53
  version: mcpPkg.version || '2.0.0',
@@ -40,52 +55,7 @@ const server = new McpServer({
40
55
 
41
56
  const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
42
57
 
43
- async function guardPath(requestedPath) {
44
- const target = normalizeRootPath(requestedPath);
45
- if (!BOUND_ROOT) {
46
- const cwd = path.resolve(process.cwd());
47
- if (!target.startsWith(cwd + path.sep) && target !== cwd) {
48
- return {
49
- ok: false,
50
- httpStatus: 403,
51
- code: 'NO_LOCK_OUT_OF_CWD',
52
- message:
53
- `No lock configured and "${target}" is outside process.cwd() "${cwd}". ` +
54
- 'Run vibesecur-mcp bind <folder> and add VIBESECUR_BOUND_ROOT to your MCP config.',
55
- };
56
- }
57
- return { ok: true, resolvedRoot: target };
58
- }
59
- if (!target.startsWith(BOUND_ROOT + path.sep) && target !== BOUND_ROOT) {
60
- return {
61
- ok: false,
62
- httpStatus: 403,
63
- code: 'OUT_OF_FOLDER',
64
- message:
65
- `Path "${target}" is outside the locked project folder "${BOUND_ROOT}". ` +
66
- 'Vibesecur MCP is bound to one folder per install. ' +
67
- 'To scan a different folder, run "vibesecur-mcp rebind <new-folder>".',
68
- rebindHint: `vibesecur-mcp rebind ${target}`,
69
- };
70
- }
71
- const result = await validateScanPath(target, INSTALL_TOKEN);
72
- if (!result.ok) return result;
73
- return { ok: true, resolvedRoot: target, lock: result.lock };
74
- }
75
-
76
- function guardError(guard) {
77
- const status = guard.httpStatus ? ` (${guard.httpStatus})` : '';
78
- const text = JSON.stringify({
79
- error: guard.code || 'SCAN_BLOCKED',
80
- message: guard.message,
81
- rebindHint: guard.rebindHint || null,
82
- docs: 'https://vibesecur.com/docs/mcp-setup',
83
- }, null, 2);
84
- return {
85
- content: [{ type: 'text', text: `Security Lock Error${status}:\n${text}` }],
86
- isError: true,
87
- };
88
- }
58
+ const guardError = formatGuardError;
89
59
 
90
60
  function buildScanMeta(resolvedRoot, includeGlobs, excludeGlobs, maxFiles, matchedLen, scannedLen) {
91
61
  return {
@@ -96,7 +66,8 @@ function buildScanMeta(resolvedRoot, includeGlobs, excludeGlobs, maxFiles, match
96
66
  matchedFiles: matchedLen,
97
67
  scannedFiles: scannedLen,
98
68
  cappedByMaxFiles: matchedLen > maxFiles,
99
- boundRoot: BOUND_ROOT || 'unconfigured',
69
+ boundRoot: BOUND_ROOT || (UNIVERSAL_MODE ? 'account-wide' : 'unconfigured'),
70
+ mode: UNIVERSAL_MODE ? 'universal' : (BOUND_ROOT ? 'single-folder' : 'unconfigured'),
100
71
  };
101
72
  }
102
73
 
@@ -108,9 +79,40 @@ function humanRepoSummary(meta, agg) {
108
79
  return parts.join(' ');
109
80
  }
110
81
 
82
+ async function syncRepoScanToDashboard({ aggregate, findings, projectRoot, guard }) {
83
+ const installToken = guard.installToken || INSTALL_TOKEN;
84
+ const lockedRootHash = guard.lockedRootHash || guard.lock?.lockedRootHash || guard.lock?.rootHash;
85
+ if (!installToken || !lockedRootHash) {
86
+ return { ok: false, reason: 'missing install token or locked root hash' };
87
+ }
88
+ try {
89
+ const logRes = await persistRepoScanLog({
90
+ aggregate,
91
+ findings,
92
+ projectRoot,
93
+ installToken,
94
+ lockedRootHash,
95
+ });
96
+ if (logRes?.ok && logRes.json?.success) {
97
+ return { ok: true, scanId: logRes.json?.data?.scanId || null };
98
+ }
99
+ return {
100
+ ok: false,
101
+ reason: logRes?.reason || logRes?.error || logRes?.json?.error || `status ${logRes?.status || 'unknown'}`,
102
+ };
103
+ } catch (err) {
104
+ process.stderr.write(`[vibesecur] scan log failed (non-fatal): ${err.message}\n`);
105
+ return { ok: false, reason: err.message };
106
+ }
107
+ }
108
+
111
109
  function flattenFindings(fileResults) {
112
110
  return fileResults.flatMap((fr) =>
113
- (fr.result.findings || []).map((f) => ({ ...f, filePath: fr.filePath })),
111
+ (fr.result.findings || []).map((f) => ({
112
+ ...f,
113
+ filePath: fr.filePath,
114
+ snippetPreview: f.snippetPreview || f.snippet || '',
115
+ })),
114
116
  );
115
117
  }
116
118
 
@@ -124,10 +126,11 @@ function pickTopFindings(fileResults, n) {
124
126
  return flat.slice(0, n).map((f) => ({
125
127
  filePath: f.filePath,
126
128
  lineNumber: f.lineNumber,
129
+ endLineNumber: f.endLineNumber || f.lineNumber,
127
130
  ruleId: f.ruleId,
128
131
  ruleName: f.ruleName,
129
132
  severity: f.severity,
130
- snippetPreview: (f.snippet || '').slice(0, 120),
133
+ snippetPreview: (f.snippetPreview || '').slice(0, 120),
131
134
  }));
132
135
  }
133
136
 
@@ -144,9 +147,13 @@ server.registerTool('health', {
144
147
  ok: true,
145
148
  server: { name: 'vibesecur-mcp-server', version: mcpPkg.version },
146
149
  lock: {
147
- configured: !!(INSTALL_TOKEN && BOUND_ROOT),
150
+ configured: UNIVERSAL_MODE || !!(INSTALL_TOKEN && BOUND_ROOT),
151
+ mode: UNIVERSAL_MODE ? 'universal' : 'single-folder',
148
152
  boundRoot: BOUND_ROOT || null,
153
+ accountWide: UNIVERSAL_MODE,
149
154
  healthy: diag.healthy,
155
+ runtimeCompatible: diag.runtimeCompatible || false,
156
+ source: diag.source || null,
150
157
  issues: diag.issues || [],
151
158
  },
152
159
  rules: {
@@ -165,9 +172,11 @@ server.registerTool('health', {
165
172
 
166
173
  if (detail === 'full') {
167
174
  payload.envHints = {
175
+ VIBESECUR_AUTH_TOKEN: AUTH_TOKEN ? '***set***' : 'NOT SET',
168
176
  VIBESECUR_INSTALL_TOKEN: INSTALL_TOKEN ? '***set***' : 'NOT SET',
169
177
  VIBESECUR_BOUND_ROOT: BOUND_ROOT || 'NOT SET',
170
- VIBESECUR_API_BASE: process.env.VIBESECUR_API_BASE || 'NOT SET',
178
+ VIBESECUR_API_BASE: API_BASE || 'NOT SET',
179
+ universalMode: UNIVERSAL_MODE,
171
180
  CURSOR_WORKSPACE_PATH: process.env.CURSOR_WORKSPACE_PATH ?? null,
172
181
  WORKSPACE_PATH: process.env.WORKSPACE_PATH ?? null,
173
182
  };
@@ -178,7 +187,9 @@ server.registerTool('health', {
178
187
  ok: true,
179
188
  version: mcpPkg.version,
180
189
  lockConfigured: payload.lock.configured,
190
+ lockMode: payload.lock.mode,
181
191
  lockHealthy: payload.lock.healthy,
192
+ runtimeCompatible: payload.lock.runtimeCompatible,
182
193
  boundRoot: BOUND_ROOT || null,
183
194
  totalRules: JS_RULES.length + PY_RULES.length,
184
195
  processCwd: cwd,
@@ -218,92 +229,29 @@ server.registerTool('installDiagnostic', {
218
229
  };
219
230
  });
220
231
 
221
- server.registerTool('localScan', {
222
- title: 'Local Security Scan',
223
- description: 'Run Vibesecur rule-engine on code. Restricted to bound project folder.',
224
- inputSchema: {
225
- code: z.string().min(1).max(50000).describe('Source code to scan'),
226
- lang: z.enum(['js', 'ts', 'py', 'json', 'auto']).default('auto'),
227
- projectRoot: z.string().default('.').describe('Must be inside bound folder'),
228
- },
229
- }, async ({ code, lang = 'auto', projectRoot = '.' }) => {
230
- const guard = await guardPath(projectRoot);
231
- if (!guard.ok) return guardError(guard);
232
- const remote = await postRemoteLocalScan({
233
- code,
234
- lang,
235
- projectRoot: guard.resolvedRoot,
236
- platform: 'mcp',
237
- token: INSTALL_TOKEN,
238
- });
239
- if (!remote.skipped && remote.status === 402) {
240
- return {
241
- content: [{ type: 'text', text: JSON.stringify({ ...remote.json, upgradeUrl: 'https://vibesecur.com/#pricing' }, null, 2) }],
242
- isError: true,
243
- };
244
- }
245
- if (!remote.skipped && remote.ok && remote.json?.success && remote.json?.data) {
246
- const data = remote.json.data;
247
- const humanSummary = `${data.verdict || ''} Score ${data.score} (${data.grade}) - ${(data.findings || []).length} finding(s).`;
248
- const enriched = { ...data, humanSummary, engineVersion: mcpPkg.version, quota: remote.json.quota };
249
- return { content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }], structuredContent: enriched };
250
- }
251
- const result = localScan(code, lang);
252
- const humanSummary = `${result.verdict} Score ${result.score} (${result.grade}) - ${result.findings.length} finding(s).`;
253
- const enriched = { ...result, humanSummary, engineVersion: mcpPkg.version, mode: 'offline' };
254
- return { content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }], structuredContent: enriched };
232
+ registerProjectTools(server, {
233
+ authToken: AUTH_TOKEN,
234
+ apiBase: API_BASE,
235
+ normalizeRootPath,
255
236
  });
256
237
 
257
- server.registerTool('scanFile', {
258
- title: 'Scan File',
259
- description: 'Scan a single file. Must be inside the bound project folder.',
260
- inputSchema: {
261
- filePath: z.string().min(1).describe('Absolute or relative file path (must be inside bound folder)'),
262
- lang: z.enum(['js', 'ts', 'py', 'json', 'auto']).default('auto'),
263
- },
264
- }, async ({ filePath, lang = 'auto' }) => {
265
- try {
266
- const resolvedPath = path.resolve(filePath);
267
- const guard = await guardPath(path.dirname(resolvedPath));
268
- if (!guard.ok) return guardError(guard);
269
- const code = await fs.readFile(resolvedPath, 'utf8');
270
- const useLang = lang === 'auto' ? inferLang(resolvedPath) : lang;
271
- const remote = await postRemoteLocalScan({
272
- code,
273
- lang: useLang,
274
- projectRoot: guard.resolvedRoot,
275
- platform: 'mcp',
276
- token: INSTALL_TOKEN,
277
- });
278
- let result;
279
- if (!remote.skipped && remote.ok && remote.json?.success && remote.json?.data) {
280
- result = remote.json.data;
281
- } else if (!remote.skipped && remote.status === 402) {
282
- return { content: [{ type: 'text', text: JSON.stringify(remote.json, null, 2) }], isError: true };
283
- } else {
284
- result = localScan(code, useLang);
285
- }
286
- const findings = result.findings || [];
287
- const bySev = findings.reduce((a, f) => {
288
- a[f.severity] = (a[f.severity] || 0) + 1;
289
- return a;
290
- }, { critical: 0, high: 0, medium: 0, low: 0 });
291
- const humanSummary = `File "${resolvedPath}": score ${result.score} (${result.grade}), ${findings.length} issue(s).`;
292
- const body = {
293
- humanSummary,
294
- filePath: resolvedPath,
295
- lang: useLang,
296
- score: result.score,
297
- grade: result.grade,
298
- findings: findings.length,
299
- bySeverity: bySev,
300
- checklist: result.checklist,
301
- result,
302
- };
303
- return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], structuredContent: body };
304
- } catch (e) {
305
- return { content: [{ type: 'text', text: `scanFile failed: ${e.message}` }], isError: true };
306
- }
238
+ registerLocalScanTool(server, {
239
+ guardPath,
240
+ guardError,
241
+ installToken: INSTALL_TOKEN,
242
+ engineVersion: mcpPkg.version,
243
+ });
244
+
245
+ registerScanFileTool(server, {
246
+ guardPath,
247
+ guardError,
248
+ installToken: INSTALL_TOKEN,
249
+ engineVersion: mcpPkg.version,
250
+ });
251
+
252
+ registerDeepScanTools(server, {
253
+ guardPath,
254
+ guardError,
307
255
  });
308
256
 
309
257
  server.registerTool('scanRepo', {
@@ -323,7 +271,24 @@ server.registerTool('scanRepo', {
323
271
  await ensureDirectory(resolvedRoot);
324
272
  const { matchedFiles, limitedFiles, fileResults, aggregate, topRiskFiles } =
325
273
  await gatherRepoScan(resolvedRoot, includeGlobs, excludeGlobs, maxFiles);
274
+ const allFindings = flattenFindings(fileResults).map((f) => ({
275
+ filePath: f.filePath,
276
+ lineNumber: f.lineNumber,
277
+ endLineNumber: f.endLineNumber || f.lineNumber,
278
+ ruleId: f.ruleId,
279
+ ruleName: f.ruleName,
280
+ severity: f.severity,
281
+ category: f.category,
282
+ snippetPreview: (f.snippetPreview || '').slice(0, 120),
283
+ fix: f.fix,
284
+ }));
326
285
  const meta = buildScanMeta(resolvedRoot, includeGlobs, excludeGlobs, maxFiles, matchedFiles.length, limitedFiles.length);
286
+ const logged = await syncRepoScanToDashboard({
287
+ aggregate,
288
+ findings: allFindings,
289
+ projectRoot: resolvedRoot,
290
+ guard,
291
+ });
327
292
  const body = {
328
293
  meta,
329
294
  humanSummary: humanRepoSummary(meta, aggregate),
@@ -334,6 +299,8 @@ server.registerTool('scanRepo', {
334
299
  summary: aggregate.summary,
335
300
  checklist: aggregate.checklist,
336
301
  topRiskFiles,
302
+ allFindings,
303
+ logged,
337
304
  };
338
305
  return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], structuredContent: { ...body, fileResults } };
339
306
  } catch (e) {
@@ -350,8 +317,16 @@ server.registerTool('scanSummary', {
350
317
  excludeGlobs: z.array(z.string()).default(DEFAULT_EXCLUDE),
351
318
  maxFiles: z.number().int().min(1).max(5000).default(200),
352
319
  topFindings: z.number().int().min(1).max(50).default(20),
320
+ maxFindings: z.number().int().min(20).max(500).default(200),
353
321
  },
354
- }, async ({ rootPath, includeGlobs = DEFAULT_INCLUDE, excludeGlobs = DEFAULT_EXCLUDE, maxFiles = 200, topFindings = 20 }) => {
322
+ }, async ({
323
+ rootPath,
324
+ includeGlobs = DEFAULT_INCLUDE,
325
+ excludeGlobs = DEFAULT_EXCLUDE,
326
+ maxFiles = 200,
327
+ topFindings = 20,
328
+ maxFindings = 200,
329
+ }) => {
355
330
  try {
356
331
  const guard = await guardPath(rootPath);
357
332
  if (!guard.ok) return guardError(guard);
@@ -361,12 +336,24 @@ server.registerTool('scanSummary', {
361
336
  await gatherRepoScan(resolvedRoot, includeGlobs, excludeGlobs, maxFiles);
362
337
  const meta = buildScanMeta(resolvedRoot, includeGlobs, excludeGlobs, maxFiles, matchedFiles.length, limitedFiles.length);
363
338
  const top = pickTopFindings(fileResults, topFindings);
339
+ const allFindings = flattenFindings(fileResults).slice(0, maxFindings).map((f) => ({
340
+ filePath: f.filePath,
341
+ lineNumber: f.lineNumber,
342
+ endLineNumber: f.endLineNumber || f.lineNumber,
343
+ ruleId: f.ruleId,
344
+ ruleName: f.ruleName,
345
+ severity: f.severity,
346
+ category: f.category,
347
+ snippetPreview: (f.snippetPreview || '').slice(0, 120),
348
+ fix: f.fix,
349
+ }));
364
350
  const payload = {
365
351
  meta,
366
352
  humanSummary: humanRepoSummary(meta, aggregate),
367
353
  summary: aggregate.summary,
368
354
  checklist: { passed: aggregate.checklist.filter((c) => c.pass).length, total: aggregate.checklist.length },
369
355
  topFindings: top,
356
+ allFindings,
370
357
  };
371
358
  return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }], structuredContent: payload };
372
359
  } catch (e) {
@@ -396,7 +383,24 @@ server.registerTool('scanCurrentWorkspace', {
396
383
  await ensureDirectory(guard.resolvedRoot);
397
384
  const { matchedFiles, limitedFiles, fileResults, aggregate, topRiskFiles } =
398
385
  await gatherRepoScan(guard.resolvedRoot, includeGlobs, excludeGlobs, maxFiles);
386
+ const allFindings = flattenFindings(fileResults).map((f) => ({
387
+ filePath: f.filePath,
388
+ lineNumber: f.lineNumber,
389
+ endLineNumber: f.endLineNumber || f.lineNumber,
390
+ ruleId: f.ruleId,
391
+ ruleName: f.ruleName,
392
+ severity: f.severity,
393
+ category: f.category,
394
+ snippetPreview: (f.snippetPreview || '').slice(0, 120),
395
+ fix: f.fix,
396
+ }));
399
397
  const meta = buildScanMeta(guard.resolvedRoot, includeGlobs, excludeGlobs, maxFiles, matchedFiles.length, limitedFiles.length);
398
+ const logged = await syncRepoScanToDashboard({
399
+ aggregate,
400
+ findings: allFindings,
401
+ projectRoot: guard.resolvedRoot,
402
+ guard,
403
+ });
400
404
  const body = {
401
405
  meta,
402
406
  humanSummary: humanRepoSummary(meta, aggregate),
@@ -407,6 +411,8 @@ server.registerTool('scanCurrentWorkspace', {
407
411
  summary: aggregate.summary,
408
412
  checklist: aggregate.checklist,
409
413
  topRiskFiles,
414
+ allFindings,
415
+ logged,
410
416
  };
411
417
  return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], structuredContent: { ...body, fileResults } };
412
418
  } catch (e) {
@@ -476,6 +482,16 @@ server.registerTool('buildClaudePrompt', {
476
482
  });
477
483
 
478
484
  async function main() {
485
+ if (!INSTALL_TOKEN || !BOUND_ROOT) {
486
+ throw new Error(
487
+ 'VIBESECUR_INSTALL_TOKEN and VIBESECUR_BOUND_ROOT are required. ' +
488
+ 'Run "vibesecur-mcp bind <folder>" and update your MCP config env.',
489
+ );
490
+ }
491
+ const startupGuard = await guardPath(BOUND_ROOT);
492
+ if (!startupGuard.ok) {
493
+ throw new Error(`${startupGuard.code}: ${startupGuard.message}`);
494
+ }
479
495
  const transport = new StdioServerTransport();
480
496
  await server.connect(transport);
481
497
  process.stderr.write(