ucn 3.4.3 → 3.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -24
- package/cli/index.js +7 -1
- package/core/discovery.js +2 -1
- package/core/project.js +9 -11
- package/languages/java.js +1 -1
- package/languages/rust.js +1 -1
- package/mcp/server.js +1566 -0
- package/package.json +7 -2
- package/test/mcp-edge-cases.js +453 -0
- package/test/parser.test.js +158 -0
package/README.md
CHANGED
|
@@ -134,6 +134,27 @@ EXTERNAL:
|
|
|
134
134
|
fs, path, crypto
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
## Workflows
|
|
138
|
+
|
|
139
|
+
**Investigating a bug:**
|
|
140
|
+
```bash
|
|
141
|
+
ucn about problematic_function # Understand it fully
|
|
142
|
+
ucn trace problematic_function --depth=2 # See what it calls
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Before modifying a function:**
|
|
146
|
+
```bash
|
|
147
|
+
ucn impact the_function # Who will break?
|
|
148
|
+
ucn smart the_function # See it + its helpers
|
|
149
|
+
# ... make your changes ...
|
|
150
|
+
ucn verify the_function # Did all call sites survive?
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Periodic cleanup:**
|
|
154
|
+
```bash
|
|
155
|
+
ucn deadcode --exclude=test # What can be deleted?
|
|
156
|
+
ucn toc # Project overview
|
|
157
|
+
```
|
|
137
158
|
## Supported Languages
|
|
138
159
|
|
|
139
160
|
JavaScript, TypeScript, Python, Go, Rust, Java
|
|
@@ -144,9 +165,88 @@ JavaScript, TypeScript, Python, Go, Rust, Java
|
|
|
144
165
|
npm install -g ucn
|
|
145
166
|
```
|
|
146
167
|
|
|
147
|
-
###
|
|
168
|
+
### MCP Server
|
|
169
|
+
|
|
170
|
+
UCN includes a built-in [MCP](https://modelcontextprotocol.io) server, so any MCP-compatible AI client can use it as a tool.
|
|
171
|
+
|
|
172
|
+
**Claude Code** (`~/.claude/mcp-config.json`):
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"mcpServers": {
|
|
176
|
+
"ucn": {
|
|
177
|
+
"command": "npx",
|
|
178
|
+
"args": ["-y", "ucn", "--mcp"]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Claude Desktop** (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"mcpServers": {
|
|
188
|
+
"ucn": {
|
|
189
|
+
"command": "npx",
|
|
190
|
+
"args": ["-y", "ucn", "--mcp"]
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json` in project):
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"mcpServers": {
|
|
200
|
+
"ucn": {
|
|
201
|
+
"command": "npx",
|
|
202
|
+
"args": ["-y", "ucn", "--mcp"]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Windsurf** (`~/.codeium/windsurf/mcp_config.json`):
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"mcpServers": {
|
|
212
|
+
"ucn": {
|
|
213
|
+
"command": "npx",
|
|
214
|
+
"args": ["-y", "ucn", "--mcp"]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**VS Code Copilot** (`.vscode/mcp.json`):
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"servers": {
|
|
224
|
+
"ucn": {
|
|
225
|
+
"type": "stdio",
|
|
226
|
+
"command": "npx",
|
|
227
|
+
"args": ["-y", "ucn", "--mcp"]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Zed** (Settings > `settings.json`):
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"context_servers": {
|
|
237
|
+
"ucn": {
|
|
238
|
+
"command": "npx",
|
|
239
|
+
"args": ["-y", "ucn", "--mcp"]
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The MCP server exposes 27 tools: `ucn_about`, `ucn_context`, `ucn_impact`, `ucn_smart`, `ucn_trace`, `ucn_find`, `ucn_usages`, `ucn_toc`, `ucn_deadcode`, `ucn_fn`, `ucn_class`, `ucn_verify`, `ucn_imports`, `ucn_exporters`, `ucn_tests`, `ucn_related`, `ucn_graph`, `ucn_file_exports`, `ucn_search`, `ucn_plan`, `ucn_typedef`, `ucn_stacktrace`, `ucn_example`, `ucn_expand`, `ucn_lines`, `ucn_api`, `ucn_stats`.
|
|
148
246
|
|
|
149
|
-
|
|
247
|
+
### Claude Code Skill (alternative)
|
|
248
|
+
|
|
249
|
+
To use UCN as a skill in Claude Code (alternative to MCP):
|
|
150
250
|
|
|
151
251
|
```bash
|
|
152
252
|
mkdir -p ~/.claude/skills
|
|
@@ -257,6 +357,7 @@ Common Flags:
|
|
|
257
357
|
--clear-cache Clear cache before running
|
|
258
358
|
--no-follow-symlinks Don't follow symbolic links
|
|
259
359
|
-i, --interactive Keep index in memory for multiple queries
|
|
360
|
+
--mcp Start as MCP server (stdio transport)
|
|
260
361
|
|
|
261
362
|
Quick Start:
|
|
262
363
|
ucn toc # See project structure (compact)
|
|
@@ -267,28 +368,6 @@ Quick Start:
|
|
|
267
368
|
ucn --interactive # Multiple queries
|
|
268
369
|
```
|
|
269
370
|
|
|
270
|
-
## Workflows
|
|
271
|
-
|
|
272
|
-
**Investigating a bug:**
|
|
273
|
-
```bash
|
|
274
|
-
ucn about problematic_function # Understand it fully
|
|
275
|
-
ucn trace problematic_function --depth=2 # See what it calls
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
**Before modifying a function:**
|
|
279
|
-
```bash
|
|
280
|
-
ucn impact the_function # Who will break?
|
|
281
|
-
ucn smart the_function # See it + its helpers
|
|
282
|
-
# ... make your changes ...
|
|
283
|
-
ucn verify the_function # Did all call sites survive?
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
**Periodic cleanup:**
|
|
287
|
-
```bash
|
|
288
|
-
ucn deadcode --exclude=test # What can be deleted?
|
|
289
|
-
ucn toc # Project overview
|
|
290
|
-
```
|
|
291
|
-
|
|
292
371
|
## License
|
|
293
372
|
|
|
294
373
|
MIT
|
package/cli/index.js
CHANGED
|
@@ -22,6 +22,10 @@ const output = require('../core/output');
|
|
|
22
22
|
|
|
23
23
|
const rawArgs = process.argv.slice(2);
|
|
24
24
|
|
|
25
|
+
// MCP server mode — launch server and skip CLI
|
|
26
|
+
if (rawArgs.includes('--mcp')) {
|
|
27
|
+
require('../mcp/server.js');
|
|
28
|
+
} else {
|
|
25
29
|
// Support -- to separate flags from positional arguments
|
|
26
30
|
const doubleDashIdx = rawArgs.indexOf('--');
|
|
27
31
|
const args = doubleDashIdx === -1 ? rawArgs : rawArgs.slice(0, doubleDashIdx);
|
|
@@ -78,7 +82,7 @@ if (fileArgIdx !== -1 && args[fileArgIdx + 1]) {
|
|
|
78
82
|
|
|
79
83
|
// Known flags for validation
|
|
80
84
|
const knownFlags = new Set([
|
|
81
|
-
'--help', '-h',
|
|
85
|
+
'--help', '-h', '--mcp',
|
|
82
86
|
'--json', '--verbose', '--no-quiet', '--quiet',
|
|
83
87
|
'--code-only', '--with-types', '--top-level', '--exact',
|
|
84
88
|
'--no-cache', '--clear-cache', '--include-tests',
|
|
@@ -2542,3 +2546,5 @@ if (flags.interactive) {
|
|
|
2542
2546
|
} else {
|
|
2543
2547
|
main();
|
|
2544
2548
|
}
|
|
2549
|
+
|
|
2550
|
+
} // end of --mcp else block
|
package/core/discovery.js
CHANGED
|
@@ -298,7 +298,8 @@ function shouldIgnore(name, ignores, parentDir) {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
// Check conditional ignores (only if parentDir provided)
|
|
301
|
-
|
|
301
|
+
// Use Array.isArray to avoid matching Object.prototype properties (e.g. dir named "constructor")
|
|
302
|
+
if (parentDir && Array.isArray(CONDITIONAL_IGNORES[name])) {
|
|
302
303
|
const markers = CONDITIONAL_IGNORES[name];
|
|
303
304
|
for (const marker of markers) {
|
|
304
305
|
if (fs.existsSync(path.join(parentDir, marker))) {
|
package/core/project.js
CHANGED
|
@@ -1021,8 +1021,8 @@ class ProjectIndex {
|
|
|
1021
1021
|
const matchesDef = definitions.some(d => d.className === targetClass);
|
|
1022
1022
|
if (!matchesDef) continue;
|
|
1023
1023
|
// Falls through to add as caller
|
|
1024
|
-
} else if (
|
|
1025
|
-
// self.method() / cls.method() — resolve to same-class method
|
|
1024
|
+
} else if (['self', 'cls', 'this'].includes(call.receiver)) {
|
|
1025
|
+
// self.method() / cls.method() / this.method() — resolve to same-class method
|
|
1026
1026
|
const callerSymbol = this.findEnclosingFunction(filePath, call.line, true);
|
|
1027
1027
|
if (!callerSymbol?.className) continue;
|
|
1028
1028
|
// Check if any definition of searched function belongs to caller's class
|
|
@@ -1030,8 +1030,6 @@ class ProjectIndex {
|
|
|
1030
1030
|
if (!matchesDef) continue;
|
|
1031
1031
|
// Falls through to add as caller
|
|
1032
1032
|
} else {
|
|
1033
|
-
// Always skip this/self/cls calls (internal state access, not function calls)
|
|
1034
|
-
if (['this', 'self', 'cls'].includes(call.receiver)) continue;
|
|
1035
1033
|
// Go doesn't use this/self/cls - always include Go method calls
|
|
1036
1034
|
// For other languages, skip method calls unless explicitly requested
|
|
1037
1035
|
if (fileEntry.language !== 'go' && !options.includeMethods) continue;
|
|
@@ -1143,14 +1141,14 @@ class ProjectIndex {
|
|
|
1143
1141
|
|
|
1144
1142
|
// Smart method call handling:
|
|
1145
1143
|
// - Go: include all method calls (Go doesn't use this/self/cls)
|
|
1146
|
-
// -
|
|
1144
|
+
// - self/this.method(): resolve to same-class method (handled below)
|
|
1147
1145
|
// - Python self.attr.method(): resolve via selfAttribute (handled below)
|
|
1148
1146
|
// - Other languages: skip method calls unless explicitly requested
|
|
1149
1147
|
if (call.isMethod) {
|
|
1150
1148
|
if (call.selfAttribute && language === 'python') {
|
|
1151
1149
|
// Will be resolved in second pass below
|
|
1152
|
-
} else if (
|
|
1153
|
-
// self.method() / cls.method() — resolve to same-class method below
|
|
1150
|
+
} else if (['self', 'cls', 'this'].includes(call.receiver)) {
|
|
1151
|
+
// self.method() / cls.method() / this.method() — resolve to same-class method below
|
|
1154
1152
|
} else if (language !== 'go' && !options.includeMethods) {
|
|
1155
1153
|
continue;
|
|
1156
1154
|
}
|
|
@@ -1166,8 +1164,8 @@ class ProjectIndex {
|
|
|
1166
1164
|
continue;
|
|
1167
1165
|
}
|
|
1168
1166
|
|
|
1169
|
-
// Collect
|
|
1170
|
-
if (
|
|
1167
|
+
// Collect self/this.method() calls for same-class resolution
|
|
1168
|
+
if (call.isMethod && ['self', 'cls', 'this'].includes(call.receiver)) {
|
|
1171
1169
|
if (!selfMethodCalls) selfMethodCalls = [];
|
|
1172
1170
|
selfMethodCalls.push(call);
|
|
1173
1171
|
continue;
|
|
@@ -1255,7 +1253,7 @@ class ProjectIndex {
|
|
|
1255
1253
|
}
|
|
1256
1254
|
}
|
|
1257
1255
|
|
|
1258
|
-
// Third pass: resolve
|
|
1256
|
+
// Third pass: resolve self/this.method() calls to same-class methods
|
|
1259
1257
|
if (selfMethodCalls && def.className) {
|
|
1260
1258
|
for (const call of selfMethodCalls) {
|
|
1261
1259
|
const symbols = this.symbols.get(call.name);
|
|
@@ -1590,7 +1588,7 @@ class ProjectIndex {
|
|
|
1590
1588
|
const fileEntry = this.files.get(filePath);
|
|
1591
1589
|
if (!fileEntry) return null;
|
|
1592
1590
|
|
|
1593
|
-
const nonCallableTypes = new Set(['class', 'struct', 'interface', 'type', 'state']);
|
|
1591
|
+
const nonCallableTypes = new Set(['class', 'struct', 'interface', 'type', 'state', 'impl']);
|
|
1594
1592
|
for (const symbol of fileEntry.symbols) {
|
|
1595
1593
|
if (!nonCallableTypes.has(symbol.type) &&
|
|
1596
1594
|
symbol.startLine <= lineNum &&
|
package/languages/java.js
CHANGED
|
@@ -567,7 +567,7 @@ function findCallsInCode(code, parser) {
|
|
|
567
567
|
name: nameNode.text,
|
|
568
568
|
line: node.startPosition.row + 1,
|
|
569
569
|
isMethod: !!objNode,
|
|
570
|
-
receiver: objNode?.type === 'identifier' ? objNode.text : undefined,
|
|
570
|
+
receiver: (objNode?.type === 'identifier' || objNode?.type === 'this') ? objNode.text : undefined,
|
|
571
571
|
enclosingFunction
|
|
572
572
|
});
|
|
573
573
|
}
|
package/languages/rust.js
CHANGED
|
@@ -622,7 +622,7 @@ function findCallsInCode(code, parser) {
|
|
|
622
622
|
name: fieldNode.text,
|
|
623
623
|
line: node.startPosition.row + 1,
|
|
624
624
|
isMethod: true,
|
|
625
|
-
receiver: valueNode?.type === 'identifier' ? valueNode.text : undefined,
|
|
625
|
+
receiver: (valueNode?.type === 'identifier' || valueNode?.type === 'self') ? valueNode.text : undefined,
|
|
626
626
|
enclosingFunction
|
|
627
627
|
});
|
|
628
628
|
}
|