project-graph-mcp 1.2.4 → 1.5.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/src/tool-defs.js CHANGED
@@ -98,7 +98,7 @@ export const TOOLS = [
98
98
  // Test Checklist Tools
99
99
  {
100
100
  name: 'get_pending_tests',
101
- description: 'Get list of pending browser tests from @test/@expect annotations.',
101
+ description: 'Get list of pending tests from ## Tests sections in .ctx.md files.',
102
102
  inputSchema: {
103
103
  type: 'object',
104
104
  properties: {
@@ -269,7 +269,7 @@ export const TOOLS = [
269
269
  level: {
270
270
  type: 'string',
271
271
  enum: ['tests', 'params', 'all'],
272
- description: 'Strictness: tests (default) = @test/@expect, params = +@param/@returns, all = +description',
272
+ description: 'Strictness: tests (default) = .ctx.md checklists, params = +@param/@returns, all = +description',
273
273
  },
274
274
  },
275
275
  required: ['path'],
@@ -474,4 +474,320 @@ export const TOOLS = [
474
474
  },
475
475
  },
476
476
  },
477
+
478
+ // Database Analysis
479
+ {
480
+ name: 'get_db_schema',
481
+ description: 'Get database schema from SQL files. Extracts tables, columns, and types from schema.sql or migration files in the project.',
482
+ inputSchema: {
483
+ type: 'object',
484
+ properties: {
485
+ path: {
486
+ type: 'string',
487
+ description: 'Path to scan (e.g., "." or "database/")',
488
+ },
489
+ },
490
+ required: ['path'],
491
+ },
492
+ },
493
+ {
494
+ name: 'get_table_usage',
495
+ description: 'Show which functions read/write database tables. Traces SQL queries in JS/TS/Python/Go code to table references.',
496
+ inputSchema: {
497
+ type: 'object',
498
+ properties: {
499
+ path: {
500
+ type: 'string',
501
+ description: 'Path to scan (e.g., "src/")',
502
+ },
503
+ table: {
504
+ type: 'string',
505
+ description: 'Optional: filter to a specific table name',
506
+ },
507
+ },
508
+ required: ['path'],
509
+ },
510
+ },
511
+ {
512
+ name: 'get_db_dead_tables',
513
+ description: 'Find tables and columns defined in schema but never referenced in code queries. Requires .sql schema files in the project.',
514
+ inputSchema: {
515
+ type: 'object',
516
+ properties: {
517
+ path: {
518
+ type: 'string',
519
+ description: 'Path to scan (e.g., ".")',
520
+ },
521
+ },
522
+ required: ['path'],
523
+ },
524
+ },
525
+
526
+ // AI Context Tools
527
+ {
528
+ name: 'get_compressed_file',
529
+ description: 'Get AI-optimized compressed version of a JS source file. Terser-minified with export legend. Saves 20-55% tokens (more with heavy JSDoc).',
530
+ inputSchema: {
531
+ type: 'object',
532
+ properties: {
533
+ path: {
534
+ type: 'string',
535
+ description: 'Path to JS/MJS file',
536
+ },
537
+ beautify: {
538
+ type: 'boolean',
539
+ description: 'Readable multi-line output (default: true). Set false for maximum compression.',
540
+ },
541
+ legend: {
542
+ type: 'boolean',
543
+ description: 'Include compact export legend header (default: true)',
544
+ },
545
+ },
546
+ required: ['path'],
547
+ },
548
+ },
549
+ {
550
+ name: 'get_project_docs',
551
+ description: 'Get compact project documentation in doc-dialect format. Returns architecture, patterns, edge cases. Merges auto-generated docs with manual .context/ files.',
552
+ inputSchema: {
553
+ type: 'object',
554
+ properties: {
555
+ path: {
556
+ type: 'string',
557
+ description: 'Project root path',
558
+ },
559
+ file: {
560
+ type: 'string',
561
+ description: 'Optional: get docs for a specific file only',
562
+ },
563
+ },
564
+ required: ['path'],
565
+ },
566
+ },
567
+ {
568
+ name: 'generate_context_docs',
569
+ description: 'Generate .context/ doc-dialect files from AST. Creates rich templates with {DESCRIBE} markers. Use agent-pool with doc-enricher skill to auto-fill descriptions. Templates include function signatures, call graphs, and DB access patterns extracted from AST.',
570
+ inputSchema: {
571
+ type: 'object',
572
+ properties: {
573
+ path: {
574
+ type: 'string',
575
+ description: 'Project root path',
576
+ },
577
+ overwrite: {
578
+ type: 'boolean',
579
+ description: 'Overwrite existing .ctx files (default: false). Existing descriptions are preserved via merge.',
580
+ },
581
+ scope: {
582
+ description: 'Scope filter: "all" (default), "focus" (git diff — recently changed files only), or array of specific file paths.',
583
+ oneOf: [
584
+ { type: 'string', enum: ['all', 'focus'] },
585
+ { type: 'array', items: { type: 'string' } },
586
+ ],
587
+ },
588
+ },
589
+ required: ['path'],
590
+ },
591
+ },
592
+ {
593
+ name: 'check_stale_docs',
594
+ description: 'Check which .ctx documentation files are outdated. Compares AST signature hashes to detect structural changes (new functions, renamed exports). Use for CI/CD doc health audits.',
595
+ inputSchema: {
596
+ type: 'object',
597
+ properties: {
598
+ path: {
599
+ type: 'string',
600
+ description: 'Project root path',
601
+ },
602
+ },
603
+ required: ['path'],
604
+ },
605
+ },
606
+ {
607
+ name: 'get_ai_context',
608
+ description: 'Boot AI agent context: skeleton + doc-dialect + optional compressed files in one call. Call FIRST when starting work on a new project. Returns totalTokens and savings vs reading raw source.',
609
+ inputSchema: {
610
+ type: 'object',
611
+ properties: {
612
+ path: {
613
+ type: 'string',
614
+ description: 'Project root path',
615
+ },
616
+ includeFiles: {
617
+ type: 'array',
618
+ items: { type: 'string' },
619
+ description: 'Specific files to include compressed (e.g., ["parser.js", "tools.js"])',
620
+ },
621
+ includeDocs: {
622
+ type: 'boolean',
623
+ description: 'Include doc-dialect documentation (default: true)',
624
+ },
625
+ includeSkeleton: {
626
+ type: 'boolean',
627
+ description: 'Include project skeleton (default: true)',
628
+ },
629
+ },
630
+ required: ['path'],
631
+ },
632
+ },
633
+
634
+ // JSDoc Consistency
635
+ {
636
+ name: 'check_jsdoc_consistency',
637
+ description: 'Validate JSDoc annotations against actual function signatures. Finds param count/name mismatches, missing @returns, type inconsistencies.',
638
+ inputSchema: {
639
+ type: 'object',
640
+ properties: {
641
+ path: {
642
+ type: 'string',
643
+ description: 'Path to scan (e.g., "src/")',
644
+ },
645
+ },
646
+ required: ['path'],
647
+ },
648
+ },
649
+
650
+ // Type Checker (optional tsc)
651
+ {
652
+ name: 'check_types',
653
+ description: 'Run TypeScript type checking on JS files with JSDoc types. Requires tsc in PATH (npm i -g typescript). Returns structured diagnostics or graceful fallback if tsc not available.',
654
+ inputSchema: {
655
+ type: 'object',
656
+ properties: {
657
+ path: {
658
+ type: 'string',
659
+ description: 'Directory to check',
660
+ },
661
+ files: {
662
+ type: 'array',
663
+ items: { type: 'string' },
664
+ description: 'Specific files to check (optional)',
665
+ },
666
+ maxDiagnostics: {
667
+ type: 'number',
668
+ description: 'Max diagnostics to return (default: 50)',
669
+ },
670
+ },
671
+ required: ['path'],
672
+ },
673
+ },
674
+
675
+ // Monorepo & Performance Tools
676
+ {
677
+ name: 'discover_sub_projects',
678
+ description: 'Find sub-projects in a monorepo. Scans packages/, apps/, services/, modules/, libs/, plugins/ for package.json.',
679
+ inputSchema: {
680
+ type: 'object',
681
+ properties: {
682
+ path: {
683
+ type: 'string',
684
+ description: 'Root path to scan for sub-projects',
685
+ },
686
+ },
687
+ required: ['path'],
688
+ },
689
+ },
690
+ {
691
+ name: 'get_analysis_summary',
692
+ description: 'Quick health score — runs only cached per-file metrics (complexity, undocumented, JSDoc). Much faster than get_full_analysis for large codebases.',
693
+ inputSchema: {
694
+ type: 'object',
695
+ properties: {
696
+ path: {
697
+ type: 'string',
698
+ description: 'Path to scan',
699
+ },
700
+ },
701
+ required: ['path'],
702
+ },
703
+ },
704
+ {
705
+ name: 'compact_project',
706
+ description: 'Compact all JS files in a directory — strips comments, whitespace, dead code. Preserves all names (mangle: false). Use with .ctx docs for AI-first workflow. Irreversible without git — use --dry-run first.',
707
+ inputSchema: {
708
+ type: 'object',
709
+ properties: {
710
+ path: {
711
+ type: 'string',
712
+ description: 'Directory to compact (e.g., "src/")',
713
+ },
714
+ dryRun: {
715
+ type: 'boolean',
716
+ description: 'Preview savings without modifying files (default: false)',
717
+ },
718
+ },
719
+ required: ['path'],
720
+ },
721
+ },
722
+ {
723
+ name: 'beautify_project',
724
+ description: 'Beautify/expand all JS files in a directory — formats with proper indentation. Inverse of compact_project. Preserves all names.',
725
+ inputSchema: {
726
+ type: 'object',
727
+ properties: {
728
+ path: {
729
+ type: 'string',
730
+ description: 'Directory to beautify (e.g., "src/")',
731
+ },
732
+ dryRun: {
733
+ type: 'boolean',
734
+ description: 'Preview without modifying files (default: false)',
735
+ },
736
+ },
737
+ required: ['path'],
738
+ },
739
+ },
740
+ {
741
+ name: 'validate_ctx_contracts',
742
+ description: 'Validate .ctx contracts against actual source code AST. Checks param count/names, export status, and reports mismatches. Zero-dependency alternative to tsc.',
743
+ inputSchema: {
744
+ type: 'object',
745
+ properties: {
746
+ path: { type: 'string', description: 'Project root path' },
747
+ strict: { type: 'boolean', description: 'Also report functions missing from .ctx (default: false)' },
748
+ },
749
+ required: ['path'],
750
+ },
751
+ },
752
+ {
753
+ name: 'edit_compressed',
754
+ description: 'Edit a function or class in source code by symbol name. Agent sends new code (compressed or formatted); server finds the symbol via AST, replaces it, validates syntax, and optionally beautifies. Use with get_compressed_file for token-efficient editing workflow.',
755
+ inputSchema: {
756
+ type: 'object',
757
+ properties: {
758
+ path: { type: 'string', description: 'Path to JS/MJS file' },
759
+ symbol: { type: 'string', description: 'Function or class name to replace' },
760
+ code: { type: 'string', description: 'New code for the symbol (full function/class definition)' },
761
+ beautify: { type: 'boolean', description: 'Beautify result after editing (default: true)' },
762
+ dryRun: { type: 'boolean', description: 'Preview without writing (default: false)' },
763
+ },
764
+ required: ['path', 'symbol', 'code'],
765
+ },
766
+ },
767
+ {
768
+ name: 'get_mode',
769
+ description: 'Get current compact code mode configuration. Returns mode (1=compact, 2=full, 3=IDE), preferences, and recommended workflow for agent.',
770
+ inputSchema: {
771
+ type: 'object',
772
+ properties: {
773
+ path: { type: 'string', description: 'Project root path' },
774
+ },
775
+ required: ['path'],
776
+ },
777
+ },
778
+ {
779
+ name: 'set_mode',
780
+ description: 'Set compact code mode for a project. Mode 1: store minified, edit directly. Mode 2: store full, use get_compressed_file + edit_compressed. Mode 3: IDE integration (future).',
781
+ inputSchema: {
782
+ type: 'object',
783
+ properties: {
784
+ path: { type: 'string', description: 'Project root path' },
785
+ mode: { type: 'number', description: 'Mode number: 1, 2, or 3' },
786
+ beautify: { type: 'boolean', description: 'Beautify code after edits (default: true)' },
787
+ autoValidate: { type: 'boolean', description: 'Auto-run .ctx validation after edits (default: false)' },
788
+ stripJSDoc: { type: 'boolean', description: 'Auto-strip JSDoc when compacting (default: false)' },
789
+ },
790
+ required: ['path', 'mode'],
791
+ },
792
+ },
477
793
  ];
package/src/tools.js CHANGED
@@ -81,7 +81,7 @@ function loadDiskCache(path) {
81
81
  * @param {string} path
82
82
  * @returns {Promise<import('./graph-builder.js').Graph>}
83
83
  */
84
- async function getGraph(path) {
84
+ export async function getGraph(path) {
85
85
  // Different path = full rebuild
86
86
  if (cachedGraph && cachedPath === path) {
87
87
  // Check for file changes via mtime
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Optional Type Checker (tsc wrapper)
3
+ * Provides JSDoc type validation via TypeScript compiler
4
+ *
5
+ * Requires `tsc` in PATH (npm i -g typescript)
6
+ * Graceful fallback if not available
7
+ */
8
+
9
+ import { execSync, spawn } from 'child_process';
10
+ import { existsSync } from 'fs';
11
+ import { resolve, join } from 'path';
12
+
13
+ /**
14
+ * @typedef {Object} TypeDiagnostic
15
+ * @property {string} file
16
+ * @property {number} line
17
+ * @property {number} column
18
+ * @property {'error'|'warning'} severity
19
+ * @property {string} message
20
+ * @property {string} code - TS error code (e.g. "TS2345")
21
+ */
22
+
23
+ /**
24
+ * Check if tsc is available
25
+ * @returns {{ available: boolean, version: string|null, path: string|null }}
26
+ */
27
+ function detectTsc() {
28
+ try {
29
+ const version = execSync('tsc --version', { encoding: 'utf-8', timeout: 5000 }).trim();
30
+ const tscPath = execSync('which tsc', { encoding: 'utf-8', timeout: 5000 }).trim();
31
+ return { available: true, version, path: tscPath };
32
+ } catch (e) {
33
+ // Try npx
34
+ try {
35
+ const version = execSync('npx tsc --version', { encoding: 'utf-8', timeout: 15000 }).trim();
36
+ return { available: true, version, path: 'npx tsc' };
37
+ } catch (e2) {
38
+ return { available: false, version: null, path: null };
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Parse tsc output line into structured diagnostic
45
+ * @param {string} line
46
+ * @param {string} baseDir
47
+ * @returns {TypeDiagnostic|null}
48
+ */
49
+ function parseDiagnosticLine(line, baseDir) {
50
+ // Format: file.js(line,col): error TS1234: message
51
+ const match = line.match(/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/);
52
+ if (!match) return null;
53
+
54
+ return {
55
+ file: match[1],
56
+ line: parseInt(match[2]),
57
+ column: parseInt(match[3]),
58
+ severity: match[4],
59
+ message: match[6],
60
+ code: match[5],
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Build tsc arguments
66
+ * @param {string} dir
67
+ * @param {Object} options
68
+ * @returns {string[]}
69
+ */
70
+ function buildArgs(dir, options = {}) {
71
+ const args = ['--noEmit'];
72
+
73
+ // Check for existing config
74
+ const tsconfig = join(dir, 'tsconfig.json');
75
+ const jsconfig = join(dir, 'jsconfig.json');
76
+
77
+ if (existsSync(tsconfig)) {
78
+ args.push('--project', tsconfig);
79
+ } else if (existsSync(jsconfig)) {
80
+ args.push('--project', jsconfig);
81
+ } else {
82
+ // No config — use sensible defaults for JS projects
83
+ args.push('--allowJs', '--checkJs');
84
+ args.push('--target', 'ESNext');
85
+ args.push('--module', 'NodeNext');
86
+ args.push('--moduleResolution', 'NodeNext');
87
+ args.push('--skipLibCheck');
88
+
89
+ // Include the directory
90
+ if (options.files?.length) {
91
+ args.push(...options.files);
92
+ } else {
93
+ args.push('--rootDir', dir);
94
+ }
95
+ }
96
+
97
+ return args;
98
+ }
99
+
100
+ /**
101
+ * Run type checking on a directory
102
+ * @param {string} dir - Directory to check
103
+ * @param {Object} [options]
104
+ * @param {string[]} [options.files] - Specific files to check
105
+ * @param {number} [options.maxDiagnostics=50] - Max diagnostics to return
106
+ * @returns {Promise<{ available: boolean, version: string|null, diagnostics: TypeDiagnostic[], summary: Object, hint: string|null }>}
107
+ */
108
+ export async function checkTypes(dir, options = {}) {
109
+ const maxDiagnostics = options.maxDiagnostics || 50;
110
+ const resolvedDir = resolve(dir);
111
+
112
+ // Detect tsc
113
+ const tsc = detectTsc();
114
+ if (!tsc.available) {
115
+ return {
116
+ available: false,
117
+ version: null,
118
+ diagnostics: [],
119
+ summary: { total: 0, errors: 0, warnings: 0 },
120
+ hint: 'TypeScript not found. Install: npm i -g typescript',
121
+ };
122
+ }
123
+
124
+ // Build and run command
125
+ const args = buildArgs(resolvedDir, options);
126
+ const cmd = tsc.path.includes('npx') ? 'npx' : 'tsc';
127
+ const cmdArgs = tsc.path.includes('npx') ? ['tsc', ...args] : args;
128
+
129
+ const result = await new Promise((res) => {
130
+ const child = spawn(cmd, cmdArgs, {
131
+ cwd: resolvedDir,
132
+ stdio: ['ignore', 'pipe', 'pipe'],
133
+ });
134
+
135
+ let stdout = '';
136
+ let stderr = '';
137
+ child.stdout.on('data', (d) => { stdout += d; });
138
+ child.stderr.on('data', (d) => { stderr += d; });
139
+
140
+ const timer = setTimeout(() => {
141
+ child.kill('SIGTERM');
142
+ res({ stdout, stderr, killed: true });
143
+ }, 60000);
144
+
145
+ child.on('close', () => {
146
+ clearTimeout(timer);
147
+ res({ stdout, stderr, killed: false });
148
+ });
149
+
150
+ child.on('error', (e) => {
151
+ clearTimeout(timer);
152
+ res({ stdout: '', stderr: e.message, killed: false });
153
+ });
154
+ });
155
+
156
+ // Parse output (tsc exits with 1 on errors, stdout has diagnostics)
157
+ const output = (result.stdout || '') + (result.stderr || '');
158
+ const lines = output.split('\n').filter(l => l.trim());
159
+
160
+ const diagnostics = [];
161
+ for (const line of lines) {
162
+ const diag = parseDiagnosticLine(line, resolvedDir);
163
+ if (diag && diagnostics.length < maxDiagnostics) {
164
+ diagnostics.push(diag);
165
+ }
166
+ }
167
+
168
+ const errors = diagnostics.filter(d => d.severity === 'error').length;
169
+ const warnings = diagnostics.filter(d => d.severity === 'warning').length;
170
+
171
+ const byFile = {};
172
+ for (const d of diagnostics) {
173
+ byFile[d.file] = (byFile[d.file] || 0) + 1;
174
+ }
175
+
176
+ return {
177
+ available: true,
178
+ version: tsc.version,
179
+ diagnostics,
180
+ summary: {
181
+ total: diagnostics.length,
182
+ errors,
183
+ warnings,
184
+ byFile,
185
+ },
186
+ hint: null,
187
+ };
188
+ }
@@ -74,8 +74,8 @@ function extractComments(code) {
74
74
 
75
75
  /**
76
76
  * Find JSDoc comment before a target line
77
- * @param {Array<{text: string, endLine: number}>} comments
78
- * @param {number} targetLine
77
+ * @param {Array<{text: string, endLine: number}>} comments - Extracted JSDoc comments
78
+ * @param {number} targetLine - Line number to search before
79
79
  * @returns {string|null}
80
80
  */
81
81
  function findJSDocBefore(comments, targetLine) {
@@ -100,15 +100,9 @@ function checkMissing(jsdoc, level) {
100
100
  if (!jsdoc) {
101
101
  if (level === 'all') missing.push('description');
102
102
  if (level === 'params' || level === 'all') missing.push('@param', '@returns');
103
- if (level === 'tests' || level === 'params' || level === 'all') missing.push('@test', '@expect');
104
103
  return missing;
105
104
  }
106
105
 
107
- if (level === 'tests' || level === 'params' || level === 'all') {
108
- if (!jsdoc.includes('@test')) missing.push('@test');
109
- if (!jsdoc.includes('@expect')) missing.push('@expect');
110
- }
111
-
112
106
  if (level === 'params' || level === 'all') {
113
107
  if (!jsdoc.includes('@param')) missing.push('@param');
114
108
  if (!jsdoc.includes('@returns') && !jsdoc.includes('@return')) missing.push('@returns');
@@ -124,13 +118,13 @@ const SKIP_METHODS = [
124
118
  ];
125
119
 
126
120
  /**
127
- * Parse file using AST and find undocumented items
121
+ * Parse file using AST and find undocumented items (per-file export for cache integration)
128
122
  * @param {string} code
129
123
  * @param {string} filePath
130
124
  * @param {'tests'|'params'|'all'} level
131
125
  * @returns {UndocumentedItem[]}
132
126
  */
133
- function parseFile(code, filePath, level) {
127
+ export function checkUndocumentedFile(code, filePath, level) {
134
128
  const results = [];
135
129
 
136
130
  let ast;
@@ -223,8 +217,13 @@ export function getUndocumented(dir, level = 'tests') {
223
217
  const results = [];
224
218
 
225
219
  for (const file of files) {
226
- const content = readFileSync(file, 'utf-8');
227
- const items = parseFile(content, relative(resolvedDir, file), level);
220
+ let content;
221
+ try {
222
+ content = readFileSync(file, 'utf-8');
223
+ } catch (e) {
224
+ continue; // File deleted between findJSFiles and read
225
+ }
226
+ const items = checkUndocumentedFile(content, relative(resolvedDir, file), level);
228
227
  results.push(...items);
229
228
  }
230
229
 
package/src/workspace.js CHANGED
@@ -51,7 +51,7 @@ export function getWorkspaceRoot() {
51
51
  * Resolve a path argument against workspace root.
52
52
  * Absolute paths are returned as-is.
53
53
  * Relative paths are resolved against the workspace root.
54
- * @param {string} path
54
+ * @param {string} inputPath
55
55
  * @returns {string}
56
56
  */
57
57
  export function resolvePath(inputPath) {