corpus-mcp 0.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.
- package/dist/index.d.ts +18 -0
- package/dist/index.js +424 -0
- package/dist/standalone.d.ts +102 -0
- package/dist/standalone.js +200 -0
- package/package.json +26 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Corpus MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes security scanning tools to AI coding agents via the
|
|
6
|
+
* Model Context Protocol (MCP). The AI can check proposed code
|
|
7
|
+
* for secrets, PII, and unsafe patterns BEFORE writing to disk.
|
|
8
|
+
*
|
|
9
|
+
* Tools:
|
|
10
|
+
* scan_content - Scan code content for security issues
|
|
11
|
+
* check_secret - Check if a string looks like a secret/credential
|
|
12
|
+
* check_safety - Check code for unsafe patterns (eval, innerHTML, etc.)
|
|
13
|
+
* get_policy - Get the verdict for an action type
|
|
14
|
+
*
|
|
15
|
+
* Setup in Claude Code / .mcp.json:
|
|
16
|
+
* { "command": "npx", "args": ["corpus-mcp"], "type": "stdio" }
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Corpus MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Exposes security scanning tools to AI coding agents via the
|
|
6
|
+
* Model Context Protocol (MCP). The AI can check proposed code
|
|
7
|
+
* for secrets, PII, and unsafe patterns BEFORE writing to disk.
|
|
8
|
+
*
|
|
9
|
+
* Tools:
|
|
10
|
+
* scan_content - Scan code content for security issues
|
|
11
|
+
* check_secret - Check if a string looks like a secret/credential
|
|
12
|
+
* check_safety - Check code for unsafe patterns (eval, innerHTML, etc.)
|
|
13
|
+
* get_policy - Get the verdict for an action type
|
|
14
|
+
*
|
|
15
|
+
* Setup in Claude Code / .mcp.json:
|
|
16
|
+
* { "command": "npx", "args": ["corpus-mcp"], "type": "stdio" }
|
|
17
|
+
*/
|
|
18
|
+
import { detectSecrets } from '@corpus/core';
|
|
19
|
+
import { checkCodeSafety } from '@corpus/core';
|
|
20
|
+
import { scanForInjection } from '@corpus/core';
|
|
21
|
+
import { scanPayload } from '@corpus/core';
|
|
22
|
+
import { computeFileTrust } from '@corpus/core';
|
|
23
|
+
import { checkForCVEs } from '@corpus/core';
|
|
24
|
+
import { checkDependencies } from '@corpus/core';
|
|
25
|
+
// ── Tool Definitions ─────────────────────────────────────────────────────────
|
|
26
|
+
const TOOLS = [
|
|
27
|
+
{
|
|
28
|
+
name: 'scan_content',
|
|
29
|
+
description: 'Scan code content for secrets, PII, injection patterns, and unsafe code. Call this before writing any file to check for security issues.',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
content: { type: 'string', description: 'The code content to scan' },
|
|
34
|
+
filename: { type: 'string', description: 'The filename (used to determine scan rules)' },
|
|
35
|
+
},
|
|
36
|
+
required: ['content', 'filename'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'check_secret',
|
|
41
|
+
description: 'Check if a string value looks like a secret, API key, token, or credential. Use this before hardcoding any string value.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
value: { type: 'string', description: 'The string value to check' },
|
|
46
|
+
},
|
|
47
|
+
required: ['value'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'check_safety',
|
|
52
|
+
description: 'Check code for unsafe patterns like eval(), innerHTML, SQL injection, command injection, disabled SSL, wildcard CORS.',
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
content: { type: 'string', description: 'The code to check' },
|
|
57
|
+
filename: { type: 'string', description: 'The filename' },
|
|
58
|
+
},
|
|
59
|
+
required: ['content', 'filename'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'check_injection',
|
|
64
|
+
description: 'Scan text content for prompt injection patterns. Use this before including any external content in LLM context.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
content: { type: 'string', description: 'The content to scan' },
|
|
69
|
+
source: { type: 'string', description: 'Where this content came from' },
|
|
70
|
+
},
|
|
71
|
+
required: ['content'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'verify_file',
|
|
76
|
+
description: 'Compute a deterministic trust score (0-100) for a file with line-by-line findings and fix suggestions. Call this BEFORE writing any file. If score is below 50, do NOT write the file -- fix the issues first.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
content: { type: 'string', description: 'The code content to verify' },
|
|
81
|
+
filename: { type: 'string', description: 'The filename' },
|
|
82
|
+
},
|
|
83
|
+
required: ['content', 'filename'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'corpus_check',
|
|
88
|
+
description: 'CORPUS IMMUNE SYSTEM: Check code against the codebase contract graph BEFORE writing. If this returns VIOLATES, you MUST fix the violations and re-check. Do NOT write the file until this returns VERIFIED. This catches behavioral breakage: removed functions, deleted guard clauses, changed signatures.',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
content: { type: 'string', description: 'The code content to check against the graph' },
|
|
93
|
+
filepath: { type: 'string', description: 'The absolute file path being written' },
|
|
94
|
+
project_root: { type: 'string', description: 'The project root directory (where .corpus/ lives)' },
|
|
95
|
+
},
|
|
96
|
+
required: ['content', 'filepath'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'check_dependencies',
|
|
101
|
+
description: 'CORPUS IMMUNE SYSTEM: Check imports for hallucinated or typosquatted npm packages. AI coding tools sometimes invent package names that do not exist. Call this before writing any file with new imports. If CRITICAL findings are returned, the imports MUST be fixed.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
content: { type: 'string', description: 'The code content to check for dependency issues' },
|
|
106
|
+
filename: { type: 'string', description: 'The filename' },
|
|
107
|
+
project_root: { type: 'string', description: 'The project root directory (to read package.json)' },
|
|
108
|
+
},
|
|
109
|
+
required: ['content', 'filename'],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'corpus_health',
|
|
114
|
+
description: 'Get the current health status of the codebase immune system. Returns overall health score, verified/violating/uncertain counts, and recent changes.',
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
project_root: { type: 'string', description: 'The project root directory' },
|
|
119
|
+
},
|
|
120
|
+
required: ['project_root'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
// ── Tool Handlers ────────────────────────────────────────────────────────────
|
|
125
|
+
async function handleToolCall(name, args) {
|
|
126
|
+
switch (name) {
|
|
127
|
+
case 'scan_content': {
|
|
128
|
+
const content = String(args.content ?? '');
|
|
129
|
+
const filename = String(args.filename ?? 'unknown');
|
|
130
|
+
const secrets = detectSecrets(content, filename);
|
|
131
|
+
const safety = checkCodeSafety(content, filename);
|
|
132
|
+
const pii = scanPayload(content);
|
|
133
|
+
const issues = [
|
|
134
|
+
...secrets.map((s) => ({
|
|
135
|
+
severity: s.severity,
|
|
136
|
+
type: s.type,
|
|
137
|
+
line: s.line,
|
|
138
|
+
message: s.message,
|
|
139
|
+
isAiPattern: s.isAiPattern,
|
|
140
|
+
})),
|
|
141
|
+
...safety.map((s) => ({
|
|
142
|
+
severity: s.severity,
|
|
143
|
+
type: s.rule,
|
|
144
|
+
line: s.line,
|
|
145
|
+
message: s.message,
|
|
146
|
+
isAiPattern: false,
|
|
147
|
+
})),
|
|
148
|
+
...(pii.hasPii ? pii.matches.map((m) => ({
|
|
149
|
+
severity: 'WARNING',
|
|
150
|
+
type: `PII: ${m.type}`,
|
|
151
|
+
line: 0,
|
|
152
|
+
message: `${m.type} found in content`,
|
|
153
|
+
isAiPattern: false,
|
|
154
|
+
})) : []),
|
|
155
|
+
];
|
|
156
|
+
// CVE pattern detection
|
|
157
|
+
try {
|
|
158
|
+
const cveFindings = checkForCVEs(content, filename);
|
|
159
|
+
for (const cve of cveFindings) {
|
|
160
|
+
issues.push({
|
|
161
|
+
severity: cve.severity === 'HIGH' ? 'CRITICAL' : cve.severity === 'MEDIUM' ? 'WARNING' : cve.severity,
|
|
162
|
+
type: `CVE: ${cve.cveId}`,
|
|
163
|
+
line: cve.line,
|
|
164
|
+
message: `${cve.name}: ${cve.description}`,
|
|
165
|
+
isAiPattern: false,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch { }
|
|
170
|
+
const clean = issues.length === 0;
|
|
171
|
+
const text = clean
|
|
172
|
+
? `CLEAN: No security issues found in ${filename}.`
|
|
173
|
+
: `FOUND ${issues.length} issue(s) in ${filename}:\n${issues.map((i) => ` [${i.severity}] ${i.message}${i.isAiPattern ? ' (AI pattern)' : ''}`).join('\n')}`;
|
|
174
|
+
return { content: [{ type: 'text', text }] };
|
|
175
|
+
}
|
|
176
|
+
case 'check_secret': {
|
|
177
|
+
const value = String(args.value ?? '');
|
|
178
|
+
const findings = detectSecrets(`const x = "${value}";`, 'check.ts');
|
|
179
|
+
const isSecret = findings.length > 0;
|
|
180
|
+
return {
|
|
181
|
+
content: [{
|
|
182
|
+
type: 'text',
|
|
183
|
+
text: isSecret
|
|
184
|
+
? `WARNING: This looks like a ${findings[0].type}. Do NOT hardcode this value. Use an environment variable instead.`
|
|
185
|
+
: `OK: This value does not match known secret patterns.`,
|
|
186
|
+
}],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
case 'check_safety': {
|
|
190
|
+
const content = String(args.content ?? '');
|
|
191
|
+
const filename = String(args.filename ?? 'unknown');
|
|
192
|
+
const findings = checkCodeSafety(content, filename);
|
|
193
|
+
if (findings.length === 0) {
|
|
194
|
+
return { content: [{ type: 'text', text: 'SAFE: No unsafe code patterns found.' }] };
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
content: [{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: `FOUND ${findings.length} unsafe pattern(s):\n${findings.map((f) => ` [${f.severity}] ${f.message}\n Fix: ${f.suggestion}`).join('\n')}`,
|
|
200
|
+
}],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
case 'check_injection': {
|
|
204
|
+
const content = String(args.content ?? '');
|
|
205
|
+
const source = String(args.source ?? 'unknown');
|
|
206
|
+
const result = scanForInjection(content, source);
|
|
207
|
+
return {
|
|
208
|
+
content: [{
|
|
209
|
+
type: 'text',
|
|
210
|
+
text: result.severity === 'CLEAN'
|
|
211
|
+
? 'CLEAN: No injection patterns found.'
|
|
212
|
+
: `${result.severity}: ${result.message}`,
|
|
213
|
+
}],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
case 'verify_file': {
|
|
217
|
+
const content = String(args.content ?? '');
|
|
218
|
+
const filename = String(args.filename ?? 'unknown');
|
|
219
|
+
const result = computeFileTrust(content, filename);
|
|
220
|
+
// Also check for dependency issues
|
|
221
|
+
let depWarning = '';
|
|
222
|
+
try {
|
|
223
|
+
const depFindings = await checkDependencies(content, filename, { projectRoot: process.cwd() });
|
|
224
|
+
if (depFindings.length > 0) {
|
|
225
|
+
depWarning = `\n\nDEPENDENCY WARNINGS:\n${depFindings.map(f => ` [${f.severity}] Package '${f.package}': ${f.suggestion}`).join('\n')}`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
229
|
+
if (result.findings.length === 0 && !depWarning) {
|
|
230
|
+
return {
|
|
231
|
+
content: [{
|
|
232
|
+
type: 'text',
|
|
233
|
+
text: `TRUST SCORE: ${result.score}/100 - CLEAN. No issues found in ${filename}. Safe to write.`,
|
|
234
|
+
}],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const findingsText = result.findings
|
|
238
|
+
.map((f) => ` [${f.severity}] Line ${f.line}: ${f.message}\n FIX: ${f.fix}`)
|
|
239
|
+
.join('\n');
|
|
240
|
+
const recommendation = result.score < 50
|
|
241
|
+
? 'DO NOT write this file. Fix the critical issues first.'
|
|
242
|
+
: result.score < 80
|
|
243
|
+
? 'File has warnings. Consider fixing before writing.'
|
|
244
|
+
: 'File is acceptable. Safe to write.';
|
|
245
|
+
return {
|
|
246
|
+
content: [{
|
|
247
|
+
type: 'text',
|
|
248
|
+
text: `TRUST SCORE: ${result.score}/100 for ${filename}\n${recommendation}\n\n${result.findings.length} finding(s):\n${findingsText}${depWarning}`,
|
|
249
|
+
}],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
case 'corpus_check': {
|
|
253
|
+
const content = String(args.content ?? '');
|
|
254
|
+
const filepath = String(args.filepath ?? '');
|
|
255
|
+
const projectRoot = String(args.project_root ?? process.cwd());
|
|
256
|
+
try {
|
|
257
|
+
// Dynamic import to avoid breaking if graph-engine isn't built yet
|
|
258
|
+
const { checkFile } = await import('@corpus/core');
|
|
259
|
+
const result = checkFile(projectRoot, filepath, content);
|
|
260
|
+
if (result.verdict === 'VERIFIED') {
|
|
261
|
+
return {
|
|
262
|
+
content: [{
|
|
263
|
+
type: 'text',
|
|
264
|
+
text: `CORPUS VERIFIED: All contracts satisfied for ${result.file}. Safe to write.`,
|
|
265
|
+
}],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (result.verdict === 'UNCERTAIN') {
|
|
269
|
+
return {
|
|
270
|
+
content: [{
|
|
271
|
+
type: 'text',
|
|
272
|
+
text: `CORPUS UNCERTAIN: ${result.instructions}`,
|
|
273
|
+
}],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// VIOLATES - return fix instructions
|
|
277
|
+
return {
|
|
278
|
+
content: [{
|
|
279
|
+
type: 'text',
|
|
280
|
+
text: `CORPUS VIOLATES: ${result.instructions}\n\nYou MUST fix these violations before writing the file. Regenerate the code with these fixes applied, then call corpus_check again.`,
|
|
281
|
+
}],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
return {
|
|
286
|
+
content: [{
|
|
287
|
+
type: 'text',
|
|
288
|
+
text: `CORPUS UNCERTAIN: Could not check graph contracts. ${e instanceof Error ? e.message : 'Unknown error'}. Run \`corpus init\` to build the immune system.`,
|
|
289
|
+
}],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
case 'corpus_health': {
|
|
294
|
+
const projectRoot = String(args.project_root ?? process.cwd());
|
|
295
|
+
try {
|
|
296
|
+
const { getHealthSummary } = await import('@corpus/core');
|
|
297
|
+
const health = getHealthSummary(projectRoot);
|
|
298
|
+
const status = health.healthy ? 'HEALTHY' : 'DEGRADED';
|
|
299
|
+
return {
|
|
300
|
+
content: [{
|
|
301
|
+
type: 'text',
|
|
302
|
+
text: `CORPUS ${status}: Score ${health.score}/100 | ${health.totalNodes} nodes | ${health.verified} verified | ${health.violating} violating | ${health.uncertain} uncertain`,
|
|
303
|
+
}],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
return {
|
|
308
|
+
content: [{
|
|
309
|
+
type: 'text',
|
|
310
|
+
text: `CORPUS: No immune system found. Run \`corpus init\` to build the codebase graph.`,
|
|
311
|
+
}],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
case 'check_dependencies': {
|
|
316
|
+
const content = String(args.content ?? '');
|
|
317
|
+
const filename = String(args.filename ?? 'unknown');
|
|
318
|
+
const projectRoot = String(args.project_root ?? process.cwd());
|
|
319
|
+
try {
|
|
320
|
+
const findings = await checkDependencies(content, filename, { projectRoot });
|
|
321
|
+
if (findings.length === 0) {
|
|
322
|
+
return { content: [{ type: 'text', text: 'CORPUS VERIFIED: All imported packages are legitimate. No hallucinated dependencies detected.' }] };
|
|
323
|
+
}
|
|
324
|
+
const text = findings.map(f => {
|
|
325
|
+
let msg = `[${f.severity}] Package '${f.package}': ${f.reason === 'nonexistent' ? 'DOES NOT EXIST on npm' : f.reason === 'typosquat' ? 'Possible typosquat' : 'Suspiciously unpopular'}`;
|
|
326
|
+
if (f.similarPackages?.length)
|
|
327
|
+
msg += `\n Did you mean: ${f.similarPackages.join(', ')}?`;
|
|
328
|
+
msg += `\n ${f.suggestion}`;
|
|
329
|
+
return msg;
|
|
330
|
+
}).join('\n');
|
|
331
|
+
return { content: [{ type: 'text', text: `CORPUS ALERT: ${findings.length} suspicious dependency(ies) found:\n${text}\n\nFix these imports before writing the file.` }] };
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
return { content: [{ type: 'text', text: `CORPUS: Dependency check unavailable. ${e instanceof Error ? e.message : 'Unknown error'}` }] };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
default:
|
|
338
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// ── MCP stdio transport ──────────────────────────────────────────────────────
|
|
342
|
+
function sendResponse(response) {
|
|
343
|
+
const json = JSON.stringify(response);
|
|
344
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
345
|
+
}
|
|
346
|
+
function handleRequest(request) {
|
|
347
|
+
switch (request.method) {
|
|
348
|
+
case 'initialize':
|
|
349
|
+
sendResponse({
|
|
350
|
+
jsonrpc: '2.0',
|
|
351
|
+
id: request.id,
|
|
352
|
+
result: {
|
|
353
|
+
protocolVersion: '2024-11-05',
|
|
354
|
+
capabilities: { tools: {} },
|
|
355
|
+
serverInfo: { name: 'corpus-mcp', version: '0.2.0' },
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
break;
|
|
359
|
+
case 'tools/list':
|
|
360
|
+
sendResponse({
|
|
361
|
+
jsonrpc: '2.0',
|
|
362
|
+
id: request.id,
|
|
363
|
+
result: { tools: TOOLS },
|
|
364
|
+
});
|
|
365
|
+
break;
|
|
366
|
+
case 'tools/call': {
|
|
367
|
+
const params = request.params;
|
|
368
|
+
handleToolCall(params.name, params.arguments ?? {}).then((result) => {
|
|
369
|
+
sendResponse({
|
|
370
|
+
jsonrpc: '2.0',
|
|
371
|
+
id: request.id,
|
|
372
|
+
result,
|
|
373
|
+
});
|
|
374
|
+
}).catch((err) => {
|
|
375
|
+
sendResponse({
|
|
376
|
+
jsonrpc: '2.0',
|
|
377
|
+
id: request.id,
|
|
378
|
+
error: { code: -32603, message: err instanceof Error ? err.message : 'Internal error' },
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case 'notifications/initialized':
|
|
384
|
+
// No response needed for notifications
|
|
385
|
+
break;
|
|
386
|
+
default:
|
|
387
|
+
sendResponse({
|
|
388
|
+
jsonrpc: '2.0',
|
|
389
|
+
id: request.id,
|
|
390
|
+
error: { code: -32601, message: `Method not found: ${request.method}` },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
395
|
+
let buffer = '';
|
|
396
|
+
process.stdin.setEncoding('utf-8');
|
|
397
|
+
process.stdin.on('data', (chunk) => {
|
|
398
|
+
buffer += chunk;
|
|
399
|
+
while (true) {
|
|
400
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
401
|
+
if (headerEnd === -1)
|
|
402
|
+
break;
|
|
403
|
+
const header = buffer.slice(0, headerEnd);
|
|
404
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
405
|
+
if (!match) {
|
|
406
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const contentLength = parseInt(match[1], 10);
|
|
410
|
+
const bodyStart = headerEnd + 4;
|
|
411
|
+
if (buffer.length < bodyStart + contentLength)
|
|
412
|
+
break;
|
|
413
|
+
const body = buffer.slice(bodyStart, bodyStart + contentLength);
|
|
414
|
+
buffer = buffer.slice(bodyStart + contentLength);
|
|
415
|
+
try {
|
|
416
|
+
const request = JSON.parse(body);
|
|
417
|
+
handleRequest(request);
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Invalid JSON, skip
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
process.stderr.write('[corpus-mcp] Server started\n');
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Corpus MCP Server (Standalone)
|
|
4
|
+
* Self-contained -- all scanner logic inline, no external imports.
|
|
5
|
+
* Works from any directory without workspace resolution.
|
|
6
|
+
*/
|
|
7
|
+
declare const SECRET_PATTERNS: {
|
|
8
|
+
name: string;
|
|
9
|
+
regex: RegExp;
|
|
10
|
+
fix: string;
|
|
11
|
+
}[];
|
|
12
|
+
declare const SAFETY_PATTERNS: {
|
|
13
|
+
name: string;
|
|
14
|
+
regex: RegExp;
|
|
15
|
+
severity: string;
|
|
16
|
+
fix: string;
|
|
17
|
+
}[];
|
|
18
|
+
declare const INJECTION_PATTERNS: string[];
|
|
19
|
+
declare const PLACEHOLDER_SKIP: RegExp[];
|
|
20
|
+
interface Finding {
|
|
21
|
+
severity: string;
|
|
22
|
+
type: string;
|
|
23
|
+
line: number;
|
|
24
|
+
message: string;
|
|
25
|
+
fix: string;
|
|
26
|
+
}
|
|
27
|
+
declare function scanContent(content: string, filename: string): Finding[];
|
|
28
|
+
declare function computeTrustScore(findings: Finding[]): number;
|
|
29
|
+
declare function checkInjection(content: string, source: string): {
|
|
30
|
+
severity: string;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
declare const TOOLS: ({
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: string;
|
|
38
|
+
properties: {
|
|
39
|
+
content: {
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
filename: {
|
|
44
|
+
type: string;
|
|
45
|
+
description: string;
|
|
46
|
+
};
|
|
47
|
+
value?: undefined;
|
|
48
|
+
source?: undefined;
|
|
49
|
+
};
|
|
50
|
+
required: string[];
|
|
51
|
+
};
|
|
52
|
+
} | {
|
|
53
|
+
name: string;
|
|
54
|
+
description: string;
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: string;
|
|
57
|
+
properties: {
|
|
58
|
+
value: {
|
|
59
|
+
type: string;
|
|
60
|
+
description: string;
|
|
61
|
+
};
|
|
62
|
+
content?: undefined;
|
|
63
|
+
filename?: undefined;
|
|
64
|
+
source?: undefined;
|
|
65
|
+
};
|
|
66
|
+
required: string[];
|
|
67
|
+
};
|
|
68
|
+
} | {
|
|
69
|
+
name: string;
|
|
70
|
+
description: string;
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: string;
|
|
73
|
+
properties: {
|
|
74
|
+
content: {
|
|
75
|
+
type: string;
|
|
76
|
+
description: string;
|
|
77
|
+
};
|
|
78
|
+
source: {
|
|
79
|
+
type: string;
|
|
80
|
+
description: string;
|
|
81
|
+
};
|
|
82
|
+
filename?: undefined;
|
|
83
|
+
value?: undefined;
|
|
84
|
+
};
|
|
85
|
+
required: string[];
|
|
86
|
+
};
|
|
87
|
+
})[];
|
|
88
|
+
declare function handleTool(name: string, args: Record<string, unknown>): {
|
|
89
|
+
content: {
|
|
90
|
+
type: string;
|
|
91
|
+
text: string;
|
|
92
|
+
}[];
|
|
93
|
+
};
|
|
94
|
+
interface McpRequest {
|
|
95
|
+
jsonrpc: string;
|
|
96
|
+
id: number | string;
|
|
97
|
+
method: string;
|
|
98
|
+
params?: Record<string, unknown>;
|
|
99
|
+
}
|
|
100
|
+
declare function send(response: object): void;
|
|
101
|
+
declare function handle(req: McpRequest): void;
|
|
102
|
+
declare let buffer: string;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Corpus MCP Server (Standalone)
|
|
5
|
+
* Self-contained -- all scanner logic inline, no external imports.
|
|
6
|
+
* Works from any directory without workspace resolution.
|
|
7
|
+
*/
|
|
8
|
+
// ── Inline Scanners ──────────────────────────────────────────────────────────
|
|
9
|
+
const SECRET_PATTERNS = [
|
|
10
|
+
{ name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/g, fix: 'Move to process.env.AWS_ACCESS_KEY_ID' },
|
|
11
|
+
{ name: 'GitHub Token', regex: /gh[pousr]_[A-Za-z0-9_]{36,}/g, fix: 'Move to process.env.GITHUB_TOKEN' },
|
|
12
|
+
{ name: 'OpenAI/Anthropic Key', regex: /sk-[A-Za-z0-9]{20,}/g, fix: 'Move to process.env.API_KEY' },
|
|
13
|
+
{ name: 'Stripe Live Key', regex: /[sr]k_live_[A-Za-z0-9]{20,}/g, fix: 'Move to process.env.STRIPE_SECRET_KEY' },
|
|
14
|
+
{ name: 'Private Key', regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, fix: 'Never hardcode private keys' },
|
|
15
|
+
{ name: 'Database URL', regex: /(?:postgres|mysql|mongodb|redis):\/\/[^\s'"]+:[^\s'"]+@[^\s'"]+/g, fix: 'Move to process.env.DATABASE_URL' },
|
|
16
|
+
{ name: 'Slack Token', regex: /xox[baprs]-[0-9a-zA-Z-]{10,}/g, fix: 'Move to process.env.SLACK_TOKEN' },
|
|
17
|
+
{ name: 'Hardcoded Password', regex: /(?:password|passwd|pwd)\s*[=:]\s*['"]([^'"]{8,})['"]/gi, fix: 'Use environment variable' },
|
|
18
|
+
];
|
|
19
|
+
const SAFETY_PATTERNS = [
|
|
20
|
+
{ name: 'eval()', regex: /\beval\s*\(/g, severity: 'CRITICAL', fix: 'Use JSON.parse() or Function()' },
|
|
21
|
+
{ name: 'innerHTML', regex: /\.innerHTML\s*=/g, severity: 'WARNING', fix: 'Use textContent or DOMPurify' },
|
|
22
|
+
{ name: 'Disabled SSL', regex: /rejectUnauthorized\s*:\s*false/g, severity: 'WARNING', fix: 'Set rejectUnauthorized: true' },
|
|
23
|
+
{ name: 'Wildcard CORS', regex: /(?:cors|origin)\s*[=:]\s*['"]\*['"]/gi, severity: 'WARNING', fix: 'Set specific origin' },
|
|
24
|
+
{ name: 'chmod 777', regex: /chmod\s+777/g, severity: 'WARNING', fix: 'Use 755 or 644' },
|
|
25
|
+
{ name: 'Wildcard IAM', regex: /"(?:Action|Resource)"\s*:\s*"\*"/g, severity: 'CRITICAL', fix: 'Use least-privilege' },
|
|
26
|
+
{ name: 'dangerouslySetInnerHTML', regex: /dangerouslySetInnerHTML/g, severity: 'WARNING', fix: 'Sanitize HTML first' },
|
|
27
|
+
];
|
|
28
|
+
const INJECTION_PATTERNS = [
|
|
29
|
+
'ignore previous instructions', 'ignore all previous', 'you are now',
|
|
30
|
+
'system prompt', 'developer mode', 'jailbreak', 'bypass safety',
|
|
31
|
+
'new instructions:', 'forget everything',
|
|
32
|
+
];
|
|
33
|
+
const PLACEHOLDER_SKIP = [/test|fake|dummy|placeholder|example|changeme|xxx|your_/i];
|
|
34
|
+
function scanContent(content, filename) {
|
|
35
|
+
const findings = [];
|
|
36
|
+
// Secrets
|
|
37
|
+
for (const pat of SECRET_PATTERNS) {
|
|
38
|
+
pat.regex.lastIndex = 0;
|
|
39
|
+
let m;
|
|
40
|
+
while ((m = pat.regex.exec(content)) !== null) {
|
|
41
|
+
const val = m[1] ?? m[0];
|
|
42
|
+
if (PLACEHOLDER_SKIP.some(p => p.test(val)))
|
|
43
|
+
continue;
|
|
44
|
+
const line = content.slice(0, m.index).split('\n').length;
|
|
45
|
+
findings.push({ severity: 'CRITICAL', type: pat.name, line, message: `${pat.name} detected`, fix: pat.fix });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Safety
|
|
49
|
+
for (const pat of SAFETY_PATTERNS) {
|
|
50
|
+
pat.regex.lastIndex = 0;
|
|
51
|
+
let m;
|
|
52
|
+
while ((m = pat.regex.exec(content)) !== null) {
|
|
53
|
+
const line = content.slice(0, m.index).split('\n').length;
|
|
54
|
+
findings.push({ severity: pat.severity, type: pat.name, line, message: pat.name, fix: pat.fix });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// PII
|
|
58
|
+
if (/\b\d{3}-\d{2}-\d{4}\b/.test(content)) {
|
|
59
|
+
const idx = content.search(/\b\d{3}-\d{2}-\d{4}\b/);
|
|
60
|
+
findings.push({ severity: 'CRITICAL', type: 'SSN', line: content.slice(0, idx).split('\n').length, message: 'SSN in source', fix: 'Remove immediately' });
|
|
61
|
+
}
|
|
62
|
+
return findings;
|
|
63
|
+
}
|
|
64
|
+
function computeTrustScore(findings) {
|
|
65
|
+
let critD = 0, warnD = 0, infoD = 0;
|
|
66
|
+
for (const f of findings) {
|
|
67
|
+
if (f.severity === 'CRITICAL')
|
|
68
|
+
critD += 15;
|
|
69
|
+
else if (f.severity === 'WARNING')
|
|
70
|
+
warnD += 5;
|
|
71
|
+
else
|
|
72
|
+
infoD += 1;
|
|
73
|
+
}
|
|
74
|
+
return Math.max(0, Math.round(100 - Math.min(critD, 60) - Math.min(warnD, 25) - Math.min(infoD, 10)));
|
|
75
|
+
}
|
|
76
|
+
function checkInjection(content, source) {
|
|
77
|
+
const lower = content.toLowerCase();
|
|
78
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
79
|
+
if (lower.includes(pattern)) {
|
|
80
|
+
return { severity: 'BLOCK', message: `Injection pattern "${pattern}" from ${source}` };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { severity: 'CLEAN', message: '' };
|
|
84
|
+
}
|
|
85
|
+
// ── MCP Protocol ─────────────────────────────────────────────────────────────
|
|
86
|
+
const TOOLS = [
|
|
87
|
+
{
|
|
88
|
+
name: 'verify_file',
|
|
89
|
+
description: 'Compute a trust score (0-100) for code with line-by-line findings and fixes. If score < 50, do NOT write the file.',
|
|
90
|
+
inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Code content' }, filename: { type: 'string', description: 'Filename' } }, required: ['content', 'filename'] },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'scan_content',
|
|
94
|
+
description: 'Scan code for secrets, PII, and unsafe patterns.',
|
|
95
|
+
inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Code content' }, filename: { type: 'string', description: 'Filename' } }, required: ['content', 'filename'] },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'check_secret',
|
|
99
|
+
description: 'Check if a string value is a secret/credential.',
|
|
100
|
+
inputSchema: { type: 'object', properties: { value: { type: 'string', description: 'Value to check' } }, required: ['value'] },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'check_safety',
|
|
104
|
+
description: 'Check code for unsafe patterns like eval(), innerHTML, SQL injection.',
|
|
105
|
+
inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Code' }, filename: { type: 'string', description: 'Filename' } }, required: ['content', 'filename'] },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'check_injection',
|
|
109
|
+
description: 'Scan text for prompt injection patterns.',
|
|
110
|
+
inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Content to scan' }, source: { type: 'string', description: 'Source' } }, required: ['content'] },
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
function handleTool(name, args) {
|
|
114
|
+
const text = (t) => ({ content: [{ type: 'text', text: t }] });
|
|
115
|
+
switch (name) {
|
|
116
|
+
case 'verify_file': {
|
|
117
|
+
const content = String(args.content ?? '');
|
|
118
|
+
const filename = String(args.filename ?? 'unknown');
|
|
119
|
+
const findings = scanContent(content, filename);
|
|
120
|
+
const score = computeTrustScore(findings);
|
|
121
|
+
if (findings.length === 0)
|
|
122
|
+
return text(`TRUST SCORE: ${score}/100 - CLEAN. Safe to write.`);
|
|
123
|
+
const rec = score < 50 ? 'DO NOT write this file. Fix critical issues first.' : score < 80 ? 'Warnings found. Consider fixing.' : 'Safe to write.';
|
|
124
|
+
const details = findings.map(f => ` [${f.severity}] Line ${f.line}: ${f.message}\n FIX: ${f.fix}`).join('\n');
|
|
125
|
+
return text(`TRUST SCORE: ${score}/100 for ${filename}\n${rec}\n\n${findings.length} finding(s):\n${details}`);
|
|
126
|
+
}
|
|
127
|
+
case 'scan_content': {
|
|
128
|
+
const findings = scanContent(String(args.content ?? ''), String(args.filename ?? 'unknown'));
|
|
129
|
+
if (findings.length === 0)
|
|
130
|
+
return text('CLEAN: No issues found.');
|
|
131
|
+
return text(`FOUND ${findings.length} issue(s):\n${findings.map(f => ` [${f.severity}] ${f.message}`).join('\n')}`);
|
|
132
|
+
}
|
|
133
|
+
case 'check_secret': {
|
|
134
|
+
const findings = scanContent(`const x = "${args.value}";`, 'check.ts');
|
|
135
|
+
return text(findings.length > 0 ? `WARNING: Looks like a ${findings[0].type}. Use an environment variable.` : 'OK: Not a known secret pattern.');
|
|
136
|
+
}
|
|
137
|
+
case 'check_safety': {
|
|
138
|
+
const findings = scanContent(String(args.content ?? ''), String(args.filename ?? 'unknown')).filter(f => ['eval()', 'innerHTML', 'Disabled SSL', 'Wildcard CORS', 'chmod 777', 'Wildcard IAM', 'dangerouslySetInnerHTML'].includes(f.type));
|
|
139
|
+
if (findings.length === 0)
|
|
140
|
+
return text('SAFE: No unsafe patterns.');
|
|
141
|
+
return text(`FOUND ${findings.length} unsafe pattern(s):\n${findings.map(f => ` [${f.severity}] ${f.message}\n FIX: ${f.fix}`).join('\n')}`);
|
|
142
|
+
}
|
|
143
|
+
case 'check_injection': {
|
|
144
|
+
const result = checkInjection(String(args.content ?? ''), String(args.source ?? 'unknown'));
|
|
145
|
+
return text(result.severity === 'CLEAN' ? 'CLEAN: No injection patterns.' : `${result.severity}: ${result.message}`);
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
return text(`Unknown tool: ${name}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function send(response) {
|
|
152
|
+
const json = JSON.stringify(response);
|
|
153
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
154
|
+
}
|
|
155
|
+
function handle(req) {
|
|
156
|
+
switch (req.method) {
|
|
157
|
+
case 'initialize':
|
|
158
|
+
send({ jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'corpus-mcp', version: '0.1.0' } } });
|
|
159
|
+
break;
|
|
160
|
+
case 'tools/list':
|
|
161
|
+
send({ jsonrpc: '2.0', id: req.id, result: { tools: TOOLS } });
|
|
162
|
+
break;
|
|
163
|
+
case 'tools/call': {
|
|
164
|
+
const p = req.params;
|
|
165
|
+
send({ jsonrpc: '2.0', id: req.id, result: handleTool(p.name, p.arguments ?? {}) });
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'notifications/initialized':
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
send({ jsonrpc: '2.0', id: req.id, error: { code: -32601, message: `Method not found: ${req.method}` } });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
let buffer = '';
|
|
175
|
+
process.stdin.setEncoding('utf-8');
|
|
176
|
+
process.stdin.on('data', (chunk) => {
|
|
177
|
+
buffer += chunk;
|
|
178
|
+
while (true) {
|
|
179
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
180
|
+
if (headerEnd === -1)
|
|
181
|
+
break;
|
|
182
|
+
const header = buffer.slice(0, headerEnd);
|
|
183
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
184
|
+
if (!match) {
|
|
185
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const len = parseInt(match[1], 10);
|
|
189
|
+
const bodyStart = headerEnd + 4;
|
|
190
|
+
if (buffer.length < bodyStart + len)
|
|
191
|
+
break;
|
|
192
|
+
const body = buffer.slice(bodyStart, bodyStart + len);
|
|
193
|
+
buffer = buffer.slice(bodyStart + len);
|
|
194
|
+
try {
|
|
195
|
+
handle(JSON.parse(body));
|
|
196
|
+
}
|
|
197
|
+
catch { }
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
process.stderr.write('[corpus-mcp] Server started\n');
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "corpus-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Corpus — AI coding tools check security before writing code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/standalone.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"corpus-mcp": "dist/standalone.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && tsc src/standalone.ts --outDir dist --target ES2022 --module CommonJS --moduleResolution node --esModuleInterop --skipLibCheck"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"corpus-core": "0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^20.11.0",
|
|
21
|
+
"typescript": "^5.3.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["mcp", "ai", "security", "corpus", "claude", "cursor", "copilot", "trust-score", "jac"],
|
|
24
|
+
"repository": { "type": "git", "url": "https://github.com/fluentflier/corpus" },
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|