@securitychecks/mcp 0.1.1-rc.1

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/LICENSE ADDED
@@ -0,0 +1,87 @@
1
+ SecurityChecks Proprietary License
2
+
3
+ Copyright (c) 2024-2026 SecurityChecks. All rights reserved.
4
+
5
+ TERMS AND CONDITIONS
6
+
7
+ 1. DEFINITIONS
8
+
9
+ "Software" means the SecurityChecks CLI, collector, MCP server, and all
10
+ associated documentation, source code, and compiled binaries.
11
+
12
+ "License Key" means a valid subscription key obtained from SecurityChecks.
13
+
14
+ "Free Tier" means usage of the Software without a License Key, subject to
15
+ the limitations described in Section 3.
16
+
17
+ 2. GRANT OF LICENSE
18
+
19
+ Subject to the terms of this License, SecurityChecks grants you a limited,
20
+ non-exclusive, non-transferable license to:
21
+
22
+ a) Install and use the Software for your internal business purposes
23
+ b) Make copies of the Software for backup purposes only
24
+ c) Use the Software in continuous integration/continuous deployment pipelines
25
+
26
+ 3. FREE TIER LIMITATIONS
27
+
28
+ Without a valid License Key, you may use the Software subject to:
29
+
30
+ a) Maximum of 10 scans per calendar month
31
+ b) Basic finding output only (no SARIF export)
32
+ c) No access to calibration API features
33
+ d) No access to Pro patterns
34
+ e) Community support only
35
+
36
+ 4. RESTRICTIONS
37
+
38
+ You may NOT:
39
+
40
+ a) Distribute, sublicense, lease, rent, or lend the Software to third parties
41
+ b) Modify, adapt, translate, reverse engineer, decompile, or disassemble the Software
42
+ c) Remove or alter any proprietary notices, labels, or marks on the Software
43
+ d) Use the Software to create a competing product or service
44
+ e) Share or publish License Keys
45
+ f) Circumvent any license enforcement mechanisms
46
+ g) Use the Software for illegal purposes
47
+
48
+ 5. INTELLECTUAL PROPERTY
49
+
50
+ The Software and all copies thereof are proprietary to SecurityChecks and
51
+ title thereto remains exclusively with SecurityChecks. All rights in the
52
+ Software not specifically granted in this License are reserved to SecurityChecks.
53
+
54
+ 6. DISCLAIMER OF WARRANTY
55
+
56
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
57
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
58
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
59
+
60
+ 7. LIMITATION OF LIABILITY
61
+
62
+ IN NO EVENT SHALL SECURITYCHECKS BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
63
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN CONNECTION
64
+ WITH THIS LICENSE OR THE USE OF THE SOFTWARE.
65
+
66
+ 8. TERMINATION
67
+
68
+ This License is effective until terminated. SecurityChecks may terminate
69
+ this License immediately if you breach any term. Upon termination, you must
70
+ destroy all copies of the Software in your possession.
71
+
72
+ 9. GOVERNING LAW
73
+
74
+ This License shall be governed by and construed in accordance with the laws
75
+ of the State of Delaware, United States, without regard to its conflict of
76
+ law provisions.
77
+
78
+ 10. ENTIRE AGREEMENT
79
+
80
+ This License constitutes the entire agreement between you and SecurityChecks
81
+ regarding the Software and supersedes all prior agreements and understandings.
82
+
83
+ ---
84
+
85
+ For licensing inquiries: licensing@securitychecks.ai
86
+ For support: support@securitychecks.ai
87
+ Website: https://securitychecks.ai
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @securitychecks/mcp
2
+
3
+ > **Review AI-generated code with AI** — MCP server for production-ready code review.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@securitychecks/mcp.svg?style=flat-square)](https://www.npmjs.com/package/@securitychecks/mcp)
6
+
7
+ MCP server that lets Claude catch what Copilot misses — webhook idempotency, service-layer auth, transaction safety.
8
+
9
+ ## What is this?
10
+
11
+ Your AI assistant writes code. This gives it the ability to review that code for production-readiness.
12
+
13
+ **The loop:** Copilot/Cursor writes → Claude reviews via MCP → Ship with confidence.
14
+
15
+ Claude can now check for the patterns that cause production incidents, based on what staff engineers actually catch in review.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install -g @securitychecks/mcp
21
+ ```
22
+
23
+ ## Usage with Claude Code
24
+
25
+ Add to your Claude Code MCP configuration:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "scheck": {
31
+ "command": "scheck-mcp",
32
+ "args": [],
33
+ "env": {
34
+ "SCHECK_MCP_ALLOWED_ROOTS": "."
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### Allowed roots (required)
42
+ For safety, `scheck-mcp` will only run inside the allowed roots. If you don’t set `SCHECK_MCP_ALLOWED_ROOTS` and the server is not started inside a git repository, it will refuse to scan.
43
+
44
+ ## Available Tools
45
+
46
+ ### `scheck_run`
47
+ Run scheck on the codebase.
48
+
49
+ ```
50
+ Arguments:
51
+ - path (optional): Target path to audit
52
+ - include_context (optional): Include code context snippets in results
53
+ - max_findings (optional): Limit number of findings returned (default: 200)
54
+ - only (optional): Only run specific invariant checks by ID
55
+ - skip (optional): Skip specific invariant checks by ID
56
+ ```
57
+
58
+ ### `scheck_list_findings`
59
+ List current findings from the last run.
60
+
61
+ ```
62
+ Arguments:
63
+ - severity (optional): Filter by severity (P0, P1, P2)
64
+ - include_context (optional): Include code context snippets in results
65
+ - max_findings (optional): Limit number of findings returned (default: 200)
66
+ ```
67
+
68
+ ### `scheck_explain`
69
+ Explain an invariant - what a staff engineer would say about it.
70
+
71
+ ```
72
+ Arguments:
73
+ - invariant_id: The invariant to explain (e.g., "AUTHZ.SERVICE_LAYER.ENFORCED")
74
+ ```
75
+
76
+ ### `scheck_list_invariants`
77
+ List all patterns a staff engineer checks for.
78
+
79
+ ### `scheck_generate_test`
80
+ Generate a test skeleton to prove an invariant is satisfied.
81
+
82
+ ```
83
+ Arguments:
84
+ - invariant_id: The invariant to generate a test for
85
+ - framework (optional): Test framework (jest, vitest, playwright)
86
+ - context (optional): Extra context to generate a more targeted test
87
+ ```
88
+
89
+ ### `scheck_feedback`
90
+ Report whether a finding was a true positive or false positive.
91
+
92
+ ```
93
+ Arguments:
94
+ - invariant_id: Invariant ID (e.g., AUTHZ.SERVICE_LAYER.ENFORCED)
95
+ - verdict: true_positive or false_positive
96
+ - reason (optional): not_applicable, acceptable_risk, wrong_location, outdated_pattern, missing_context, other
97
+ ```
98
+
99
+ ## Example Session
100
+
101
+ ```
102
+ User: Check my code for issues a senior engineer would catch
103
+
104
+ Claude: [calls scheck_run]
105
+
106
+ Found 2 issues a staff engineer would flag:
107
+
108
+ 1. **AUTHZ.SERVICE_LAYER.ENFORCED** (P0)
109
+ Service "MembershipService" has exports without auth checks
110
+ Location: src/services/membership.ts:12
111
+
112
+ A staff engineer would ask: "What happens when a background
113
+ job calls removeMember() directly, bypassing the route?"
114
+
115
+ 2. **WEBHOOK.IDEMPOTENT** (P0)
116
+ Webhook handler missing idempotency check
117
+ Location: src/api/webhooks/stripe.ts:45
118
+
119
+ A staff engineer would ask: "What happens when Stripe
120
+ retries this webhook?"
121
+
122
+ User: Explain the webhook issue
123
+
124
+ Claude: [calls scheck_explain with invariant_id="WEBHOOK.IDEMPOTENT"]
125
+
126
+ Webhooks can be delivered multiple times. Without idempotency,
127
+ you might double-charge customers, send duplicate emails, or
128
+ corrupt data...
129
+ ```
130
+
131
+ ## Why MCP?
132
+
133
+ AI writes code fast but doesn't reason about production scenarios:
134
+
135
+ - Webhook retries → double-charges
136
+ - Internal service calls → auth bypass
137
+ - Transaction rollbacks → phantom emails
138
+
139
+ This MCP server gives Claude the ability to catch these patterns — the things AI-generated code routinely misses.
140
+
141
+ ## Enterprise
142
+
143
+ For teams with compliance requirements:
144
+
145
+ - **Audit trails:** Every AI-assisted review is logged
146
+ - **Local analysis:** SOC2 compliant — no source code transmission
147
+ - **Consistent patterns:** Same staff check for all developers
148
+
149
+ ## License
150
+
151
+ Proprietary. See [LICENSE](../../LICENSE) for details.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { getInvariantById, ALL_INVARIANTS } from '@securitychecks/collector';
6
+ import { generateTestSkeleton, getStaffQuestion, audit } from '@securitychecks/cli';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+ import { execSync } from 'child_process';
10
+
11
+ function parseAllowedRootsFromEnv(env, cwd) {
12
+ const raw = env["SCHECK_MCP_ALLOWED_ROOTS"] ?? env["MCP_ALLOWED_ROOTS"] ?? env["SCHECK_ALLOWED_ROOTS"];
13
+ const roots = (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean).map((p) => path.resolve(cwd, p));
14
+ if (roots.length > 0) return roots;
15
+ const gitRoot = getGitRoot(cwd);
16
+ if (gitRoot) return [gitRoot];
17
+ throw new Error(
18
+ "Refusing to scan because no allowed roots are configured and no git repository was detected. Run scheck-mcp from inside a git repo, or set SCHECK_MCP_ALLOWED_ROOTS (or MCP_ALLOWED_ROOTS)."
19
+ );
20
+ }
21
+ function getGitRoot(cwd) {
22
+ try {
23
+ const out = execSync("git rev-parse --show-toplevel", {
24
+ cwd,
25
+ stdio: ["ignore", "pipe", "ignore"],
26
+ encoding: "utf-8"
27
+ }).trim();
28
+ return out.length > 0 ? out : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function isWithinRoot(candidatePath, root) {
34
+ const relative = path.relative(root, candidatePath);
35
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
36
+ }
37
+ function resolveAndValidateTargetPath(requestedPath, options) {
38
+ const { cwd, allowedRoots } = options;
39
+ const input = requestedPath && requestedPath.trim().length > 0 ? requestedPath : cwd;
40
+ const resolved = path.resolve(cwd, input);
41
+ if (!fs.existsSync(resolved)) {
42
+ throw new Error(`Target path does not exist: ${resolved}`);
43
+ }
44
+ const realResolved = fs.realpathSync(resolved);
45
+ const realAllowedRoots = allowedRoots.map((r) => fs.realpathSync(path.resolve(cwd, r)));
46
+ const allowed = realAllowedRoots.some((root) => isWithinRoot(realResolved, root));
47
+ if (!allowed) {
48
+ const rootsList = realAllowedRoots.join(", ");
49
+ throw new Error(
50
+ `Refusing to scan outside allowed roots. Requested: ${realResolved}. Allowed roots: ${rootsList}. Set SCHECK_MCP_ALLOWED_ROOTS (or MCP_ALLOWED_ROOTS) to override.`
51
+ );
52
+ }
53
+ return realResolved;
54
+ }
55
+ function formatEvidenceForMcp(evidence, includeContext) {
56
+ if (includeContext) return evidence;
57
+ return evidence.map(({ file, line }) => ({ file, line }));
58
+ }
59
+
60
+ // src/feedback.ts
61
+ async function handleFeedbackTool(args, options) {
62
+ const invariantId = args?.invariant_id;
63
+ const verdict = args?.verdict;
64
+ const reason = args?.reason;
65
+ if (!invariantId || !verdict) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: "text",
70
+ text: "Error: invariant_id and verdict are required."
71
+ }
72
+ ],
73
+ isError: true
74
+ };
75
+ }
76
+ if (!["true_positive", "false_positive"].includes(verdict)) {
77
+ return {
78
+ content: [
79
+ {
80
+ type: "text",
81
+ text: `Error: verdict must be "true_positive" or "false_positive", got "${verdict}"`
82
+ }
83
+ ],
84
+ isError: true
85
+ };
86
+ }
87
+ try {
88
+ const { writeFileSync, readFileSync, existsSync, mkdirSync, lstatSync } = await import('fs');
89
+ const { join } = await import('path');
90
+ const cwd = options?.cwd ?? process.cwd();
91
+ const feedbackDir = join(cwd, ".scheck");
92
+ const feedbackFile = join(feedbackDir, "feedback.json");
93
+ const now = options?.now ?? (() => /* @__PURE__ */ new Date());
94
+ if (!existsSync(feedbackDir)) {
95
+ mkdirSync(feedbackDir, { recursive: true });
96
+ } else {
97
+ const st = lstatSync(feedbackDir);
98
+ if (st.isSymbolicLink()) {
99
+ throw new Error("Refusing to write feedback: .scheck is a symlink");
100
+ }
101
+ if (!st.isDirectory()) {
102
+ throw new Error("Refusing to write feedback: .scheck is not a directory");
103
+ }
104
+ }
105
+ if (existsSync(feedbackFile)) {
106
+ const st = lstatSync(feedbackFile);
107
+ if (st.isSymbolicLink()) {
108
+ throw new Error("Refusing to write feedback: feedback.json is a symlink");
109
+ }
110
+ if (!st.isFile()) {
111
+ throw new Error("Refusing to write feedback: feedback.json is not a file");
112
+ }
113
+ }
114
+ let feedbackData = [];
115
+ if (existsSync(feedbackFile)) {
116
+ try {
117
+ feedbackData = JSON.parse(readFileSync(feedbackFile, "utf-8"));
118
+ } catch {
119
+ feedbackData = [];
120
+ }
121
+ }
122
+ feedbackData.push({
123
+ invariantId,
124
+ verdict,
125
+ reason,
126
+ timestamp: now().toISOString()
127
+ });
128
+ writeFileSync(feedbackFile, JSON.stringify(feedbackData, null, 2));
129
+ try {
130
+ const clientVersion = "0.1.1-rc.1";
131
+ const endpoint = "https://api.securitychecks.ai/v1/feedback";
132
+ const controller = new AbortController();
133
+ const timeoutId = setTimeout(() => controller.abort(), 3e3);
134
+ const fetchFn = options?.fetchFn ?? (typeof fetch === "function" ? fetch : void 0);
135
+ if (fetchFn) {
136
+ fetchFn(endpoint, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify({ invariantId, verdict, reason, clientVersion }),
140
+ signal: controller.signal
141
+ }).catch(() => {
142
+ }).finally(() => clearTimeout(timeoutId));
143
+ } else {
144
+ clearTimeout(timeoutId);
145
+ }
146
+ } catch {
147
+ }
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: JSON.stringify(
153
+ {
154
+ recorded: true,
155
+ invariantId,
156
+ verdict,
157
+ reason: reason ?? null,
158
+ storedLocally: true
159
+ },
160
+ null,
161
+ 2
162
+ )
163
+ }
164
+ ]
165
+ };
166
+ } catch (error) {
167
+ return {
168
+ content: [
169
+ {
170
+ type: "text",
171
+ text: `Error recording feedback: ${error instanceof Error ? error.message : String(error)}`
172
+ }
173
+ ],
174
+ isError: true
175
+ };
176
+ }
177
+ }
178
+
179
+ // src/index.ts
180
+ var version = "0.1.1-rc.1";
181
+ var server = new Server(
182
+ {
183
+ name: "scheck",
184
+ version
185
+ },
186
+ {
187
+ capabilities: {
188
+ tools: {}
189
+ }
190
+ }
191
+ );
192
+ var TOOL_PREFIXES = ["scheck"];
193
+ var TOOL_DEFS = [
194
+ {
195
+ suffix: "run",
196
+ description: "Run scheck \u2014 the patterns a senior engineer would flag in review. Catches webhook idempotency, auth at service layer, transaction safety, and more.",
197
+ inputSchema: {
198
+ type: "object",
199
+ properties: {
200
+ path: {
201
+ type: "string",
202
+ description: "Target path to audit (default: current directory)"
203
+ },
204
+ include_context: {
205
+ type: "boolean",
206
+ description: "Include code context snippets in results (may expose source code to the assistant)."
207
+ },
208
+ max_findings: {
209
+ type: "integer",
210
+ minimum: 1,
211
+ maximum: 500,
212
+ description: "Limit number of findings returned (default: 200)"
213
+ },
214
+ only: {
215
+ type: "array",
216
+ items: { type: "string" },
217
+ description: "Only run specific invariant checks by ID"
218
+ },
219
+ skip: {
220
+ type: "array",
221
+ items: { type: "string" },
222
+ description: "Skip specific invariant checks by ID"
223
+ }
224
+ }
225
+ }
226
+ },
227
+ {
228
+ suffix: "list_findings",
229
+ description: "List issues a staff engineer would flag \u2014 current findings from the last run, by severity.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ severity: {
234
+ type: "string",
235
+ enum: ["P0", "P1", "P2"],
236
+ description: "Filter findings by severity"
237
+ },
238
+ include_context: {
239
+ type: "boolean",
240
+ description: "Include code context snippets in results (may expose source code to the assistant)."
241
+ },
242
+ max_findings: {
243
+ type: "integer",
244
+ minimum: 1,
245
+ maximum: 500,
246
+ description: "Limit number of findings returned (default: 200)"
247
+ }
248
+ }
249
+ }
250
+ },
251
+ {
252
+ suffix: "explain",
253
+ description: "What a staff engineer checks: explain why a pattern matters, real incidents it prevents, and proof needed.",
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {
257
+ invariant_id: {
258
+ type: "string",
259
+ description: "The invariant ID to explain (e.g., AUTHZ.SERVICE_LAYER.ENFORCED)"
260
+ }
261
+ },
262
+ required: ["invariant_id"]
263
+ }
264
+ },
265
+ {
266
+ suffix: "list_invariants",
267
+ description: "List all patterns a staff engineer checks for \u2014 the patterns that prevent production incidents.",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ category: {
272
+ type: "string",
273
+ description: "Filter by category (authz, revocation, webhooks, transactions, etc.)"
274
+ },
275
+ severity: {
276
+ type: "string",
277
+ enum: ["P0", "P1", "P2"],
278
+ description: "Filter by severity"
279
+ }
280
+ }
281
+ }
282
+ },
283
+ {
284
+ suffix: "generate_test",
285
+ description: "Generate test code that proves a pattern is enforced \u2014 the proof a staff engineer would ask for.",
286
+ inputSchema: {
287
+ type: "object",
288
+ properties: {
289
+ invariant_id: {
290
+ type: "string",
291
+ description: "The invariant ID to generate a test for"
292
+ },
293
+ framework: {
294
+ type: "string",
295
+ enum: ["jest", "vitest", "playwright"],
296
+ description: "Test framework to generate code for (default: vitest)"
297
+ },
298
+ context: {
299
+ type: "string",
300
+ description: "Additional context about the specific violation to generate a more targeted test"
301
+ }
302
+ },
303
+ required: ["invariant_id"]
304
+ }
305
+ },
306
+ {
307
+ suffix: "feedback",
308
+ description: "Report whether a finding was a true positive or false positive to improve accuracy.",
309
+ inputSchema: {
310
+ type: "object",
311
+ properties: {
312
+ invariant_id: {
313
+ type: "string",
314
+ description: "Invariant ID (e.g., AUTHZ.SERVICE_LAYER.ENFORCED)"
315
+ },
316
+ verdict: {
317
+ type: "string",
318
+ enum: ["true_positive", "false_positive"],
319
+ description: "Whether the finding was a true positive or false positive"
320
+ },
321
+ reason: {
322
+ type: "string",
323
+ enum: [
324
+ "not_applicable",
325
+ "acceptable_risk",
326
+ "wrong_location",
327
+ "outdated_pattern",
328
+ "missing_context"
329
+ ],
330
+ description: "Reason for the verdict"
331
+ }
332
+ },
333
+ required: ["invariant_id", "verdict"]
334
+ }
335
+ }
336
+ ];
337
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
338
+ return {
339
+ tools: TOOL_PREFIXES.flatMap(
340
+ (prefix) => TOOL_DEFS.map((def) => ({
341
+ name: `${prefix}_${def.suffix}`,
342
+ description: def.description,
343
+ inputSchema: def.inputSchema
344
+ }))
345
+ )
346
+ };
347
+ });
348
+ var lastAuditResult = null;
349
+ server.setRequestHandler(
350
+ CallToolRequestSchema,
351
+ async (request) => {
352
+ const { name, arguments: args } = request.params;
353
+ const tool = normalizeToolName(name);
354
+ if (!tool) {
355
+ return {
356
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
357
+ isError: true
358
+ };
359
+ }
360
+ switch (tool) {
361
+ case "run": {
362
+ const path2 = args?.["path"] || process.cwd();
363
+ const only = args?.["only"];
364
+ const skip = args?.["skip"];
365
+ const includeContext = args?.["include_context"] === true;
366
+ const maxFindings = typeof args?.["max_findings"] === "number" ? Math.max(1, Math.min(500, args["max_findings"])) : 200;
367
+ try {
368
+ const allowedRoots = parseAllowedRootsFromEnv(process.env, process.cwd());
369
+ const targetPath = resolveAndValidateTargetPath(path2, {
370
+ cwd: process.cwd(),
371
+ allowedRoots
372
+ });
373
+ const result = await audit({
374
+ targetPath,
375
+ only,
376
+ skip
377
+ });
378
+ lastAuditResult = result;
379
+ const findings = result.results.flatMap((r) => r.findings);
380
+ const truncated = findings.length > maxFindings;
381
+ const findingsToReturn = truncated ? findings.slice(0, maxFindings) : findings;
382
+ const summary = {
383
+ total_checks: result.summary.total,
384
+ passed: result.summary.passed,
385
+ failed: result.summary.failed,
386
+ findings_count: findings.length,
387
+ by_severity: result.summary.byPriority,
388
+ truncated,
389
+ max_findings: maxFindings
390
+ };
391
+ const formattedFindings = findingsToReturn.map((f) => ({
392
+ invariant_id: f.invariantId,
393
+ severity: f.severity,
394
+ message: f.message,
395
+ evidence: formatEvidenceForMcp(f.evidence, includeContext),
396
+ required_proof: f.requiredProof,
397
+ suggested_test: f.suggestedTest,
398
+ staff_engineer_asks: getStaffQuestion(f.invariantId)
399
+ }));
400
+ return {
401
+ content: [
402
+ {
403
+ type: "text",
404
+ text: JSON.stringify(
405
+ {
406
+ summary,
407
+ findings: formattedFindings
408
+ },
409
+ null,
410
+ 2
411
+ )
412
+ }
413
+ ]
414
+ };
415
+ } catch (error) {
416
+ return {
417
+ content: [
418
+ {
419
+ type: "text",
420
+ text: formatMcpRunError(error)
421
+ }
422
+ ],
423
+ isError: true
424
+ };
425
+ }
426
+ }
427
+ case "list_findings": {
428
+ if (!lastAuditResult) {
429
+ return {
430
+ content: [
431
+ {
432
+ type: "text",
433
+ text: "No scan has been run yet. Use scheck_run first."
434
+ }
435
+ ]
436
+ };
437
+ }
438
+ const severity = args?.["severity"];
439
+ const includeContext = args?.["include_context"] === true;
440
+ const maxFindings = typeof args?.["max_findings"] === "number" ? Math.max(1, Math.min(500, args["max_findings"])) : 200;
441
+ let findings = lastAuditResult.results.flatMap((r) => r.findings);
442
+ if (severity) {
443
+ findings = findings.filter((f) => f.severity === severity);
444
+ }
445
+ const truncated = findings.length > maxFindings;
446
+ const findingsToReturn = truncated ? findings.slice(0, maxFindings) : findings;
447
+ return {
448
+ content: [
449
+ {
450
+ type: "text",
451
+ text: JSON.stringify(
452
+ {
453
+ findings: findingsToReturn.map((f) => ({
454
+ invariant_id: f.invariantId,
455
+ severity: f.severity,
456
+ message: f.message,
457
+ evidence: formatEvidenceForMcp(f.evidence, includeContext)
458
+ })),
459
+ meta: {
460
+ total: findings.length,
461
+ truncated,
462
+ max_findings: maxFindings
463
+ }
464
+ },
465
+ null,
466
+ 2
467
+ )
468
+ }
469
+ ]
470
+ };
471
+ }
472
+ case "explain": {
473
+ const invariantId = args?.["invariant_id"];
474
+ const invariant = getInvariantById(invariantId);
475
+ if (!invariant) {
476
+ return {
477
+ content: [
478
+ {
479
+ type: "text",
480
+ text: `Unknown pattern: ${invariantId}. Use scheck_list_invariants to see available patterns.`
481
+ }
482
+ ]
483
+ };
484
+ }
485
+ return {
486
+ content: [
487
+ {
488
+ type: "text",
489
+ text: JSON.stringify(
490
+ {
491
+ id: invariant.id,
492
+ name: invariant.name,
493
+ severity: invariant.severity,
494
+ category: invariant.category,
495
+ description: invariant.description,
496
+ required_proof: invariant.requiredProof,
497
+ staff_engineer_asks: getStaffQuestion(invariant.id)
498
+ },
499
+ null,
500
+ 2
501
+ )
502
+ }
503
+ ]
504
+ };
505
+ }
506
+ case "list_invariants": {
507
+ const category = args?.["category"];
508
+ const severity = args?.["severity"];
509
+ let invariants = ALL_INVARIANTS;
510
+ if (category) {
511
+ invariants = invariants.filter((i) => i.category === category);
512
+ }
513
+ if (severity) {
514
+ invariants = invariants.filter((i) => i.severity === severity);
515
+ }
516
+ return {
517
+ content: [
518
+ {
519
+ type: "text",
520
+ text: JSON.stringify(
521
+ invariants.map((i) => ({
522
+ id: i.id,
523
+ name: i.name,
524
+ severity: i.severity,
525
+ category: i.category
526
+ })),
527
+ null,
528
+ 2
529
+ )
530
+ }
531
+ ]
532
+ };
533
+ }
534
+ case "generate_test": {
535
+ const invariantId = args?.["invariant_id"];
536
+ const framework = args?.["framework"] || "vitest";
537
+ const context = args?.["context"];
538
+ const invariant = getInvariantById(invariantId);
539
+ if (!invariant) {
540
+ return {
541
+ content: [
542
+ {
543
+ type: "text",
544
+ text: `Unknown invariant: ${invariantId}`
545
+ }
546
+ ]
547
+ };
548
+ }
549
+ const test = generateTestSkeleton(invariant, framework, context);
550
+ return {
551
+ content: [
552
+ {
553
+ type: "text",
554
+ text: test
555
+ }
556
+ ]
557
+ };
558
+ }
559
+ case "feedback": {
560
+ return handleFeedbackTool(args);
561
+ }
562
+ }
563
+ }
564
+ );
565
+ function normalizeToolName(name) {
566
+ const suffix = name.replace(/^scheck_/, "");
567
+ const allowed = /* @__PURE__ */ new Set([
568
+ "run",
569
+ "list_findings",
570
+ "explain",
571
+ "list_invariants",
572
+ "generate_test",
573
+ "feedback"
574
+ ]);
575
+ return allowed.has(suffix) ? suffix : null;
576
+ }
577
+ function formatMcpRunError(error) {
578
+ const message = error instanceof Error ? error.message : String(error);
579
+ if (/no allowed roots are configured and no git repository was detected/i.test(message)) {
580
+ return [
581
+ "Refusing to scan: no git repository detected and no allowed roots configured.",
582
+ "",
583
+ "Fix:",
584
+ "- Start the MCP server from inside the repo you want to scan, or",
585
+ "- Set SCHECK_MCP_ALLOWED_ROOTS (or MCP_ALLOWED_ROOTS) in your MCP server config.",
586
+ "",
587
+ "Example (Claude Code):",
588
+ "{",
589
+ ' "mcpServers": {',
590
+ ' "scheck": {',
591
+ ' "command": "scheck-mcp",',
592
+ ' "env": { "SCHECK_MCP_ALLOWED_ROOTS": "." }',
593
+ " }",
594
+ " }",
595
+ "}"
596
+ ].join("\n");
597
+ }
598
+ return `Error running audit: ${message}`;
599
+ }
600
+ async function main() {
601
+ const transport = new StdioServerTransport();
602
+ await server.connect(transport);
603
+ console.error("SecurityChecks MCP server (scheck) running on stdio");
604
+ }
605
+ main().catch(console.error);
606
+ //# sourceMappingURL=index.js.map
607
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/safety.ts","../src/feedback.ts","../src/index.ts"],"names":["path"],"mappings":"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","file":"index.js","sourcesContent":["import path from 'node:path';\nimport fs from 'node:fs';\nimport { execSync } from 'node:child_process';\n\nexport type McpSafetyOptions = {\n cwd: string;\n allowedRoots: string[];\n};\n\nexport function parseAllowedRootsFromEnv(\n env: Record<string, string | undefined>,\n cwd: string\n): string[] {\n const raw =\n env['SCHECK_MCP_ALLOWED_ROOTS'] ??\n env['MCP_ALLOWED_ROOTS'] ??\n env['SCHECK_ALLOWED_ROOTS'];\n\n const roots = (raw ?? '')\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n .map((p) => path.resolve(cwd, p));\n\n if (roots.length > 0) return roots;\n\n const gitRoot = getGitRoot(cwd);\n if (gitRoot) return [gitRoot];\n\n throw new Error(\n 'Refusing to scan because no allowed roots are configured and no git repository was detected. ' +\n 'Run scheck-mcp from inside a git repo, or set SCHECK_MCP_ALLOWED_ROOTS (or MCP_ALLOWED_ROOTS).'\n );\n}\n\nfunction getGitRoot(cwd: string): string | null {\n try {\n const out = execSync('git rev-parse --show-toplevel', {\n cwd,\n stdio: ['ignore', 'pipe', 'ignore'],\n encoding: 'utf-8',\n }).trim();\n return out.length > 0 ? out : null;\n } catch {\n return null;\n }\n}\n\nfunction isWithinRoot(candidatePath: string, root: string): boolean {\n const relative = path.relative(root, candidatePath);\n // `path.relative()` returns '' when equal. Equality should be allowed.\n return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));\n}\n\nexport function resolveAndValidateTargetPath(\n requestedPath: string | undefined,\n options: McpSafetyOptions\n): string {\n const { cwd, allowedRoots } = options;\n const input = (requestedPath && requestedPath.trim().length > 0)\n ? requestedPath\n : cwd;\n\n const resolved = path.resolve(cwd, input);\n\n if (!fs.existsSync(resolved)) {\n throw new Error(`Target path does not exist: ${resolved}`);\n }\n\n // Fail closed on symlink escapes: validate based on real paths, not just `path.resolve`.\n const realResolved = fs.realpathSync(resolved);\n const realAllowedRoots = allowedRoots.map((r) => fs.realpathSync(path.resolve(cwd, r)));\n\n const allowed = realAllowedRoots.some((root) => isWithinRoot(realResolved, root));\n\n if (!allowed) {\n const rootsList = realAllowedRoots.join(', ');\n throw new Error(\n `Refusing to scan outside allowed roots. Requested: ${realResolved}. Allowed roots: ${rootsList}. ` +\n `Set SCHECK_MCP_ALLOWED_ROOTS (or MCP_ALLOWED_ROOTS) to override.`\n );\n }\n\n return realResolved;\n}\n\nexport type EvidenceForMcp = { file: string; line: number; context?: string };\n\nexport function formatEvidenceForMcp(\n evidence: EvidenceForMcp[],\n includeContext: boolean\n): EvidenceForMcp[] {\n if (includeContext) return evidence;\n return evidence.map(({ file, line }) => ({ file, line }));\n}\n","export type FeedbackVerdict = 'true_positive' | 'false_positive';\n\nexport interface FeedbackToolArgs {\n invariant_id?: string;\n verdict?: string;\n reason?: string;\n}\n\nexport interface FeedbackToolResponse {\n content: Array<{ type: 'text'; text: string }>;\n isError?: boolean;\n}\n\n// Keep tool responses aligned with the MCP SDK result types.\n// (Our response object is a valid CallToolResult / CompatibilityCallToolResult.)\nexport type McpToolResult = import('@modelcontextprotocol/sdk/types.js').CompatibilityCallToolResult;\n\nexport async function handleFeedbackTool(\n args: FeedbackToolArgs | undefined,\n options?: {\n cwd?: string;\n now?: () => Date;\n fetchFn?: typeof fetch;\n }\n): Promise<McpToolResult> {\n const invariantId = args?.invariant_id;\n const verdict = args?.verdict as FeedbackVerdict | undefined;\n const reason = args?.reason;\n\n if (!invariantId || !verdict) {\n return {\n content: [\n {\n type: 'text',\n text: 'Error: invariant_id and verdict are required.',\n },\n ],\n isError: true,\n };\n }\n\n if (!['true_positive', 'false_positive'].includes(verdict)) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: verdict must be \"true_positive\" or \"false_positive\", got \"${verdict}\"`,\n },\n ],\n isError: true,\n };\n }\n\n try {\n const { writeFileSync, readFileSync, existsSync, mkdirSync, lstatSync } = await import('fs');\n const { join } = await import('path');\n const cwd = options?.cwd ?? process.cwd();\n const feedbackDir = join(cwd, '.scheck');\n const feedbackFile = join(feedbackDir, 'feedback.json');\n const now = options?.now ?? (() => new Date());\n\n if (!existsSync(feedbackDir)) {\n mkdirSync(feedbackDir, { recursive: true });\n } else {\n const st = lstatSync(feedbackDir);\n // Avoid writing through malicious symlinks (e.g., repo-controlled `.scheck` -> /etc).\n if (st.isSymbolicLink()) {\n throw new Error('Refusing to write feedback: .scheck is a symlink');\n }\n if (!st.isDirectory()) {\n throw new Error('Refusing to write feedback: .scheck is not a directory');\n }\n }\n\n if (existsSync(feedbackFile)) {\n const st = lstatSync(feedbackFile);\n if (st.isSymbolicLink()) {\n throw new Error('Refusing to write feedback: feedback.json is a symlink');\n }\n if (!st.isFile()) {\n throw new Error('Refusing to write feedback: feedback.json is not a file');\n }\n }\n\n let feedbackData: Array<{\n invariantId: string;\n verdict: string;\n reason?: string;\n timestamp: string;\n }> = [];\n if (existsSync(feedbackFile)) {\n try {\n feedbackData = JSON.parse(readFileSync(feedbackFile, 'utf-8'));\n } catch {\n feedbackData = [];\n }\n }\n\n feedbackData.push({\n invariantId,\n verdict,\n reason,\n timestamp: now().toISOString(),\n });\n\n writeFileSync(feedbackFile, JSON.stringify(feedbackData, null, 2));\n\n try {\n const clientVersion = process.env['MCP_VERSION'] ?? '0.0.0-dev';\n const endpoint = 'https://api.securitychecks.ai/v1/feedback';\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 3000);\n const fetchFn = options?.fetchFn ?? (typeof fetch === 'function' ? fetch : undefined);\n if (fetchFn) {\n fetchFn(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ invariantId, verdict, reason, clientVersion }),\n signal: controller.signal,\n })\n .catch(() => {})\n .finally(() => clearTimeout(timeoutId));\n } else {\n clearTimeout(timeoutId);\n }\n } catch {\n // Silent failure for API reporting\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n recorded: true,\n invariantId,\n verdict,\n reason: reason ?? null,\n storedLocally: true,\n },\n null,\n 2\n ),\n },\n ],\n };\n } catch (error) {\n return {\n content: [\n {\n type: 'text',\n text: `Error recording feedback: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n isError: true,\n };\n }\n}\n","#!/usr/bin/env node\n\n/**\n * SecurityChecks MCP Server (scheck)\n *\n * Catch what Copilot misses — production-ready code review.\n * MCP server that exposes scheck tools for LLM integration.\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\nimport type { CompatibilityCallToolResult, CreateTaskResult } from '@modelcontextprotocol/sdk/types.js';\nimport {\n getInvariantById,\n ALL_INVARIANTS,\n type AuditResult,\n type Finding,\n} from '@securitychecks/collector';\nimport { audit } from '@securitychecks/cli';\nimport {\n formatEvidenceForMcp,\n parseAllowedRootsFromEnv,\n resolveAndValidateTargetPath,\n type EvidenceForMcp,\n} from './safety.js';\nimport { generateTestSkeleton, getStaffQuestion } from '@securitychecks/cli';\nimport { handleFeedbackTool } from './feedback.js';\n\n// Version injected at build time via tsup define\nconst version = process.env['MCP_VERSION'] ?? '0.0.0-dev';\n\nconst server = new Server(\n {\n name: 'scheck',\n version: version,\n },\n {\n capabilities: {\n tools: {},\n },\n }\n);\n\nconst TOOL_PREFIXES = ['scheck'] as const;\ntype ToolSuffix = 'run' | 'list_findings' | 'explain' | 'list_invariants' | 'generate_test' | 'feedback';\ntype ToolDef = {\n suffix: ToolSuffix;\n description: string;\n inputSchema: unknown;\n};\n\nconst TOOL_DEFS: ToolDef[] = [\n {\n suffix: 'run',\n description:\n 'Run scheck — the patterns a senior engineer would flag in review. ' +\n 'Catches webhook idempotency, auth at service layer, transaction safety, and more.',\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Target path to audit (default: current directory)',\n },\n include_context: {\n type: 'boolean',\n description:\n 'Include code context snippets in results (may expose source code to the assistant).',\n },\n max_findings: {\n type: 'integer',\n minimum: 1,\n maximum: 500,\n description: 'Limit number of findings returned (default: 200)',\n },\n only: {\n type: 'array',\n items: { type: 'string' },\n description: 'Only run specific invariant checks by ID',\n },\n skip: {\n type: 'array',\n items: { type: 'string' },\n description: 'Skip specific invariant checks by ID',\n },\n },\n },\n },\n {\n suffix: 'list_findings',\n description:\n 'List issues a staff engineer would flag — current findings from the last run, by severity.',\n inputSchema: {\n type: 'object',\n properties: {\n severity: {\n type: 'string',\n enum: ['P0', 'P1', 'P2'],\n description: 'Filter findings by severity',\n },\n include_context: {\n type: 'boolean',\n description:\n 'Include code context snippets in results (may expose source code to the assistant).',\n },\n max_findings: {\n type: 'integer',\n minimum: 1,\n maximum: 500,\n description: 'Limit number of findings returned (default: 200)',\n },\n },\n },\n },\n {\n suffix: 'explain',\n description:\n 'What a staff engineer checks: explain why a pattern matters, real incidents it prevents, and proof needed.',\n inputSchema: {\n type: 'object',\n properties: {\n invariant_id: {\n type: 'string',\n description: 'The invariant ID to explain (e.g., AUTHZ.SERVICE_LAYER.ENFORCED)',\n },\n },\n required: ['invariant_id'],\n },\n },\n {\n suffix: 'list_invariants',\n description:\n 'List all patterns a staff engineer checks for — the patterns that prevent production incidents.',\n inputSchema: {\n type: 'object',\n properties: {\n category: {\n type: 'string',\n description: 'Filter by category (authz, revocation, webhooks, transactions, etc.)',\n },\n severity: {\n type: 'string',\n enum: ['P0', 'P1', 'P2'],\n description: 'Filter by severity',\n },\n },\n },\n },\n {\n suffix: 'generate_test',\n description:\n 'Generate test code that proves a pattern is enforced — the proof a staff engineer would ask for.',\n inputSchema: {\n type: 'object',\n properties: {\n invariant_id: {\n type: 'string',\n description: 'The invariant ID to generate a test for',\n },\n framework: {\n type: 'string',\n enum: ['jest', 'vitest', 'playwright'],\n description: 'Test framework to generate code for (default: vitest)',\n },\n context: {\n type: 'string',\n description:\n 'Additional context about the specific violation to generate a more targeted test',\n },\n },\n required: ['invariant_id'],\n },\n },\n {\n suffix: 'feedback',\n description:\n 'Report whether a finding was a true positive or false positive to improve accuracy.',\n inputSchema: {\n type: 'object',\n properties: {\n invariant_id: {\n type: 'string',\n description: 'Invariant ID (e.g., AUTHZ.SERVICE_LAYER.ENFORCED)',\n },\n verdict: {\n type: 'string',\n enum: ['true_positive', 'false_positive'],\n description: 'Whether the finding was a true positive or false positive',\n },\n reason: {\n type: 'string',\n enum: [\n 'not_applicable',\n 'acceptable_risk',\n 'wrong_location',\n 'outdated_pattern',\n 'missing_context',\n ],\n description: 'Reason for the verdict',\n },\n },\n required: ['invariant_id', 'verdict'],\n },\n },\n];\n\n// List available tools\nserver.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: TOOL_PREFIXES.flatMap((prefix) =>\n TOOL_DEFS.map((def) => ({\n name: `${prefix}_${def.suffix}`,\n description: def.description,\n inputSchema: def.inputSchema,\n }))\n ),\n };\n});\n\n// Track last audit result for list_findings\nlet lastAuditResult: AuditResult | null = null;\n\n// Handle tool calls\nserver.setRequestHandler(\n CallToolRequestSchema,\n async (request): Promise<CompatibilityCallToolResult | CreateTaskResult> => {\n const { name, arguments: args } = request.params;\n const tool = normalizeToolName(name);\n if (!tool) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${name}` }],\n isError: true,\n };\n }\n\n switch (tool) {\n case 'run': {\n const path = (args?.['path'] as string) || process.cwd();\n const only = args?.['only'] as string[] | undefined;\n const skip = args?.['skip'] as string[] | undefined;\n const includeContext = args?.['include_context'] === true;\n const maxFindings =\n typeof args?.['max_findings'] === 'number'\n ? Math.max(1, Math.min(500, args['max_findings'] as number))\n : 200;\n\n try {\n const allowedRoots = parseAllowedRootsFromEnv(process.env, process.cwd());\n const targetPath = resolveAndValidateTargetPath(path, {\n cwd: process.cwd(),\n allowedRoots,\n });\n\n const result = await audit({\n targetPath,\n only,\n skip,\n });\n\n lastAuditResult = result;\n\n // Format findings for LLM consumption\n const findings: Finding[] = result.results.flatMap((r) => r.findings);\n const truncated = findings.length > maxFindings;\n const findingsToReturn = truncated ? findings.slice(0, maxFindings) : findings;\n const summary = {\n total_checks: result.summary.total,\n passed: result.summary.passed,\n failed: result.summary.failed,\n findings_count: findings.length,\n by_severity: result.summary.byPriority,\n truncated,\n max_findings: maxFindings,\n };\n\n const formattedFindings = findingsToReturn.map((f: Finding) => ({\n invariant_id: f.invariantId,\n severity: f.severity,\n message: f.message,\n evidence: formatEvidenceForMcp(f.evidence as EvidenceForMcp[], includeContext),\n required_proof: f.requiredProof,\n suggested_test: f.suggestedTest,\n staff_engineer_asks: getStaffQuestion(f.invariantId),\n }));\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n summary,\n findings: formattedFindings,\n },\n null,\n 2\n ),\n },\n ],\n };\n } catch (error) {\n return {\n content: [\n {\n type: 'text',\n text: formatMcpRunError(error),\n },\n ],\n isError: true,\n };\n }\n }\n\n case 'list_findings': {\n if (!lastAuditResult) {\n return {\n content: [\n {\n type: 'text',\n text: 'No scan has been run yet. Use scheck_run first.',\n },\n ],\n };\n }\n\n const severity = args?.['severity'] as string | undefined;\n const includeContext = args?.['include_context'] === true;\n const maxFindings =\n typeof args?.['max_findings'] === 'number'\n ? Math.max(1, Math.min(500, args['max_findings'] as number))\n : 200;\n let findings: Finding[] = lastAuditResult.results.flatMap((r) => r.findings);\n\n if (severity) {\n findings = findings.filter((f: Finding) => f.severity === severity);\n }\n\n const truncated = findings.length > maxFindings;\n const findingsToReturn = truncated ? findings.slice(0, maxFindings) : findings;\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n findings: findingsToReturn.map((f: Finding) => ({\n invariant_id: f.invariantId,\n severity: f.severity,\n message: f.message,\n evidence: formatEvidenceForMcp(f.evidence as EvidenceForMcp[], includeContext),\n })),\n meta: {\n total: findings.length,\n truncated,\n max_findings: maxFindings,\n },\n },\n null,\n 2\n ),\n },\n ],\n };\n }\n\n case 'explain': {\n const invariantId = args?.['invariant_id'] as string;\n const invariant = getInvariantById(invariantId);\n\n if (!invariant) {\n return {\n content: [\n {\n type: 'text',\n text: `Unknown pattern: ${invariantId}. Use scheck_list_invariants to see available patterns.`,\n },\n ],\n };\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n {\n id: invariant.id,\n name: invariant.name,\n severity: invariant.severity,\n category: invariant.category,\n description: invariant.description,\n required_proof: invariant.requiredProof,\n staff_engineer_asks: getStaffQuestion(invariant.id),\n },\n null,\n 2\n ),\n },\n ],\n };\n }\n\n case 'list_invariants': {\n const category = args?.['category'] as string | undefined;\n const severity = args?.['severity'] as string | undefined;\n\n let invariants = ALL_INVARIANTS;\n\n if (category) {\n invariants = invariants.filter((i) => i.category === category);\n }\n if (severity) {\n invariants = invariants.filter((i) => i.severity === severity);\n }\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n invariants.map((i) => ({\n id: i.id,\n name: i.name,\n severity: i.severity,\n category: i.category,\n })),\n null,\n 2\n ),\n },\n ],\n };\n }\n\n case 'generate_test': {\n const invariantId = args?.['invariant_id'] as string;\n const framework = (args?.['framework'] as string) || 'vitest';\n const context = args?.['context'] as string | undefined;\n\n const invariant = getInvariantById(invariantId);\n if (!invariant) {\n return {\n content: [\n {\n type: 'text',\n text: `Unknown invariant: ${invariantId}`,\n },\n ],\n };\n }\n\n const test = generateTestSkeleton(invariant, framework, context);\n\n return {\n content: [\n {\n type: 'text',\n text: test,\n },\n ],\n };\n }\n\n case 'feedback': {\n return handleFeedbackTool(args as Record<string, unknown> | undefined);\n }\n }\n }\n);\n\nfunction normalizeToolName(name: string): ToolSuffix | null {\n const suffix = name.replace(/^scheck_/, '') as ToolSuffix;\n const allowed = new Set<ToolSuffix>([\n 'run',\n 'list_findings',\n 'explain',\n 'list_invariants',\n 'generate_test',\n 'feedback',\n ]);\n return allowed.has(suffix) ? suffix : null;\n}\n\nfunction formatMcpRunError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n\n if (/no allowed roots are configured and no git repository was detected/i.test(message)) {\n return [\n 'Refusing to scan: no git repository detected and no allowed roots configured.',\n '',\n 'Fix:',\n '- Start the MCP server from inside the repo you want to scan, or',\n '- Set SCHECK_MCP_ALLOWED_ROOTS (or MCP_ALLOWED_ROOTS) in your MCP server config.',\n '',\n 'Example (Claude Code):',\n '{',\n ' \"mcpServers\": {',\n ' \"scheck\": {',\n ' \"command\": \"scheck-mcp\",',\n ' \"env\": { \"SCHECK_MCP_ALLOWED_ROOTS\": \".\" }',\n ' }',\n ' }',\n '}',\n ].join('\\n');\n }\n\n return `Error running audit: ${message}`;\n}\n\n// Start the server\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error('SecurityChecks MCP server (scheck) running on stdio');\n}\n\nmain().catch(console.error);\n"]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@securitychecks/mcp",
3
+ "version": "0.1.1-rc.1",
4
+ "description": "MCP server for SecurityChecks - expose scheck tools to AI assistants",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "author": "SecurityChecks <hello@securitychecks.ai>",
8
+ "homepage": "https://securitychecks.ai",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/securitychecks/securitychecks.ai"
12
+ },
13
+ "keywords": [
14
+ "scheck",
15
+ "securitychecks",
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "llm",
19
+ "claude",
20
+ "ai-assistant",
21
+ "code-review",
22
+ "cursor",
23
+ "anthropic"
24
+ ],
25
+ "bin": {
26
+ "scheck-mcp": "./dist/index.js"
27
+ },
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.26.0",
35
+ "@securitychecks/cli": "0.1.1-rc.1",
36
+ "@securitychecks/collector": "0.1.1-rc.1"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^25.1.0",
40
+ "tsup": "^8.5.1",
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "clean": "rm -rf dist"
54
+ }
55
+ }