project-graph-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/AGENT_ROLE.md +126 -0
- package/AGENT_ROLE_MINIMAL.md +54 -0
- package/CONFIGURATION.md +188 -0
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/package.json +46 -0
- package/references/symbiote-3x.md +834 -0
- package/rules/express-5.json +76 -0
- package/rules/fastify-5.json +75 -0
- package/rules/nestjs-10.json +88 -0
- package/rules/nextjs-15.json +87 -0
- package/rules/node-22.json +156 -0
- package/rules/react-18.json +87 -0
- package/rules/react-19.json +76 -0
- package/rules/symbiote-2x.json +158 -0
- package/rules/symbiote-3x.json +221 -0
- package/rules/typescript-5.json +69 -0
- package/rules/vue-3.json +79 -0
- package/src/cli-handlers.js +140 -0
- package/src/cli.js +83 -0
- package/src/complexity.js +223 -0
- package/src/custom-rules.js +583 -0
- package/src/dead-code.js +468 -0
- package/src/filters.js +226 -0
- package/src/framework-references.js +177 -0
- package/src/full-analysis.js +159 -0
- package/src/graph-builder.js +269 -0
- package/src/instructions.js +175 -0
- package/src/jsdoc-generator.js +214 -0
- package/src/large-files.js +162 -0
- package/src/mcp-server.js +375 -0
- package/src/outdated-patterns.js +295 -0
- package/src/parser.js +293 -0
- package/src/server.js +28 -0
- package/src/similar-functions.js +278 -0
- package/src/test-annotations.js +301 -0
- package/src/tool-defs.js +444 -0
- package/src/tools.js +240 -0
- package/src/undocumented.js +260 -0
- package/src/workspace.js +70 -0
- package/vendor/acorn.mjs +6145 -0
- package/vendor/walk.mjs +437 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Entry Point for Project Graph MCP
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CLI_HANDLERS } from './cli-handlers.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Print CLI help
|
|
9
|
+
*/
|
|
10
|
+
export function printHelp() {
|
|
11
|
+
console.log(`
|
|
12
|
+
project-graph-mcp - MCP server for AI agents
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
node src/server.js Start MCP stdio server
|
|
16
|
+
node src/server.js <command> [args] Run CLI command
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
skeleton <path> Get compact project overview
|
|
20
|
+
expand <symbol> Expand minified symbol (e.g., SN, SN.togglePin)
|
|
21
|
+
deps <symbol> Get dependency tree
|
|
22
|
+
usages <symbol> Find all usages
|
|
23
|
+
pending <path> List pending @test/@expect tests
|
|
24
|
+
summary <path> Get test progress summary
|
|
25
|
+
undocumented <path> Find missing JSDoc (--level=tests|params|all)
|
|
26
|
+
deadcode <path> Find unused functions/classes
|
|
27
|
+
jsdoc <file> Generate JSDoc for file
|
|
28
|
+
similar <path> Find similar functions (--threshold=60)
|
|
29
|
+
complexity <path> Analyze cyclomatic complexity (--min=1)
|
|
30
|
+
largefiles <path> Find files needing split (--problematic)
|
|
31
|
+
outdated <path> Find legacy patterns & redundant deps
|
|
32
|
+
analyze <path> Run ALL checks with Health Score
|
|
33
|
+
filters Show current filter configuration
|
|
34
|
+
instructions Show agent guidelines (JSDoc, Arch)
|
|
35
|
+
help Show this help
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
node src/server.js skeleton src/components
|
|
39
|
+
node src/server.js expand SN
|
|
40
|
+
node src/server.js pending src/
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run CLI command
|
|
46
|
+
* @param {string} command
|
|
47
|
+
* @param {string[]} args
|
|
48
|
+
*/
|
|
49
|
+
export async function runCLI(command, args) {
|
|
50
|
+
// Handle help commands
|
|
51
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
52
|
+
printHelp();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Look up handler
|
|
57
|
+
const def = CLI_HANDLERS[command];
|
|
58
|
+
if (!def) {
|
|
59
|
+
console.error(`Unknown command: ${command}`);
|
|
60
|
+
console.error('Run with "help" for usage information');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate required arg
|
|
65
|
+
if (def.requiresArg && !args[0]) {
|
|
66
|
+
console.error(def.argError || `Argument required for: ${command}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const result = await def.handler(args);
|
|
72
|
+
|
|
73
|
+
// Handle raw output (like instructions)
|
|
74
|
+
if (def.rawOutput) {
|
|
75
|
+
console.log(result);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(JSON.stringify(result, null, 2));
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error:', error.message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cyclomatic Complexity Analyzer
|
|
3
|
+
* Measures function complexity based on decision points
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
7
|
+
import { join, relative, resolve } from 'path';
|
|
8
|
+
import { parse } from '../vendor/acorn.mjs';
|
|
9
|
+
import * as walk from '../vendor/walk.mjs';
|
|
10
|
+
import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ComplexityItem
|
|
14
|
+
* @property {string} name
|
|
15
|
+
* @property {string} type - 'function' | 'method'
|
|
16
|
+
* @property {string} file
|
|
17
|
+
* @property {number} line
|
|
18
|
+
* @property {number} complexity - Cyclomatic complexity score
|
|
19
|
+
* @property {string} rating - 'low' | 'moderate' | 'high' | 'critical'
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find all JS files
|
|
24
|
+
* @param {string} dir
|
|
25
|
+
* @param {string} rootDir
|
|
26
|
+
* @returns {string[]}
|
|
27
|
+
*/
|
|
28
|
+
function findJSFiles(dir, rootDir = dir) {
|
|
29
|
+
if (dir === rootDir) parseGitignore(rootDir);
|
|
30
|
+
const files = [];
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
for (const entry of readdirSync(dir)) {
|
|
34
|
+
const fullPath = join(dir, entry);
|
|
35
|
+
const relativePath = relative(rootDir, fullPath);
|
|
36
|
+
const stat = statSync(fullPath);
|
|
37
|
+
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
if (!shouldExcludeDir(entry, relativePath)) {
|
|
40
|
+
files.push(...findJSFiles(fullPath, rootDir));
|
|
41
|
+
}
|
|
42
|
+
} else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
|
|
43
|
+
if (!shouldExcludeFile(entry, relativePath)) {
|
|
44
|
+
files.push(fullPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (e) { }
|
|
49
|
+
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate complexity of a function body
|
|
55
|
+
* @param {Object} body
|
|
56
|
+
* @returns {number}
|
|
57
|
+
*/
|
|
58
|
+
function calculateComplexity(body) {
|
|
59
|
+
let complexity = 1; // Base complexity
|
|
60
|
+
|
|
61
|
+
walk.simple(body, {
|
|
62
|
+
// Branching
|
|
63
|
+
IfStatement() { complexity++; },
|
|
64
|
+
ConditionalExpression() { complexity++; }, // ternary
|
|
65
|
+
|
|
66
|
+
// Loops
|
|
67
|
+
ForStatement() { complexity++; },
|
|
68
|
+
ForOfStatement() { complexity++; },
|
|
69
|
+
ForInStatement() { complexity++; },
|
|
70
|
+
WhileStatement() { complexity++; },
|
|
71
|
+
DoWhileStatement() { complexity++; },
|
|
72
|
+
|
|
73
|
+
// Switch cases
|
|
74
|
+
SwitchCase(node) {
|
|
75
|
+
if (node.test) complexity++; // Skip default case
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// Logical operators
|
|
79
|
+
LogicalExpression(node) {
|
|
80
|
+
if (node.operator === '&&' || node.operator === '||') {
|
|
81
|
+
complexity++;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// Nullish coalescing
|
|
86
|
+
BinaryExpression(node) {
|
|
87
|
+
if (node.operator === '??') {
|
|
88
|
+
complexity++;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// Error handling
|
|
93
|
+
CatchClause() { complexity++; },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return complexity;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get rating from complexity score
|
|
101
|
+
* @param {number} complexity
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function getRating(complexity) {
|
|
105
|
+
if (complexity <= 5) return 'low';
|
|
106
|
+
if (complexity <= 10) return 'moderate';
|
|
107
|
+
if (complexity <= 20) return 'high';
|
|
108
|
+
return 'critical';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Analyze complexity of file
|
|
113
|
+
* @param {string} filePath
|
|
114
|
+
* @returns {ComplexityItem[]}
|
|
115
|
+
*/
|
|
116
|
+
function analyzeFile(filePath, rootDir) {
|
|
117
|
+
const code = readFileSync(filePath, 'utf-8');
|
|
118
|
+
const relPath = relative(rootDir, filePath);
|
|
119
|
+
const items = [];
|
|
120
|
+
|
|
121
|
+
let ast;
|
|
122
|
+
try {
|
|
123
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return items;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
walk.simple(ast, {
|
|
129
|
+
FunctionDeclaration(node) {
|
|
130
|
+
if (!node.id) return;
|
|
131
|
+
const complexity = calculateComplexity(node.body);
|
|
132
|
+
items.push({
|
|
133
|
+
name: node.id.name,
|
|
134
|
+
type: 'function',
|
|
135
|
+
file: relPath,
|
|
136
|
+
line: node.loc.start.line,
|
|
137
|
+
complexity,
|
|
138
|
+
rating: getRating(complexity),
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
ArrowFunctionExpression(node) {
|
|
143
|
+
// Skip small arrow functions
|
|
144
|
+
if (node.body.type !== 'BlockStatement') return;
|
|
145
|
+
const complexity = calculateComplexity(node.body);
|
|
146
|
+
if (complexity > 5) {
|
|
147
|
+
// Only report complex arrow functions
|
|
148
|
+
items.push({
|
|
149
|
+
name: '(arrow)',
|
|
150
|
+
type: 'function',
|
|
151
|
+
file: relPath,
|
|
152
|
+
line: node.loc.start.line,
|
|
153
|
+
complexity,
|
|
154
|
+
rating: getRating(complexity),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
MethodDefinition(node) {
|
|
160
|
+
if (node.kind !== 'method') return;
|
|
161
|
+
const name = node.key.name || node.key.value;
|
|
162
|
+
const complexity = calculateComplexity(node.value.body);
|
|
163
|
+
items.push({
|
|
164
|
+
name,
|
|
165
|
+
type: 'method',
|
|
166
|
+
file: relPath,
|
|
167
|
+
line: node.loc.start.line,
|
|
168
|
+
complexity,
|
|
169
|
+
rating: getRating(complexity),
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return items;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get complexity analysis for directory
|
|
179
|
+
* @param {string} dir
|
|
180
|
+
* @param {Object} [options]
|
|
181
|
+
* @param {number} [options.minComplexity=1] - Minimum complexity to include
|
|
182
|
+
* @param {boolean} [options.onlyProblematic=false] - Only show high/critical
|
|
183
|
+
* @returns {Promise<{total: number, stats: Object, items: ComplexityItem[]}>}
|
|
184
|
+
*/
|
|
185
|
+
export async function getComplexity(dir, options = {}) {
|
|
186
|
+
const minComplexity = options.minComplexity || 1;
|
|
187
|
+
const onlyProblematic = options.onlyProblematic || false;
|
|
188
|
+
const resolvedDir = resolve(dir);
|
|
189
|
+
|
|
190
|
+
const files = findJSFiles(dir);
|
|
191
|
+
let allItems = [];
|
|
192
|
+
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
allItems.push(...analyzeFile(file, resolvedDir));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Filter
|
|
198
|
+
allItems = allItems.filter(item => {
|
|
199
|
+
if (item.complexity < minComplexity) return false;
|
|
200
|
+
if (onlyProblematic && (item.rating === 'low' || item.rating === 'moderate')) return false;
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Sort by complexity descending
|
|
205
|
+
allItems.sort((a, b) => b.complexity - a.complexity);
|
|
206
|
+
|
|
207
|
+
// Calculate stats
|
|
208
|
+
const stats = {
|
|
209
|
+
low: allItems.filter(i => i.rating === 'low').length,
|
|
210
|
+
moderate: allItems.filter(i => i.rating === 'moderate').length,
|
|
211
|
+
high: allItems.filter(i => i.rating === 'high').length,
|
|
212
|
+
critical: allItems.filter(i => i.rating === 'critical').length,
|
|
213
|
+
average: allItems.length > 0
|
|
214
|
+
? Math.round(allItems.reduce((s, i) => s + i.complexity, 0) / allItems.length * 10) / 10
|
|
215
|
+
: 0,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
total: allItems.length,
|
|
220
|
+
stats,
|
|
221
|
+
items: allItems.slice(0, 30),
|
|
222
|
+
};
|
|
223
|
+
}
|