@saiteja1123/mcp-server 1.1.4 → 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,6 +79,33 @@ 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
111
  (fr.result.findings || []).map((f) => ({
@@ -149,9 +147,13 @@ server.registerTool('health', {
149
147
  ok: true,
150
148
  server: { name: 'vibesecur-mcp-server', version: mcpPkg.version },
151
149
  lock: {
152
- configured: !!(INSTALL_TOKEN && BOUND_ROOT),
150
+ configured: UNIVERSAL_MODE || !!(INSTALL_TOKEN && BOUND_ROOT),
151
+ mode: UNIVERSAL_MODE ? 'universal' : 'single-folder',
153
152
  boundRoot: BOUND_ROOT || null,
153
+ accountWide: UNIVERSAL_MODE,
154
154
  healthy: diag.healthy,
155
+ runtimeCompatible: diag.runtimeCompatible || false,
156
+ source: diag.source || null,
155
157
  issues: diag.issues || [],
156
158
  },
157
159
  rules: {
@@ -170,9 +172,11 @@ server.registerTool('health', {
170
172
 
171
173
  if (detail === 'full') {
172
174
  payload.envHints = {
175
+ VIBESECUR_AUTH_TOKEN: AUTH_TOKEN ? '***set***' : 'NOT SET',
173
176
  VIBESECUR_INSTALL_TOKEN: INSTALL_TOKEN ? '***set***' : 'NOT SET',
174
177
  VIBESECUR_BOUND_ROOT: BOUND_ROOT || 'NOT SET',
175
- VIBESECUR_API_BASE: process.env.VIBESECUR_API_BASE || 'NOT SET',
178
+ VIBESECUR_API_BASE: API_BASE || 'NOT SET',
179
+ universalMode: UNIVERSAL_MODE,
176
180
  CURSOR_WORKSPACE_PATH: process.env.CURSOR_WORKSPACE_PATH ?? null,
177
181
  WORKSPACE_PATH: process.env.WORKSPACE_PATH ?? null,
178
182
  };
@@ -183,7 +187,9 @@ server.registerTool('health', {
183
187
  ok: true,
184
188
  version: mcpPkg.version,
185
189
  lockConfigured: payload.lock.configured,
190
+ lockMode: payload.lock.mode,
186
191
  lockHealthy: payload.lock.healthy,
192
+ runtimeCompatible: payload.lock.runtimeCompatible,
187
193
  boundRoot: BOUND_ROOT || null,
188
194
  totalRules: JS_RULES.length + PY_RULES.length,
189
195
  processCwd: cwd,
@@ -223,100 +229,29 @@ server.registerTool('installDiagnostic', {
223
229
  };
224
230
  });
225
231
 
226
- server.registerTool('localScan', {
227
- title: 'Local Security Scan',
228
- description: 'Run Vibesecur rule-engine on code. Restricted to bound project folder.',
229
- inputSchema: {
230
- code: z.string().min(1).max(50000).describe('Source code to scan'),
231
- lang: z.enum(['js', 'ts', 'py', 'json', 'auto']).default('auto'),
232
- projectRoot: z.string().default('.').describe('Must be inside bound folder'),
233
- },
234
- }, async ({ code, lang = 'auto', projectRoot = '.' }) => {
235
- const guard = await guardPath(projectRoot);
236
- if (!guard.ok) return guardError(guard);
237
- const remote = await postRemoteLocalScan({
238
- code,
239
- lang,
240
- projectRoot: guard.resolvedRoot,
241
- platform: 'mcp',
242
- token: INSTALL_TOKEN,
243
- });
244
- if (!remote.skipped && remote.status === 402) {
245
- return {
246
- content: [{ type: 'text', text: JSON.stringify({ ...remote.json, upgradeUrl: 'https://vibesecur.com/#pricing' }, null, 2) }],
247
- isError: true,
248
- };
249
- }
250
- if (!remote.skipped && remote.ok && remote.json?.success && remote.json?.data) {
251
- const data = remote.json.data;
252
- const humanSummary = `${data.verdict || ''} Score ${data.score} (${data.grade}) - ${(data.findings || []).length} finding(s).`;
253
- const enriched = { ...data, humanSummary, engineVersion: mcpPkg.version, quota: remote.json.quota };
254
- return { content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }], structuredContent: enriched };
255
- }
256
- const result = localScan(code, lang);
257
- const humanSummary = `${result.verdict} Score ${result.score} (${result.grade}) - ${result.findings.length} finding(s).`;
258
- const enriched = { ...result, humanSummary, engineVersion: mcpPkg.version, mode: 'offline' };
259
- 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,
260
236
  });
261
237
 
262
- server.registerTool('scanFile', {
263
- title: 'Scan File',
264
- description: 'Scan a single file. Must be inside the bound project folder.',
265
- inputSchema: {
266
- filePath: z.string().min(1).describe('Absolute or relative file path (must be inside bound folder)'),
267
- lang: z.enum(['js', 'ts', 'py', 'json', 'auto']).default('auto'),
268
- },
269
- }, async ({ filePath, lang = 'auto' }) => {
270
- try {
271
- const resolvedPath = path.resolve(filePath);
272
- const guard = await guardPath(path.dirname(resolvedPath));
273
- if (!guard.ok) return guardError(guard);
274
- const code = await fs.readFile(resolvedPath, 'utf8');
275
- const useLang = lang === 'auto' ? inferLang(resolvedPath) : lang;
276
- const remote = await postRemoteLocalScan({
277
- code,
278
- lang: useLang,
279
- projectRoot: guard.resolvedRoot,
280
- platform: 'mcp',
281
- token: INSTALL_TOKEN,
282
- });
283
- let result;
284
- if (!remote.skipped && remote.ok && remote.json?.success && remote.json?.data) {
285
- result = remote.json.data;
286
- } else if (!remote.skipped && remote.status === 402) {
287
- return { content: [{ type: 'text', text: JSON.stringify(remote.json, null, 2) }], isError: true };
288
- } else {
289
- result = localScan(code, useLang);
290
- }
291
- const findings = result.findings || [];
292
- const findingsWithLocation = findings.map((f) => ({
293
- ...f,
294
- filePath: resolvedPath,
295
- snippetPreview: f.snippetPreview || f.snippet || '',
296
- }));
297
- const bySev = findings.reduce((a, f) => {
298
- a[f.severity] = (a[f.severity] || 0) + 1;
299
- return a;
300
- }, { critical: 0, high: 0, medium: 0, low: 0 });
301
- const humanSummary = `File "${resolvedPath}": score ${result.score} (${result.grade}), ${findings.length} issue(s).`;
302
- const body = {
303
- humanSummary,
304
- filePath: resolvedPath,
305
- lang: useLang,
306
- score: result.score,
307
- grade: result.grade,
308
- findings: findingsWithLocation.length,
309
- bySeverity: bySev,
310
- checklist: result.checklist,
311
- result: {
312
- ...result,
313
- findings: findingsWithLocation,
314
- },
315
- };
316
- return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], structuredContent: body };
317
- } catch (e) {
318
- return { content: [{ type: 'text', text: `scanFile failed: ${e.message}` }], isError: true };
319
- }
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,
320
255
  });
321
256
 
322
257
  server.registerTool('scanRepo', {
@@ -348,6 +283,12 @@ server.registerTool('scanRepo', {
348
283
  fix: f.fix,
349
284
  }));
350
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
+ });
351
292
  const body = {
352
293
  meta,
353
294
  humanSummary: humanRepoSummary(meta, aggregate),
@@ -359,6 +300,7 @@ server.registerTool('scanRepo', {
359
300
  checklist: aggregate.checklist,
360
301
  topRiskFiles,
361
302
  allFindings,
303
+ logged,
362
304
  };
363
305
  return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], structuredContent: { ...body, fileResults } };
364
306
  } catch (e) {
@@ -453,6 +395,12 @@ server.registerTool('scanCurrentWorkspace', {
453
395
  fix: f.fix,
454
396
  }));
455
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
+ });
456
404
  const body = {
457
405
  meta,
458
406
  humanSummary: humanRepoSummary(meta, aggregate),
@@ -464,6 +412,7 @@ server.registerTool('scanCurrentWorkspace', {
464
412
  checklist: aggregate.checklist,
465
413
  topRiskFiles,
466
414
  allFindings,
415
+ logged,
467
416
  };
468
417
  return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], structuredContent: { ...body, fileResults } };
469
418
  } catch (e) {
@@ -533,6 +482,16 @@ server.registerTool('buildClaudePrompt', {
533
482
  });
534
483
 
535
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
+ }
536
495
  const transport = new StdioServerTransport();
537
496
  await server.connect(transport);
538
497
  process.stderr.write(
@@ -0,0 +1,286 @@
1
+ import * as z from 'zod';
2
+ import {
3
+ DeepScanRuntime,
4
+ LocalDeepScanStore,
5
+ StepperRegistry,
6
+ buildDeepScanStatus,
7
+ createSampleStepperRegistry,
8
+ appendAcceptedRiskRecord,
9
+ } from '../deep-scan/index.js';
10
+
11
+ const buildRuntime = (rootPath) => {
12
+ const registry = createSampleStepperRegistry(new StepperRegistry());
13
+ const store = new LocalDeepScanStore({ rootPath });
14
+ return new DeepScanRuntime({ registry, store });
15
+ };
16
+
17
+ const localDeepScanProfile = ({
18
+ requireHumanApproval = false,
19
+ previousDeterministicScanRef = null,
20
+ previousFailureStateRef = null,
21
+ } = {}) => ({
22
+ id: 'local-deep-scan-v1',
23
+ version: '1.0.0',
24
+ checkpoint: true,
25
+ ralphLoop: {
26
+ retry: {
27
+ maxAttempts: 3,
28
+ escalateAt: 2,
29
+ trackSeverities: ['critical', 'high'],
30
+ },
31
+ },
32
+ steppers: [
33
+ ...(requireHumanApproval ? [{ id: 'sample.needsHuman', required: true }] : []),
34
+ { id: 'project.map', required: true },
35
+ { id: 'rules.scan', required: true },
36
+ { id: 'tests.plan', required: true },
37
+ { id: 'ralph.tasks', required: true },
38
+ { id: 'ralph.compare', required: true, config: previousDeterministicScanRef ? { previousDeterministicScanRef } : {} },
39
+ { id: 'ralph.accept', required: true },
40
+ { id: 'ralph.track', required: true, config: previousFailureStateRef ? { previousFailureStateRef } : {} },
41
+ ],
42
+ });
43
+
44
+ const artifactRefFromRun = (artifacts, { stepperId, type, fallbackId }) => {
45
+ const ref = artifacts[stepperId]
46
+ || Object.values(artifacts).find((item) => item?.type === type);
47
+ if (!ref?.uri || !ref?.hash) return null;
48
+ return {
49
+ id: ref.id || fallbackId,
50
+ type,
51
+ storage: ref.storage || 'local',
52
+ uri: ref.uri,
53
+ hash: ref.hash,
54
+ };
55
+ };
56
+
57
+ const resolvePreviousRunRefs = async ({ rootPath, previousRunId, previousDeterministicScanRef }) => {
58
+ if (previousDeterministicScanRef?.uri && previousDeterministicScanRef?.hash) {
59
+ return {
60
+ previousDeterministicScanRef: {
61
+ id: previousDeterministicScanRef.id || 'artifact-previous-deterministic-scan',
62
+ type: 'deterministic_scan',
63
+ storage: previousDeterministicScanRef.storage || 'local',
64
+ uri: previousDeterministicScanRef.uri,
65
+ hash: previousDeterministicScanRef.hash,
66
+ },
67
+ previousFailureStateRef: null,
68
+ };
69
+ }
70
+ if (!previousRunId) {
71
+ return { previousDeterministicScanRef: null, previousFailureStateRef: null };
72
+ }
73
+ const store = new LocalDeepScanStore({ rootPath });
74
+ const previousState = await store.getRun(previousRunId);
75
+ const artifacts = previousState?.artifacts || {};
76
+ const scanRef = artifactRefFromRun(artifacts, {
77
+ stepperId: 'rules.scan',
78
+ type: 'deterministic_scan',
79
+ fallbackId: `artifact-${previousRunId}-deterministic-scan`,
80
+ });
81
+ if (!scanRef) {
82
+ throw new Error(`Run ${previousRunId} does not contain a deterministic_scan artifact with uri/hash.`);
83
+ }
84
+ return {
85
+ previousDeterministicScanRef: scanRef,
86
+ previousFailureStateRef: artifactRefFromRun(artifacts, {
87
+ stepperId: 'ralph.track',
88
+ type: 'ralph_failure_state',
89
+ fallbackId: `artifact-${previousRunId}-ralph-failure-state`,
90
+ }),
91
+ };
92
+ };
93
+
94
+ const toolResult = (payload) => ({
95
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
96
+ structuredContent: payload,
97
+ });
98
+
99
+ export function registerDeepScanTools(server, { guardPath, guardError }) {
100
+ server.registerTool('deepScanStart', {
101
+ title: 'Start Deep Scan Project Map, Deterministic Scan, Test Plan, Ralph Tasks, Comparison, And Failure Tracking',
102
+ description: 'Create and run local-first Deep Scan Project Map, deterministic scan, metadata-only Security Test Plan, source-safe Ralph Agent Fix Tasks, optional rerun comparison, and repeated failure tracking with checkpoints, receipts, and artifact references.',
103
+ inputSchema: {
104
+ rootPath: z.string().default('.').describe('Project root, inside the bound folder'),
105
+ projectId: z.string().min(1).max(160).default('local-project'),
106
+ projectHash: z.string().regex(/^[a-f0-9]{64}$/i).optional(),
107
+ requireHumanApproval: z.boolean().default(false),
108
+ previousRunId: z.string().min(1).max(160).optional(),
109
+ previousDeterministicScanRef: z.object({
110
+ id: z.string().min(1).max(160).optional(),
111
+ storage: z.string().min(1).max(40).optional(),
112
+ uri: z.string().min(1).max(1000),
113
+ hash: z.string().regex(/^[a-f0-9]{64}$/i),
114
+ }).optional(),
115
+ },
116
+ }, async ({
117
+ rootPath = '.',
118
+ projectId = 'local-project',
119
+ projectHash = undefined,
120
+ requireHumanApproval = false,
121
+ previousRunId = undefined,
122
+ previousDeterministicScanRef = undefined,
123
+ }) => {
124
+ const guard = await guardPath(rootPath);
125
+ if (!guard.ok) return guardError(guard);
126
+ const runtime = buildRuntime(guard.resolvedRoot);
127
+ const { previousDeterministicScanRef: baselineRef, previousFailureStateRef } = await resolvePreviousRunRefs({
128
+ rootPath: guard.resolvedRoot,
129
+ previousRunId,
130
+ previousDeterministicScanRef,
131
+ });
132
+ const run = await runtime.createRun({
133
+ projectId,
134
+ projectHash: projectHash || null,
135
+ graphProfile: localDeepScanProfile({
136
+ requireHumanApproval,
137
+ previousDeterministicScanRef: baselineRef,
138
+ previousFailureStateRef,
139
+ }),
140
+ metadata: {
141
+ runtimeBoundary: 'mcp-local',
142
+ artifactStorage: 'refs-only',
143
+ trustModel: 'No raw source is stored in Deep Scan checkpoints.',
144
+ },
145
+ });
146
+ const state = await runtime.startRun(run.runId);
147
+ return toolResult(buildDeepScanStatus(state));
148
+ });
149
+
150
+ server.registerTool('deepScanStatus', {
151
+ title: 'Deep Scan Status',
152
+ description: 'Inspect a local-first Deep Scan run without exposing raw source.',
153
+ inputSchema: {
154
+ rootPath: z.string().default('.').describe('Project root, inside the bound folder'),
155
+ runId: z.string().min(1).max(160),
156
+ },
157
+ }, async ({ rootPath = '.', runId }) => {
158
+ const guard = await guardPath(rootPath);
159
+ if (!guard.ok) return guardError(guard);
160
+ const store = new LocalDeepScanStore({ rootPath: guard.resolvedRoot });
161
+ const state = await store.getRun(runId);
162
+ return toolResult(buildDeepScanStatus(state));
163
+ });
164
+
165
+ server.registerTool('deepScanApprove', {
166
+ title: 'Approve Deep Scan Step',
167
+ description: 'Record an auditable human approval or denial for a paused local Deep Scan run.',
168
+ inputSchema: {
169
+ rootPath: z.string().default('.').describe('Project root, inside the bound folder'),
170
+ runId: z.string().min(1).max(160),
171
+ requirementId: z.string().min(1).max(160),
172
+ decision: z.enum(['approved', 'denied']),
173
+ approvedBy: z.string().min(1).max(255),
174
+ reason: z.string().min(1).max(1000),
175
+ relatedFindingId: z.string().max(120).optional(),
176
+ relatedArtifactId: z.string().max(120).optional(),
177
+ metadata: z.record(z.string(), z.unknown()).default({}),
178
+ },
179
+ }, async ({
180
+ rootPath = '.',
181
+ runId,
182
+ requirementId,
183
+ decision,
184
+ approvedBy,
185
+ reason,
186
+ relatedFindingId,
187
+ relatedArtifactId,
188
+ metadata = {},
189
+ }) => {
190
+ const guard = await guardPath(rootPath);
191
+ if (!guard.ok) return guardError(guard);
192
+ const runtime = buildRuntime(guard.resolvedRoot);
193
+ const approval = await runtime.recordApproval({
194
+ runId,
195
+ requirementId,
196
+ decision,
197
+ approvedBy,
198
+ reason,
199
+ relatedFindingId,
200
+ relatedArtifactId,
201
+ metadata,
202
+ });
203
+ const store = new LocalDeepScanStore({ rootPath: guard.resolvedRoot });
204
+ const state = await store.getRun(runId);
205
+ return toolResult({
206
+ approval,
207
+ status: buildDeepScanStatus(state),
208
+ nextAction: 'Resume the Deep Scan run from deepScanResume.',
209
+ });
210
+ });
211
+
212
+ server.registerTool('deepScanResume', {
213
+ title: 'Resume Deep Scan Runtime',
214
+ description: 'Resume a local-first Deep Scan run after checkpoint, block, or human approval.',
215
+ inputSchema: {
216
+ rootPath: z.string().default('.').describe('Project root, inside the bound folder'),
217
+ runId: z.string().min(1).max(160),
218
+ },
219
+ }, async ({ rootPath = '.', runId }) => {
220
+ const guard = await guardPath(rootPath);
221
+ if (!guard.ok) return guardError(guard);
222
+ const runtime = buildRuntime(guard.resolvedRoot);
223
+ const state = await runtime.resumeRun(runId);
224
+ return toolResult(buildDeepScanStatus(state));
225
+ });
226
+
227
+ server.registerTool('deepScanAcceptRisk', {
228
+ title: 'Record Accepted Risk For A Deep Scan Finding',
229
+ description: 'Record an explicit, attributable accepted-risk decision for a finding or task in the local accepted-risk register. High and critical findings require reason, reviewer, and an expiry/review date. Accepted risk never removes the finding from history and reverts to unresolved after expiry.',
230
+ inputSchema: {
231
+ rootPath: z.string().default('.').describe('Project root, inside the bound folder'),
232
+ findingKey: z.string().regex(/^[a-f0-9]{64}$/i).optional().describe('Stable finding key from the Ralph comparison artifact'),
233
+ findingId: z.string().max(120).optional().describe('Deterministic scan finding id'),
234
+ taskId: z.string().max(120).optional().describe('Related Agent Fix Task id'),
235
+ severity: z.enum(['critical', 'high', 'medium', 'low']),
236
+ reason: z.string().max(1000).optional().describe('Required for high/critical findings'),
237
+ reviewer: z.string().max(255).optional().describe('Required for high/critical findings'),
238
+ acceptedAt: z.string().max(40).optional(),
239
+ expiresAt: z.string().max(40).optional().describe('Required for high/critical findings; ISO date'),
240
+ reviewBy: z.string().max(40).optional(),
241
+ reportVisibility: z.enum(['visible', 'summary_only']).default('visible'),
242
+ riskId: z.string().max(160).optional(),
243
+ metadata: z.record(z.string(), z.unknown()).default({}),
244
+ },
245
+ }, async ({
246
+ rootPath = '.',
247
+ findingKey = undefined,
248
+ findingId = undefined,
249
+ taskId = undefined,
250
+ severity,
251
+ reason = undefined,
252
+ reviewer = undefined,
253
+ acceptedAt = undefined,
254
+ expiresAt = undefined,
255
+ reviewBy = undefined,
256
+ reportVisibility = 'visible',
257
+ riskId = undefined,
258
+ metadata = {},
259
+ }) => {
260
+ const guard = await guardPath(rootPath);
261
+ if (!guard.ok) return guardError(guard);
262
+ const result = await appendAcceptedRiskRecord({
263
+ rootPath: guard.resolvedRoot,
264
+ record: {
265
+ riskId,
266
+ findingKey,
267
+ findingId,
268
+ taskId,
269
+ severity,
270
+ reason,
271
+ reviewer,
272
+ acceptedAt,
273
+ expiresAt,
274
+ reviewBy,
275
+ reportVisibility,
276
+ metadata: { ...metadata, source: 'mcp' },
277
+ },
278
+ });
279
+ return toolResult({
280
+ record: result.record,
281
+ registerRef: { uri: result.uri, hash: result.hash },
282
+ activeRecordCount: result.register.records.length,
283
+ nextAction: 'Rerun the Deep Scan so ralph.compare and ralph.accept apply this accepted risk and reports show it separately from resolved findings.',
284
+ });
285
+ });
286
+ }