@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
|
@@ -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
|
+
}
|