@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/package.json +59 -55
- package/src/api-scan.mjs +362 -93
- package/src/cli.js +713 -322
- package/src/deep-scan/contracts.js +201 -0
- package/src/deep-scan/deterministic-scan.js +337 -0
- package/src/deep-scan/index.js +109 -0
- package/src/deep-scan/project-map.js +507 -0
- package/src/deep-scan/ralph-accept.js +510 -0
- package/src/deep-scan/ralph-compare.js +498 -0
- package/src/deep-scan/ralph-tasks.js +598 -0
- package/src/deep-scan/ralph-track.js +548 -0
- package/src/deep-scan/registry.js +159 -0
- package/src/deep-scan/runtime.js +275 -0
- package/src/deep-scan/sample-steppers.js +128 -0
- package/src/deep-scan/sourceSafe.js +73 -0
- package/src/deep-scan/status.js +70 -0
- package/src/deep-scan/store.js +57 -0
- package/src/deep-scan/test-plan.js +760 -0
- package/src/index.js +6 -5
- package/src/lock.mjs +55 -14
- package/src/middleware/governance.js +135 -0
- package/src/orchestrator/runScan.js +211 -0
- package/src/project-bindings.mjs +215 -0
- package/src/rule-engine/index.js +2 -1
- package/src/rule-engine/localScan.js +39 -12
- package/src/rule-engine/metadata.js +20 -0
- package/src/rule-engine/prompt.js +6 -5
- package/src/rule-engine/rules.js +71 -43
- package/src/rule-engine/score.js +5 -4
- package/src/security/pathGuard.js +170 -0
- package/src/selftest.js +2473 -0
- package/src/server.js +109 -150
- package/src/tools/deepScan.js +286 -0
- package/src/tools/localScan.js +85 -0
- package/src/tools/projects.js +124 -0
- package/src/tools/scanFile.js +131 -0
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
|
-
|
|
13
|
+
normalizeRootPath, ensureDirectory,
|
|
14
14
|
gatherRepoScan, readGitignoreChecks,
|
|
15
15
|
detectWorkspacePath, isHomePath,
|
|
16
16
|
} from './repo-scan.mjs';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
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
|
-
|
|
29
|
-
process.
|
|
30
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
}
|