@saiteja1123/mcp-server 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
- /**
2
- * Re-exports the Vibesecur rule engine for MCP servers, Cursor hooks, or other tooling.
3
- * Implement MCP protocol handlers in a separate file that imports from here or from
4
- * `@vibesecur/rule-engine` directly.
5
- */
6
- export * from '@vibesecur/rule-engine';
1
+ /**
2
+ * Re-export bundled local rule engine for MCP tooling.
3
+ * This keeps the MCP package self-contained at runtime.
4
+ */
5
+ export * from './rule-engine/index.js';
6
+ export * from './deep-scan/index.js';
package/src/lock.mjs CHANGED
@@ -11,6 +11,23 @@ export function randomToken() {
11
11
  return crypto.randomBytes(32).toString('hex');
12
12
  }
13
13
 
14
+ export function normalizeLockedRoot(rawPath = '') {
15
+ if (typeof rawPath !== 'string') return '';
16
+ const trimmed = rawPath.trim();
17
+ if (!trimmed) return '';
18
+ const slashed = trimmed.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
19
+ const withoutTrailing = slashed.replace(/\/+$/, '');
20
+ return withoutTrailing.toLowerCase();
21
+ }
22
+
23
+ export function buildLockedRootHash(lockedRootPath) {
24
+ return sha256Hex(`vibesecur:mcp:root:${normalizeLockedRoot(lockedRootPath)}`);
25
+ }
26
+
27
+ export function isRuntimeCompatibleLock(lock) {
28
+ return lock?.source === 'server';
29
+ }
30
+
14
31
  export function deriveLockDir(rootPath) {
15
32
  return path.join(path.resolve(rootPath), '.vibesecur');
16
33
  }
@@ -34,17 +51,19 @@ export async function writeLock(rootPath, lockData) {
34
51
  await fs.writeFile(deriveLockFile(rootPath), JSON.stringify(lockData, null, 2), 'utf8');
35
52
  }
36
53
 
37
- export async function createLock({ rootPath, account }) {
54
+ export async function createLock({ rootPath, account, installToken, lockedRootHash, source = 'local' }) {
38
55
  const resolved = path.resolve(rootPath);
39
- const rootHash = sha256Hex(`vibesecur:lock:${resolved}`);
40
- const installToken = randomToken();
56
+ const finalInstallToken = installToken || randomToken();
57
+ const finalLockedRootHash = lockedRootHash || buildLockedRootHash(resolved);
41
58
  const lock = {
42
- installToken,
43
- rootHash,
59
+ installToken: finalInstallToken,
60
+ rootHash: finalLockedRootHash,
61
+ lockedRootHash: finalLockedRootHash,
44
62
  boundRoot: resolved,
45
63
  createdAt: new Date().toISOString(),
46
64
  account: account || 'anonymous',
47
- version: 1,
65
+ source,
66
+ version: 2,
48
67
  };
49
68
  await writeLock(resolved, lock);
50
69
  return lock;
@@ -66,10 +85,23 @@ export async function validateScanPath(requestedPath, installToken) {
66
85
  };
67
86
  }
68
87
 
69
- if (installToken && lock.installToken !== installToken) {
88
+ if (!/^[a-f0-9]{64}$/i.test(String(installToken || ''))) {
89
+ return {
90
+ ok: false,
91
+ code: 'TOKEN_MISSING',
92
+ httpStatus: 403,
93
+ message:
94
+ 'Missing or invalid install token. ' +
95
+ 'Use a server-issued binding via "vibesecur-mcp bind <folder>" and update MCP env.',
96
+ rebindHint: `vibesecur-mcp rebind ${lock.boundRoot}`,
97
+ };
98
+ }
99
+
100
+ if (lock.installToken !== installToken) {
70
101
  return {
71
102
  ok: false,
72
103
  code: 'TOKEN_MISMATCH',
104
+ httpStatus: 403,
73
105
  message:
74
106
  'Install token does not match the lock for this folder. ' +
75
107
  'Run "vibesecur-mcp rebind" to get a new token.',
@@ -91,17 +123,19 @@ export async function validateScanPath(requestedPath, installToken) {
91
123
  };
92
124
  }
93
125
 
94
- const expectedHash = sha256Hex(`vibesecur:lock:${boundRoot}`);
95
- if (lock.rootHash !== expectedHash) {
126
+ const expectedHash = buildLockedRootHash(boundRoot);
127
+ const storedHash = lock.lockedRootHash || lock.rootHash || '';
128
+ if (storedHash !== expectedHash) {
96
129
  return {
97
130
  ok: false,
98
131
  code: 'HASH_MISMATCH',
132
+ httpStatus: 403,
99
133
  message: 'Lock rootHash does not match bound folder. The lock file may be corrupted. Rebind.',
100
134
  rebindHint: `vibesecur-mcp rebind ${boundRoot}`,
101
135
  };
102
136
  }
103
137
 
104
- return { ok: true, lock, boundRoot };
138
+ return { ok: true, lock, boundRoot, lockedRootHash: expectedHash };
105
139
  }
106
140
 
107
141
  export async function findLockForPath(startPath) {
@@ -127,16 +161,17 @@ export async function findLockForPath(startPath) {
127
161
  return null;
128
162
  }
129
163
 
130
- export async function rebindLock({ rootPath, account }) {
164
+ export async function rebindLock({ rootPath, account, source = 'local' }) {
131
165
  const resolved = path.resolve(rootPath);
132
166
  const oldLock = await readLock(resolved);
133
- const newLock = await createLock({ rootPath: resolved, account });
167
+ const newLock = await createLock({ rootPath: resolved, account, source });
134
168
 
135
169
  return {
136
170
  previousToken: oldLock?.installToken || null,
137
171
  newToken: newLock.installToken,
138
172
  boundRoot: newLock.boundRoot,
139
173
  rootHash: newLock.rootHash,
174
+ source: newLock.source,
140
175
  message: 'Lock rebound. Previous token is now invalid. Copy your new install token.',
141
176
  };
142
177
  }
@@ -154,22 +189,28 @@ export async function diagnosticLock(rootPath) {
154
189
  };
155
190
  }
156
191
 
157
- const expectedHash = sha256Hex(`vibesecur:lock:${path.resolve(lock.boundRoot)}`);
158
- const hashOk = lock.rootHash === expectedHash;
192
+ const expectedHash = buildLockedRootHash(path.resolve(lock.boundRoot));
193
+ const storedHash = lock.lockedRootHash || lock.rootHash || '';
194
+ const hashOk = storedHash === expectedHash;
159
195
  const boundRoot = path.resolve(lock.boundRoot);
160
196
  const inFolder = resolved.startsWith(boundRoot + path.sep) || resolved === boundRoot;
161
197
 
162
198
  return {
163
199
  healthy: hashOk && inFolder,
200
+ runtimeCompatible: hashOk && inFolder && isRuntimeCompatibleLock(lock),
164
201
  lockFile: deriveLockFile(lock.boundRoot),
165
202
  boundRoot: lock.boundRoot,
166
203
  account: lock.account,
167
204
  createdAt: lock.createdAt,
205
+ source: lock.source || 'unknown',
168
206
  rootHash: { expected: expectedHash, stored: lock.rootHash, ok: hashOk },
169
207
  pathInBoundFolder: inFolder,
170
208
  issues: [
171
209
  !hashOk ? 'rootHash mismatch - lock may be corrupted' : null,
172
210
  !inFolder ? `requested path ${resolved} is outside bound folder ${boundRoot}` : null,
211
+ !isRuntimeCompatibleLock(lock)
212
+ ? 'lock is local diagnostic-only; run vibesecur-mcp bind <folder> with a backend auth token'
213
+ : null,
173
214
  ].filter(Boolean),
174
215
  };
175
216
  }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * src/middleware/governance.js
3
+ *
4
+ * Dynamic Context Governance middleware interface.
5
+ *
6
+ * ── Phase 1 behaviour ────────────────────────────────────────────────────────
7
+ * This module is a transparent pass-through. Every request is allowed.
8
+ * No governance policies exist yet. No blocking occurs.
9
+ * No hardcoded permissions. No simulated enforcement.
10
+ *
11
+ * ── Why this exists now ──────────────────────────────────────────────────────
12
+ * Inserting this seam now means future governance features (rate limiting,
13
+ * quota enforcement, context-aware scan policies, tier restrictions) can be
14
+ * added here without touching any tool handler, scan orchestrator, or MCP
15
+ * server bootstrap code. The interface is locked; the implementation evolves.
16
+ *
17
+ * ── Future phases will add inside evaluateGovernance() ──────────────────────
18
+ * Phase 2: fetch live governance context from VIBESECUR_API_BASE/mcp/governance
19
+ * Phase 3: evaluate local policy rules against GovernanceContext
20
+ * Phase 4: propagate GovernanceDecision.annotations into scan results
21
+ *
22
+ * ── What this module does NOT own ────────────────────────────────────────────
23
+ * - Path guard / binding validation (orchestrator caller's responsibility)
24
+ * - Scan execution (runScan.js)
25
+ * - Response formatting (tool handler)
26
+ * - Authentication (lock.mjs + api-scan.mjs)
27
+ *
28
+ * IMPORTS: only from api-scan.mjs — never from server.js or runScan.js.
29
+ */
30
+
31
+ import { getMcpSessionId } from '../api-scan.mjs';
32
+
33
+ // ── Type definitions ──────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Context object passed to the governance middleware.
37
+ * Built by the scan orchestrator from validated guard + caller inputs.
38
+ *
39
+ * @typedef {Object} GovernanceContext
40
+ * @property {string} toolName MCP tool being invoked ('localScan' | 'scanFile' | …)
41
+ * @property {string} requestedPath Resolved, normalised project root (from guard.resolvedRoot)
42
+ * @property {string} installToken Opaque install token — do not log raw value
43
+ * @property {string} lockedRootHash SHA-256 of the bound root — safe to log
44
+ * @property {string} sessionId Process-stable session identifier
45
+ * @property {GovernanceScanMetadata} scanMetadata Scan-specific metadata
46
+ * @property {string} executionMode Caller hint: 'unknown' before scan executes
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} GovernanceScanMetadata
51
+ * @property {string} lang Resolved language ('js'|'ts'|'py'|'json'|'auto')
52
+ * @property {number} codeSize Length of the code string in characters
53
+ * @property {number|undefined} maxFiles File cap for repo tools; undefined for code-string tools
54
+ */
55
+
56
+ /**
57
+ * Decision returned by the governance middleware to the scan orchestrator.
58
+ *
59
+ * @typedef {Object} GovernanceDecision
60
+ * @property {boolean} allowed Whether the request is permitted to proceed
61
+ * @property {string} reason Human-readable explanation (logged, not returned to IDE)
62
+ * @property {Record<string,*>} annotations Metadata to attach to the scan result (currently empty)
63
+ * @property {Record<string,*>} metadata Governance system metadata (tier, policy id, etc.)
64
+ */
65
+
66
+ // ── Helpers ───────────────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Build a GovernanceContext from the parameters available at scan-time.
70
+ * The caller (runScan) passes these directly from its own validated inputs.
71
+ *
72
+ * @param {Object} params
73
+ * @param {string} params.toolName
74
+ * @param {string} params.requestedPath
75
+ * @param {string} params.installToken
76
+ * @param {string} params.lockedRootHash
77
+ * @param {string} params.lang
78
+ * @param {number} params.codeSize
79
+ * @param {number|undefined} params.maxFiles
80
+ * @returns {GovernanceContext}
81
+ */
82
+ export function buildGovernanceContext({
83
+ toolName,
84
+ requestedPath,
85
+ installToken,
86
+ lockedRootHash,
87
+ lang,
88
+ codeSize,
89
+ maxFiles,
90
+ }) {
91
+ return {
92
+ toolName,
93
+ requestedPath,
94
+ installToken, // caller must not log this field
95
+ lockedRootHash, // SHA-256 — safe to log
96
+ sessionId: getMcpSessionId(),
97
+ scanMetadata: {
98
+ lang,
99
+ codeSize,
100
+ maxFiles,
101
+ },
102
+ executionMode: 'unknown', // resolved after scan returns
103
+ };
104
+ }
105
+
106
+ // ── Core middleware ───────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Evaluate governance policy for the given execution context.
110
+ *
111
+ * Phase 1: transparent pass-through. Always returns allowed=true.
112
+ * No network calls. No blocking. No policy evaluation. Zero latency overhead.
113
+ *
114
+ * Contract for future phases:
115
+ * - Must never throw — errors must be caught and result in fail-open (allowed=true)
116
+ * during Phase 1. Future phases may choose fail-closed after explicit opt-in.
117
+ * - Must return a GovernanceDecision synchronously or asynchronously.
118
+ * - Must not mutate the GovernanceContext object.
119
+ * - Must not perform authentication — that is the path guard's responsibility.
120
+ * - Must not read or write lock files.
121
+ *
122
+ * @param {GovernanceContext} ctx
123
+ * @returns {Promise<GovernanceDecision>}
124
+ */
125
+ export async function evaluateGovernance(ctx) { // eslint-disable-line no-unused-vars
126
+ // ── Phase 1: pass-through ─────────────────────────────────────────────────
127
+ // ctx is accepted but intentionally not evaluated.
128
+ // The parameter is kept to lock the function signature for future phases.
129
+ return {
130
+ allowed: true,
131
+ reason: 'pass-through',
132
+ annotations: {},
133
+ metadata: {},
134
+ };
135
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * src/orchestrator/runScan.js
3
+ *
4
+ * Central scan execution pipeline.
5
+ *
6
+ * Owns ONLY the remote→local decision tree:
7
+ * 1. Attempt remote scan via /api/v1/scan/local
8
+ * 2. On 402 → surface upgrade requirement (checked first; never fall back)
9
+ * 3. On remote unavailable/skipped/error → surface hard error (no silent fallback)
10
+ * 4. On remote success → return remote result with quota metadata
11
+ * 5. On remote ok-but-no-data → fall back to local rule engine
12
+ *
13
+ * DOES NOT OWN:
14
+ * - Path guard / binding validation (caller's responsibility)
15
+ * - File I/O / realpath (caller's responsibility)
16
+ * - Language inference from filename (caller resolves before passing)
17
+ * - MCP response formatting (caller's responsibility)
18
+ *
19
+ * This function is the future insertion point for Dynamic Context Governance.
20
+ * A governance middleware call will be added between the guard result and the
21
+ * postRemoteLocalScan() call — no other files need to change at that time.
22
+ *
23
+ * IMPORTS: only from api-scan.mjs and rule-engine — never from server.js.
24
+ */
25
+
26
+ import { postRemoteLocalScan, postScanLog } from '../api-scan.mjs';
27
+ import { localScan } from '../rule-engine/index.js';
28
+ import { buildGovernanceContext, evaluateGovernance } from '../middleware/governance.js';
29
+
30
+ const UPGRADE_URL = 'https://vibesecur.com/#pricing';
31
+
32
+ /**
33
+ * @typedef {Object} ScanOutcomeError
34
+ * @property {false} ok
35
+ * @property {'REMOTE_VERIFICATION_REQUIRED'|'UPGRADE_REQUIRED'|'GOVERNANCE_BLOCKED'} errorCode
36
+ * @property {Object} errorPayload - Shape ready for JSON serialisation in error response
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} ScanOutcomeSuccess
41
+ * @property {true} ok
42
+ * @property {'remote'|'offline'} mode
43
+ * @property {Object} result - Raw scan result (findings, score, grade, verdict, checklist…)
44
+ * @property {Object|undefined} quota - Remote quota metadata; undefined when mode='offline'
45
+ * @property {string} engineVersion - mcpPkg.version passed in by caller
46
+ */
47
+
48
+ /** @typedef {ScanOutcomeError|ScanOutcomeSuccess} ScanOutcome */
49
+
50
+ /**
51
+ * Execute a single-code-string security scan.
52
+ *
53
+ * Callers must resolve 'auto' language before passing when the exact language
54
+ * is known (e.g. via inferLang). Passing 'auto' is safe — both
55
+ * postRemoteLocalScan and localScan handle it.
56
+ *
57
+ * @param {Object} params
58
+ * @param {string} params.code Source code to scan
59
+ * @param {string} params.lang Language: 'js'|'ts'|'py'|'json'|'auto'
60
+ * @param {string} params.projectRoot Resolved root path (from guard.resolvedRoot)
61
+ * @param {string} params.installToken VIBESECUR_INSTALL_TOKEN value
62
+ * @param {string} [params.lockedRootHash] From guard.lock?.lockedRootHash || guard.lock?.rootHash
63
+ * @param {string} params.engineVersion Package version string for result annotation
64
+ * @returns {Promise<ScanOutcome>}
65
+ */
66
+ export async function runScan({
67
+ code,
68
+ lang,
69
+ projectRoot,
70
+ installToken,
71
+ lockedRootHash,
72
+ engineVersion,
73
+ }) {
74
+ // ── Governance middleware ─────────────────────────────────────────────────
75
+ // Phase 1: always passes through (evaluateGovernance returns allowed:true).
76
+ // Future phases: evaluateGovernance may return allowed:false to block the
77
+ // scan before any compute or network cost is incurred.
78
+ //
79
+ // Fail-open policy: if governance itself throws, we log and allow.
80
+ // This keeps Phase 1 safe — governance bugs never silently break scans.
81
+ // Future phases that want fail-closed must handle errors inside
82
+ // evaluateGovernance() before returning a GovernanceDecision.
83
+ let govDecision;
84
+ try {
85
+ const govCtx = buildGovernanceContext({
86
+ toolName: 'scan', // refined callers may pass this explicitly in Phase 2
87
+ requestedPath: projectRoot,
88
+ installToken,
89
+ lockedRootHash: lockedRootHash || '',
90
+ lang,
91
+ codeSize: code.length,
92
+ maxFiles: undefined, // code-string scan; repo tools will pass maxFiles in Phase 2
93
+ });
94
+ govDecision = await evaluateGovernance(govCtx);
95
+ } catch (govErr) {
96
+ // Governance error — fail-open in Phase 1, surface to stderr for observability
97
+ process.stderr.write(
98
+ `[vibesecur] governance middleware error (fail-open): ${govErr.message}\n`,
99
+ );
100
+ govDecision = { allowed: true, reason: 'error-fail-open', annotations: {}, metadata: {} };
101
+ }
102
+
103
+ if (!govDecision.allowed) {
104
+ // Phase 1: this branch is unreachable (allowed is always true).
105
+ // Present now so Phase 2 only needs to set allowed:false inside
106
+ // evaluateGovernance() — no changes to this file or any tool handler.
107
+ return {
108
+ ok: false,
109
+ errorCode: 'GOVERNANCE_BLOCKED',
110
+ errorPayload: {
111
+ error: 'GOVERNANCE_BLOCKED',
112
+ message: govDecision.reason || 'Request blocked by governance policy.',
113
+ },
114
+ };
115
+ }
116
+
117
+ // ── Remote scan attempt ───────────────────────────────────────────────────
118
+ const remote = await postRemoteLocalScan({
119
+ code,
120
+ lang,
121
+ projectRoot,
122
+ platform: 'mcp',
123
+ installToken,
124
+ lockedRootHash,
125
+ });
126
+
127
+ // ── Case 1: Payment required (402) ───────────────────────────────────────
128
+ // MUST be checked before the generic !remote.ok guard. Fetch API sets
129
+ // Response.ok = false for all non-2xx statuses including 402, so without
130
+ // this early check the generic guard would intercept quota-exceeded
131
+ // responses and surface REMOTE_VERIFICATION_REQUIRED instead.
132
+ // upgradeUrl is always included so both callers (localScan tool, scanFile
133
+ // tool) get a consistent, actionable error shape.
134
+ if (remote.status === 402) {
135
+ return {
136
+ ok: false,
137
+ errorCode: 'UPGRADE_REQUIRED',
138
+ errorPayload: {
139
+ ...remote.json,
140
+ upgradeUrl: UPGRADE_URL,
141
+ },
142
+ };
143
+ }
144
+
145
+ // ── Case 2: Remote unavailable or network/auth failure ───────────────────
146
+ // This covers: VIBESECUR_API_BASE not set (skipped:true), fetch error,
147
+ // non-2xx response (ok:false). We surface a hard error — there is no silent
148
+ // local fallback for this case. The caller must have a verified server
149
+ // binding before a scan is meaningful.
150
+ if (remote.skipped || !remote.ok) {
151
+ return {
152
+ ok: false,
153
+ errorCode: 'REMOTE_VERIFICATION_REQUIRED',
154
+ errorPayload: {
155
+ error: 'REMOTE_VERIFICATION_REQUIRED',
156
+ message:
157
+ remote.error
158
+ || remote.reason
159
+ || `Remote scan failed with status ${remote.status || 'unknown'}`,
160
+ },
161
+ };
162
+ }
163
+
164
+ // ── Case 3: Remote success ────────────────────────────────────────────────
165
+ if (remote.ok && remote.json?.success && remote.json?.data) {
166
+ const result = remote.json.data;
167
+
168
+ // Persist scan metadata so the Projects dashboard reflects this scan.
169
+ // Best-effort and metadata-only: a logging failure must never fail the
170
+ // scan the user already paid quota for. Reuses codeHash + scanReceipt so
171
+ // the backend does not double-count quota.
172
+ let logged = { ok: false };
173
+ try {
174
+ const logRes = await postScanLog({
175
+ result,
176
+ lang,
177
+ projectRoot,
178
+ platform: 'mcp',
179
+ installToken,
180
+ lockedRootHash,
181
+ });
182
+ logged = logRes?.ok
183
+ ? { ok: true, scanId: logRes.json?.data?.scanId || null }
184
+ : { ok: false, reason: logRes?.reason || logRes?.error || `status ${logRes?.status || 'unknown'}` };
185
+ } catch (logErr) {
186
+ process.stderr.write(`[vibesecur] scan log failed (non-fatal): ${logErr.message}\n`);
187
+ logged = { ok: false, reason: logErr.message };
188
+ }
189
+
190
+ return {
191
+ ok: true,
192
+ mode: 'remote',
193
+ result,
194
+ quota: remote.json.quota,
195
+ engineVersion,
196
+ logged,
197
+ };
198
+ }
199
+
200
+ // ── Case 4: Remote returned 2xx but no usable data ────────────────────────
201
+ // Edge case: API responded OK but body lacks success/data (e.g. partial
202
+ // degradation). Fall back to the local rule engine so the scan still runs.
203
+ const result = localScan(code, lang);
204
+ return {
205
+ ok: true,
206
+ mode: 'offline',
207
+ result,
208
+ quota: undefined,
209
+ engineVersion,
210
+ };
211
+ }