ucn 3.4.4 → 3.4.6
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.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/README.md +87 -41
- package/cli/index.js +7 -1
- package/core/discovery.js +2 -1
- package/core/project.js +6 -7
- package/mcp/server.js +1566 -0
- package/package.json +7 -2
- package/test/mcp-edge-cases.js +490 -0
- package/test/parser.test.js +239 -0
- package/test/reliability-test-prompt.md +0 -58
package/mcp/server.js
ADDED
|
@@ -0,0 +1,1566 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Universal Code Navigator (UCN) - MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Stdio-based MCP server that wraps ProjectIndex methods.
|
|
7
|
+
* Keeps a per-project index cache for fast repeat queries.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// MCP SDK IMPORTS (dynamic, to handle missing dependency gracefully)
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
let McpServer, StdioServerTransport, z;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
({ McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'));
|
|
21
|
+
({ StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'));
|
|
22
|
+
z = require('zod');
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error('Missing dependencies. Install with:');
|
|
25
|
+
console.error(' npm install @modelcontextprotocol/sdk zod');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// UCN CORE IMPORTS
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const { ProjectIndex } = require('../core/project');
|
|
34
|
+
const { findProjectRoot, isTestFile } = require('../core/discovery');
|
|
35
|
+
const { detectLanguage } = require('../core/parser');
|
|
36
|
+
const { getParser, PARSE_OPTIONS } = require('../languages');
|
|
37
|
+
const output = require('../core/output');
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// INDEX CACHE
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
const indexCache = new Map(); // projectDir → { index, checkedAt }
|
|
44
|
+
const expandCache = new Map(); // projectDir → { items, root }
|
|
45
|
+
const MAX_CACHE_SIZE = 10;
|
|
46
|
+
const STALE_CHECK_INTERVAL = 30000; // 30s
|
|
47
|
+
|
|
48
|
+
function getIndex(projectDir) {
|
|
49
|
+
const absDir = path.resolve(projectDir);
|
|
50
|
+
if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
|
|
51
|
+
throw new Error(`Project directory not found: ${absDir}`);
|
|
52
|
+
}
|
|
53
|
+
const root = findProjectRoot(absDir);
|
|
54
|
+
const cached = indexCache.get(root);
|
|
55
|
+
|
|
56
|
+
if (cached && (Date.now() - cached.checkedAt < STALE_CHECK_INTERVAL)) {
|
|
57
|
+
return cached.index;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (cached) {
|
|
61
|
+
if (!cached.index.isCacheStale()) {
|
|
62
|
+
cached.checkedAt = Date.now();
|
|
63
|
+
return cached.index;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build new index
|
|
68
|
+
const index = new ProjectIndex(root);
|
|
69
|
+
const loaded = index.loadCache();
|
|
70
|
+
if (loaded && !index.isCacheStale()) {
|
|
71
|
+
// Cache is fresh
|
|
72
|
+
} else {
|
|
73
|
+
index.build(null, { quiet: true });
|
|
74
|
+
index.saveCache();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// LRU eviction
|
|
78
|
+
if (indexCache.size >= MAX_CACHE_SIZE) {
|
|
79
|
+
let oldestKey = null;
|
|
80
|
+
let oldestTime = Infinity;
|
|
81
|
+
for (const [key, val] of indexCache) {
|
|
82
|
+
if (val.checkedAt < oldestTime) {
|
|
83
|
+
oldestTime = val.checkedAt;
|
|
84
|
+
oldestKey = key;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (oldestKey) indexCache.delete(oldestKey);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
indexCache.set(root, { index, checkedAt: Date.now() });
|
|
91
|
+
return index;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// TEXT FORMATTERS (for commands not in core/output.js)
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
function formatTocText(toc) {
|
|
99
|
+
const lines = [];
|
|
100
|
+
const t = toc.totals;
|
|
101
|
+
lines.push(`PROJECT: ${t.files} files, ${t.lines} lines`);
|
|
102
|
+
lines.push(` ${t.functions} functions, ${t.classes} classes, ${t.state} state objects`);
|
|
103
|
+
|
|
104
|
+
const meta = toc.meta || {};
|
|
105
|
+
const warnings = [];
|
|
106
|
+
if (meta.dynamicImports) warnings.push(`${meta.dynamicImports} dynamic import(s)`);
|
|
107
|
+
if (meta.uncertain) warnings.push(`${meta.uncertain} uncertain reference(s)`);
|
|
108
|
+
if (warnings.length) {
|
|
109
|
+
lines.push(` Note: ${warnings.join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (toc.summary) {
|
|
113
|
+
if (toc.summary.topFunctionFiles?.length) {
|
|
114
|
+
const hint = toc.summary.topFunctionFiles.map(f => `${f.file} (${f.functions})`).join(', ');
|
|
115
|
+
lines.push(` Most functions: ${hint}`);
|
|
116
|
+
}
|
|
117
|
+
if (toc.summary.topLineFiles?.length) {
|
|
118
|
+
const hint = toc.summary.topLineFiles.map(f => `${f.file} (${f.lines})`).join(', ');
|
|
119
|
+
lines.push(` Largest files: ${hint}`);
|
|
120
|
+
}
|
|
121
|
+
if (toc.summary.entryFiles?.length) {
|
|
122
|
+
lines.push(` Entry points: ${toc.summary.entryFiles.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
lines.push('═'.repeat(60));
|
|
127
|
+
const hasDetail = toc.files.some(f => f.symbols);
|
|
128
|
+
for (const file of toc.files) {
|
|
129
|
+
const parts = [`${file.lines} lines`];
|
|
130
|
+
if (file.functions) parts.push(`${file.functions} fn`);
|
|
131
|
+
if (file.classes) parts.push(`${file.classes} cls`);
|
|
132
|
+
if (file.state) parts.push(`${file.state} state`);
|
|
133
|
+
|
|
134
|
+
if (hasDetail) {
|
|
135
|
+
lines.push(`\n${file.file} (${parts.join(', ')})`);
|
|
136
|
+
if (file.symbols) {
|
|
137
|
+
for (const fn of file.symbols.functions) {
|
|
138
|
+
lines.push(` ${output.lineRange(fn.startLine, fn.endLine)} ${output.formatFunctionSignature(fn)}`);
|
|
139
|
+
}
|
|
140
|
+
for (const cls of file.symbols.classes) {
|
|
141
|
+
lines.push(` ${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
lines.push(` ${file.file} — ${parts.join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!hasDetail) {
|
|
150
|
+
lines.push(`\nUse detailed=true to list all functions and classes.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatFindText(symbols, query, top) {
|
|
157
|
+
if (symbols.length === 0) {
|
|
158
|
+
return `No symbols found for "${query}"`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const lines = [];
|
|
162
|
+
const limit = (top && top > 0) ? Math.min(symbols.length, top) : Math.min(symbols.length, 10);
|
|
163
|
+
const hidden = symbols.length - limit;
|
|
164
|
+
|
|
165
|
+
if (hidden > 0) {
|
|
166
|
+
lines.push(`Found ${symbols.length} match(es) for "${query}" (showing top ${limit}):`);
|
|
167
|
+
} else {
|
|
168
|
+
lines.push(`Found ${symbols.length} match(es) for "${query}":`);
|
|
169
|
+
}
|
|
170
|
+
lines.push('─'.repeat(60));
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < limit; i++) {
|
|
173
|
+
const s = symbols[i];
|
|
174
|
+
const sig = s.params !== undefined
|
|
175
|
+
? output.formatFunctionSignature(s)
|
|
176
|
+
: output.formatClassSignature(s);
|
|
177
|
+
lines.push(`${s.relativePath}:${s.startLine} ${sig}`);
|
|
178
|
+
if (s.usageCounts !== undefined) {
|
|
179
|
+
const c = s.usageCounts;
|
|
180
|
+
const parts = [];
|
|
181
|
+
if (c.calls > 0) parts.push(`${c.calls} calls`);
|
|
182
|
+
if (c.definitions > 0) parts.push(`${c.definitions} def`);
|
|
183
|
+
if (c.imports > 0) parts.push(`${c.imports} imports`);
|
|
184
|
+
if (c.references > 0) parts.push(`${c.references} refs`);
|
|
185
|
+
lines.push(` (${c.total} usages: ${parts.join(', ')})`);
|
|
186
|
+
} else if (s.usageCount !== undefined) {
|
|
187
|
+
lines.push(` (${s.usageCount} usages)`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (hidden > 0) {
|
|
192
|
+
lines.push(`... ${hidden} more result(s).`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatUsagesText(usages, name) {
|
|
199
|
+
const defs = usages.filter(u => u.isDefinition);
|
|
200
|
+
const calls = usages.filter(u => u.usageType === 'call');
|
|
201
|
+
const imports = usages.filter(u => u.usageType === 'import');
|
|
202
|
+
const refs = usages.filter(u => !u.isDefinition && u.usageType === 'reference');
|
|
203
|
+
|
|
204
|
+
const lines = [];
|
|
205
|
+
lines.push(`Usages of "${name}": ${defs.length} definitions, ${calls.length} calls, ${imports.length} imports, ${refs.length} references`);
|
|
206
|
+
lines.push('═'.repeat(60));
|
|
207
|
+
|
|
208
|
+
if (defs.length > 0) {
|
|
209
|
+
lines.push('\nDEFINITIONS:');
|
|
210
|
+
for (const d of defs) {
|
|
211
|
+
lines.push(` ${d.relativePath}:${d.line || d.startLine}`);
|
|
212
|
+
if (d.signature) lines.push(` ${d.signature}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (calls.length > 0) {
|
|
217
|
+
lines.push('\nCALLS:');
|
|
218
|
+
for (const c of calls) {
|
|
219
|
+
lines.push(` ${c.relativePath}:${c.line}`);
|
|
220
|
+
lines.push(` ${c.content.trim()}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (imports.length > 0) {
|
|
225
|
+
lines.push('\nIMPORTS:');
|
|
226
|
+
for (const i of imports) {
|
|
227
|
+
lines.push(` ${i.relativePath}:${i.line}`);
|
|
228
|
+
lines.push(` ${i.content.trim()}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (refs.length > 0) {
|
|
233
|
+
lines.push('\nREFERENCES:');
|
|
234
|
+
for (const r of refs) {
|
|
235
|
+
lines.push(` ${r.relativePath}:${r.line}`);
|
|
236
|
+
lines.push(` ${r.content.trim()}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatContextText(ctx) {
|
|
244
|
+
if (!ctx) return { text: 'Symbol not found.', expandable: [] };
|
|
245
|
+
|
|
246
|
+
const lines = [];
|
|
247
|
+
const expandable = [];
|
|
248
|
+
let itemNum = 1;
|
|
249
|
+
|
|
250
|
+
// Handle struct/interface types
|
|
251
|
+
if (ctx.type && ['class', 'struct', 'interface', 'type'].includes(ctx.type)) {
|
|
252
|
+
lines.push(`Context for ${ctx.type} ${ctx.name}:`);
|
|
253
|
+
lines.push('═'.repeat(60));
|
|
254
|
+
|
|
255
|
+
if (ctx.warnings && ctx.warnings.length > 0) {
|
|
256
|
+
for (const w of ctx.warnings) {
|
|
257
|
+
lines.push(` Note: ${w.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const methods = ctx.methods || [];
|
|
262
|
+
lines.push(`\nMETHODS (${methods.length}):`);
|
|
263
|
+
for (const m of methods) {
|
|
264
|
+
const receiver = m.receiver ? `(${m.receiver}) ` : '';
|
|
265
|
+
const params = m.params || '...';
|
|
266
|
+
const returnType = m.returnType ? `: ${m.returnType}` : '';
|
|
267
|
+
lines.push(` [${itemNum}] ${receiver}${m.name}(${params})${returnType}`);
|
|
268
|
+
lines.push(` ${m.file}:${m.line}`);
|
|
269
|
+
expandable.push({
|
|
270
|
+
num: itemNum++,
|
|
271
|
+
type: 'method',
|
|
272
|
+
name: m.name,
|
|
273
|
+
file: m.file,
|
|
274
|
+
relativePath: m.file,
|
|
275
|
+
startLine: m.line,
|
|
276
|
+
endLine: m.endLine || m.line
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const callers = ctx.callers || [];
|
|
281
|
+
lines.push(`\nUSAGES (${callers.length}):`);
|
|
282
|
+
for (const c of callers) {
|
|
283
|
+
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
284
|
+
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
|
|
285
|
+
lines.push(` ${c.content.trim()}`);
|
|
286
|
+
expandable.push({
|
|
287
|
+
num: itemNum++,
|
|
288
|
+
type: 'caller',
|
|
289
|
+
name: c.callerName || '(module level)',
|
|
290
|
+
file: c.callerFile || c.file,
|
|
291
|
+
relativePath: c.relativePath,
|
|
292
|
+
line: c.line,
|
|
293
|
+
startLine: c.callerStartLine || c.line,
|
|
294
|
+
endLine: c.callerEndLine || c.line
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (expandable.length > 0) {
|
|
299
|
+
lines.push(`\nUse ucn_expand with item number to see code for any item.`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { text: lines.join('\n'), expandable };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Standard function/method context
|
|
306
|
+
lines.push(`Context for ${ctx.function}:`);
|
|
307
|
+
lines.push('═'.repeat(60));
|
|
308
|
+
|
|
309
|
+
if (ctx.meta) {
|
|
310
|
+
const notes = [];
|
|
311
|
+
if (ctx.meta.dynamicImports) notes.push(`${ctx.meta.dynamicImports} dynamic import(s)`);
|
|
312
|
+
if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
|
|
313
|
+
if (notes.length) {
|
|
314
|
+
lines.push(` Note: ${notes.join(', ')}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (ctx.warnings && ctx.warnings.length > 0) {
|
|
319
|
+
for (const w of ctx.warnings) {
|
|
320
|
+
lines.push(` Note: ${w.message}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const callers = ctx.callers || [];
|
|
325
|
+
lines.push(`\nCALLERS (${callers.length}):`);
|
|
326
|
+
for (const c of callers) {
|
|
327
|
+
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
328
|
+
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
|
|
329
|
+
lines.push(` ${c.content.trim()}`);
|
|
330
|
+
expandable.push({
|
|
331
|
+
num: itemNum++,
|
|
332
|
+
type: 'caller',
|
|
333
|
+
name: c.callerName || '(module level)',
|
|
334
|
+
file: c.callerFile || c.file,
|
|
335
|
+
relativePath: c.relativePath,
|
|
336
|
+
line: c.line,
|
|
337
|
+
startLine: c.callerStartLine || c.line,
|
|
338
|
+
endLine: c.callerEndLine || c.line
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const callees = ctx.callees || [];
|
|
343
|
+
lines.push(`\nCALLEES (${callees.length}):`);
|
|
344
|
+
for (const c of callees) {
|
|
345
|
+
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
346
|
+
lines.push(` [${itemNum}] ${c.name}${weight} - ${c.relativePath}:${c.startLine}`);
|
|
347
|
+
expandable.push({
|
|
348
|
+
num: itemNum++,
|
|
349
|
+
type: 'callee',
|
|
350
|
+
name: c.name,
|
|
351
|
+
file: c.file,
|
|
352
|
+
relativePath: c.relativePath,
|
|
353
|
+
startLine: c.startLine,
|
|
354
|
+
endLine: c.endLine
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (expandable.length > 0) {
|
|
359
|
+
lines.push(`\nUse ucn_expand with item number to see code for any item.`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { text: lines.join('\n'), expandable };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function formatSmartText(smart) {
|
|
366
|
+
if (!smart) return 'Function not found.';
|
|
367
|
+
|
|
368
|
+
const lines = [];
|
|
369
|
+
lines.push(`${smart.target.name} (${smart.target.file}:${smart.target.startLine})`);
|
|
370
|
+
lines.push('═'.repeat(60));
|
|
371
|
+
|
|
372
|
+
if (smart.meta) {
|
|
373
|
+
const notes = [];
|
|
374
|
+
if (smart.meta.dynamicImports) notes.push(`${smart.meta.dynamicImports} dynamic import(s)`);
|
|
375
|
+
if (smart.meta.uncertain) notes.push(`${smart.meta.uncertain} uncertain call(s) skipped`);
|
|
376
|
+
if (notes.length) {
|
|
377
|
+
lines.push(` Note: ${notes.join(', ')}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
lines.push(smart.target.code);
|
|
382
|
+
|
|
383
|
+
if (smart.dependencies.length > 0) {
|
|
384
|
+
lines.push('\n─── DEPENDENCIES ───');
|
|
385
|
+
for (const dep of smart.dependencies) {
|
|
386
|
+
const weight = dep.weight && dep.weight !== 'normal' ? ` [${dep.weight}]` : '';
|
|
387
|
+
lines.push(`\n// ${dep.name}${weight} (${dep.relativePath}:${dep.startLine})`);
|
|
388
|
+
lines.push(dep.code);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (smart.types && smart.types.length > 0) {
|
|
393
|
+
lines.push('\n─── TYPES ───');
|
|
394
|
+
for (const t of smart.types) {
|
|
395
|
+
lines.push(`\n// ${t.name} (${t.relativePath}:${t.startLine})`);
|
|
396
|
+
lines.push(t.code);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return lines.join('\n');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function formatDeadcodeText(results) {
|
|
404
|
+
if (results.length === 0) return 'No dead code found.';
|
|
405
|
+
|
|
406
|
+
const lines = [];
|
|
407
|
+
lines.push(`Dead code: ${results.length} unused symbol(s)\n`);
|
|
408
|
+
|
|
409
|
+
let currentFile = null;
|
|
410
|
+
for (const item of results) {
|
|
411
|
+
if (item.file !== currentFile) {
|
|
412
|
+
currentFile = item.file;
|
|
413
|
+
lines.push(item.file);
|
|
414
|
+
}
|
|
415
|
+
const exported = item.isExported ? ' [exported]' : '';
|
|
416
|
+
lines.push(` ${output.lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return lines.join('\n');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function formatFnText(match, fnCode) {
|
|
423
|
+
const lines = [];
|
|
424
|
+
lines.push(`${match.relativePath}:${match.startLine}`);
|
|
425
|
+
lines.push(`${output.lineRange(match.startLine, match.endLine)} ${output.formatFunctionSignature(match)}`);
|
|
426
|
+
lines.push('─'.repeat(60));
|
|
427
|
+
lines.push(fnCode);
|
|
428
|
+
return lines.join('\n');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function formatClassText(cls, clsCode) {
|
|
432
|
+
const lines = [];
|
|
433
|
+
lines.push(`${cls.relativePath || cls.file}:${cls.startLine}`);
|
|
434
|
+
lines.push(`${output.lineRange(cls.startLine, cls.endLine)} ${output.formatClassSignature(cls)}`);
|
|
435
|
+
lines.push('─'.repeat(60));
|
|
436
|
+
lines.push(clsCode);
|
|
437
|
+
return lines.join('\n');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function pickBestDefinition(matches) {
|
|
441
|
+
const typeOrder = new Set(['class', 'struct', 'interface', 'type', 'impl']);
|
|
442
|
+
const scored = matches.map(m => {
|
|
443
|
+
let score = 0;
|
|
444
|
+
const rp = m.relativePath || '';
|
|
445
|
+
if (typeOrder.has(m.type)) score += 1000;
|
|
446
|
+
if (isTestFile(rp, detectLanguage(m.file))) score -= 500;
|
|
447
|
+
if (/^(examples?|docs?|vendor|third[_-]?party|benchmarks?|samples?)\//i.test(rp)) score -= 300;
|
|
448
|
+
if (/^(lib|src|core|internal|pkg|crates)\//i.test(rp)) score += 200;
|
|
449
|
+
if (m.startLine && m.endLine) {
|
|
450
|
+
score += Math.min(m.endLine - m.startLine, 100);
|
|
451
|
+
}
|
|
452
|
+
return { match: m, score };
|
|
453
|
+
});
|
|
454
|
+
scored.sort((a, b) => b.score - a.score);
|
|
455
|
+
return scored[0].match;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function formatGraphText(graph) {
|
|
459
|
+
if (graph.nodes.length === 0) return 'File not found.';
|
|
460
|
+
|
|
461
|
+
const rootEntry = graph.nodes.find(n => n.file === graph.root);
|
|
462
|
+
const rootRelPath = rootEntry ? rootEntry.relativePath : graph.root;
|
|
463
|
+
const lines = [];
|
|
464
|
+
lines.push(`Dependency graph for ${rootRelPath}`);
|
|
465
|
+
lines.push('═'.repeat(60));
|
|
466
|
+
|
|
467
|
+
const printed = new Set();
|
|
468
|
+
const maxChildren = 8;
|
|
469
|
+
|
|
470
|
+
function printNode(file, indent) {
|
|
471
|
+
const fileEntry = graph.nodes.find(n => n.file === file);
|
|
472
|
+
const relPath = fileEntry ? fileEntry.relativePath : file;
|
|
473
|
+
const prefix = indent === 0 ? '' : ' '.repeat(indent - 1) + '├── ';
|
|
474
|
+
|
|
475
|
+
if (printed.has(file)) {
|
|
476
|
+
lines.push(`${prefix}${relPath} (circular)`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
printed.add(file);
|
|
480
|
+
lines.push(`${prefix}${relPath}`);
|
|
481
|
+
|
|
482
|
+
const edges = graph.edges.filter(e => e.from === file);
|
|
483
|
+
const displayEdges = edges.slice(0, maxChildren);
|
|
484
|
+
const hiddenCount = edges.length - displayEdges.length;
|
|
485
|
+
|
|
486
|
+
for (const edge of displayEdges) {
|
|
487
|
+
printNode(edge.to, indent + 1);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (hiddenCount > 0) {
|
|
491
|
+
lines.push(`${' '.repeat(indent)}└── ... and ${hiddenCount} more`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
printNode(graph.root, 0);
|
|
496
|
+
return lines.join('\n');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function formatSearchText(results, term) {
|
|
500
|
+
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
501
|
+
if (totalMatches === 0) return `No matches found for "${term}"`;
|
|
502
|
+
|
|
503
|
+
const lines = [];
|
|
504
|
+
lines.push(`Found ${totalMatches} matches for "${term}" in ${results.length} files:`);
|
|
505
|
+
lines.push('═'.repeat(60));
|
|
506
|
+
|
|
507
|
+
for (const result of results) {
|
|
508
|
+
lines.push(`\n${result.file}`);
|
|
509
|
+
for (const m of result.matches) {
|
|
510
|
+
lines.push(` ${m.line}: ${m.content.trim()}`);
|
|
511
|
+
if (m.before && m.before.length > 0) {
|
|
512
|
+
for (const line of m.before) {
|
|
513
|
+
lines.push(` ... ${line.trim()}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (m.after && m.after.length > 0) {
|
|
517
|
+
for (const line of m.after) {
|
|
518
|
+
lines.push(` ... ${line.trim()}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return lines.join('\n');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function formatFileExportsText(exports, filePath) {
|
|
528
|
+
if (exports.length === 0) return `No exports found in ${filePath}`;
|
|
529
|
+
|
|
530
|
+
const lines = [];
|
|
531
|
+
lines.push(`Exports from ${filePath}:\n`);
|
|
532
|
+
for (const exp of exports) {
|
|
533
|
+
lines.push(` ${output.lineRange(exp.startLine, exp.endLine)} ${exp.signature || exp.name}`);
|
|
534
|
+
}
|
|
535
|
+
return lines.join('\n');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function analyzeCallSiteAST(filePath, lineNum, funcName) {
|
|
539
|
+
const result = {
|
|
540
|
+
isAwait: false,
|
|
541
|
+
isDestructured: false,
|
|
542
|
+
isTypedAssignment: false,
|
|
543
|
+
isInReturn: false,
|
|
544
|
+
isInCatch: false,
|
|
545
|
+
isInConditional: false,
|
|
546
|
+
hasComment: false,
|
|
547
|
+
isStandalone: false
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const language = detectLanguage(filePath);
|
|
552
|
+
if (!language) return result;
|
|
553
|
+
|
|
554
|
+
const parser = getParser(language);
|
|
555
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
556
|
+
const tree = parser.parse(content, undefined, PARSE_OPTIONS);
|
|
557
|
+
|
|
558
|
+
const row = lineNum - 1;
|
|
559
|
+
const node = tree.rootNode.descendantForPosition({ row, column: 0 });
|
|
560
|
+
if (!node) return result;
|
|
561
|
+
|
|
562
|
+
let current = node;
|
|
563
|
+
let foundCall = false;
|
|
564
|
+
|
|
565
|
+
while (current) {
|
|
566
|
+
const type = current.type;
|
|
567
|
+
|
|
568
|
+
if (!foundCall && (type === 'call_expression' || type === 'call')) {
|
|
569
|
+
const calleeNode = current.childForFieldName('function') || current.namedChild(0);
|
|
570
|
+
if (calleeNode && calleeNode.text === funcName) {
|
|
571
|
+
foundCall = true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (foundCall) {
|
|
576
|
+
if (type === 'await_expression') result.isAwait = true;
|
|
577
|
+
if (type === 'variable_declarator' || type === 'assignment_expression') {
|
|
578
|
+
const parent = current.parent;
|
|
579
|
+
if (parent && (parent.type === 'lexical_declaration' || parent.type === 'variable_declaration')) {
|
|
580
|
+
result.isTypedAssignment = true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (type === 'array_pattern' || type === 'object_pattern') result.isDestructured = true;
|
|
584
|
+
if (type === 'return_statement') result.isInReturn = true;
|
|
585
|
+
if (type === 'catch_clause' || type === 'except_clause') result.isInCatch = true;
|
|
586
|
+
if (type === 'if_statement' || type === 'conditional_expression' || type === 'ternary_expression') result.isInConditional = true;
|
|
587
|
+
if (type === 'expression_statement') result.isStandalone = true;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
current = current.parent;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const contentLines = content.split('\n');
|
|
594
|
+
if (lineNum > 1) {
|
|
595
|
+
const prevLine = contentLines[lineNum - 2].trim();
|
|
596
|
+
if (prevLine.startsWith('//') || prevLine.startsWith('#') || prevLine.endsWith('*/')) {
|
|
597
|
+
result.hasComment = true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch (e) {
|
|
601
|
+
// Return default result on error
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function findBestExample(index, name) {
|
|
608
|
+
const usages = index.usages(name, {
|
|
609
|
+
codeOnly: true,
|
|
610
|
+
exclude: ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'],
|
|
611
|
+
context: 5
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const calls = usages.filter(u => u.usageType === 'call' && !u.isDefinition);
|
|
615
|
+
|
|
616
|
+
if (calls.length === 0) {
|
|
617
|
+
return `No call examples found for "${name}"`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const scored = calls.map(call => {
|
|
621
|
+
let score = 0;
|
|
622
|
+
const reasons = [];
|
|
623
|
+
const line = call.content.trim();
|
|
624
|
+
|
|
625
|
+
const astInfo = analyzeCallSiteAST(call.file, call.line, name);
|
|
626
|
+
|
|
627
|
+
if (astInfo.isTypedAssignment) { score += 15; reasons.push('typed assignment'); }
|
|
628
|
+
if (astInfo.isInReturn) { score += 10; reasons.push('in return'); }
|
|
629
|
+
if (astInfo.isAwait) { score += 10; reasons.push('async usage'); }
|
|
630
|
+
if (astInfo.isDestructured) { score += 8; reasons.push('destructured'); }
|
|
631
|
+
if (astInfo.isStandalone) { score += 5; reasons.push('standalone'); }
|
|
632
|
+
if (astInfo.hasComment) { score += 3; reasons.push('documented'); }
|
|
633
|
+
if (astInfo.isInCatch) { score -= 5; reasons.push('in catch block'); }
|
|
634
|
+
if (astInfo.isInConditional) { score -= 3; reasons.push('in conditional'); }
|
|
635
|
+
|
|
636
|
+
if (score === 0) {
|
|
637
|
+
if (/^(const|let|var|return)\s/.test(line) || /^\w+\s*=/.test(line)) {
|
|
638
|
+
score += 10; reasons.push('return value used');
|
|
639
|
+
}
|
|
640
|
+
if (line.startsWith(name + '(') || /^(const|let|var)\s+\w+\s*=\s*\w*$/.test(line.split(name)[0])) {
|
|
641
|
+
score += 5; reasons.push('clear usage');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (call.before && call.before.length > 0) score += 3;
|
|
646
|
+
if (call.after && call.after.length > 0) score += 3;
|
|
647
|
+
if (call.before?.length > 0 && call.after?.length > 0) reasons.push('has context');
|
|
648
|
+
|
|
649
|
+
const beforeCall = line.split(name + '(')[0];
|
|
650
|
+
if (!beforeCall.includes('(') || /^\s*(const|let|var|return)?\s*\w+\s*=\s*$/.test(beforeCall)) {
|
|
651
|
+
score += 2;
|
|
652
|
+
}
|
|
653
|
+
if (call.line < 100) score += 1;
|
|
654
|
+
|
|
655
|
+
return { ...call, score, reasons };
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
scored.sort((a, b) => b.score - a.score);
|
|
659
|
+
const best = scored[0];
|
|
660
|
+
|
|
661
|
+
const lines = [];
|
|
662
|
+
lines.push(`Best example of "${name}":`);
|
|
663
|
+
lines.push('═'.repeat(60));
|
|
664
|
+
lines.push(`${best.relativePath}:${best.line}`);
|
|
665
|
+
lines.push('');
|
|
666
|
+
|
|
667
|
+
if (best.before) {
|
|
668
|
+
for (let i = 0; i < best.before.length; i++) {
|
|
669
|
+
const ln = best.line - best.before.length + i;
|
|
670
|
+
lines.push(`${ln.toString().padStart(4)}| ${best.before[i]}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
lines.push(`${best.line.toString().padStart(4)}| ${best.content} <--`);
|
|
675
|
+
|
|
676
|
+
if (best.after) {
|
|
677
|
+
for (let i = 0; i < best.after.length; i++) {
|
|
678
|
+
const ln = best.line + i + 1;
|
|
679
|
+
lines.push(`${ln.toString().padStart(4)}| ${best.after[i]}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
lines.push('');
|
|
684
|
+
lines.push(`Score: ${best.score} (${calls.length} total calls)`);
|
|
685
|
+
lines.push(`Why: ${best.reasons.length > 0 ? best.reasons.join(', ') : 'first available call'}`);
|
|
686
|
+
|
|
687
|
+
return lines.join('\n');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ============================================================================
|
|
691
|
+
// SHARED SCHEMA DEFINITIONS
|
|
692
|
+
// ============================================================================
|
|
693
|
+
|
|
694
|
+
const projectDirParam = z.string().describe('Absolute or relative path to the project root directory');
|
|
695
|
+
const nameParam = z.string().describe('Symbol name to analyze (function, class, method, etc.)');
|
|
696
|
+
const fileParam = z.string().optional().describe('Filter by file path pattern for disambiguation (e.g. "parser", "src/core")');
|
|
697
|
+
const excludeParam = z.string().optional().describe('Comma-separated patterns to exclude (e.g. "test,mock,vendor")');
|
|
698
|
+
const includeTestsParam = z.boolean().optional().describe('Include test files in results (excluded by default)');
|
|
699
|
+
const includeMethodsParam = z.boolean().optional().describe('Include obj.method() calls in caller/callee analysis');
|
|
700
|
+
const includeUncertainParam = z.boolean().optional().describe('Include uncertain/ambiguous matches');
|
|
701
|
+
|
|
702
|
+
// ============================================================================
|
|
703
|
+
// SERVER SETUP
|
|
704
|
+
// ============================================================================
|
|
705
|
+
|
|
706
|
+
const server = new McpServer({
|
|
707
|
+
name: 'ucn',
|
|
708
|
+
version: require('../package.json').version
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// ============================================================================
|
|
712
|
+
// TOOL HELPERS
|
|
713
|
+
// ============================================================================
|
|
714
|
+
|
|
715
|
+
function addTestExclusions(exclude) {
|
|
716
|
+
const testPatterns = ['test', 'spec', '__tests__', '__mocks__', 'fixture', 'mock'];
|
|
717
|
+
const existing = new Set((exclude || []).map(e => e.toLowerCase()));
|
|
718
|
+
const additions = testPatterns.filter(p => !existing.has(p));
|
|
719
|
+
return [...(exclude || []), ...additions];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function parseExclude(excludeStr) {
|
|
723
|
+
if (!excludeStr) return [];
|
|
724
|
+
return excludeStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const MAX_OUTPUT_CHARS = 100000; // ~100KB, safe for all MCP clients
|
|
728
|
+
|
|
729
|
+
function toolResult(text) {
|
|
730
|
+
if (text.length > MAX_OUTPUT_CHARS) {
|
|
731
|
+
const truncated = text.substring(0, MAX_OUTPUT_CHARS);
|
|
732
|
+
// Cut at last newline to avoid breaking mid-line
|
|
733
|
+
const lastNewline = truncated.lastIndexOf('\n');
|
|
734
|
+
const cleanCut = lastNewline > MAX_OUTPUT_CHARS * 0.8 ? truncated.substring(0, lastNewline) : truncated;
|
|
735
|
+
return { content: [{ type: 'text', text: cleanCut + '\n\n... (output truncated — refine query or use --file/--in to narrow scope)' }] };
|
|
736
|
+
}
|
|
737
|
+
return { content: [{ type: 'text', text }] };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function toolError(message) {
|
|
741
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function requireName(name) {
|
|
745
|
+
if (!name || !name.trim()) {
|
|
746
|
+
return toolError('Symbol name is required.');
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ============================================================================
|
|
752
|
+
// TOOL REGISTRATIONS
|
|
753
|
+
// ============================================================================
|
|
754
|
+
|
|
755
|
+
// --- ucn_toc ---
|
|
756
|
+
server.registerTool(
|
|
757
|
+
'ucn_toc',
|
|
758
|
+
{
|
|
759
|
+
description: 'Project overview: file counts, line counts, function/class counts per file. Use detailed=true to list all symbols. Works on JS/TS, Python, Go, Rust, Java.',
|
|
760
|
+
inputSchema: z.object({
|
|
761
|
+
project_dir: projectDirParam,
|
|
762
|
+
detailed: z.boolean().optional().describe('Show full symbol listing per file')
|
|
763
|
+
})
|
|
764
|
+
},
|
|
765
|
+
async ({ project_dir, detailed }) => {
|
|
766
|
+
try {
|
|
767
|
+
const index = getIndex(project_dir);
|
|
768
|
+
const toc = index.getToc({ detailed: detailed || false });
|
|
769
|
+
return toolResult(formatTocText(toc));
|
|
770
|
+
} catch (e) {
|
|
771
|
+
return toolError(e.message);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
// --- ucn_find ---
|
|
777
|
+
server.registerTool(
|
|
778
|
+
'ucn_find',
|
|
779
|
+
{
|
|
780
|
+
description: 'Find where a symbol is defined. Returns top matches sorted by usage count with signatures.',
|
|
781
|
+
inputSchema: z.object({
|
|
782
|
+
project_dir: projectDirParam,
|
|
783
|
+
name: nameParam,
|
|
784
|
+
file: fileParam,
|
|
785
|
+
exclude: excludeParam,
|
|
786
|
+
include_tests: includeTestsParam,
|
|
787
|
+
exact: z.boolean().optional().describe('Exact name match only (no substring matching)'),
|
|
788
|
+
in: z.string().optional().describe('Only search in this directory path (e.g. "src/core")'),
|
|
789
|
+
top: z.number().optional().describe('Maximum number of results to show (default: 10)')
|
|
790
|
+
})
|
|
791
|
+
},
|
|
792
|
+
async ({ project_dir, name, file, exclude, include_tests, exact, in: inPath, top }) => {
|
|
793
|
+
const err = requireName(name);
|
|
794
|
+
if (err) return err;
|
|
795
|
+
try {
|
|
796
|
+
const index = getIndex(project_dir);
|
|
797
|
+
const excludeArr = include_tests ? parseExclude(exclude) : addTestExclusions(parseExclude(exclude));
|
|
798
|
+
const found = index.find(name, { file, exclude: excludeArr, exact: exact || false, in: inPath });
|
|
799
|
+
return toolResult(formatFindText(found, name, top));
|
|
800
|
+
} catch (e) {
|
|
801
|
+
return toolError(e.message);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// --- ucn_about ---
|
|
807
|
+
server.registerTool(
|
|
808
|
+
'ucn_about',
|
|
809
|
+
{
|
|
810
|
+
description: 'Everything about a code symbol: definition, source code, callers, callees, tests. First stop when investigating any function or class. Works on JS/TS, Python, Go, Rust, Java.',
|
|
811
|
+
inputSchema: z.object({
|
|
812
|
+
project_dir: projectDirParam,
|
|
813
|
+
name: nameParam,
|
|
814
|
+
file: fileParam,
|
|
815
|
+
with_types: z.boolean().optional().describe('Include related type definitions in output')
|
|
816
|
+
})
|
|
817
|
+
},
|
|
818
|
+
async ({ project_dir, name, file, with_types }) => {
|
|
819
|
+
const err = requireName(name);
|
|
820
|
+
if (err) return err;
|
|
821
|
+
try {
|
|
822
|
+
const index = getIndex(project_dir);
|
|
823
|
+
const result = index.about(name, { file, withTypes: with_types || false });
|
|
824
|
+
return toolResult(output.formatAbout(result));
|
|
825
|
+
} catch (e) {
|
|
826
|
+
return toolError(e.message);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
// --- ucn_context ---
|
|
832
|
+
server.registerTool(
|
|
833
|
+
'ucn_context',
|
|
834
|
+
{
|
|
835
|
+
description: 'Quick view of who calls a function and what it calls. Shows callers and callees with file locations and call weights.',
|
|
836
|
+
inputSchema: z.object({
|
|
837
|
+
project_dir: projectDirParam,
|
|
838
|
+
name: nameParam,
|
|
839
|
+
file: fileParam,
|
|
840
|
+
include_methods: includeMethodsParam,
|
|
841
|
+
include_uncertain: includeUncertainParam
|
|
842
|
+
})
|
|
843
|
+
},
|
|
844
|
+
async ({ project_dir, name, file, include_methods, include_uncertain }) => {
|
|
845
|
+
const err = requireName(name);
|
|
846
|
+
if (err) return err;
|
|
847
|
+
try {
|
|
848
|
+
const index = getIndex(project_dir);
|
|
849
|
+
const ctx = index.context(name, {
|
|
850
|
+
includeMethods: include_methods || false,
|
|
851
|
+
includeUncertain: include_uncertain || false,
|
|
852
|
+
file
|
|
853
|
+
});
|
|
854
|
+
const { text, expandable } = formatContextText(ctx);
|
|
855
|
+
if (expandable.length > 0) {
|
|
856
|
+
expandCache.set(index.root, { items: expandable, root: index.root });
|
|
857
|
+
}
|
|
858
|
+
return toolResult(text);
|
|
859
|
+
} catch (e) {
|
|
860
|
+
return toolError(e.message);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
// --- ucn_impact ---
|
|
866
|
+
server.registerTool(
|
|
867
|
+
'ucn_impact',
|
|
868
|
+
{
|
|
869
|
+
description: 'Before changing a function, see every call site grouped by file. Shows arguments used at each call site. Essential for signature changes.',
|
|
870
|
+
inputSchema: z.object({
|
|
871
|
+
project_dir: projectDirParam,
|
|
872
|
+
name: nameParam,
|
|
873
|
+
file: fileParam
|
|
874
|
+
})
|
|
875
|
+
},
|
|
876
|
+
async ({ project_dir, name, file }) => {
|
|
877
|
+
const err = requireName(name);
|
|
878
|
+
if (err) return err;
|
|
879
|
+
try {
|
|
880
|
+
const index = getIndex(project_dir);
|
|
881
|
+
const result = index.impact(name, { file });
|
|
882
|
+
return toolResult(output.formatImpact(result));
|
|
883
|
+
} catch (e) {
|
|
884
|
+
return toolError(e.message);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
// --- ucn_smart ---
|
|
890
|
+
server.registerTool(
|
|
891
|
+
'ucn_smart',
|
|
892
|
+
{
|
|
893
|
+
description: 'Function source code with all its dependencies expanded inline. Everything you need to understand or modify a function in one response.',
|
|
894
|
+
inputSchema: z.object({
|
|
895
|
+
project_dir: projectDirParam,
|
|
896
|
+
name: nameParam,
|
|
897
|
+
file: fileParam,
|
|
898
|
+
include_methods: includeMethodsParam,
|
|
899
|
+
include_uncertain: includeUncertainParam,
|
|
900
|
+
with_types: z.boolean().optional().describe('Include related type definitions in output')
|
|
901
|
+
})
|
|
902
|
+
},
|
|
903
|
+
async ({ project_dir, name, file, include_methods, include_uncertain, with_types }) => {
|
|
904
|
+
const err = requireName(name);
|
|
905
|
+
if (err) return err;
|
|
906
|
+
try {
|
|
907
|
+
const index = getIndex(project_dir);
|
|
908
|
+
const result = index.smart(name, {
|
|
909
|
+
file,
|
|
910
|
+
withTypes: with_types || false,
|
|
911
|
+
includeMethods: include_methods || false,
|
|
912
|
+
includeUncertain: include_uncertain || false
|
|
913
|
+
});
|
|
914
|
+
return toolResult(formatSmartText(result));
|
|
915
|
+
} catch (e) {
|
|
916
|
+
return toolError(e.message);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
// --- ucn_trace ---
|
|
922
|
+
server.registerTool(
|
|
923
|
+
'ucn_trace',
|
|
924
|
+
{
|
|
925
|
+
description: 'Call tree visualization showing execution flow. Traces what a function calls, what those call, etc. Depth-limited.',
|
|
926
|
+
inputSchema: z.object({
|
|
927
|
+
project_dir: projectDirParam,
|
|
928
|
+
name: nameParam,
|
|
929
|
+
file: fileParam,
|
|
930
|
+
depth: z.number().optional().describe('Maximum call tree depth (default: 3)')
|
|
931
|
+
})
|
|
932
|
+
},
|
|
933
|
+
async ({ project_dir, name, file, depth }) => {
|
|
934
|
+
const err = requireName(name);
|
|
935
|
+
if (err) return err;
|
|
936
|
+
try {
|
|
937
|
+
const index = getIndex(project_dir);
|
|
938
|
+
const result = index.trace(name, { depth: depth ?? 3, file });
|
|
939
|
+
return toolResult(output.formatTrace(result));
|
|
940
|
+
} catch (e) {
|
|
941
|
+
return toolError(e.message);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// --- ucn_usages ---
|
|
947
|
+
server.registerTool(
|
|
948
|
+
'ucn_usages',
|
|
949
|
+
{
|
|
950
|
+
description: 'All usages of a symbol grouped by type: definitions, calls, imports, references.',
|
|
951
|
+
inputSchema: z.object({
|
|
952
|
+
project_dir: projectDirParam,
|
|
953
|
+
name: nameParam,
|
|
954
|
+
exclude: excludeParam,
|
|
955
|
+
include_tests: includeTestsParam,
|
|
956
|
+
code_only: z.boolean().optional().describe('Exclude matches in comments and strings'),
|
|
957
|
+
context: z.number().optional().describe('Lines of context around each match'),
|
|
958
|
+
in: z.string().optional().describe('Only search in this directory path (e.g. "src/core")')
|
|
959
|
+
})
|
|
960
|
+
},
|
|
961
|
+
async ({ project_dir, name, exclude, include_tests, code_only, context, in: inPath }) => {
|
|
962
|
+
const err = requireName(name);
|
|
963
|
+
if (err) return err;
|
|
964
|
+
try {
|
|
965
|
+
const index = getIndex(project_dir);
|
|
966
|
+
const excludeArr = include_tests ? parseExclude(exclude) : addTestExclusions(parseExclude(exclude));
|
|
967
|
+
const result = index.usages(name, {
|
|
968
|
+
exclude: excludeArr,
|
|
969
|
+
codeOnly: code_only || false,
|
|
970
|
+
context: context || 0,
|
|
971
|
+
in: inPath
|
|
972
|
+
});
|
|
973
|
+
return toolResult(formatUsagesText(result, name));
|
|
974
|
+
} catch (e) {
|
|
975
|
+
return toolError(e.message);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
// --- ucn_deadcode ---
|
|
981
|
+
server.registerTool(
|
|
982
|
+
'ucn_deadcode',
|
|
983
|
+
{
|
|
984
|
+
description: 'Find unused functions and classes with zero callers across the project.',
|
|
985
|
+
inputSchema: z.object({
|
|
986
|
+
project_dir: projectDirParam,
|
|
987
|
+
include_exported: z.boolean().optional().describe('Include exported symbols (excluded by default)'),
|
|
988
|
+
include_tests: includeTestsParam
|
|
989
|
+
})
|
|
990
|
+
},
|
|
991
|
+
async ({ project_dir, include_exported, include_tests }) => {
|
|
992
|
+
try {
|
|
993
|
+
const index = getIndex(project_dir);
|
|
994
|
+
const result = index.deadcode({
|
|
995
|
+
includeExported: include_exported || false,
|
|
996
|
+
includeTests: include_tests || false
|
|
997
|
+
});
|
|
998
|
+
return toolResult(formatDeadcodeText(result));
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
return toolError(e.message);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
// --- ucn_fn ---
|
|
1006
|
+
server.registerTool(
|
|
1007
|
+
'ucn_fn',
|
|
1008
|
+
{
|
|
1009
|
+
description: "Extract a single function's source code from the project. Use file parameter to disambiguate when multiple functions share the same name.",
|
|
1010
|
+
inputSchema: z.object({
|
|
1011
|
+
project_dir: projectDirParam,
|
|
1012
|
+
name: nameParam,
|
|
1013
|
+
file: fileParam
|
|
1014
|
+
})
|
|
1015
|
+
},
|
|
1016
|
+
async ({ project_dir, name, file }) => {
|
|
1017
|
+
const err = requireName(name);
|
|
1018
|
+
if (err) return err;
|
|
1019
|
+
try {
|
|
1020
|
+
const index = getIndex(project_dir);
|
|
1021
|
+
const matches = index.find(name, { file }).filter(m => m.type === 'function' || m.params !== undefined);
|
|
1022
|
+
|
|
1023
|
+
if (matches.length === 0) {
|
|
1024
|
+
return toolResult(`Function "${name}" not found.`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
|
|
1028
|
+
const code = fs.readFileSync(match.file, 'utf-8');
|
|
1029
|
+
const codeLines = code.split('\n');
|
|
1030
|
+
const fnCode = codeLines.slice(match.startLine - 1, match.endLine).join('\n');
|
|
1031
|
+
|
|
1032
|
+
let note = '';
|
|
1033
|
+
if (matches.length > 1 && !file) {
|
|
1034
|
+
note = `Note: Found ${matches.length} definitions for "${name}". Showing ${match.relativePath}:${match.startLine}. Use file parameter to disambiguate.\n\n`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return toolResult(note + formatFnText(match, fnCode));
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
return toolError(e.message);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
// --- ucn_class ---
|
|
1045
|
+
server.registerTool(
|
|
1046
|
+
'ucn_class',
|
|
1047
|
+
{
|
|
1048
|
+
description: 'Extract a single class/struct/interface source code from the project.',
|
|
1049
|
+
inputSchema: z.object({
|
|
1050
|
+
project_dir: projectDirParam,
|
|
1051
|
+
name: nameParam,
|
|
1052
|
+
file: fileParam
|
|
1053
|
+
})
|
|
1054
|
+
},
|
|
1055
|
+
async ({ project_dir, name, file }) => {
|
|
1056
|
+
const err = requireName(name);
|
|
1057
|
+
if (err) return err;
|
|
1058
|
+
try {
|
|
1059
|
+
const index = getIndex(project_dir);
|
|
1060
|
+
const { extractClass } = require('../core/parser');
|
|
1061
|
+
const matches = index.find(name, { file }).filter(m =>
|
|
1062
|
+
['class', 'interface', 'type', 'enum', 'struct', 'trait'].includes(m.type)
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
if (matches.length === 0) {
|
|
1066
|
+
return toolResult(`Class "${name}" not found.`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const match = matches.length > 1 ? pickBestDefinition(matches) : matches[0];
|
|
1070
|
+
const code = fs.readFileSync(match.file, 'utf-8');
|
|
1071
|
+
const language = detectLanguage(match.file);
|
|
1072
|
+
const { cls, code: clsCode } = extractClass(code, language, match.name);
|
|
1073
|
+
|
|
1074
|
+
if (!cls) {
|
|
1075
|
+
return toolResult(`Class "${name}" could not be extracted.`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
let note = '';
|
|
1079
|
+
if (matches.length > 1 && !file) {
|
|
1080
|
+
note = `Note: Found ${matches.length} definitions for "${name}". Showing ${match.relativePath}:${match.startLine}. Use file parameter to disambiguate.\n\n`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return toolResult(note + formatClassText(cls, clsCode));
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
return toolError(e.message);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
// --- ucn_verify ---
|
|
1091
|
+
server.registerTool(
|
|
1092
|
+
'ucn_verify',
|
|
1093
|
+
{
|
|
1094
|
+
description: "Check all call sites match a function's parameter count. Use before changing a signature.",
|
|
1095
|
+
inputSchema: z.object({
|
|
1096
|
+
project_dir: projectDirParam,
|
|
1097
|
+
name: nameParam,
|
|
1098
|
+
file: fileParam
|
|
1099
|
+
})
|
|
1100
|
+
},
|
|
1101
|
+
async ({ project_dir, name, file }) => {
|
|
1102
|
+
const err = requireName(name);
|
|
1103
|
+
if (err) return err;
|
|
1104
|
+
try {
|
|
1105
|
+
const index = getIndex(project_dir);
|
|
1106
|
+
const result = index.verify(name, { file });
|
|
1107
|
+
return toolResult(output.formatVerify(result));
|
|
1108
|
+
} catch (e) {
|
|
1109
|
+
return toolError(e.message);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
// --- ucn_imports ---
|
|
1115
|
+
server.registerTool(
|
|
1116
|
+
'ucn_imports',
|
|
1117
|
+
{
|
|
1118
|
+
description: 'What does a file import? Shows all import dependencies with resolved paths.',
|
|
1119
|
+
inputSchema: z.object({
|
|
1120
|
+
project_dir: projectDirParam,
|
|
1121
|
+
file: z.string().describe('File path (relative to project root or absolute) to analyze imports for')
|
|
1122
|
+
})
|
|
1123
|
+
},
|
|
1124
|
+
async ({ project_dir, file }) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const index = getIndex(project_dir);
|
|
1127
|
+
const result = index.imports(file);
|
|
1128
|
+
return toolResult(output.formatImports(result, file));
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
return toolError(e.message);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
// --- ucn_exporters ---
|
|
1136
|
+
server.registerTool(
|
|
1137
|
+
'ucn_exporters',
|
|
1138
|
+
{
|
|
1139
|
+
description: 'Who imports this file? Shows all files that depend on it.',
|
|
1140
|
+
inputSchema: z.object({
|
|
1141
|
+
project_dir: projectDirParam,
|
|
1142
|
+
file: z.string().describe('File path (relative to project root or absolute) to find importers of')
|
|
1143
|
+
})
|
|
1144
|
+
},
|
|
1145
|
+
async ({ project_dir, file }) => {
|
|
1146
|
+
try {
|
|
1147
|
+
const index = getIndex(project_dir);
|
|
1148
|
+
const result = index.exporters(file);
|
|
1149
|
+
return toolResult(output.formatExporters(result, file));
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
return toolError(e.message);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
);
|
|
1155
|
+
|
|
1156
|
+
// --- ucn_tests ---
|
|
1157
|
+
server.registerTool(
|
|
1158
|
+
'ucn_tests',
|
|
1159
|
+
{
|
|
1160
|
+
description: 'Find test files and test cases for a function or file. Shows test-case matches, imports, and call sites in test files.',
|
|
1161
|
+
inputSchema: z.object({
|
|
1162
|
+
project_dir: projectDirParam,
|
|
1163
|
+
name: nameParam
|
|
1164
|
+
})
|
|
1165
|
+
},
|
|
1166
|
+
async ({ project_dir, name }) => {
|
|
1167
|
+
const err = requireName(name);
|
|
1168
|
+
if (err) return err;
|
|
1169
|
+
try {
|
|
1170
|
+
const index = getIndex(project_dir);
|
|
1171
|
+
const result = index.tests(name);
|
|
1172
|
+
return toolResult(output.formatTests(result, name));
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
return toolError(e.message);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
// --- ucn_related ---
|
|
1180
|
+
server.registerTool(
|
|
1181
|
+
'ucn_related',
|
|
1182
|
+
{
|
|
1183
|
+
description: 'Find functions related to a symbol: same file, similar names, shared callers/callees. Useful for discovering associated code.',
|
|
1184
|
+
inputSchema: z.object({
|
|
1185
|
+
project_dir: projectDirParam,
|
|
1186
|
+
name: nameParam,
|
|
1187
|
+
file: fileParam
|
|
1188
|
+
})
|
|
1189
|
+
},
|
|
1190
|
+
async ({ project_dir, name, file }) => {
|
|
1191
|
+
const err = requireName(name);
|
|
1192
|
+
if (err) return err;
|
|
1193
|
+
try {
|
|
1194
|
+
const index = getIndex(project_dir);
|
|
1195
|
+
const result = index.related(name, { file });
|
|
1196
|
+
return toolResult(output.formatRelated(result));
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
return toolError(e.message);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
// --- ucn_graph ---
|
|
1204
|
+
server.registerTool(
|
|
1205
|
+
'ucn_graph',
|
|
1206
|
+
{
|
|
1207
|
+
description: 'Dependency graph for a file. Shows import/export tree as a visual hierarchy.',
|
|
1208
|
+
inputSchema: z.object({
|
|
1209
|
+
project_dir: projectDirParam,
|
|
1210
|
+
file: z.string().describe('File path (relative to project root or absolute) to graph dependencies for'),
|
|
1211
|
+
depth: z.number().optional().describe('Maximum graph depth (default: 2)'),
|
|
1212
|
+
direction: z.enum(['imports', 'importers', 'both']).optional().describe('Graph direction: imports (what this file uses), importers (who uses this file), both (default: both)')
|
|
1213
|
+
})
|
|
1214
|
+
},
|
|
1215
|
+
async ({ project_dir, file, depth, direction }) => {
|
|
1216
|
+
try {
|
|
1217
|
+
const index = getIndex(project_dir);
|
|
1218
|
+
const result = index.graph(file, { direction: direction || 'both', maxDepth: depth ?? 2 });
|
|
1219
|
+
return toolResult(formatGraphText(result));
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
return toolError(e.message);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
// --- ucn_file_exports ---
|
|
1227
|
+
server.registerTool(
|
|
1228
|
+
'ucn_file_exports',
|
|
1229
|
+
{
|
|
1230
|
+
description: "Show what a file exports (its public API). Lists exported functions, classes, and variables with signatures.",
|
|
1231
|
+
inputSchema: z.object({
|
|
1232
|
+
project_dir: projectDirParam,
|
|
1233
|
+
file: z.string().describe('File path (relative to project root or absolute) to list exports for')
|
|
1234
|
+
})
|
|
1235
|
+
},
|
|
1236
|
+
async ({ project_dir, file }) => {
|
|
1237
|
+
try {
|
|
1238
|
+
const index = getIndex(project_dir);
|
|
1239
|
+
const result = index.fileExports(file);
|
|
1240
|
+
return toolResult(formatFileExportsText(result, file));
|
|
1241
|
+
} catch (e) {
|
|
1242
|
+
return toolError(e.message);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
// --- ucn_search ---
|
|
1248
|
+
server.registerTool(
|
|
1249
|
+
'ucn_search',
|
|
1250
|
+
{
|
|
1251
|
+
description: 'Text search across project files. Respects project ignores. Optionally filter to code only (exclude comments/strings).',
|
|
1252
|
+
inputSchema: z.object({
|
|
1253
|
+
project_dir: projectDirParam,
|
|
1254
|
+
term: z.string().describe('Search term (plain text, not regex)'),
|
|
1255
|
+
code_only: z.boolean().optional().describe('Exclude matches in comments and strings'),
|
|
1256
|
+
context: z.number().optional().describe('Lines of context around each match')
|
|
1257
|
+
})
|
|
1258
|
+
},
|
|
1259
|
+
async ({ project_dir, term, code_only, context }) => {
|
|
1260
|
+
if (!term || !term.trim()) {
|
|
1261
|
+
return toolError('Search term is required.');
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
const index = getIndex(project_dir);
|
|
1265
|
+
const result = index.search(term, {
|
|
1266
|
+
codeOnly: code_only || false,
|
|
1267
|
+
context: context || 0
|
|
1268
|
+
});
|
|
1269
|
+
return toolResult(formatSearchText(result, term));
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
return toolError(e.message);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
);
|
|
1275
|
+
|
|
1276
|
+
// --- ucn_plan ---
|
|
1277
|
+
server.registerTool(
|
|
1278
|
+
'ucn_plan',
|
|
1279
|
+
{
|
|
1280
|
+
description: 'Preview a refactoring: add/remove parameter or rename function. Shows before/after signatures and all call sites that need updating.',
|
|
1281
|
+
inputSchema: z.object({
|
|
1282
|
+
project_dir: projectDirParam,
|
|
1283
|
+
name: nameParam,
|
|
1284
|
+
add_param: z.string().optional().describe('Parameter name to add'),
|
|
1285
|
+
remove_param: z.string().optional().describe('Parameter name to remove'),
|
|
1286
|
+
rename_to: z.string().optional().describe('New function name'),
|
|
1287
|
+
default_value: z.string().optional().describe('Default value for added parameter (makes change backward-compatible)')
|
|
1288
|
+
})
|
|
1289
|
+
},
|
|
1290
|
+
async ({ project_dir, name, add_param, remove_param, rename_to, default_value }) => {
|
|
1291
|
+
const err = requireName(name);
|
|
1292
|
+
if (err) return err;
|
|
1293
|
+
if (!add_param && !remove_param && !rename_to) {
|
|
1294
|
+
return toolError('Plan requires an operation: add_param, remove_param, or rename_to');
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
const index = getIndex(project_dir);
|
|
1298
|
+
const result = index.plan(name, {
|
|
1299
|
+
addParam: add_param,
|
|
1300
|
+
removeParam: remove_param,
|
|
1301
|
+
renameTo: rename_to,
|
|
1302
|
+
defaultValue: default_value
|
|
1303
|
+
});
|
|
1304
|
+
return toolResult(output.formatPlan(result));
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
return toolError(e.message);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1310
|
+
|
|
1311
|
+
// --- ucn_typedef ---
|
|
1312
|
+
server.registerTool(
|
|
1313
|
+
'ucn_typedef',
|
|
1314
|
+
{
|
|
1315
|
+
description: 'Find type/interface/enum/struct/trait definitions matching a name.',
|
|
1316
|
+
inputSchema: z.object({
|
|
1317
|
+
project_dir: projectDirParam,
|
|
1318
|
+
name: nameParam
|
|
1319
|
+
})
|
|
1320
|
+
},
|
|
1321
|
+
async ({ project_dir, name }) => {
|
|
1322
|
+
const err = requireName(name);
|
|
1323
|
+
if (err) return err;
|
|
1324
|
+
try {
|
|
1325
|
+
const index = getIndex(project_dir);
|
|
1326
|
+
const result = index.typedef(name);
|
|
1327
|
+
return toolResult(output.formatTypedef(result, name));
|
|
1328
|
+
} catch (e) {
|
|
1329
|
+
return toolError(e.message);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
// --- ucn_stacktrace ---
|
|
1335
|
+
server.registerTool(
|
|
1336
|
+
'ucn_stacktrace',
|
|
1337
|
+
{
|
|
1338
|
+
description: 'Parse a stack trace and show source code context for each frame. Supports JS, Python, Go, Rust, Java stack formats.',
|
|
1339
|
+
inputSchema: z.object({
|
|
1340
|
+
project_dir: projectDirParam,
|
|
1341
|
+
stack: z.string().describe('The stack trace text to parse')
|
|
1342
|
+
})
|
|
1343
|
+
},
|
|
1344
|
+
async ({ project_dir, stack }) => {
|
|
1345
|
+
if (!stack || !stack.trim()) {
|
|
1346
|
+
return toolError('Stack trace text is required.');
|
|
1347
|
+
}
|
|
1348
|
+
try {
|
|
1349
|
+
const index = getIndex(project_dir);
|
|
1350
|
+
const result = index.parseStackTrace(stack);
|
|
1351
|
+
return toolResult(output.formatStackTrace(result));
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
return toolError(e.message);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
);
|
|
1357
|
+
|
|
1358
|
+
// --- ucn_example ---
|
|
1359
|
+
server.registerTool(
|
|
1360
|
+
'ucn_example',
|
|
1361
|
+
{
|
|
1362
|
+
description: 'Find the best usage example of a function. Scores call sites by quality (typed assignment, destructuring, context) and returns the best one with surrounding code.',
|
|
1363
|
+
inputSchema: z.object({
|
|
1364
|
+
project_dir: projectDirParam,
|
|
1365
|
+
name: nameParam
|
|
1366
|
+
})
|
|
1367
|
+
},
|
|
1368
|
+
async ({ project_dir, name }) => {
|
|
1369
|
+
const err = requireName(name);
|
|
1370
|
+
if (err) return err;
|
|
1371
|
+
try {
|
|
1372
|
+
const index = getIndex(project_dir);
|
|
1373
|
+
return toolResult(findBestExample(index, name));
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
return toolError(e.message);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
// --- ucn_expand ---
|
|
1381
|
+
server.registerTool(
|
|
1382
|
+
'ucn_expand',
|
|
1383
|
+
{
|
|
1384
|
+
description: 'Show source code for a numbered item from the last ucn_context call. Run ucn_context first to get numbered callers/callees, then use this to drill into any item.',
|
|
1385
|
+
inputSchema: z.object({
|
|
1386
|
+
project_dir: projectDirParam,
|
|
1387
|
+
item: z.number().describe('Item number from ucn_context output (e.g. 1, 2, 3)')
|
|
1388
|
+
})
|
|
1389
|
+
},
|
|
1390
|
+
async ({ project_dir, item }) => {
|
|
1391
|
+
try {
|
|
1392
|
+
const index = getIndex(project_dir);
|
|
1393
|
+
const cached = expandCache.get(index.root);
|
|
1394
|
+
if (!cached || !cached.items || cached.items.length === 0) {
|
|
1395
|
+
return toolError('No expandable items found. Run ucn_context first to get numbered items.');
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const match = cached.items.find(i => i.num === item);
|
|
1399
|
+
if (!match) {
|
|
1400
|
+
return toolError(`Item ${item} not found. Available: 1-${cached.items.length}`);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const filePath = match.file || (cached.root && match.relativePath ? path.join(cached.root, match.relativePath) : null);
|
|
1404
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
1405
|
+
return toolError(`Cannot locate file for ${match.name}`);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1409
|
+
const fileLines = content.split('\n');
|
|
1410
|
+
const startLine = match.startLine || match.line || 1;
|
|
1411
|
+
const endLine = match.endLine || startLine + 20;
|
|
1412
|
+
|
|
1413
|
+
const lines = [];
|
|
1414
|
+
lines.push(`[${match.num}] ${match.name} (${match.type})`);
|
|
1415
|
+
lines.push(`${match.relativePath}:${startLine}-${endLine}`);
|
|
1416
|
+
lines.push('═'.repeat(60));
|
|
1417
|
+
|
|
1418
|
+
for (let i = startLine - 1; i < Math.min(endLine, fileLines.length); i++) {
|
|
1419
|
+
lines.push(fileLines[i]);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return toolResult(lines.join('\n'));
|
|
1423
|
+
} catch (e) {
|
|
1424
|
+
return toolError(e.message);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
);
|
|
1428
|
+
|
|
1429
|
+
// --- ucn_lines ---
|
|
1430
|
+
server.registerTool(
|
|
1431
|
+
'ucn_lines',
|
|
1432
|
+
{
|
|
1433
|
+
description: 'Extract a range of lines from a project file. Supports "10-20" or single line "15".',
|
|
1434
|
+
inputSchema: z.object({
|
|
1435
|
+
project_dir: projectDirParam,
|
|
1436
|
+
file: z.string().describe('File path (relative to project root or absolute)'),
|
|
1437
|
+
range: z.string().describe('Line range, e.g. "10-20" or "15"')
|
|
1438
|
+
})
|
|
1439
|
+
},
|
|
1440
|
+
async ({ project_dir, file, range }) => {
|
|
1441
|
+
if (!range || !range.trim()) {
|
|
1442
|
+
return toolError('Line range is required (e.g. "10-20" or "15").');
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
const index = getIndex(project_dir);
|
|
1446
|
+
const filePath = index.findFile(file);
|
|
1447
|
+
if (!filePath) {
|
|
1448
|
+
return toolError(`File not found: ${file}`);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
const parts = range.split('-');
|
|
1452
|
+
const start = parseInt(parts[0], 10);
|
|
1453
|
+
const end = parts.length > 1 ? parseInt(parts[1], 10) : start;
|
|
1454
|
+
|
|
1455
|
+
if (isNaN(start) || isNaN(end)) {
|
|
1456
|
+
return toolError(`Invalid line range: "${range}". Expected format: <start>-<end> or <line>`);
|
|
1457
|
+
}
|
|
1458
|
+
if (start < 1) {
|
|
1459
|
+
return toolError(`Invalid start line: ${start}. Line numbers must be >= 1`);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1463
|
+
const fileLines = content.split('\n');
|
|
1464
|
+
|
|
1465
|
+
const startLine = Math.min(start, end);
|
|
1466
|
+
const endLine = Math.max(start, end);
|
|
1467
|
+
|
|
1468
|
+
if (startLine > fileLines.length) {
|
|
1469
|
+
return toolError(`Line ${startLine} is out of bounds. File has ${fileLines.length} lines.`);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const actualEnd = Math.min(endLine, fileLines.length);
|
|
1473
|
+
const lines = [];
|
|
1474
|
+
const relPath = path.relative(index.root, filePath);
|
|
1475
|
+
lines.push(`${relPath}:${startLine}-${actualEnd}`);
|
|
1476
|
+
lines.push('─'.repeat(60));
|
|
1477
|
+
for (let i = startLine - 1; i < actualEnd; i++) {
|
|
1478
|
+
lines.push(`${output.lineNum(i + 1)} | ${fileLines[i]}`);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return toolResult(lines.join('\n'));
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
return toolError(e.message);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
);
|
|
1487
|
+
|
|
1488
|
+
// ── ucn_api ──────────────────────────────────────────────────────────────────
|
|
1489
|
+
|
|
1490
|
+
server.registerTool(
|
|
1491
|
+
'ucn_api',
|
|
1492
|
+
{
|
|
1493
|
+
description: 'Show exported/public symbols in the project or a specific file. Lists the public API surface.',
|
|
1494
|
+
inputSchema: z.object({
|
|
1495
|
+
project_dir: projectDirParam,
|
|
1496
|
+
file: z.string().optional().describe('Optional file path to show exports for (relative to project root)')
|
|
1497
|
+
})
|
|
1498
|
+
},
|
|
1499
|
+
async ({ project_dir, file }) => {
|
|
1500
|
+
try {
|
|
1501
|
+
const index = getIndex(project_dir);
|
|
1502
|
+
const symbols = index.api(file || undefined);
|
|
1503
|
+
return toolResult(output.formatApi(symbols, file || '.'));
|
|
1504
|
+
} catch (e) {
|
|
1505
|
+
return toolError(e.message);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
// ── ucn_stats ────────────────────────────────────────────────────────────────
|
|
1511
|
+
|
|
1512
|
+
server.registerTool(
|
|
1513
|
+
'ucn_stats',
|
|
1514
|
+
{
|
|
1515
|
+
description: 'Show project statistics: file counts, symbol counts, lines of code, breakdown by language and type.',
|
|
1516
|
+
inputSchema: z.object({
|
|
1517
|
+
project_dir: projectDirParam
|
|
1518
|
+
})
|
|
1519
|
+
},
|
|
1520
|
+
async ({ project_dir }) => {
|
|
1521
|
+
try {
|
|
1522
|
+
const index = getIndex(project_dir);
|
|
1523
|
+
const stats = index.getStats();
|
|
1524
|
+
return toolResult(formatStatsText(stats));
|
|
1525
|
+
} catch (e) {
|
|
1526
|
+
return toolError(e.message);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
);
|
|
1530
|
+
|
|
1531
|
+
function formatStatsText(stats) {
|
|
1532
|
+
const lines = [];
|
|
1533
|
+
lines.push('PROJECT STATISTICS');
|
|
1534
|
+
lines.push('═'.repeat(60));
|
|
1535
|
+
lines.push(`Root: ${stats.root}`);
|
|
1536
|
+
lines.push(`Files: ${stats.files}`);
|
|
1537
|
+
lines.push(`Symbols: ${stats.symbols}`);
|
|
1538
|
+
lines.push(`Build time: ${stats.buildTime}ms`);
|
|
1539
|
+
|
|
1540
|
+
lines.push('\nBy Language:');
|
|
1541
|
+
for (const [lang, info] of Object.entries(stats.byLanguage)) {
|
|
1542
|
+
lines.push(` ${lang}: ${info.files} files, ${info.lines} lines, ${info.symbols} symbols`);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
lines.push('\nBy Type:');
|
|
1546
|
+
for (const [type, count] of Object.entries(stats.byType)) {
|
|
1547
|
+
lines.push(` ${type}: ${count}`);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
return lines.join('\n');
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// ============================================================================
|
|
1554
|
+
// START SERVER
|
|
1555
|
+
// ============================================================================
|
|
1556
|
+
|
|
1557
|
+
async function main() {
|
|
1558
|
+
const transport = new StdioServerTransport();
|
|
1559
|
+
await server.connect(transport);
|
|
1560
|
+
console.error('UCN MCP server running on stdio');
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
main().catch(e => {
|
|
1564
|
+
console.error('UCN MCP server failed to start:', e);
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
});
|