@vibefoundry/vibe-shield 0.0.0-stage → 1.1.0

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +571 -3
  3. package/SECURITY.md +110 -0
  4. package/SKILL.md +124 -0
  5. package/dist/bin/vibe-shield.d.ts +59 -0
  6. package/dist/bin/vibe-shield.js +541 -0
  7. package/dist/src/index.d.ts +25 -0
  8. package/dist/src/index.js +108 -0
  9. package/dist/src/init/scaffold.d.ts +122 -0
  10. package/dist/src/init/scaffold.js +573 -0
  11. package/dist/src/mcp/server.d.ts +127 -0
  12. package/dist/src/mcp/server.js +479 -0
  13. package/dist/src/mcp-guard/skill-auditor.d.ts +105 -0
  14. package/dist/src/mcp-guard/skill-auditor.js +1129 -0
  15. package/dist/src/reporter/console-reporter.d.ts +58 -0
  16. package/dist/src/reporter/console-reporter.js +254 -0
  17. package/dist/src/reporter/formatters.d.ts +38 -0
  18. package/dist/src/reporter/formatters.js +262 -0
  19. package/dist/src/reporter/patch-prompt-writer.d.ts +35 -0
  20. package/dist/src/reporter/patch-prompt-writer.js +238 -0
  21. package/dist/src/scanner/context.d.ts +51 -0
  22. package/dist/src/scanner/context.js +568 -0
  23. package/dist/src/scanner/engine.d.ts +130 -0
  24. package/dist/src/scanner/engine.js +722 -0
  25. package/dist/src/scanner/gitignore.d.ts +49 -0
  26. package/dist/src/scanner/gitignore.js +195 -0
  27. package/dist/src/scanner/lexicon.d.ts +56 -0
  28. package/dist/src/scanner/lexicon.js +357 -0
  29. package/dist/src/scanner/rules/ai-vulnerabilities.d.ts +15 -0
  30. package/dist/src/scanner/rules/ai-vulnerabilities.js +859 -0
  31. package/dist/src/scanner/rules/client-exposure.d.ts +18 -0
  32. package/dist/src/scanner/rules/client-exposure.js +410 -0
  33. package/dist/src/scanner/rules/database-exposure.d.ts +15 -0
  34. package/dist/src/scanner/rules/database-exposure.js +626 -0
  35. package/dist/src/scanner/rules/helpers.d.ts +31 -0
  36. package/dist/src/scanner/rules/helpers.js +85 -0
  37. package/dist/src/scanner/rules/index.d.ts +30 -0
  38. package/dist/src/scanner/rules/index.js +95 -0
  39. package/dist/src/scanner/rules/secret-entropy.d.ts +17 -0
  40. package/dist/src/scanner/rules/secret-entropy.js +338 -0
  41. package/dist/src/scanner/rules/shared.d.ts +100 -0
  42. package/dist/src/scanner/rules/shared.js +161 -0
  43. package/dist/src/scanner/rules/unguarded-routes.d.ts +16 -0
  44. package/dist/src/scanner/rules/unguarded-routes.js +526 -0
  45. package/dist/src/scanner/text.d.ts +90 -0
  46. package/dist/src/scanner/text.js +472 -0
  47. package/dist/src/scanner/types.d.ts +340 -0
  48. package/dist/src/scanner/types.js +195 -0
  49. package/dist/src/secrets/patterns.d.ts +63 -0
  50. package/dist/src/secrets/patterns.js +392 -0
  51. package/package.json +94 -4
@@ -0,0 +1,127 @@
1
+ /**
2
+ * vibe-shield :: Model Context Protocol server
3
+ * -------------------------------------------
4
+ * A zero-dependency MCP server over stdio, so Claude Code, Claude Desktop,
5
+ * Cursor and Codex can call the scanner as a tool instead of shelling out and
6
+ * parsing console output.
7
+ *
8
+ * Transport is newline-delimited JSON-RPC 2.0. Requests are handled in arrival
9
+ * order and every response carries the matching `id`; notifications (no `id`)
10
+ * are never answered, per the JSON-RPC spec.
11
+ *
12
+ * Three tools are exposed:
13
+ * - `vibe_shield_scan` run the 60-rule scanner over a project
14
+ * - `vibe_shield_audit_skills` audit the agent surface for poisoning
15
+ * - `vibe_shield_explain_rule` full CWE + remediation detail for one rule
16
+ *
17
+ * Deliberate design constraints:
18
+ * - stdout is protocol-only. Diagnostics go to stderr, because a stray
19
+ * `console.log` corrupts the stream and breaks the client session.
20
+ * - No request is ever executed against a path outside the client's `roots`
21
+ * or the server's own cwd tree without an absolute path being explicit.
22
+ * - Every tool returns `isError` on failure rather than throwing, so a bad
23
+ * argument degrades one call instead of killing the session.
24
+ */
25
+ /** JSON-RPC 2.0 error codes used by this server. */
26
+ export declare const RpcError: {
27
+ readonly parseError: -32700;
28
+ readonly invalidRequest: -32600;
29
+ readonly methodNotFound: -32601;
30
+ readonly invalidParams: -32602;
31
+ readonly internalError: -32603;
32
+ };
33
+ export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
34
+ export declare const SERVER_NAME = "vibe-shield";
35
+ /** The subset of `InitializeResult` this server advertises. */
36
+ export interface McpInitializeResult {
37
+ protocolVersion: string;
38
+ capabilities: {
39
+ tools: {
40
+ listChanged: boolean;
41
+ };
42
+ };
43
+ serverInfo: {
44
+ name: string;
45
+ version: string;
46
+ title: string;
47
+ };
48
+ instructions: string;
49
+ }
50
+ export interface JsonRpcRequest {
51
+ jsonrpc: '2.0';
52
+ id?: string | number | null;
53
+ method: string;
54
+ params?: Record<string, unknown>;
55
+ }
56
+ export interface JsonRpcSuccess {
57
+ jsonrpc: '2.0';
58
+ id: string | number | null;
59
+ result: unknown;
60
+ }
61
+ export interface JsonRpcFailure {
62
+ jsonrpc: '2.0';
63
+ id: string | number | null;
64
+ error: {
65
+ code: number;
66
+ message: string;
67
+ data?: unknown;
68
+ };
69
+ }
70
+ export type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure;
71
+ /** A single MCP tool description, as returned by `tools/list`. */
72
+ export interface McpToolDefinition {
73
+ name: string;
74
+ title: string;
75
+ description: string;
76
+ inputSchema: Record<string, unknown>;
77
+ }
78
+ export interface McpTextContent {
79
+ type: 'text';
80
+ text: string;
81
+ }
82
+ export interface McpToolResult {
83
+ content: McpTextContent[];
84
+ structuredContent?: Record<string, unknown>;
85
+ isError?: boolean;
86
+ }
87
+ /**
88
+ * Tool catalogue. Descriptions are written for a model deciding *whether* to
89
+ * call, not for a human reading docs — they say when to reach for the tool.
90
+ */
91
+ export declare const MCP_TOOLS: readonly McpToolDefinition[];
92
+ export interface McpServerOptions {
93
+ /** Directory that relative `target` values resolve against. */
94
+ cwd?: string;
95
+ /** Version string reported in `initialize` and scan results. */
96
+ version?: string;
97
+ }
98
+ /** Execute one tool by name. Never throws: failures become `isError` results. */
99
+ export declare function callTool(name: string, args: Record<string, unknown>, options?: McpServerOptions): Promise<McpToolResult>;
100
+ /**
101
+ * Handle one decoded JSON-RPC message.
102
+ * Returns `null` for notifications, which the spec forbids answering.
103
+ */
104
+ export declare function handleMessage(message: unknown, options?: McpServerOptions): Promise<JsonRpcResponse | null>;
105
+ export interface McpServerIo {
106
+ stdin: NodeJS.ReadableStream;
107
+ stdout: {
108
+ write(chunk: string): unknown;
109
+ };
110
+ stderr: {
111
+ write(chunk: string): unknown;
112
+ };
113
+ }
114
+ export interface McpServeOptions extends McpServerOptions {
115
+ /** Called after the transport stops, with the number of messages handled. */
116
+ onClose?: (handled: number) => void;
117
+ }
118
+ /**
119
+ * Serve MCP over newline-delimited JSON-RPC until stdin closes.
120
+ *
121
+ * Messages are processed sequentially so responses preserve request order and a
122
+ * slow scan cannot interleave with a later `tools/call`.
123
+ */
124
+ export declare function serveMcp(io: McpServerIo, options?: McpServeOptions): Promise<number>;
125
+ /** Convenience wrapper used by `vibe-shield mcp`. */
126
+ export declare const startMcpServer: (options?: McpServerOptions) => Promise<number>;
127
+ export declare const MCP_TOOL_NAMES: readonly ["vibe_shield_scan", "vibe_shield_audit_skills", "vibe_shield_explain_rule"];
@@ -0,0 +1,479 @@
1
+ "use strict";
2
+ /**
3
+ * vibe-shield :: Model Context Protocol server
4
+ * -------------------------------------------
5
+ * A zero-dependency MCP server over stdio, so Claude Code, Claude Desktop,
6
+ * Cursor and Codex can call the scanner as a tool instead of shelling out and
7
+ * parsing console output.
8
+ *
9
+ * Transport is newline-delimited JSON-RPC 2.0. Requests are handled in arrival
10
+ * order and every response carries the matching `id`; notifications (no `id`)
11
+ * are never answered, per the JSON-RPC spec.
12
+ *
13
+ * Three tools are exposed:
14
+ * - `vibe_shield_scan` run the 60-rule scanner over a project
15
+ * - `vibe_shield_audit_skills` audit the agent surface for poisoning
16
+ * - `vibe_shield_explain_rule` full CWE + remediation detail for one rule
17
+ *
18
+ * Deliberate design constraints:
19
+ * - stdout is protocol-only. Diagnostics go to stderr, because a stray
20
+ * `console.log` corrupts the stream and breaks the client session.
21
+ * - No request is ever executed against a path outside the client's `roots`
22
+ * or the server's own cwd tree without an absolute path being explicit.
23
+ * - Every tool returns `isError` on failure rather than throwing, so a bad
24
+ * argument degrades one call instead of killing the session.
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.MCP_TOOL_NAMES = exports.startMcpServer = exports.MCP_TOOLS = exports.SERVER_NAME = exports.MCP_PROTOCOL_VERSION = exports.RpcError = void 0;
61
+ exports.callTool = callTool;
62
+ exports.handleMessage = handleMessage;
63
+ exports.serveMcp = serveMcp;
64
+ const path = __importStar(require("node:path"));
65
+ const skill_auditor_1 = require("../mcp-guard/skill-auditor");
66
+ const engine_1 = require("../scanner/engine");
67
+ const rules_1 = require("../scanner/rules");
68
+ const types_1 = require("../scanner/types");
69
+ /** JSON-RPC 2.0 error codes used by this server. */
70
+ exports.RpcError = {
71
+ parseError: -32700,
72
+ invalidRequest: -32600,
73
+ methodNotFound: -32601,
74
+ invalidParams: -32602,
75
+ internalError: -32603,
76
+ };
77
+ exports.MCP_PROTOCOL_VERSION = '2025-06-18';
78
+ exports.SERVER_NAME = 'vibe-shield';
79
+ const TOOL_SCAN = 'vibe_shield_scan';
80
+ const TOOL_AUDIT = 'vibe_shield_audit_skills';
81
+ const TOOL_EXPLAIN = 'vibe_shield_explain_rule';
82
+ /**
83
+ * Tool catalogue. Descriptions are written for a model deciding *whether* to
84
+ * call, not for a human reading docs — they say when to reach for the tool.
85
+ */
86
+ exports.MCP_TOOLS = [
87
+ {
88
+ name: TOOL_SCAN,
89
+ title: 'Scan a project for AI-introduced security bugs',
90
+ description: 'Runs the 60-rule vibe-shield scanner over a project directory or a set of paths and returns the grade, score and every finding with its exact file, line, CWE and remediation steps. Call this after writing or modifying application code — API routes, server actions, database migrations, auth logic, environment variables, LLM calls or webhook handlers — and before reporting a task as complete. Also call it when the user asks for a security review or audit.',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ target: {
95
+ type: 'string',
96
+ description: 'Directory or file to scan. Relative paths resolve against the server working directory. Default: the working directory.',
97
+ },
98
+ paths: {
99
+ type: 'array',
100
+ items: { type: 'string' },
101
+ description: 'Explicit files or directories to scan, instead of the whole project.',
102
+ },
103
+ severity: {
104
+ type: 'string',
105
+ enum: ['critical', 'high', 'medium', 'low', 'info'],
106
+ description: 'Only return findings at or above this severity.',
107
+ },
108
+ rules: {
109
+ type: 'array',
110
+ items: { type: 'string' },
111
+ description: 'Restrict to these rule ids or prefixes, e.g. ["VSB-CLIENT-001"] or ["VSB-DB-*"].',
112
+ },
113
+ includeTests: { type: 'boolean', description: 'Include test, spec and fixture files.' },
114
+ includeDocs: { type: 'boolean', description: 'Include Markdown and documentation files.' },
115
+ limit: {
116
+ type: 'integer',
117
+ description: 'Maximum findings to return in the text summary. Default 25, 0 for all.',
118
+ },
119
+ },
120
+ additionalProperties: false,
121
+ },
122
+ },
123
+ {
124
+ name: TOOL_AUDIT,
125
+ title: 'Audit agent skills and MCP configuration for poisoning',
126
+ description: 'Audits .claude/skills, .cursor rules, .cursorrules, AGENTS.md, CLAUDE.md and MCP config files (mcp.json, .mcp.json) for agent-poisoning: hidden Unicode instructions, credential exfiltration commands, reads of .env or ~/.ssh, unpinned MCP servers, literal credentials and safety-override language. Call this whenever you add or modify an agent instruction file, a skill, a Cursor rule or an MCP server config.',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ target: {
131
+ type: 'string',
132
+ description: 'Project root to audit. Default: the server working directory.',
133
+ },
134
+ paths: {
135
+ type: 'array',
136
+ items: { type: 'string' },
137
+ description: 'Specific agent files or directories to audit.',
138
+ },
139
+ limit: { type: 'integer', description: 'Maximum findings to return. Default 25.' },
140
+ },
141
+ additionalProperties: false,
142
+ },
143
+ },
144
+ {
145
+ name: TOOL_EXPLAIN,
146
+ title: 'Explain a detection rule',
147
+ description: 'Returns the full documentation for one rule id or slug: severity, confidence, CWE, why it is exploitable, the vulnerable code shape, the fixed code shape, ordered remediation steps and references. Use this when you need to understand a finding before fixing it, or when the user asks what a rule means.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ required: ['ruleId'],
151
+ properties: {
152
+ ruleId: {
153
+ type: 'string',
154
+ description: 'Rule id or slug, e.g. "VSB-CLIENT-001" or "public-env-secret-exposure".',
155
+ },
156
+ },
157
+ additionalProperties: false,
158
+ },
159
+ },
160
+ ];
161
+ /* -------------------------------------------------------------------------- */
162
+ /* Argument helpers */
163
+ /* -------------------------------------------------------------------------- */
164
+ const asString = (value) => typeof value === 'string' && value.trim().length ? value.trim() : undefined;
165
+ const asBoolean = (value) => (typeof value === 'boolean' ? value : undefined);
166
+ const asStringArray = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : undefined;
167
+ const asLimit = (value, fallback) => {
168
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
169
+ return fallback;
170
+ return Math.floor(value);
171
+ };
172
+ /* -------------------------------------------------------------------------- */
173
+ /* Result rendering */
174
+ /* -------------------------------------------------------------------------- */
175
+ const SEVERITY_ORDER = ['critical', 'high', 'medium', 'low', 'info'];
176
+ const atOrAbove = (severity, minimum) => SEVERITY_ORDER.indexOf(severity) <= SEVERITY_ORDER.indexOf(minimum);
177
+ /** Compact, model-readable rendering of one finding. */
178
+ const renderFinding = (finding) => {
179
+ const lines = [
180
+ `${finding.severity.toUpperCase()} ${finding.filePath}:${finding.line}:${finding.column} ${finding.ruleId} — ${finding.title}`,
181
+ ` ${finding.message}`,
182
+ ` CWE: ${finding.cwe.id} ${finding.cwe.name}`,
183
+ ];
184
+ if (finding.snippet)
185
+ lines.push(` > ${finding.snippet}`);
186
+ return lines.join('\n');
187
+ };
188
+ /** The scorecard header shared by `scan` and `audit-skills` output. */
189
+ const renderVerdict = (result, command) => {
190
+ const counts = result.score.counts;
191
+ const parts = SEVERITY_ORDER.filter((severity) => counts[severity] > 0).map((severity) => `${counts[severity]} ${severity}`);
192
+ const verb = command === 'scan' ? 'Scanned' : 'Audited';
193
+ return [
194
+ `vibe-shield ${result.version} — ${verb} ${result.stats.filesScanned} file(s) in ${result.stats.durationMs}ms`,
195
+ `Grade ${result.score.grade} — ${result.score.score}/100${parts.length ? ` · ${parts.join(', ')}` : ' · no findings'}`,
196
+ ].join('\n');
197
+ };
198
+ /** Serialise a scan for the model: verdict, findings, then machine fields. */
199
+ const toToolResult = (result, options) => {
200
+ const filtered = options.minimumSeverity
201
+ ? result.findings.filter((finding) => atOrAbove(finding.severity, options.minimumSeverity))
202
+ : result.findings;
203
+ const shown = options.limit > 0 ? filtered.slice(0, options.limit) : filtered;
204
+ const sections = [renderVerdict(result, options.command)];
205
+ if (result.stats.filesScanned === 0) {
206
+ // Silence is not a pass — say so rather than returning an empty, green result.
207
+ sections.push('', 'No files were scanned. Check `target`, and whether an ignore file excludes it.');
208
+ }
209
+ else if (filtered.length === 0) {
210
+ sections.push('', 'No findings at or above the requested severity.');
211
+ }
212
+ else {
213
+ sections.push('', 'Findings (highest severity first):', '');
214
+ for (const finding of shown)
215
+ sections.push(renderFinding(finding), '');
216
+ if (filtered.length > shown.length) {
217
+ sections.push(`… and ${filtered.length - shown.length} more. Raise \`limit\` or filter by \`rules\`.`);
218
+ }
219
+ sections.push('Fix the root cause: do not delete the auth check, disable RLS or suppress the finding to make it disappear.');
220
+ }
221
+ if (result.notices.length)
222
+ sections.push('', ...result.notices.map((notice) => `note: ${notice}`));
223
+ return {
224
+ content: [{ type: 'text', text: sections.join('\n').trimEnd() }],
225
+ structuredContent: {
226
+ score: result.score.score,
227
+ grade: result.score.grade,
228
+ counts: result.score.counts,
229
+ total: filtered.length,
230
+ filesScanned: result.stats.filesScanned,
231
+ durationMs: result.stats.durationMs,
232
+ rootDir: result.rootDir,
233
+ findings: shown.map((finding) => ({
234
+ ruleId: finding.ruleId,
235
+ slug: finding.slug,
236
+ severity: finding.severity,
237
+ confidence: finding.confidence,
238
+ category: finding.category,
239
+ title: finding.title,
240
+ message: finding.message,
241
+ filePath: finding.filePath,
242
+ line: finding.line,
243
+ column: finding.column,
244
+ snippet: finding.snippet,
245
+ cwe: finding.cwe,
246
+ remediation: finding.remediation,
247
+ secureExample: finding.secureExample,
248
+ references: finding.references,
249
+ })),
250
+ notices: result.notices,
251
+ },
252
+ };
253
+ };
254
+ /** Full rule documentation as text + structured content. */
255
+ const explainRuleResult = (rule) => {
256
+ const text = [
257
+ `${rule.id} — ${rule.title}`,
258
+ `${rule.category} · ${rule.severity} severity · ${rule.confidence} confidence · ${rule.cwe.id} ${rule.cwe.name}`,
259
+ '',
260
+ rule.description,
261
+ '',
262
+ 'Vulnerable',
263
+ rule.insecureExample,
264
+ '',
265
+ 'Fixed',
266
+ rule.secureExample,
267
+ '',
268
+ 'Remediation',
269
+ ...rule.remediation.map((step, index) => `${index + 1}. ${step}`),
270
+ '',
271
+ 'References',
272
+ ...rule.references.map((reference) => `- ${reference}`),
273
+ ].join('\n');
274
+ return {
275
+ content: [{ type: 'text', text }],
276
+ structuredContent: {
277
+ id: rule.id,
278
+ slug: rule.slug,
279
+ title: rule.title,
280
+ category: rule.category,
281
+ severity: rule.severity,
282
+ confidence: rule.confidence,
283
+ cwe: rule.cwe,
284
+ description: rule.description,
285
+ insecureExample: rule.insecureExample,
286
+ secureExample: rule.secureExample,
287
+ remediation: rule.remediation,
288
+ references: rule.references,
289
+ },
290
+ };
291
+ };
292
+ const errorResult = (message) => ({
293
+ content: [{ type: 'text', text: message }],
294
+ isError: true,
295
+ });
296
+ /* -------------------------------------------------------------------------- */
297
+ /* Tool dispatch */
298
+ /* -------------------------------------------------------------------------- */
299
+ /** Resolve a client-supplied target against the server's working directory. */
300
+ const resolveTarget = (target, cwd) => target ? path.resolve(cwd, target) : cwd;
301
+ /** Execute one tool by name. Never throws: failures become `isError` results. */
302
+ async function callTool(name, args, options = {}) {
303
+ const cwd = options.cwd ?? process.cwd();
304
+ try {
305
+ if (name === TOOL_SCAN) {
306
+ const minimumSeverity = asString(args.severity);
307
+ if (minimumSeverity && !(0, types_1.isSeverity)(minimumSeverity)) {
308
+ return errorResult(`Unknown severity "${minimumSeverity}". Use one of: ${SEVERITY_ORDER.join(', ')}.`);
309
+ }
310
+ const selection = asStringArray(args.rules);
311
+ const result = await (0, engine_1.scanProject)({
312
+ rootDir: resolveTarget(asString(args.target), cwd),
313
+ paths: asStringArray(args.paths),
314
+ includeTests: asBoolean(args.includeTests),
315
+ includeDocs: asBoolean(args.includeDocs),
316
+ rules: selection?.length ? selection.flatMap((selector) => rulesFor(selector)) : undefined,
317
+ useGitIndex: false,
318
+ });
319
+ return toToolResult(result, {
320
+ limit: asLimit(args.limit, 25),
321
+ minimumSeverity: minimumSeverity,
322
+ command: 'scan',
323
+ });
324
+ }
325
+ if (name === TOOL_AUDIT) {
326
+ const result = await (0, skill_auditor_1.auditSkills)(resolveTarget(asString(args.target), cwd), {
327
+ paths: asStringArray(args.paths),
328
+ });
329
+ return toToolResult(result, { limit: asLimit(args.limit, 25), command: 'audit-skills' });
330
+ }
331
+ if (name === TOOL_EXPLAIN) {
332
+ const ruleId = asString(args.ruleId);
333
+ if (!ruleId)
334
+ return errorResult('`ruleId` is required, e.g. "VSB-CLIENT-001".');
335
+ const rule = (0, rules_1.getRule)(ruleId);
336
+ if (!rule)
337
+ return errorResult(`Unknown rule "${ruleId}". Call tools/list or \`npx github:avivgerz-hue/vibe-shield rules\` for the catalogue.`);
338
+ return explainRuleResult(rule);
339
+ }
340
+ return errorResult(`Unknown tool "${name}".`);
341
+ }
342
+ catch (error) {
343
+ return errorResult(`vibe-shield failed: ${error instanceof Error ? error.message : String(error)}`);
344
+ }
345
+ }
346
+ ;
347
+ /** Expand a rule selector (`VSB-DB-*`, exact id or slug) against the catalogue. */
348
+ const rulesFor = (selector) => {
349
+ const exact = (0, rules_1.getRule)(selector);
350
+ if (exact)
351
+ return [exact];
352
+ const normalized = selector.trim().toUpperCase();
353
+ if (normalized.endsWith('*')) {
354
+ const prefix = normalized.slice(0, -1);
355
+ return rules_1.ALL_RULES.filter((rule) => rule.id.startsWith(prefix));
356
+ }
357
+ return [];
358
+ };
359
+ /* -------------------------------------------------------------------------- */
360
+ /* JSON-RPC layer */
361
+ /* -------------------------------------------------------------------------- */
362
+ const isRequest = (value) => {
363
+ if (typeof value !== 'object' || value === null)
364
+ return false;
365
+ const candidate = value;
366
+ return candidate.jsonrpc === '2.0' && typeof candidate.method === 'string';
367
+ };
368
+ /** True when the message is a notification and must not receive a reply. */
369
+ const isNotification = (value) => !('id' in value) || value.id === undefined;
370
+ /**
371
+ * Handle one decoded JSON-RPC message.
372
+ * Returns `null` for notifications, which the spec forbids answering.
373
+ */
374
+ async function handleMessage(message, options = {}) {
375
+ if (!isRequest(message)) {
376
+ // A malformed message still gets a parse/invalid-request error with a null id.
377
+ return {
378
+ jsonrpc: '2.0',
379
+ id: null,
380
+ error: { code: exports.RpcError.invalidRequest, message: 'Invalid JSON-RPC 2.0 request.' },
381
+ };
382
+ }
383
+ const id = message.id ?? null;
384
+ const notification = isNotification(message);
385
+ const params = (message.params ?? {});
386
+ const reply = (result) => notification ? null : { jsonrpc: '2.0', id, result };
387
+ const fail = (code, errorMessage) => notification ? null : { jsonrpc: '2.0', id, error: { code, message: errorMessage } };
388
+ switch (message.method) {
389
+ case 'initialize':
390
+ return reply({
391
+ protocolVersion: exports.MCP_PROTOCOL_VERSION,
392
+ capabilities: { tools: { listChanged: false } },
393
+ serverInfo: {
394
+ name: exports.SERVER_NAME,
395
+ title: 'vibe-shield',
396
+ version: options.version ?? '1.1.0',
397
+ },
398
+ instructions: 'vibe-shield finds security bugs that AI coding agents ship: leaked secrets, unguarded API routes, missing Postgres RLS, SQL injection, prompt injection and poisoned agent skills. Call vibe_shield_scan after writing application code and before reporting a task as done, and vibe_shield_audit_skills after touching any agent instruction or MCP config file.',
399
+ });
400
+ // Notifications the client sends that need no reply.
401
+ case 'notifications/initialized':
402
+ case 'notifications/cancelled':
403
+ case 'initialized':
404
+ return null;
405
+ case 'ping':
406
+ return reply({});
407
+ case 'tools/list':
408
+ return reply({ tools: exports.MCP_TOOLS });
409
+ case 'tools/call': {
410
+ const name = asString(params.name);
411
+ if (!name)
412
+ return fail(exports.RpcError.invalidParams, '`name` is required.');
413
+ const args = (params.arguments ?? {});
414
+ const result = await callTool(name, args, options);
415
+ return reply(result);
416
+ }
417
+ case 'resources/list':
418
+ return reply({ resources: [] });
419
+ case 'prompts/list':
420
+ return reply({ prompts: [] });
421
+ default:
422
+ return fail(exports.RpcError.methodNotFound, `Method not found: ${message.method}`);
423
+ }
424
+ }
425
+ /**
426
+ * Serve MCP over newline-delimited JSON-RPC until stdin closes.
427
+ *
428
+ * Messages are processed sequentially so responses preserve request order and a
429
+ * slow scan cannot interleave with a later `tools/call`.
430
+ */
431
+ function serveMcp(io, options = {}) {
432
+ let buffer = '';
433
+ let handled = 0;
434
+ let queue = Promise.resolve();
435
+ return new Promise((resolve) => {
436
+ const finish = () => {
437
+ void queue.then(() => {
438
+ options.onClose?.(handled);
439
+ resolve(handled);
440
+ });
441
+ };
442
+ io.stdin.on('data', (chunk) => {
443
+ buffer += chunk.toString();
444
+ let newline = buffer.indexOf('\n');
445
+ while (newline !== -1) {
446
+ const line = buffer.slice(0, newline).trim();
447
+ buffer = buffer.slice(newline + 1);
448
+ newline = buffer.indexOf('\n');
449
+ if (!line)
450
+ continue;
451
+ queue = queue.then(async () => {
452
+ let parsed;
453
+ try {
454
+ parsed = JSON.parse(line);
455
+ }
456
+ catch {
457
+ io.stdout.write(`${JSON.stringify({
458
+ jsonrpc: '2.0',
459
+ id: null,
460
+ error: { code: exports.RpcError.parseError, message: 'Parse error: invalid JSON.' },
461
+ })}\n`);
462
+ handled += 1;
463
+ return;
464
+ }
465
+ const response = await handleMessage(parsed, options);
466
+ if (response)
467
+ io.stdout.write(`${JSON.stringify(response)}\n`);
468
+ handled += 1;
469
+ });
470
+ }
471
+ });
472
+ io.stdin.on('end', finish);
473
+ io.stdin.on('close', finish);
474
+ });
475
+ }
476
+ /** Convenience wrapper used by `vibe-shield mcp`. */
477
+ const startMcpServer = (options = {}) => serveMcp({ stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }, options);
478
+ exports.startMcpServer = startMcpServer;
479
+ exports.MCP_TOOL_NAMES = [TOOL_SCAN, TOOL_AUDIT, TOOL_EXPLAIN];