@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/package.json +59 -54
- 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 -6
- 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 +3 -2
- package/src/rule-engine/localScan.js +41 -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 +161 -145
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/localScan.js
|
|
3
|
+
*
|
|
4
|
+
* MCP tool: 'localScan'
|
|
5
|
+
*
|
|
6
|
+
* Registers the localScan tool against a McpServer instance.
|
|
7
|
+
* All server-level dependencies (guard, token, version) are injected
|
|
8
|
+
* by the caller — this file never imports from server.js.
|
|
9
|
+
*
|
|
10
|
+
* Dependency injection contract:
|
|
11
|
+
* registerLocalScanTool(server, deps)
|
|
12
|
+
* server — McpServer instance
|
|
13
|
+
* deps.guardPath — async (path: string) => GuardResult
|
|
14
|
+
* deps.guardError — (guard: GuardResult) => McpErrorResponse
|
|
15
|
+
* deps.INSTALL_TOKEN — string | null
|
|
16
|
+
* deps.engineVersion — string (mcpPkg.version)
|
|
17
|
+
*
|
|
18
|
+
* DOES NOT OWN:
|
|
19
|
+
* - Path guard implementation (server.js)
|
|
20
|
+
* - Install token / bound root resolution (server.js)
|
|
21
|
+
* - Scan orchestration (orchestrator/runScan.js)
|
|
22
|
+
* - Governance middleware (middleware/governance.js — called inside runScan)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import * as z from 'zod';
|
|
26
|
+
import { runScan } from '../orchestrator/runScan.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register the 'localScan' MCP tool on the provided server instance.
|
|
30
|
+
*
|
|
31
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
32
|
+
* @param {Object} deps
|
|
33
|
+
* @param {Function} deps.guardPath - Bound-folder + lock validation
|
|
34
|
+
* @param {Function} deps.guardError - Guard failure → MCP error response
|
|
35
|
+
* @param {string|null} deps.INSTALL_TOKEN
|
|
36
|
+
* @param {string} deps.engineVersion
|
|
37
|
+
*/
|
|
38
|
+
export function registerLocalScanTool(server, { guardPath, guardError, installToken, engineVersion }) {
|
|
39
|
+
server.registerTool('localScan', {
|
|
40
|
+
title: 'Local Security Scan',
|
|
41
|
+
description: 'Run Vibesecur rule-engine on code. Restricted to bound project folder.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
code: z.string().min(1).max(50000).describe('Source code to scan'),
|
|
44
|
+
lang: z.enum(['js', 'ts', 'py', 'json', 'auto']).default('auto'),
|
|
45
|
+
projectRoot: z.string().default('.').describe('Must be inside bound folder'),
|
|
46
|
+
},
|
|
47
|
+
}, async ({ code, lang = 'auto', projectRoot = '.' }) => {
|
|
48
|
+
// ── Binding / path guard ──────────────────────────────────────────────
|
|
49
|
+
const guard = await guardPath(projectRoot);
|
|
50
|
+
if (!guard.ok) return guardError(guard);
|
|
51
|
+
|
|
52
|
+
// ── Scan orchestration (remote → governance → local fallback) ─────────
|
|
53
|
+
const outcome = await runScan({
|
|
54
|
+
code,
|
|
55
|
+
lang,
|
|
56
|
+
projectRoot: guard.resolvedRoot,
|
|
57
|
+
installToken: guard.installToken || installToken,
|
|
58
|
+
lockedRootHash: guard.lockedRootHash || guard.lock?.lockedRootHash || guard.lock?.rootHash,
|
|
59
|
+
engineVersion,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── Error outcome (REMOTE_VERIFICATION_REQUIRED / UPGRADE_REQUIRED /
|
|
63
|
+
// GOVERNANCE_BLOCKED) ─────────────────────────────────────────────
|
|
64
|
+
if (!outcome.ok) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: 'text', text: JSON.stringify(outcome.errorPayload, null, 2) }],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Remote success ────────────────────────────────────────────────────
|
|
72
|
+
if (outcome.mode === 'remote') {
|
|
73
|
+
const data = outcome.result;
|
|
74
|
+
const humanSummary = `${data.verdict || ''} Score ${data.score} (${data.grade}) - ${(data.findings || []).length} finding(s).`;
|
|
75
|
+
const enriched = { ...data, humanSummary, engineVersion: outcome.engineVersion, quota: outcome.quota };
|
|
76
|
+
return { content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }], structuredContent: enriched };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Offline fallback ──────────────────────────────────────────────────
|
|
80
|
+
const result = outcome.result;
|
|
81
|
+
const humanSummary = `${result.verdict} Score ${result.score} (${result.grade}) - ${result.findings.length} finding(s).`;
|
|
82
|
+
const enriched = { ...result, humanSummary, engineVersion: outcome.engineVersion, mode: 'offline' };
|
|
83
|
+
return { content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }], structuredContent: enriched };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
import { listProjects } from '../api-scan.mjs';
|
|
4
|
+
import { ensureProjectBinding, invalidateProjectsListCache } from '../project-bindings.mjs';
|
|
5
|
+
|
|
6
|
+
function resolveAuthToken(authToken) {
|
|
7
|
+
return (
|
|
8
|
+
authToken
|
|
9
|
+
|| process.env.VIBESECUR_AUTH_TOKEN
|
|
10
|
+
|| process.env.VIBESECUR_TOKEN
|
|
11
|
+
|| ''
|
|
12
|
+
).trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatApiError(res, fallback) {
|
|
16
|
+
return res.json?.error || res.error || fallback || `Request failed (${res.status || 'unknown'})`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register project management tools (CRU — create/read/update, never delete).
|
|
21
|
+
*/
|
|
22
|
+
export function registerProjectTools(server, { authToken, apiBase, normalizeRootPath }) {
|
|
23
|
+
const token = () => resolveAuthToken(authToken);
|
|
24
|
+
|
|
25
|
+
server.registerTool('projectList', {
|
|
26
|
+
title: 'List Projects',
|
|
27
|
+
description: 'List all projects in your Vibesecur account. Read-only.',
|
|
28
|
+
inputSchema: {},
|
|
29
|
+
}, async () => {
|
|
30
|
+
if (!token()) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
33
|
+
error: 'AUTH_REQUIRED',
|
|
34
|
+
message: 'Set VIBESECUR_AUTH_TOKEN in MCP config (login JWT from dashboard).',
|
|
35
|
+
}, null, 2) }],
|
|
36
|
+
isError: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const res = await listProjects({ authToken: token() });
|
|
40
|
+
if (!res.ok || !res.json?.success) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
43
|
+
error: 'PROJECT_LIST_FAILED',
|
|
44
|
+
message: formatApiError(res, 'Unable to list projects'),
|
|
45
|
+
}, null, 2) }],
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const projects = res.json.data?.projects || [];
|
|
50
|
+
const body = {
|
|
51
|
+
count: projects.length,
|
|
52
|
+
projects: projects.map((p) => ({
|
|
53
|
+
id: p.id,
|
|
54
|
+
name: p.name,
|
|
55
|
+
lockedRootHint: p.lockedRootHint,
|
|
56
|
+
mcpBound: p.mcpBound,
|
|
57
|
+
latestScan: p.latestScan,
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: 'text', text: JSON.stringify(body, null, 2) }],
|
|
62
|
+
structuredContent: body,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
server.registerTool('projectUpsert', {
|
|
67
|
+
title: 'Create or Update Project',
|
|
68
|
+
description:
|
|
69
|
+
'Register a codebase folder as a project, or update an existing one. ' +
|
|
70
|
+
'Never deletes projects. Creates install credentials for scans in that folder.',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
rootPath: z.string().min(1).describe('Absolute or relative codebase root folder'),
|
|
73
|
+
name: z.string().min(1).max(200).optional().describe('Display name (optional)'),
|
|
74
|
+
projectId: z.string().uuid().optional().describe('Existing project id to update/rebind'),
|
|
75
|
+
},
|
|
76
|
+
}, async ({ rootPath, name, projectId }) => {
|
|
77
|
+
if (!token()) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
80
|
+
error: 'AUTH_REQUIRED',
|
|
81
|
+
message: 'Set VIBESECUR_AUTH_TOKEN in MCP config (login JWT from dashboard).',
|
|
82
|
+
}, null, 2) }],
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const resolvedRoot = normalizeRootPath(rootPath);
|
|
88
|
+
const binding = await ensureProjectBinding({
|
|
89
|
+
lockedRootPath: resolvedRoot,
|
|
90
|
+
name,
|
|
91
|
+
projectId,
|
|
92
|
+
authToken: token(),
|
|
93
|
+
apiBase,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!binding.ok) {
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
99
|
+
error: binding.code || 'PROJECT_UPSERT_FAILED',
|
|
100
|
+
message: binding.message,
|
|
101
|
+
}, null, 2) }],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
invalidateProjectsListCache();
|
|
107
|
+
const body = {
|
|
108
|
+
action: binding.action,
|
|
109
|
+
project: {
|
|
110
|
+
id: binding.projectId,
|
|
111
|
+
projectHash: binding.projectHash,
|
|
112
|
+
lockedRootPath: binding.projectRoot,
|
|
113
|
+
lockedRootHash: binding.lockedRootHash,
|
|
114
|
+
},
|
|
115
|
+
message:
|
|
116
|
+
'Project registered. Scans under this folder will sync metadata to the dashboard. ' +
|
|
117
|
+
'Install token is cached for this MCP session (not returned for security).',
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: 'text', text: JSON.stringify(body, null, 2) }],
|
|
121
|
+
structuredContent: body,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/scanFile.js
|
|
3
|
+
*
|
|
4
|
+
* MCP tool: 'scanFile'
|
|
5
|
+
*
|
|
6
|
+
* Registers the scanFile tool against a McpServer instance.
|
|
7
|
+
* All server-level dependencies (guard, token, version) are injected
|
|
8
|
+
* by the caller — this file never imports from server.js.
|
|
9
|
+
*
|
|
10
|
+
* Dependency injection contract:
|
|
11
|
+
* registerScanFileTool(server, deps)
|
|
12
|
+
* server — McpServer instance
|
|
13
|
+
* deps.guardPath — async (path: string) => GuardResult
|
|
14
|
+
* deps.guardError — (guard: GuardResult) => McpErrorResponse
|
|
15
|
+
* deps.INSTALL_TOKEN — string | null
|
|
16
|
+
* deps.engineVersion — string (mcpPkg.version)
|
|
17
|
+
*
|
|
18
|
+
* Tool-owned responsibilities:
|
|
19
|
+
* - path.resolve() / fs.realpath() — symlink resolution + existence check
|
|
20
|
+
* - fs.readFile() — file content loading
|
|
21
|
+
* - inferLang() — language detection from file extension
|
|
22
|
+
* - File-report response formatting — findingsWithLocation, bySev, body shape
|
|
23
|
+
* - Top-level try/catch — wraps all I/O so ENOENT etc. are caught
|
|
24
|
+
*
|
|
25
|
+
* DOES NOT OWN:
|
|
26
|
+
* - Path guard implementation (server.js — injected)
|
|
27
|
+
* - Install token / bound root (server.js — injected)
|
|
28
|
+
* - Scan orchestration (orchestrator/runScan.js — direct import)
|
|
29
|
+
* - Governance middleware (middleware/governance.js — called inside runScan)
|
|
30
|
+
*
|
|
31
|
+
* NOTE: guardPath is called with path.dirname(realPath) — the containing
|
|
32
|
+
* directory of the resolved real file — not the raw filePath argument.
|
|
33
|
+
* This is intentional: symlinks may point outside the apparent directory.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import * as z from 'zod';
|
|
37
|
+
import fs from 'fs/promises';
|
|
38
|
+
import path from 'path';
|
|
39
|
+
import { inferLang } from '../repo-scan.mjs';
|
|
40
|
+
import { runScan } from '../orchestrator/runScan.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register the 'scanFile' MCP tool on the provided server instance.
|
|
44
|
+
*
|
|
45
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
46
|
+
* @param {Object} deps
|
|
47
|
+
* @param {Function} deps.guardPath - Bound-folder + lock validation
|
|
48
|
+
* @param {Function} deps.guardError - Guard failure → MCP error response
|
|
49
|
+
* @param {string|null} deps.INSTALL_TOKEN
|
|
50
|
+
* @param {string} deps.engineVersion
|
|
51
|
+
*/
|
|
52
|
+
export function registerScanFileTool(server, { guardPath, guardError, installToken, engineVersion }) {
|
|
53
|
+
server.registerTool('scanFile', {
|
|
54
|
+
title: 'Scan File',
|
|
55
|
+
description: 'Scan a single file. Must be inside the bound project folder.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
filePath: z.string().min(1).describe('Absolute or relative file path (must be inside bound folder)'),
|
|
58
|
+
lang: z.enum(['js', 'ts', 'py', 'json', 'auto']).default('auto'),
|
|
59
|
+
},
|
|
60
|
+
}, async ({ filePath, lang = 'auto' }) => {
|
|
61
|
+
try {
|
|
62
|
+
// ── File I/O + symlink resolution ───────────────────────────────────
|
|
63
|
+
const resolvedPath = path.resolve(filePath);
|
|
64
|
+
const realPath = await fs.realpath(resolvedPath);
|
|
65
|
+
|
|
66
|
+
// ── Binding / path guard ─────────────────────────────────────────────
|
|
67
|
+
// Guard receives dirname(realPath) — the directory of the resolved real
|
|
68
|
+
// file. Using realPath prevents symlinks from bypassing the bound folder.
|
|
69
|
+
const guard = await guardPath(path.dirname(realPath));
|
|
70
|
+
if (!guard.ok) return guardError(guard);
|
|
71
|
+
|
|
72
|
+
// ── Read file content + resolve language ────────────────────────────
|
|
73
|
+
const code = await fs.readFile(realPath, 'utf8');
|
|
74
|
+
const useLang = lang === 'auto' ? inferLang(realPath) : lang;
|
|
75
|
+
|
|
76
|
+
// ── Scan orchestration (remote → governance → local fallback) ────────
|
|
77
|
+
const outcome = await runScan({
|
|
78
|
+
code,
|
|
79
|
+
lang: useLang,
|
|
80
|
+
projectRoot: guard.resolvedRoot,
|
|
81
|
+
installToken: guard.installToken || installToken,
|
|
82
|
+
lockedRootHash: guard.lockedRootHash || guard.lock?.lockedRootHash || guard.lock?.rootHash,
|
|
83
|
+
engineVersion,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── Error outcome (REMOTE_VERIFICATION_REQUIRED / UPGRADE_REQUIRED /
|
|
87
|
+
// GOVERNANCE_BLOCKED) ──────────────────────────────────────────────
|
|
88
|
+
if (!outcome.ok) {
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: 'text', text: JSON.stringify(outcome.errorPayload, null, 2) }],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── File-report response formatting ─────────────────────────────────
|
|
96
|
+
// NOTE: findings field is the COUNT (number), not the array.
|
|
97
|
+
// The full findings array lives inside result.findings.
|
|
98
|
+
const result = outcome.result;
|
|
99
|
+
const findings = result.findings || [];
|
|
100
|
+
const findingsWithLocation = findings.map((f) => ({
|
|
101
|
+
...f,
|
|
102
|
+
filePath: realPath,
|
|
103
|
+
snippetPreview: f.snippetPreview || f.snippet || '',
|
|
104
|
+
}));
|
|
105
|
+
const bySev = findings.reduce((a, f) => {
|
|
106
|
+
a[f.severity] = (a[f.severity] || 0) + 1;
|
|
107
|
+
return a;
|
|
108
|
+
}, { critical: 0, high: 0, medium: 0, low: 0 });
|
|
109
|
+
const humanSummary = `File "${realPath}": score ${result.score} (${result.grade}), ${findings.length} issue(s).`;
|
|
110
|
+
const body = {
|
|
111
|
+
humanSummary,
|
|
112
|
+
filePath: realPath,
|
|
113
|
+
lang: useLang,
|
|
114
|
+
score: result.score,
|
|
115
|
+
grade: result.grade,
|
|
116
|
+
findings: findingsWithLocation.length, // count — not array
|
|
117
|
+
bySeverity: bySev,
|
|
118
|
+
checklist: result.checklist,
|
|
119
|
+
result: {
|
|
120
|
+
...result,
|
|
121
|
+
findings: findingsWithLocation,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }], structuredContent: body };
|
|
125
|
+
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// Catches: ENOENT from realpath, EACCES from readFile, unexpected throws
|
|
128
|
+
return { content: [{ type: 'text', text: `scanFile failed: ${e.message}` }], isError: true };
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|