project-graph-mcp 1.3.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 (113) hide show
  1. package/README.md +223 -17
  2. package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +87 -30
  3. package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +23 -8
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -0
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/ai-context.js +7 -0
  23. package/src/compact/compact.js +18 -0
  24. package/src/compact/compress.js +13 -0
  25. package/src/compact/ctx-to-jsdoc.js +29 -0
  26. package/src/compact/doc-dialect.js +30 -0
  27. package/src/compact/expand.js +37 -0
  28. package/src/compact/framework-references.js +5 -0
  29. package/src/compact/instructions.js +3 -0
  30. package/src/compact/mode-config.js +8 -0
  31. package/src/compact/validate-pipeline.js +9 -0
  32. package/src/core/event-bus.js +9 -0
  33. package/src/core/filters.js +14 -0
  34. package/src/core/graph-builder.js +12 -0
  35. package/src/core/parser.js +31 -0
  36. package/src/core/workspace.js +8 -0
  37. package/src/lang/lang-go.js +17 -0
  38. package/src/lang/lang-python.js +12 -0
  39. package/src/lang/lang-sql.js +23 -0
  40. package/src/lang/lang-typescript.js +9 -0
  41. package/src/lang/lang-utils.js +4 -0
  42. package/src/mcp/mcp-server.js +17 -0
  43. package/src/mcp/tool-defs.js +3 -0
  44. package/src/mcp/tools.js +25 -0
  45. package/src/network/backend-lifecycle.js +19 -0
  46. package/src/network/backend.js +5 -0
  47. package/src/network/local-gateway.js +23 -0
  48. package/src/network/mdns.js +13 -0
  49. package/src/network/server.js +10 -0
  50. package/src/network/web-server.js +34 -0
  51. package/vendor/terser.mjs +49 -0
  52. package/web/.project-graph-cache.json +1 -0
  53. package/web/app.js +16 -0
  54. package/web/components/code-block.js +3 -0
  55. package/web/components/quick-open.js +5 -0
  56. package/web/dashboard-state.js +3 -0
  57. package/web/dashboard.html +27 -0
  58. package/web/dashboard.js +8 -0
  59. package/web/highlight.js +13 -0
  60. package/web/index.html +35 -0
  61. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  62. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  63. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  64. package/web/panels/EventItem/EventItem.css.js +1 -0
  65. package/web/panels/EventItem/EventItem.js +4 -0
  66. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  67. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  69. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  70. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.js +4 -0
  72. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  73. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  74. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  77. package/web/panels/code-viewer.js +5 -0
  78. package/web/panels/ctx-panel.js +4 -0
  79. package/web/panels/dep-graph.js +6 -0
  80. package/web/panels/file-tree.js +188 -0
  81. package/web/panels/health-panel.js +3 -0
  82. package/web/panels/live-monitor.js +3 -0
  83. package/web/state.js +17 -0
  84. package/web/style.css +157 -0
  85. package/references/symbiote-3x.md +0 -834
  86. package/src/cli-handlers.js +0 -140
  87. package/src/cli.js +0 -83
  88. package/src/complexity.js +0 -223
  89. package/src/custom-rules.js +0 -583
  90. package/src/db-analysis.js +0 -194
  91. package/src/dead-code.js +0 -468
  92. package/src/filters.js +0 -227
  93. package/src/framework-references.js +0 -177
  94. package/src/full-analysis.js +0 -174
  95. package/src/graph-builder.js +0 -299
  96. package/src/instructions.js +0 -175
  97. package/src/jsdoc-generator.js +0 -214
  98. package/src/lang-go.js +0 -285
  99. package/src/lang-python.js +0 -197
  100. package/src/lang-sql.js +0 -309
  101. package/src/lang-typescript.js +0 -190
  102. package/src/lang-utils.js +0 -124
  103. package/src/large-files.js +0 -162
  104. package/src/mcp-server.js +0 -468
  105. package/src/outdated-patterns.js +0 -295
  106. package/src/parser.js +0 -452
  107. package/src/server.js +0 -28
  108. package/src/similar-functions.js +0 -278
  109. package/src/test-annotations.js +0 -301
  110. package/src/tool-defs.js +0 -525
  111. package/src/tools.js +0 -470
  112. package/src/undocumented.js +0 -260
  113. package/src/workspace.js +0 -70
@@ -1,162 +0,0 @@
1
- /**
2
- * Large Files Analyzer
3
- * Identifies files that may need splitting
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} LargeFileItem
14
- * @property {string} file
15
- * @property {number} lines
16
- * @property {number} functions
17
- * @property {number} classes
18
- * @property {number} exports
19
- * @property {string} rating - 'ok' | 'warning' | 'critical'
20
- * @property {string[]} reasons
21
- */
22
-
23
- /**
24
- * Find all JS files
25
- * @param {string} dir
26
- * @param {string} rootDir
27
- * @returns {string[]}
28
- */
29
- function findJSFiles(dir, rootDir = dir) {
30
- if (dir === rootDir) parseGitignore(rootDir);
31
- const files = [];
32
-
33
- try {
34
- for (const entry of readdirSync(dir)) {
35
- const fullPath = join(dir, entry);
36
- const relativePath = relative(rootDir, fullPath);
37
- const stat = statSync(fullPath);
38
-
39
- if (stat.isDirectory()) {
40
- if (!shouldExcludeDir(entry, relativePath)) {
41
- files.push(...findJSFiles(fullPath, rootDir));
42
- }
43
- } else if (entry.endsWith('.js') && !entry.endsWith('.css.js') && !entry.endsWith('.tpl.js')) {
44
- if (!shouldExcludeFile(entry, relativePath)) {
45
- files.push(fullPath);
46
- }
47
- }
48
- }
49
- } catch (e) { }
50
-
51
- return files;
52
- }
53
-
54
- /**
55
- * Analyze a single file
56
- * @param {string} filePath
57
- * @returns {LargeFileItem}
58
- */
59
- function analyzeFile(filePath, rootDir) {
60
- const code = readFileSync(filePath, 'utf-8');
61
- const relPath = relative(rootDir, filePath);
62
- const lines = code.split('\n').length;
63
-
64
- let functions = 0;
65
- let classes = 0;
66
- let exports = 0;
67
-
68
- let ast;
69
- try {
70
- ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
71
- } catch (e) {
72
- return { file: relPath, lines, functions: 0, classes: 0, exports: 0, rating: 'ok', reasons: [] };
73
- }
74
-
75
- walk.simple(ast, {
76
- FunctionDeclaration() { functions++; },
77
- ArrowFunctionExpression(node) {
78
- if (node.body.type === 'BlockStatement') functions++;
79
- },
80
- ClassDeclaration() { classes++; },
81
- ExportNamedDeclaration() { exports++; },
82
- ExportDefaultDeclaration() { exports++; },
83
- });
84
-
85
- // Calculate rating
86
- const reasons = [];
87
- let score = 0;
88
-
89
- if (lines > 500) {
90
- score += 2;
91
- reasons.push(`${lines} lines (>500)`);
92
- } else if (lines > 300) {
93
- score += 1;
94
- reasons.push(`${lines} lines (>300)`);
95
- }
96
-
97
- if (functions > 15) {
98
- score += 2;
99
- reasons.push(`${functions} functions (>15)`);
100
- } else if (functions > 10) {
101
- score += 1;
102
- reasons.push(`${functions} functions (>10)`);
103
- }
104
-
105
- if (classes > 3) {
106
- score += 2;
107
- reasons.push(`${classes} classes (>3)`);
108
- } else if (classes > 1) {
109
- score += 1;
110
- reasons.push(`${classes} classes (>1)`);
111
- }
112
-
113
- if (exports > 10) {
114
- score += 2;
115
- reasons.push(`${exports} exports (>10)`);
116
- } else if (exports > 5) {
117
- score += 1;
118
- reasons.push(`${exports} exports (>5)`);
119
- }
120
-
121
- let rating = 'ok';
122
- if (score >= 4) rating = 'critical';
123
- else if (score >= 2) rating = 'warning';
124
-
125
- return { file: relPath, lines, functions, classes, exports, rating, reasons };
126
- }
127
-
128
- /**
129
- * Get large files analysis
130
- * @param {string} dir
131
- * @param {Object} [options]
132
- * @param {boolean} [options.onlyProblematic=false] - Only show warning/critical
133
- * @returns {Promise<{total: number, stats: Object, items: LargeFileItem[]}>}
134
- */
135
- export async function getLargeFiles(dir, options = {}) {
136
- const onlyProblematic = options.onlyProblematic || false;
137
- const resolvedDir = resolve(dir);
138
- const files = findJSFiles(dir);
139
- let items = files.map(f => analyzeFile(f, resolvedDir));
140
-
141
- if (onlyProblematic) {
142
- items = items.filter(i => i.rating !== 'ok');
143
- }
144
-
145
- // Sort by lines descending
146
- items.sort((a, b) => b.lines - a.lines);
147
-
148
- const stats = {
149
- totalFiles: files.length,
150
- ok: items.filter(i => i.rating === 'ok').length,
151
- warning: items.filter(i => i.rating === 'warning').length,
152
- critical: items.filter(i => i.rating === 'critical').length,
153
- totalLines: items.reduce((s, i) => s + i.lines, 0),
154
- avgLines: items.length > 0 ? Math.round(items.reduce((s, i) => s + i.lines, 0) / items.length) : 0,
155
- };
156
-
157
- return {
158
- total: items.length,
159
- stats,
160
- items: items.slice(0, 30),
161
- };
162
- }
package/src/mcp-server.js DELETED
@@ -1,468 +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 } 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
-
30
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
-
32
- /**
33
- * Tool handlers registry
34
- * Maps tool names to their handler functions
35
- */
36
- const TOOL_HANDLERS = {
37
- // Graph Tools
38
- get_skeleton: (args) => getSkeleton(resolvePath(args.path)),
39
- get_focus_zone: (args) => getFocusZone({ ...args, path: resolvePath(args.path) }),
40
- expand: (args) => expand(args.symbol),
41
- deps: (args) => deps(args.symbol),
42
- usages: (args) => usages(args.symbol),
43
- get_call_chain: (args) => getCallChain({ from: args.from, to: args.to, path: args.path ? resolvePath(args.path) : undefined }),
44
- invalidate_cache: () => { invalidateCache(); return { success: true }; },
45
-
46
- // Test Checklist Tools
47
- get_pending_tests: (args) => getPendingTests(resolvePath(args.path)),
48
- mark_test_passed: (args) => markTestPassed(args.testId),
49
- mark_test_failed: (args) => markTestFailed(args.testId, args.reason),
50
- get_test_summary: (args) => getTestSummary(resolvePath(args.path)),
51
- reset_test_state: () => resetTestState(),
52
-
53
- // Filter Tools
54
- get_filters: () => getFilters(),
55
- set_filters: (args) => setFilters(args),
56
- add_excludes: (args) => addExcludes(args.dirs),
57
- remove_excludes: (args) => removeExcludes(args.dirs),
58
- reset_filters: () => resetFilters(),
59
-
60
- // Guidelines
61
- get_usage_guide: (args) => {
62
- try {
63
- const guidePath = path.join(__dirname, '..', 'GUIDE.md');
64
- const content = fs.readFileSync(guidePath, 'utf8');
65
- if (!args.topic) return content;
66
- const regex = new RegExp(`## ${args.topic}`, 'i');
67
- const match = content.match(regex);
68
- if (!match) return `Topic '${args.topic}' not found in guide.`;
69
- const start = match.index;
70
- let end = content.indexOf('\n## ', start + 1);
71
- if (end === -1) end = content.length;
72
- return content.substring(start, end).trim();
73
- } catch (e) {
74
- return `Failed to read usage guide: ${e.message}`;
75
- }
76
- },
77
- get_agent_instructions: () => getInstructions(),
78
-
79
- // Documentation
80
- get_undocumented: (args) => getUndocumentedSummary(resolvePath(args.path), args.level || 'tests'),
81
-
82
- // Code Quality
83
- get_dead_code: (args) => getDeadCode(resolvePath(args.path)),
84
- generate_jsdoc: (args) => args.name
85
- ? generateJSDocFor(resolvePath(args.path), args.name)
86
- : generateJSDoc(resolvePath(args.path)),
87
- get_similar_functions: (args) => getSimilarFunctions(resolvePath(args.path), { threshold: args.threshold }),
88
- get_complexity: (args) => getComplexity(resolvePath(args.path), {
89
- minComplexity: args.minComplexity,
90
- onlyProblematic: args.onlyProblematic,
91
- }),
92
- get_large_files: (args) => getLargeFiles(resolvePath(args.path), { onlyProblematic: args.onlyProblematic }),
93
- get_outdated_patterns: (args) => getOutdatedPatterns(resolvePath(args.path), {
94
- codeOnly: args.codeOnly,
95
- depsOnly: args.depsOnly,
96
- }),
97
- get_full_analysis: (args) => getFullAnalysis(resolvePath(args.path), { includeItems: args.includeItems }),
98
-
99
- // Custom Rules
100
- get_custom_rules: () => getCustomRules(),
101
- set_custom_rule: (args) => setCustomRule(args.ruleSet, args.rule),
102
- check_custom_rules: (args) => checkCustomRules(resolvePath(args.path), {
103
- ruleSet: args.ruleSet,
104
- severity: args.severity,
105
- }),
106
-
107
- // Framework References
108
- get_framework_reference: (args) => getFrameworkReference({
109
- framework: args.framework,
110
- path: args.path ? resolvePath(args.path) : undefined,
111
- }),
112
-
113
- // Database Analysis
114
- get_db_schema: (args) => getDBSchema(resolvePath(args.path)),
115
- get_table_usage: (args) => getTableUsage(resolvePath(args.path), args.table),
116
- get_db_dead_tables: (args) => getDBDeadTables(resolvePath(args.path)),
117
- };
118
-
119
- /**
120
- * Response hints — contextual coaching tips appended to tool responses.
121
- * Maps tool names to hint generators. Each receives the result and returns
122
- * an array of hint strings (or empty array for no hints).
123
- *
124
- * @type {Record<string, (result: any) => string[]>}
125
- */
126
- const RESPONSE_HINTS = {
127
- get_skeleton: () => [
128
- '💡 Use expand("SYMBOL") to see code for a specific class.',
129
- '💡 Use deps("SYMBOL") to see architecture dependencies.',
130
- '💡 After code changes, run invalidate_cache() to refresh the graph.',
131
- ],
132
-
133
- expand: (result) => {
134
- const hints = [];
135
- if (result.methods?.length > 10) {
136
- hints.push('💡 Large class detected. Run get_complexity() to find refactoring targets.');
137
- }
138
- hints.push('💡 Use deps() to see what depends on this symbol.');
139
- return hints;
140
- },
141
-
142
- deps: () => [
143
- '💡 Use usages() for cross-project reference search.',
144
- ],
145
-
146
- get_call_chain: (result) => {
147
- if (result.error) return [];
148
- return [
149
- '💡 Use expand() on intermediate steps to understand how data is passed along the chain.',
150
- ];
151
- },
152
-
153
- invalidate_cache: () => [
154
- '✅ Cache cleared. Run get_skeleton() to rebuild the project graph.',
155
- ],
156
-
157
- get_dead_code: (result) => {
158
- const hints = ['💡 Review each item before removing — some may be used dynamically.'];
159
- if (result.unusedExports?.length > 20) {
160
- hints.push('💡 Consider delegating cleanup to agent-pool: delegate_task({ prompt: "Remove dead code..." })');
161
- }
162
- return hints;
163
- },
164
-
165
- get_full_analysis: () => [
166
- '💡 Focus on items with "critical" severity first.',
167
- '💡 Run individual tools (get_complexity, get_dead_code) for detailed breakdowns.',
168
- ],
169
-
170
- get_complexity: () => [
171
- '💡 Functions with complexity >10 are candidates for refactoring.',
172
- '💡 Use expand() to read the function code before refactoring.',
173
- ],
174
-
175
- get_undocumented: () => [
176
- '💡 Use generate_jsdoc() to auto-generate documentation templates.',
177
- ],
178
-
179
- get_similar_functions: () => [
180
- '💡 Consider extracting duplicated logic into a shared utility.',
181
- ],
182
-
183
- get_pending_tests: () => [
184
- '💡 Use mark_test_passed(testId) or mark_test_failed(testId, reason) to track progress.',
185
- ],
186
-
187
- get_db_schema: (result) => {
188
- const hints = [];
189
- if (result.totalTables > 0) {
190
- hints.push(`💡 Found ${result.totalTables} tables. Use get_table_usage() to see which code reads/writes them.`);
191
- } else {
192
- hints.push('💡 No .sql schema files found. Add schema.sql or migrations/*.sql to your project.');
193
- }
194
- return hints;
195
- },
196
-
197
- get_table_usage: (result) => {
198
- const hints = ['💡 Use get_db_dead_tables() to find tables defined in schema but never queried.'];
199
- if (result.totalTables === 0) {
200
- hints.push('💡 No SQL queries detected. This tool finds SQL in .query(), .execute(), sql`...` patterns.');
201
- }
202
- return hints;
203
- },
204
-
205
- get_db_dead_tables: () => [
206
- '💡 Dead columns detection is best-effort — verify before removing.',
207
- ],
208
- };
209
-
210
- /**
211
- * Create MCP server instance
212
- * @param {Function} sendToClient - Function to send JSON-RPC messages to client
213
- * @returns {Object}
214
- */
215
- export function createServer(sendToClient) {
216
- let nextRequestId = 1;
217
-
218
- /** @type {Map<number, {resolve: Function, reject: Function}>} */
219
- const pendingRequests = new Map();
220
-
221
- /** @type {boolean} */
222
- let clientSupportsRoots = false;
223
-
224
- return {
225
- pendingRequests,
226
-
227
- /**
228
- * Handle incoming JSON-RPC message (request, response, or notification)
229
- * @param {Object} message
230
- * @returns {Promise<Object|null>}
231
- */
232
- async handleMessage(message) {
233
- // Check if this is a response to our server→client request
234
- if (message.result !== undefined || message.error !== undefined) {
235
- const pending = pendingRequests.get(message.id);
236
- if (pending) {
237
- pendingRequests.delete(message.id);
238
- if (message.error) {
239
- pending.reject(new Error(message.error.message));
240
- } else {
241
- pending.resolve(message.result);
242
- }
243
- }
244
- return null;
245
- }
246
-
247
- const { method, params, id } = message;
248
-
249
- // Notification (no id) — handle but don't respond
250
- if (id === undefined) {
251
- await this.handleNotification(method, params);
252
- return null;
253
- }
254
-
255
- // Request — handle and respond
256
- try {
257
- switch (method) {
258
- case 'initialize':
259
- // Track client capabilities
260
- if (params?.capabilities?.roots) {
261
- clientSupportsRoots = true;
262
- }
263
- // Also check for inline roots
264
- if (params?.roots) {
265
- setRoots(params.roots);
266
- }
267
- return {
268
- jsonrpc: '2.0',
269
- id,
270
- result: {
271
- protocolVersion: '2024-11-05',
272
- capabilities: { tools: {}, resources: {} },
273
- serverInfo: { name: 'project-graph', version: '1.1.0' },
274
- },
275
- };
276
-
277
- case 'resources/list':
278
- return {
279
- jsonrpc: '2.0',
280
- id,
281
- result: {
282
- resources: [
283
- {
284
- uri: 'project-graph://guide',
285
- name: 'Project Graph Usage Guide',
286
- description: 'Comprehensive guide with workflows and examples',
287
- mimeType: 'text/markdown',
288
- },
289
- ],
290
- },
291
- };
292
-
293
- case 'resources/read': {
294
- if (params.uri !== 'project-graph://guide') {
295
- return { jsonrpc: '2.0', id, error: { code: -32602, message: `Resource not found: ${params.uri}` } };
296
- }
297
- const content = fs.readFileSync(path.join(__dirname, '..', 'GUIDE.md'), 'utf8');
298
- return {
299
- jsonrpc: '2.0',
300
- id,
301
- result: {
302
- contents: [
303
- {
304
- uri: 'project-graph://guide',
305
- mimeType: 'text/markdown',
306
- text: content,
307
- },
308
- ],
309
- },
310
- };
311
- }
312
-
313
- case 'tools/list':
314
- return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
315
-
316
- case 'tools/call': {
317
- const result = await this.executeTool(params.name, params.arguments);
318
- const content = [{ type: 'text', text: JSON.stringify(result, null, 2) }];
319
-
320
- // Inject contextual hints
321
- const hintFn = RESPONSE_HINTS[params.name];
322
- if (hintFn) {
323
- const hints = hintFn(result);
324
- if (hints.length > 0) {
325
- content.push({ type: 'text', text: '\n' + hints.join('\n') });
326
- }
327
- }
328
-
329
- return {
330
- jsonrpc: '2.0',
331
- id,
332
- result: { content },
333
- };
334
- }
335
-
336
- default:
337
- return {
338
- jsonrpc: '2.0',
339
- id,
340
- error: { code: -32601, message: `Method not found: ${method}` },
341
- };
342
- }
343
- } catch (error) {
344
- return { jsonrpc: '2.0', id, error: { code: -32000, message: error.message } };
345
- }
346
- },
347
-
348
- /**
349
- * Handle MCP notifications
350
- * @param {string} method
351
- * @param {Object} params
352
- */
353
- async handleNotification(method, params) {
354
- switch (method) {
355
- case 'notifications/initialized':
356
- // Client is ready — request workspace roots if supported
357
- if (clientSupportsRoots) {
358
- try {
359
- const roots = await this.requestRoots();
360
- if (roots && roots.length > 0) {
361
- setRoots(roots);
362
- }
363
- } catch (e) {
364
- console.error(`[project-graph] Failed to get roots: ${e.message}`);
365
- }
366
- }
367
- break;
368
-
369
- case 'notifications/roots/list_changed':
370
- // Workspace roots changed — re-request
371
- if (clientSupportsRoots) {
372
- try {
373
- const roots = await this.requestRoots();
374
- if (roots && roots.length > 0) {
375
- setRoots(roots);
376
- invalidateCache();
377
- }
378
- } catch (e) {
379
- console.error(`[project-graph] Failed to refresh roots: ${e.message}`);
380
- }
381
- }
382
- break;
383
- }
384
- },
385
-
386
- /**
387
- * Send roots/list request to client
388
- * @returns {Promise<Array<{uri: string, name?: string}>>}
389
- */
390
- requestRoots() {
391
- return new Promise((resolve, reject) => {
392
- const id = nextRequestId++;
393
- const timeout = setTimeout(() => {
394
- pendingRequests.delete(id);
395
- reject(new Error('roots/list request timed out'));
396
- }, 5000);
397
-
398
- pendingRequests.set(id, {
399
- resolve: (result) => {
400
- clearTimeout(timeout);
401
- resolve(result.roots || []);
402
- },
403
- reject: (err) => {
404
- clearTimeout(timeout);
405
- reject(err);
406
- },
407
- });
408
-
409
- sendToClient({
410
- jsonrpc: '2.0',
411
- id,
412
- method: 'roots/list',
413
- });
414
- });
415
- },
416
-
417
- /**
418
- * Execute a tool by name
419
- * @param {string} name
420
- * @param {Object} args
421
- * @returns {Promise<any>}
422
- */
423
- async executeTool(name, args) {
424
- const handler = TOOL_HANDLERS[name];
425
- if (!handler) {
426
- throw new Error(`Unknown tool: ${name}`);
427
- }
428
- return await handler(args);
429
- },
430
- };
431
- }
432
-
433
- /**
434
- * Start server with stdio transport
435
- */
436
- export async function startStdioServer() {
437
- /**
438
- * Send JSON-RPC message to client via stdout
439
- * @param {Object} message
440
- */
441
- const sendToClient = (message) => {
442
- console.log(JSON.stringify(message));
443
- };
444
-
445
- const server = createServer(sendToClient);
446
- const readline = await import('readline');
447
-
448
- const rl = readline.createInterface({
449
- input: process.stdin,
450
- output: process.stdout,
451
- terminal: false,
452
- });
453
-
454
- rl.on('line', async (line) => {
455
- try {
456
- const message = JSON.parse(line);
457
- const response = await server.handleMessage(message);
458
- if (response !== null) {
459
- sendToClient(response);
460
- }
461
- } catch (e) {
462
- sendToClient({
463
- jsonrpc: '2.0',
464
- error: { code: -32700, message: 'Parse error' },
465
- });
466
- }
467
- });
468
- }