@misterhuydo/cairn-mcp 1.0.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/LICENSE +21 -0
- package/bin/cairn-mcp.js +21 -0
- package/how-to-use.md +61 -0
- package/index.js +141 -0
- package/package.json +43 -0
- package/src/bundler/bundleWriter.js +113 -0
- package/src/bundler/minifier.js +69 -0
- package/src/bundler/vueMinifier.js +39 -0
- package/src/graph/db.js +69 -0
- package/src/graph/edges.js +21 -0
- package/src/graph/nodes.js +30 -0
- package/src/indexer/buildParsers/gradleParser.js +14 -0
- package/src/indexer/buildParsers/mavenParser.js +15 -0
- package/src/indexer/buildParsers/npmParser.js +17 -0
- package/src/indexer/fileWalker.js +40 -0
- package/src/indexer/parserFactory.js +35 -0
- package/src/indexer/parsers/configParser.js +17 -0
- package/src/indexer/parsers/javaParser.js +31 -0
- package/src/indexer/parsers/markdownParser.js +17 -0
- package/src/indexer/parsers/pythonParser.js +25 -0
- package/src/indexer/parsers/sqlParser.js +14 -0
- package/src/indexer/parsers/tsParser.js +45 -0
- package/src/indexer/parsers/vueParser.js +25 -0
- package/src/indexer/parsers/xmlParser.js +25 -0
- package/src/indexer/securityScanner.js +103 -0
- package/src/tools/bundle.js +30 -0
- package/src/tools/codeGraph.js +111 -0
- package/src/tools/describe.js +51 -0
- package/src/tools/maintain.js +103 -0
- package/src/tools/search.js +27 -0
- package/src/tools/security.js +53 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { walkRepo, BUILD_FILES } from '../indexer/fileWalker.js';
|
|
4
|
+
import { parseFile } from '../indexer/parserFactory.js';
|
|
5
|
+
import { scanFile } from '../indexer/securityScanner.js';
|
|
6
|
+
import { parseMaven } from '../indexer/buildParsers/mavenParser.js';
|
|
7
|
+
import { parseNpm } from '../indexer/buildParsers/npmParser.js';
|
|
8
|
+
import { parseGradle } from '../indexer/buildParsers/gradleParser.js';
|
|
9
|
+
import { upsertFile, upsertSymbol, clearFileData } from '../graph/nodes.js';
|
|
10
|
+
import { insertDependency, insertBuildDep, insertSecurityFinding } from '../graph/edges.js';
|
|
11
|
+
|
|
12
|
+
function inferFileType(filePath) {
|
|
13
|
+
const fp = filePath.toLowerCase();
|
|
14
|
+
if (fp.includes('test') || fp.includes('spec')) return 'test';
|
|
15
|
+
if (fp.endsWith('pom.xml') || fp.endsWith('package.json') || fp.endsWith('build.gradle')) return 'build';
|
|
16
|
+
if (fp.endsWith('.yml') || fp.endsWith('.yaml') || fp.endsWith('.properties') || fp.endsWith('.env')) return 'config';
|
|
17
|
+
if (fp.endsWith('.md') || fp.endsWith('.html')) return 'doc';
|
|
18
|
+
return 'source';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function maintain(db, { roots, languages }) {
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
const stats = {
|
|
24
|
+
repos_indexed: 0,
|
|
25
|
+
files_by_language: {},
|
|
26
|
+
symbols_total: 0,
|
|
27
|
+
dependencies_mapped: 0,
|
|
28
|
+
security_findings: 0,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (const root of roots) {
|
|
32
|
+
const repoName = path.basename(root);
|
|
33
|
+
stats.repos_indexed++;
|
|
34
|
+
|
|
35
|
+
// Index source files
|
|
36
|
+
const files = await walkRepo(root);
|
|
37
|
+
for (const filePath of files) {
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
40
|
+
const result = await parseFile(filePath, content, repoName);
|
|
41
|
+
if (!result) continue;
|
|
42
|
+
|
|
43
|
+
const { language } = result;
|
|
44
|
+
if (languages && !languages.includes(language)) continue;
|
|
45
|
+
|
|
46
|
+
stats.files_by_language[language] = (stats.files_by_language[language] || 0) + 1;
|
|
47
|
+
|
|
48
|
+
const fileId = upsertFile(db, {
|
|
49
|
+
repo: repoName,
|
|
50
|
+
path: filePath,
|
|
51
|
+
language,
|
|
52
|
+
file_type: inferFileType(filePath),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
clearFileData(db, fileId);
|
|
56
|
+
|
|
57
|
+
for (const sym of (result.symbols || [])) {
|
|
58
|
+
upsertSymbol(db, { ...sym, file_id: fileId, language });
|
|
59
|
+
stats.symbols_total++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const imp of (result.imports || [])) {
|
|
63
|
+
insertDependency(db, { from_file_id: fileId, to_fqn: imp, dep_type: 'imports' });
|
|
64
|
+
stats.dependencies_mapped++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const findings = scanFile(filePath, content, language);
|
|
68
|
+
for (const f of findings) {
|
|
69
|
+
insertSecurityFinding(db, { file_id: fileId, ...f });
|
|
70
|
+
stats.security_findings++;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Skip unreadable/unparseable files
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Index build files
|
|
78
|
+
for (const buildFile of BUILD_FILES) {
|
|
79
|
+
const buildPath = path.join(root, buildFile);
|
|
80
|
+
try {
|
|
81
|
+
const content = await fs.readFile(buildPath, 'utf-8');
|
|
82
|
+
let deps = [];
|
|
83
|
+
if (buildFile === 'pom.xml') deps = parseMaven(buildPath, content, repoName);
|
|
84
|
+
else if (buildFile === 'package.json') deps = parseNpm(buildPath, content, repoName);
|
|
85
|
+
else if (buildFile === 'build.gradle') deps = parseGradle(buildPath, content, repoName);
|
|
86
|
+
for (const dep of deps) insertBuildDep(db, { repo: repoName, ...dep });
|
|
87
|
+
} catch {
|
|
88
|
+
// Build file not present or unreadable
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Rebuild FTS5 index
|
|
94
|
+
db.exec(`
|
|
95
|
+
DELETE FROM fts_index;
|
|
96
|
+
INSERT INTO fts_index(fqn, name, description, path, repo, language, kind)
|
|
97
|
+
SELECT s.fqn, s.name, COALESCE(s.description,''), f.path, f.repo, s.language, s.kind
|
|
98
|
+
FROM symbols s JOIN files f ON s.file_id = f.id;
|
|
99
|
+
`);
|
|
100
|
+
|
|
101
|
+
stats.duration_ms = Date.now() - startTime;
|
|
102
|
+
return { content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }] };
|
|
103
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function search(db, { query, limit = 10, language, kind }) {
|
|
2
|
+
if (!query?.trim()) {
|
|
3
|
+
return { content: [{ type: 'text', text: '[]' }] };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Build the SQL — FTS5 columns are: fqn, name, description, path, repo, language, kind
|
|
7
|
+
let sql = `
|
|
8
|
+
SELECT fqn, name, kind, language, description, path, repo,
|
|
9
|
+
bm25(fts_index) as score
|
|
10
|
+
FROM fts_index
|
|
11
|
+
WHERE fts_index MATCH ?
|
|
12
|
+
`;
|
|
13
|
+
const params = [query];
|
|
14
|
+
|
|
15
|
+
if (language) { sql += ' AND language = ?'; params.push(language); }
|
|
16
|
+
if (kind) { sql += ' AND kind = ?'; params.push(kind); }
|
|
17
|
+
|
|
18
|
+
sql += ' ORDER BY bm25(fts_index) LIMIT ?';
|
|
19
|
+
params.push(limit);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const results = db.prepare(sql).all(...params);
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function security(db, { paths, severity = 'HIGH', language }) {
|
|
2
|
+
let sql = `
|
|
3
|
+
SELECT sf.severity, sf.cwe, sf.rule_name as rule, sf.line,
|
|
4
|
+
sf.code_snippet as snippet, sf.description as fix,
|
|
5
|
+
f.path as file, f.language as lang
|
|
6
|
+
FROM security_findings sf
|
|
7
|
+
JOIN files f ON sf.file_id = f.id
|
|
8
|
+
WHERE 1=1
|
|
9
|
+
`;
|
|
10
|
+
const params = [];
|
|
11
|
+
|
|
12
|
+
if (severity === 'HIGH') {
|
|
13
|
+
sql += ` AND sf.severity = 'HIGH'`;
|
|
14
|
+
} else if (severity === 'MEDIUM') {
|
|
15
|
+
sql += ` AND sf.severity IN ('HIGH','MEDIUM')`;
|
|
16
|
+
} else if (severity !== 'ALL') {
|
|
17
|
+
sql += ' AND sf.severity = ?';
|
|
18
|
+
params.push(severity);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (language) {
|
|
22
|
+
sql += ' AND f.language = ?';
|
|
23
|
+
params.push(language);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (paths && paths.length > 0) {
|
|
27
|
+
sql += ` AND (${paths.map(() => 'f.path LIKE ?').join(' OR ')})`;
|
|
28
|
+
params.push(...paths.map(p => `${p}%`));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
sql += ' ORDER BY sf.severity, f.path';
|
|
32
|
+
|
|
33
|
+
const findings = db.prepare(sql).all(...params);
|
|
34
|
+
|
|
35
|
+
const byLanguage = {};
|
|
36
|
+
const bySeverity = {};
|
|
37
|
+
for (const f of findings) {
|
|
38
|
+
byLanguage[f.lang] = (byLanguage[f.lang] || 0) + 1;
|
|
39
|
+
bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: JSON.stringify({
|
|
46
|
+
total_findings: findings.length,
|
|
47
|
+
by_language: byLanguage,
|
|
48
|
+
by_severity: bySeverity,
|
|
49
|
+
findings: findings.slice(0, 100),
|
|
50
|
+
}, null, 2),
|
|
51
|
+
}],
|
|
52
|
+
};
|
|
53
|
+
}
|