project-graph-mcp 1.5.0 → 2.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.
Files changed (121) hide show
  1. package/README.md +128 -8
  2. package/package.json +12 -8
  3. package/src/.project-graph-cache.json +1 -1
  4. package/src/analysis/analysis-cache.js +7 -0
  5. package/src/analysis/complexity.js +14 -0
  6. package/src/analysis/custom-rules.js +36 -0
  7. package/src/analysis/db-analysis.js +9 -0
  8. package/src/analysis/dead-code.js +19 -0
  9. package/src/analysis/full-analysis.js +18 -0
  10. package/src/analysis/jsdoc-checker.js +24 -0
  11. package/src/analysis/jsdoc-generator.js +10 -0
  12. package/src/analysis/large-files.js +11 -0
  13. package/src/analysis/outdated-patterns.js +12 -0
  14. package/src/analysis/similar-functions.js +16 -0
  15. package/src/analysis/test-annotations.js +21 -0
  16. package/src/analysis/type-checker.js +8 -0
  17. package/src/analysis/undocumented.js +14 -0
  18. package/src/cli/cli-handlers.js +4 -0
  19. package/src/cli/cli.js +5 -0
  20. package/src/compact/ai-context.js +7 -0
  21. package/src/compact/compact.js +18 -0
  22. package/src/compact/compress.js +13 -0
  23. package/src/compact/ctx-to-jsdoc.js +29 -0
  24. package/src/compact/doc-dialect.js +30 -0
  25. package/src/compact/expand.js +37 -0
  26. package/src/compact/framework-references.js +5 -0
  27. package/src/compact/instructions.js +3 -0
  28. package/src/compact/mode-config.js +8 -0
  29. package/src/compact/validate-pipeline.js +9 -0
  30. package/src/core/event-bus.js +9 -0
  31. package/src/core/filters.js +14 -0
  32. package/src/core/graph-builder.js +12 -0
  33. package/src/core/parser.js +31 -0
  34. package/src/core/workspace.js +8 -0
  35. package/src/lang/lang-go.js +17 -0
  36. package/src/lang/lang-python.js +12 -0
  37. package/src/lang/lang-sql.js +23 -0
  38. package/src/lang/lang-typescript.js +9 -0
  39. package/src/lang/lang-utils.js +4 -0
  40. package/src/mcp/mcp-server.js +17 -0
  41. package/src/mcp/tool-defs.js +3 -0
  42. package/src/mcp/tools.js +25 -0
  43. package/src/network/backend-lifecycle.js +19 -0
  44. package/src/network/backend.js +5 -0
  45. package/src/network/local-gateway.js +23 -0
  46. package/src/network/mdns.js +13 -0
  47. package/src/network/server.js +10 -0
  48. package/src/network/web-server.js +34 -0
  49. package/web/.project-graph-cache.json +1 -0
  50. package/web/app.js +16 -0
  51. package/web/components/code-block.js +3 -0
  52. package/web/components/quick-open.js +5 -0
  53. package/web/dashboard-state.js +3 -0
  54. package/web/dashboard.html +27 -0
  55. package/web/dashboard.js +8 -0
  56. package/web/highlight.js +13 -0
  57. package/web/index.html +35 -0
  58. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  59. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  60. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  61. package/web/panels/EventItem/EventItem.css.js +1 -0
  62. package/web/panels/EventItem/EventItem.js +4 -0
  63. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  64. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  65. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  66. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  67. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  68. package/web/panels/ProjectList/ProjectList.js +4 -0
  69. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  70. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  71. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  72. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  73. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  74. package/web/panels/code-viewer.js +5 -0
  75. package/web/panels/ctx-panel.js +4 -0
  76. package/web/panels/dep-graph.js +6 -0
  77. package/web/panels/file-tree.js +188 -0
  78. package/web/panels/health-panel.js +3 -0
  79. package/web/panels/live-monitor.js +3 -0
  80. package/web/state.js +17 -0
  81. package/web/style.css +157 -0
  82. package/references/symbiote-3x.md +0 -834
  83. package/src/ai-context.js +0 -113
  84. package/src/analysis-cache.js +0 -155
  85. package/src/cli-handlers.js +0 -271
  86. package/src/cli.js +0 -95
  87. package/src/compact.js +0 -207
  88. package/src/complexity.js +0 -237
  89. package/src/compress.js +0 -319
  90. package/src/ctx-to-jsdoc.js +0 -514
  91. package/src/custom-rules.js +0 -584
  92. package/src/db-analysis.js +0 -194
  93. package/src/dead-code.js +0 -468
  94. package/src/doc-dialect.js +0 -716
  95. package/src/filters.js +0 -227
  96. package/src/framework-references.js +0 -177
  97. package/src/full-analysis.js +0 -470
  98. package/src/graph-builder.js +0 -299
  99. package/src/instructions.js +0 -73
  100. package/src/jsdoc-checker.js +0 -351
  101. package/src/jsdoc-generator.js +0 -203
  102. package/src/lang-go.js +0 -285
  103. package/src/lang-python.js +0 -197
  104. package/src/lang-sql.js +0 -309
  105. package/src/lang-typescript.js +0 -190
  106. package/src/lang-utils.js +0 -124
  107. package/src/large-files.js +0 -163
  108. package/src/mcp-server.js +0 -675
  109. package/src/mode-config.js +0 -127
  110. package/src/outdated-patterns.js +0 -296
  111. package/src/parser.js +0 -662
  112. package/src/server.js +0 -28
  113. package/src/similar-functions.js +0 -279
  114. package/src/test-annotations.js +0 -323
  115. package/src/tool-defs.js +0 -793
  116. package/src/tools.js +0 -470
  117. package/src/type-checker.js +0 -188
  118. package/src/undocumented.js +0 -259
  119. package/src/workspace.js +0 -70
  120. /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
  121. /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
package/src/mcp-server.js DELETED
@@ -1,675 +0,0 @@
1
- /**
2
- * Core MCP Server Logic
3
- *
4
- * Implements bidirectional JSON-RPC 2.0 over stdio:
5
- * - Handles client→server requests (tools/list, tools/call)
6
- * - Sends server→client requests (roots/list) to get workspace info
7
- */
8
-
9
- import fs from 'fs';
10
- import path from 'path';
11
- import { fileURLToPath } from 'url';
12
- import { TOOLS } from './tool-defs.js';
13
- import { getSkeleton, getFocusZone, expand, deps, usages, invalidateCache, getCallChain } from './tools.js';
14
- import { getPendingTests, markTestPassed, markTestFailed, getTestSummary, resetTestState } from './test-annotations.js';
15
- import { getFilters, setFilters, addExcludes, removeExcludes, resetFilters } from './filters.js';
16
- import { getInstructions } from './instructions.js';
17
- import { getUndocumentedSummary } from './undocumented.js';
18
- import { getDeadCode } from './dead-code.js';
19
- import { generateJSDoc, generateJSDocFor } from './jsdoc-generator.js';
20
- import { getSimilarFunctions } from './similar-functions.js';
21
- import { getComplexity } from './complexity.js';
22
- import { getLargeFiles } from './large-files.js';
23
- import { getOutdatedPatterns } from './outdated-patterns.js';
24
- import { getFullAnalysis, getAnalysisSummaryOnly } from './full-analysis.js';
25
- import { getCustomRules, setCustomRule, checkCustomRules } from './custom-rules.js';
26
- import { getFrameworkReference } from './framework-references.js';
27
- import { setRoots, resolvePath } from './workspace.js';
28
- import { getDBSchema, getTableUsage, getDBDeadTables } from './db-analysis.js';
29
- import { compressFile, editCompressed } from './compress.js';
30
- import { getProjectDocs, generateContextFiles, checkStaleness } from './doc-dialect.js';
31
- import { getGraph } from './tools.js';
32
- import { parseProject, discoverSubProjects } from './parser.js';
33
- import { getAiContext } from './ai-context.js';
34
- import { checkJSDocConsistency } from './jsdoc-checker.js';
35
- import { checkTypes } from './type-checker.js';
36
- import { compactProject, expandProject } from './compact.js';
37
- import { validateCtxContracts } from './ctx-to-jsdoc.js';
38
- import { getConfig, setConfig, getModeDescription, getModeWorkflow } from './mode-config.js';
39
-
40
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
41
-
42
- /**
43
- * Tool handlers registry
44
- * Maps tool names to their handler functions
45
- */
46
- const TOOL_HANDLERS = {
47
- // Graph Tools
48
- get_skeleton: (args) => getSkeleton(resolvePath(args.path)),
49
- get_focus_zone: (args) => getFocusZone({ ...args, path: resolvePath(args.path) }),
50
- expand: (args) => expand(args.symbol),
51
- deps: (args) => deps(args.symbol),
52
- usages: (args) => usages(args.symbol),
53
- get_call_chain: (args) => getCallChain({ from: args.from, to: args.to, path: args.path ? resolvePath(args.path) : undefined }),
54
- invalidate_cache: () => { invalidateCache(); return { success: true }; },
55
-
56
- // Test Checklist Tools
57
- get_pending_tests: (args) => getPendingTests(resolvePath(args.path)),
58
- mark_test_passed: (args) => markTestPassed(args.testId),
59
- mark_test_failed: (args) => markTestFailed(args.testId, args.reason),
60
- get_test_summary: (args) => getTestSummary(resolvePath(args.path)),
61
- reset_test_state: () => resetTestState(),
62
-
63
- // Filter Tools
64
- get_filters: () => getFilters(),
65
- set_filters: (args) => setFilters(args),
66
- add_excludes: (args) => addExcludes(args.dirs),
67
- remove_excludes: (args) => removeExcludes(args.dirs),
68
- reset_filters: () => resetFilters(),
69
-
70
- // Guidelines
71
- get_usage_guide: (args) => {
72
- try {
73
- const guidePath = path.join(__dirname, '..', 'GUIDE.md');
74
- const content = fs.readFileSync(guidePath, 'utf8');
75
- if (!args.topic) return content;
76
- const regex = new RegExp(`## ${args.topic}`, 'i');
77
- const match = content.match(regex);
78
- if (!match) return `Topic '${args.topic}' not found in guide.`;
79
- const start = match.index;
80
- let end = content.indexOf('\n## ', start + 1);
81
- if (end === -1) end = content.length;
82
- return content.substring(start, end).trim();
83
- } catch (e) {
84
- return `Failed to read usage guide: ${e.message}`;
85
- }
86
- },
87
- get_agent_instructions: () => getInstructions(),
88
-
89
- // Documentation
90
- get_undocumented: (args) => getUndocumentedSummary(resolvePath(args.path), args.level || 'tests'),
91
-
92
- // Code Quality
93
- get_dead_code: (args) => getDeadCode(resolvePath(args.path)),
94
- generate_jsdoc: (args) => args.name
95
- ? generateJSDocFor(resolvePath(args.path), args.name)
96
- : generateJSDoc(resolvePath(args.path)),
97
- get_similar_functions: (args) => getSimilarFunctions(resolvePath(args.path), { threshold: args.threshold }),
98
- get_complexity: (args) => getComplexity(resolvePath(args.path), {
99
- minComplexity: args.minComplexity,
100
- onlyProblematic: args.onlyProblematic,
101
- }),
102
- get_large_files: (args) => getLargeFiles(resolvePath(args.path), { onlyProblematic: args.onlyProblematic }),
103
- get_outdated_patterns: (args) => getOutdatedPatterns(resolvePath(args.path), {
104
- codeOnly: args.codeOnly,
105
- depsOnly: args.depsOnly,
106
- }),
107
- get_full_analysis: (args) => getFullAnalysis(resolvePath(args.path), { includeItems: args.includeItems }),
108
-
109
- // Custom Rules
110
- get_custom_rules: () => getCustomRules(),
111
- set_custom_rule: (args) => setCustomRule(args.ruleSet, args.rule),
112
- check_custom_rules: (args) => checkCustomRules(resolvePath(args.path), {
113
- ruleSet: args.ruleSet,
114
- severity: args.severity,
115
- }),
116
-
117
- // Framework References
118
- get_framework_reference: (args) => getFrameworkReference({
119
- framework: args.framework,
120
- path: args.path ? resolvePath(args.path) : undefined,
121
- }),
122
-
123
- // Database Analysis
124
- get_db_schema: (args) => getDBSchema(resolvePath(args.path)),
125
- get_table_usage: (args) => getTableUsage(resolvePath(args.path), args.table),
126
- get_db_dead_tables: (args) => getDBDeadTables(resolvePath(args.path)),
127
-
128
- // AI Context
129
- get_compressed_file: (args) => compressFile(resolvePath(args.path), {
130
- beautify: args.beautify,
131
- legend: args.legend,
132
- }),
133
- get_project_docs: async (args) => {
134
- const projectPath = resolvePath(args.path);
135
- const graph = await getGraph(projectPath);
136
- const docs = getProjectDocs(graph, projectPath, { file: args.file });
137
- // Lazy staleness check — wrapped in try-catch for projects with parse errors
138
- try {
139
- const parsed = await parseProject(projectPath);
140
- const staleness = checkStaleness(projectPath, parsed);
141
- return { docs, staleFiles: staleness.stale, freshCount: staleness.fresh };
142
- } catch { return { docs }; }
143
- },
144
- generate_context_docs: async (args) => {
145
- const projectPath = resolvePath(args.path);
146
- const graph = await getGraph(projectPath);
147
- const parsed = await parseProject(projectPath);
148
- return generateContextFiles(graph, projectPath, parsed, {
149
- overwrite: args.overwrite,
150
- scope: args.scope,
151
- });
152
- },
153
- check_stale_docs: async (args) => {
154
- const projectPath = resolvePath(args.path);
155
- const parsed = await parseProject(projectPath);
156
- return checkStaleness(projectPath, parsed);
157
- },
158
- get_ai_context: async (args) => {
159
- const projectPath = resolvePath(args.path);
160
- const result = await getAiContext(projectPath, {
161
- includeFiles: args.includeFiles,
162
- includeDocs: args.includeDocs,
163
- includeSkeleton: args.includeSkeleton,
164
- });
165
- // Add staleness info
166
- try {
167
- const parsed = await parseProject(projectPath);
168
- const staleness = checkStaleness(projectPath, parsed);
169
- result.staleFiles = staleness.stale;
170
- } catch { /* parse error — skip staleness */ }
171
- return result;
172
- },
173
-
174
- // JSDoc Consistency
175
- check_jsdoc_consistency: (args) => {
176
- return checkJSDocConsistency(resolvePath(args.path));
177
- },
178
-
179
- // Type Checker (optional tsc)
180
- check_types: async (args) => {
181
- return checkTypes(resolvePath(args.path), {
182
- files: args.files,
183
- maxDiagnostics: args.maxDiagnostics,
184
- });
185
- },
186
-
187
- // Monorepo & Performance
188
- discover_sub_projects: (args) => {
189
- return discoverSubProjects(resolvePath(args.path));
190
- },
191
- get_analysis_summary: (args) => {
192
- return getAnalysisSummaryOnly(resolvePath(args.path));
193
- },
194
- compact_project: (args) => {
195
- return compactProject(resolvePath(args.path), { dryRun: args.dryRun || false });
196
- },
197
- beautify_project: (args) => {
198
- return expandProject(resolvePath(args.path), { dryRun: args.dryRun || false });
199
- },
200
- validate_ctx_contracts: (args) => {
201
- return validateCtxContracts(resolvePath(args.path), { strict: args.strict || false });
202
- },
203
- edit_compressed: (args) => {
204
- return editCompressed(resolvePath(args.path), args.symbol, args.code, {
205
- beautify: args.beautify !== false,
206
- dryRun: args.dryRun || false,
207
- });
208
- },
209
- get_mode: (args) => {
210
- const dir = resolvePath(args.path);
211
- const config = getConfig(dir);
212
- return {
213
- ...config,
214
- description: getModeDescription(config.mode),
215
- workflow: getModeWorkflow(config.mode),
216
- };
217
- },
218
- set_mode: (args) => {
219
- const dir = resolvePath(args.path);
220
- const updates = { mode: args.mode };
221
- if (args.beautify !== undefined) updates.beautify = args.beautify;
222
- if (args.autoValidate !== undefined) updates.autoValidate = args.autoValidate;
223
- if (args.stripJSDoc !== undefined) updates.stripJSDoc = args.stripJSDoc;
224
- return setConfig(dir, updates);
225
- },
226
- };
227
-
228
- /**
229
- * Response hints — contextual coaching tips appended to tool responses.
230
- * Maps tool names to hint generators. Each receives the result and returns
231
- * an array of hint strings (or empty array for no hints).
232
- *
233
- * @type {Record<string, (result: any) => string[]>}
234
- */
235
- const RESPONSE_HINTS = {
236
- get_skeleton: () => [
237
- '💡 Use expand("SYMBOL") to see code for a specific class.',
238
- '💡 Use deps("SYMBOL") to see architecture dependencies.',
239
- '💡 After code changes, run invalidate_cache() to refresh the graph.',
240
- ],
241
-
242
- expand: (result) => {
243
- const hints = [];
244
- if (result.methods?.length > 10) {
245
- hints.push('💡 Large class detected. Run get_complexity() to find refactoring targets.');
246
- }
247
- hints.push('💡 Use deps() to see what depends on this symbol.');
248
- // Nudge: document if no .ctx exists
249
- if (result.file) {
250
- hints.push(`📝 No .ctx for ${result.file}? Run generate_context_docs({ scope: ["${result.file}"] }) to create documentation.`);
251
- }
252
- return hints;
253
- },
254
-
255
- deps: () => [
256
- '💡 Use usages() for cross-project reference search.',
257
- ],
258
-
259
- get_call_chain: (result) => {
260
- if (result.error) return [];
261
- return [
262
- '💡 Use expand() on intermediate steps to understand how data is passed along the chain.',
263
- ];
264
- },
265
-
266
- invalidate_cache: () => [
267
- '✅ Cache cleared. Run get_skeleton() to rebuild the project graph.',
268
- ],
269
-
270
- get_dead_code: (result) => {
271
- const hints = ['💡 Review each item before removing — some may be used dynamically.'];
272
- if (result.unusedExports?.length > 20) {
273
- hints.push('💡 Consider delegating cleanup to agent-pool: delegate_task({ prompt: "Remove dead code..." })');
274
- }
275
- return hints;
276
- },
277
-
278
- get_full_analysis: () => [
279
- '💡 Focus on items with "critical" severity first.',
280
- '💡 Run individual tools (get_complexity, get_dead_code) for detailed breakdowns.',
281
- ],
282
-
283
- get_complexity: () => [
284
- '💡 Functions with complexity >10 are candidates for refactoring.',
285
- '💡 Use expand() to read the function code before refactoring.',
286
- ],
287
-
288
- get_undocumented: () => [
289
- '💡 Use generate_jsdoc() to auto-generate documentation templates.',
290
- ],
291
-
292
- get_similar_functions: () => [
293
- '💡 Consider extracting duplicated logic into a shared utility.',
294
- ],
295
-
296
- get_pending_tests: () => [
297
- '💡 Use mark_test_passed(testId) or mark_test_failed(testId, reason) to track progress.',
298
- ],
299
-
300
- get_db_schema: (result) => {
301
- const hints = [];
302
- if (result.totalTables > 0) {
303
- hints.push(`💡 Found ${result.totalTables} tables. Use get_table_usage() to see which code reads/writes them.`);
304
- } else {
305
- hints.push('💡 No .sql schema files found. Add schema.sql or migrations/*.sql to your project.');
306
- }
307
- return hints;
308
- },
309
-
310
- get_table_usage: (result) => {
311
- const hints = ['💡 Use get_db_dead_tables() to find tables defined in schema but never queried.'];
312
- if (result.totalTables === 0) {
313
- hints.push('💡 No SQL queries detected. This tool finds SQL in .query(), .execute(), sql`...` patterns.');
314
- }
315
- return hints;
316
- },
317
-
318
- get_db_dead_tables: () => [
319
- '💡 Dead columns detection is best-effort — verify before removing.',
320
- ],
321
-
322
- get_compressed_file: (result) => {
323
- const hints = [`💡 Saved ${result.savings} tokens (${result.original} → ${result.compressed}).`];
324
- hints.push('💡 Use get_ai_context() for full project boot: skeleton + docs + compressed files.');
325
- if (result.file) {
326
- hints.push(`📝 Working on ${result.file}? Run generate_context_docs({ scope: ["${result.file}"] }) to document it.`);
327
- }
328
- return hints;
329
- },
330
-
331
- get_project_docs: (result) => {
332
- const hints = [
333
- '💡 Enrich docs by editing .context/*.ctx files — they are git-tracked.',
334
- '💡 Use generate_context_docs() to create initial .ctx stubs.',
335
- ];
336
- if (result.staleFiles?.length > 0) {
337
- hints.push(`⚠️ ${result.staleFiles.length} .ctx files are STALE: ${result.staleFiles.slice(0, 5).join(', ')}. Run generate_context_docs({ scope: ${JSON.stringify(result.staleFiles)}, overwrite: true }) to update (descriptions will be preserved).`);
338
- }
339
- return hints;
340
- },
341
-
342
- check_stale_docs: (result) => {
343
- const hints = [];
344
- if (result.stale?.length > 0) {
345
- hints.push(`⚠️ ${result.stale.length} stale: ${result.stale.join(', ')}`);
346
- hints.push(`💡 Run generate_context_docs({ scope: ${JSON.stringify(result.stale)}, overwrite: true }) — existing descriptions will be preserved.`);
347
- } else {
348
- hints.push('✅ All .ctx docs are up to date.');
349
- }
350
- if (result.unknown > 0) {
351
- hints.push(`ℹ️ ${result.unknown} .ctx files without @sig header (pre-staleness format).`);
352
- }
353
- return hints;
354
- },
355
-
356
- generate_context_docs: (result) => {
357
- const hints = [];
358
- if (result.created?.length > 0) {
359
- hints.push(`✅ Created ${result.created.length} .ctx files with @sig hashes.`);
360
- }
361
- if (result.skipped?.length > 0) {
362
- hints.push(`ℹ️ Skipped ${result.skipped.length} existing files. Use overwrite=true to regenerate (descriptions are preserved via merge).`);
363
- }
364
- if (result.templates && Object.keys(result.templates).length > 0) {
365
- hints.push(`📝 .ctx files have {DESCRIBE} markers. To enrich automatically:`);
366
- hints.push(` delegate_task({ prompt: "Enrich .context/*.ctx files — replace {DESCRIBE} with compact descriptions", skill: "doc-enricher" })`);
367
- hints.push(` Or enrich manually: read source files and replace {DESCRIBE} markers with pipe-separated descriptions (max 80 chars).`);
368
- }
369
- return hints;
370
- },
371
-
372
- get_ai_context: (result) => {
373
- const hints = [`💡 Context loaded: ${result.totalTokens} tokens (${result.savings} savings vs ${result.vsOriginal} original).`];
374
- hints.push('💡 Use expand() to drill into specific symbols. Use get_compressed_file() for additional files.');
375
- if (result.staleFiles?.length > 0) {
376
- hints.push(`⚠️ ${result.staleFiles.length} .ctx docs are stale. Run generate_context_docs({ scope: ${JSON.stringify(result.staleFiles)}, overwrite: true }) then delegate_task({ skill: "doc-enricher" }) to update.`);
377
- }
378
- return hints;
379
- },
380
-
381
- validate_ctx_contracts: (result) => {
382
- const hints = [];
383
- if (result.summary?.errors > 0) {
384
- hints.push(`⚠️ ${result.summary.errors} contract violations found. Run generate_context_docs({ overwrite: true }) to regenerate .ctx files.`);
385
- } else {
386
- hints.push('✅ All .ctx contracts valid — documentation matches source.');
387
- }
388
- return hints;
389
- },
390
-
391
- edit_compressed: (result) => {
392
- const hints = [];
393
- if (result.success) {
394
- hints.push(`✅ Symbol "${result.symbol}" replaced in ${result.file}.`);
395
- hints.push('💡 Run invalidate_cache() to refresh the graph after editing.');
396
- hints.push('💡 Run validate_ctx_contracts() to check if .ctx docs need updating.');
397
- }
398
- return hints;
399
- },
400
-
401
- get_mode: (result) => {
402
- const hints = [`📋 Current mode: ${result.mode} — ${result.description}`];
403
- if (result.mode === 2) {
404
- hints.push('💡 Workflow: get_compressed_file() → read → edit_compressed() → write.');
405
- }
406
- return hints;
407
- },
408
-
409
- set_mode: (result) => {
410
- if (result.saved) {
411
- return [`✅ Mode set to ${result.config.mode}. Saved to ${result.path}.`];
412
- }
413
- return [];
414
- },
415
- };
416
-
417
- /**
418
- * Create MCP server instance
419
- * @param {Function} sendToClient - Function to send JSON-RPC messages to client
420
- * @returns {Object}
421
- */
422
- export function createServer(sendToClient) {
423
- let nextRequestId = 1;
424
-
425
- /** @type {Map<number, {resolve: Function, reject: Function}>} */
426
- const pendingRequests = new Map();
427
-
428
- /** @type {boolean} */
429
- let clientSupportsRoots = false;
430
-
431
- return {
432
- pendingRequests,
433
-
434
- /**
435
- * Handle incoming JSON-RPC message (request, response, or notification)
436
- * @param {Object} message
437
- * @returns {Promise<Object|null>}
438
- */
439
- async handleMessage(message) {
440
- // Check if this is a response to our server→client request
441
- if (message.result !== undefined || message.error !== undefined) {
442
- const pending = pendingRequests.get(message.id);
443
- if (pending) {
444
- pendingRequests.delete(message.id);
445
- if (message.error) {
446
- pending.reject(new Error(message.error.message));
447
- } else {
448
- pending.resolve(message.result);
449
- }
450
- }
451
- return null;
452
- }
453
-
454
- const { method, params, id } = message;
455
-
456
- // Notification (no id) — handle but don't respond
457
- if (id === undefined) {
458
- await this.handleNotification(method, params);
459
- return null;
460
- }
461
-
462
- // Request — handle and respond
463
- try {
464
- switch (method) {
465
- case 'initialize':
466
- // Track client capabilities
467
- if (params?.capabilities?.roots) {
468
- clientSupportsRoots = true;
469
- }
470
- // Also check for inline roots
471
- if (params?.roots) {
472
- setRoots(params.roots);
473
- }
474
- return {
475
- jsonrpc: '2.0',
476
- id,
477
- result: {
478
- protocolVersion: '2024-11-05',
479
- capabilities: { tools: {}, resources: {} },
480
- serverInfo: { name: 'project-graph', version: '1.1.0' },
481
- },
482
- };
483
-
484
- case 'resources/list':
485
- return {
486
- jsonrpc: '2.0',
487
- id,
488
- result: {
489
- resources: [
490
- {
491
- uri: 'project-graph://guide',
492
- name: 'Project Graph Usage Guide',
493
- description: 'Comprehensive guide with workflows and examples',
494
- mimeType: 'text/markdown',
495
- },
496
- ],
497
- },
498
- };
499
-
500
- case 'resources/read': {
501
- if (params.uri !== 'project-graph://guide') {
502
- return { jsonrpc: '2.0', id, error: { code: -32602, message: `Resource not found: ${params.uri}` } };
503
- }
504
- const content = fs.readFileSync(path.join(__dirname, '..', 'GUIDE.md'), 'utf8');
505
- return {
506
- jsonrpc: '2.0',
507
- id,
508
- result: {
509
- contents: [
510
- {
511
- uri: 'project-graph://guide',
512
- mimeType: 'text/markdown',
513
- text: content,
514
- },
515
- ],
516
- },
517
- };
518
- }
519
-
520
- case 'tools/list':
521
- return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
522
-
523
- case 'tools/call': {
524
- const result = await this.executeTool(params.name, params.arguments);
525
- const content = [{ type: 'text', text: JSON.stringify(result, null, 2) }];
526
-
527
- // Inject contextual hints
528
- const hintFn = RESPONSE_HINTS[params.name];
529
- if (hintFn) {
530
- const hints = hintFn(result);
531
- if (hints.length > 0) {
532
- content.push({ type: 'text', text: '\n' + hints.join('\n') });
533
- }
534
- }
535
-
536
- return {
537
- jsonrpc: '2.0',
538
- id,
539
- result: { content },
540
- };
541
- }
542
-
543
- default:
544
- return {
545
- jsonrpc: '2.0',
546
- id,
547
- error: { code: -32601, message: `Method not found: ${method}` },
548
- };
549
- }
550
- } catch (error) {
551
- return { jsonrpc: '2.0', id, error: { code: -32000, message: error.message } };
552
- }
553
- },
554
-
555
- /**
556
- * Handle MCP notifications
557
- * @param {string} method
558
- * @param {Object} params
559
- */
560
- async handleNotification(method, params) {
561
- switch (method) {
562
- case 'notifications/initialized':
563
- // Client is ready — request workspace roots if supported
564
- if (clientSupportsRoots) {
565
- try {
566
- const roots = await this.requestRoots();
567
- if (roots && roots.length > 0) {
568
- setRoots(roots);
569
- }
570
- } catch (e) {
571
- console.error(`[project-graph] Failed to get roots: ${e.message}`);
572
- }
573
- }
574
- break;
575
-
576
- case 'notifications/roots/list_changed':
577
- // Workspace roots changed — re-request
578
- if (clientSupportsRoots) {
579
- try {
580
- const roots = await this.requestRoots();
581
- if (roots && roots.length > 0) {
582
- setRoots(roots);
583
- invalidateCache();
584
- }
585
- } catch (e) {
586
- console.error(`[project-graph] Failed to refresh roots: ${e.message}`);
587
- }
588
- }
589
- break;
590
- }
591
- },
592
-
593
- /**
594
- * Send roots/list request to client
595
- * @returns {Promise<Array<{uri: string, name?: string}>>}
596
- */
597
- requestRoots() {
598
- return new Promise((resolve, reject) => {
599
- const id = nextRequestId++;
600
- const timeout = setTimeout(() => {
601
- pendingRequests.delete(id);
602
- reject(new Error('roots/list request timed out'));
603
- }, 5000);
604
-
605
- pendingRequests.set(id, {
606
- resolve: (result) => {
607
- clearTimeout(timeout);
608
- resolve(result.roots || []);
609
- },
610
- reject: (err) => {
611
- clearTimeout(timeout);
612
- reject(err);
613
- },
614
- });
615
-
616
- sendToClient({
617
- jsonrpc: '2.0',
618
- id,
619
- method: 'roots/list',
620
- });
621
- });
622
- },
623
-
624
- /**
625
- * Execute a tool by name
626
- * @param {string} name
627
- * @param {Object} args
628
- * @returns {Promise<any>}
629
- */
630
- async executeTool(name, args) {
631
- const handler = TOOL_HANDLERS[name];
632
- if (!handler) {
633
- throw new Error(`Unknown tool: ${name}`);
634
- }
635
- return await handler(args);
636
- },
637
- };
638
- }
639
-
640
- /**
641
- * Start server with stdio transport
642
- */
643
- export async function startStdioServer() {
644
- /**
645
- * Send JSON-RPC message to client via stdout
646
- * @param {Object} message
647
- */
648
- const sendToClient = (message) => {
649
- console.log(JSON.stringify(message));
650
- };
651
-
652
- const server = createServer(sendToClient);
653
- const readline = await import('readline');
654
-
655
- const rl = readline.createInterface({
656
- input: process.stdin,
657
- output: process.stdout,
658
- terminal: false,
659
- });
660
-
661
- rl.on('line', async (line) => {
662
- try {
663
- const message = JSON.parse(line);
664
- const response = await server.handleMessage(message);
665
- if (response !== null) {
666
- sendToClient(response);
667
- }
668
- } catch (e) {
669
- sendToClient({
670
- jsonrpc: '2.0',
671
- error: { code: -32700, message: 'Parse error' },
672
- });
673
- }
674
- });
675
- }