Labsco
mleoca logo

ucn

โ˜… 1

from mleoca

Universal Code Navigator - a lightweight MCP server that gives AI agents call-graph-level understanding of code. Instead of reading entire files, agents ask structural questions like: "who calls this function", "what breaks if I change it", "what's unused", and get precise, AST-verified answers. UCN parses JS/TS, Python, Go, Rust, Java, and HTML inline scripts with tree-sitter, then exposes 28 navigation commands as a CLI tool, MCP server, or agent skill.

๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅโœ“ VerifiedFreeAdvanced setup

UCN - Universal Code Navigator

See what code does before you touch it.

Find symbols, trace callers, check impact, pick the right tests, extract code and spot what's dead - from the terminal.

All commands, one engine, three surfaces:

  Terminal              AI Agents           Agent Skills
       โ”‚                    โ”‚                    โ”‚
      CLI                  MCP                 Skill
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                            โ”‚
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚ UCN Engine  โ”‚
                     โ”‚  commands   โ”‚
                     โ”‚ tree-sitter โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Supports JavaScript, TypeScript, Python, Go, Rust, Java, and HTML inline scripts.

If you work with AI, add UCN as a Skill or MCP and let the agent ask better code questions instead of reading whole files. All commands ship as a single tool.

UCN is deliberately lightweight:

  • No background processes - parses on demand, answers, exits
  • No language servers - tree-sitter does the parsing, no compilation needed
  • MCP is optional - only needed if you connect UCN to an AI agent, the CLI and Skill work on their own

And it's built to be trusted. grep hands you raw text matches to verify yourself; UCN splits every "who calls this?" into what it can prove and what it can't โ€” each flagged with a reason, nothing silently dropped. That claim is measured, not promised: CI re-derives UCN's answers from the real compilers and language servers (ts-morph, pyright, gopls, rust-analyzer, jdtls) across nineteen production repos โ€” 96.6โ€“100% confirmed-tier precision on eighteen of them, zero unexplained call edges, or the build fails.


npm install -g ucn             # Node.js 20+

ucn orient                     # first look at any repo: size, hot spots, trust
ucn trace main --depth=3       # full execution flow
ucn about handleRequest        # definition + callers + callees + tests
ucn impact handleRequest       # every call site with arguments
ucn deadcode --exclude=test    # unused code, AST-verified

"What happens when build() runs?"

$ ucn trace build --depth=2

build
โ”œโ”€โ”€ detectProjectPattern (core/discovery.js:450) 1x
โ”œโ”€โ”€ parseGitignore (core/discovery.js:131) 1x
โ”œโ”€โ”€ expandGlob (core/discovery.js:199) 1x
โ”‚   โ”œโ”€โ”€ parseGlobPattern (core/discovery.js:238) 1x
โ”‚   โ”œโ”€โ”€ walkDir (core/discovery.js:295) 1x
โ”‚   โ””โ”€โ”€ compareNames (core/discovery.js:178) 1x
โ”œโ”€โ”€ parallelBuild (core/parallel-build.js:25) 1x
โ”œโ”€โ”€ indexFile (core/project.js:397) 1x
โ”‚   โ”œโ”€โ”€ addSymbol (core/project.js:502) 4x
โ”‚   โ”œโ”€โ”€ detectLanguage (languages/index.js:344) 1x
โ”‚   โ”œโ”€โ”€ parse (core/parser.js:69) 1x
โ”‚   โ”œโ”€โ”€ extractImports (core/imports.js:19) 1x
โ”‚   โ””โ”€โ”€ extractExports (core/imports.js:44) 1x
โ”œโ”€โ”€ buildImportGraph (core/project.js:798) 1x
โ””โ”€โ”€ buildInheritanceGraph (core/project.js:803) 1x
    โ€ฆ calls UCN can't prove a receiver for (arr.push(), obj.get()) show as
      [unverified] leaves โ€” abridged here

CALLEE ACCOUNT: 26 nodes expanded ยท 394 call sites = 61 confirmed + 162 unverified (162 uncertain-receiver) + 68 external/builtin + 103 excluded

One command, no files opened โ€” and the CALLEE ACCOUNT: line proves all 394 calls were sorted, nothing dropped.


Understand code you didn't write

ucn about gives you everything about a function in one shot - who calls it, what it calls, which tests cover it, and the source code.

$ ucn about expandGlob

expandGlob (function)
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
core/discovery.js:199-233  โ†’  core/discovery.js:199:expandGlob
expandGlob (pattern: string, options: number = {}) : string[]

USAGES: 8 total
  3 calls, 3 imports, 2 references

CALLERS โ€” CONFIRMED (7, 3 prod + 4 test):
  evidence: scope-match (all)
  cli/index.js:1267 [runGlobCommand]
    const files = expandGlob(pattern);
  core/cache.js:548 [isCacheStale] [unreachable]
    const currentFiles = expandGlob(pattern, globOpts);
  core/project.js:257 [build]
    files = expandGlob(pattern, globOpts);
  test callers:
  test/integration.test.js:167
    const files = expandGlob('**/*.go', { root: tmpDir });
  ... (3 more test callers)

CALLEES (3):
  evidence: exact-binding (all)
  parseGlobPattern [utility] - core/discovery.js:238 (1x)
  walkDir [utility] {fs} - core/discovery.js:295 (1x)
  compareNames [utility] - core/discovery.js:178 (1x)

ACCOUNT: "expandGlob" occurs on 14 lines in 6 files: 7 confirmed, 0 unverified,
  7 non-call (4 import, 1 definition, 2 reference, 0 other-text), 0 other-target, 0 unaccounted

TESTS: 5 matches in 1 file(s)

Callers split into CONFIRMED (binding/receiver/import evidence โ€” prod first, then tests) and UNVERIFIED (found but unproven, each with a reason). The ACCOUNT: line reconciles every occurrence; 0 unaccounted means nothing was hidden. Tune with --min-confidence / --hide-confidence / --git; walk callers upward with ucn reverse-trace fn.

Answers you can trust

UCN doesn't just find a name โ€” it tells you how sure it is. Every answer from about, context, and impact partitions every place the name appears into buckets you can act on:

$ ucn impact saveCache

CALL SITES: 2 confirmed + 11 unverified

test/regression-go.test.js (2 calls)
  :2007
    saveCache(index, cachePath);

UNVERIFIED CALL SITES (11) โ€” call syntax, no binding/receiver evidence:
  core/project.js:2126: saveCache(cachePath) { ... } (call-not-resolved)
  mcp/server.js:810: try { index.saveCache(); } catch (_) ... (method-ambiguous)
  test/cache.test.js:1804: index.saveCache(); (method-ambiguous)
  ... (8 more)

ACCOUNT: "saveCache" occurs on 55 lines in 8 files: 2 confirmed, 11 unverified,
  11 non-call (2 import, 1 definition, 1 reference, 7 other-text), 31 other-target, 0 unaccounted

UCN sorts every one of the 55 places the name appears:

  • 2 confirmed โ€” call sites it can prove resolve to this saveCache.
  • 11 unverified โ€” real call sites it found but won't claim. index.saveCache() has an untyped receiver, so UCN can't prove which saveCache runs; it shows the site and the reason (method-ambiguous) instead of guessing.
  • 31 other-target โ€” occurrences that belong to a different saveCache, kept separate so they never pollute the answer.
  • 11 non-call โ€” imports, the definition, plain text.
  • 0 unaccounted โ€” the partition is complete. Nothing was dropped on the floor.

The payoff: a confirmed answer is safe to refactor against, and a clean zero โ€” no confirmed, no unverified, 0 unaccounted โ€” is a trustworthy zero (measured: 68 of 69 clean-zero samples across the pinned-repo board agree with the compiler oracles; the one disagreement is a new X() on an old-style constructor function, which lands in the non-call counts, still visible). One caveat before deleting anything: callers aren't the only usages โ€” if the NON-CALL counts are nonzero, run ucn usages to see what they are.

Measured against ground truth

This isn't a promise โ€” it's a gate. CI re-derives UCN's caller answers from real compilers and language servers and fails the build if a single true call edge is neither shown nor accounted for:

LanguageOracleConfirmed-tier precision
TypeScript / JavaScriptts-morph99.5โ€“100%
Pythonpyright (LSP)97.7โ€“99.9%
Gogopls98.8โ€“100%
Rustrust-analyzer99.3โ€“100%*
Javajdtls96.6โ€“100%

* clap reads 92.4% โ€” its callers live behind cargo feature gates the oracle compiles out; UCN sees the text, rust-analyzer doesn't.

Nineteen pinned real-world repos (zod, preact-signals, express, hono, zustand, fastify, httpx, rich, click, cobra, grpc-go, viper, chi, ripgrep, cursive, clap, gson, javapoet, jsoup), three sampling seeds, every run gated at missing-unexplained = 0 โ€” plus a weekly fresh-repo arm: two unpinned repos the engine was never tuned on, same gate. The tree commands โ€” trace, blast, reverse-trace, affected-tests โ€” follow the same rule: confirmed trunk, uncertain branches flagged (--expand-unverified to follow). Run ucn doctor for the trust report on your repo.

Change code without breaking things

Before touching a function, check if all existing call sites match its signature:

$ ucn verify expandGlob

Verification: expandGlob
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
core/discovery.js:191
expandGlob (pattern: string, options: number = {}) : string[]

Expected arguments: 1-2

STATUS: โœ“ All calls valid
  Total calls: 7
  Valid: 7
  Mismatches: 0
  Uncertain: 0
  Patterns: 4 in try, 4 in callback

The Patterns: line surfaces structural classification of each call site โ€” inLoop, inTry, inCallback, inTestCase, awaited โ€” so you can spot risky call sites (e.g., calls inside loops, missing await) at a glance. Same line appears on impact and inside about.

Then preview the refactoring. UCN shows exactly what needs to change and where:

$ ucn plan expandGlob --rename-to=expandGlobPattern

Refactoring plan: rename
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
core/discovery.js:191

SIGNATURE CHANGE:
  Before: expandGlob (pattern: string, options: number = {}) : string[]
  After:  expandGlobPattern (pattern: string, options: number = {}) : string[]

CHANGES NEEDED: 11
  Files affected: 4

BY FILE:

cli/index.js (2 changes)
  :1190
    const files = expandGlob(pattern);
    โ†’ Rename to: const files = expandGlobPattern(pattern);
  :15
    const { expandGlob, findProjectRoot } = require('../core/discovery');
    โ†’ Update import: const { expandGlobPattern, findProjectRoot } = require('../core/discovery');

... (more changes in core/cache.js, core/project.js, test/integration.test.js)

Run ucn diff-impact --staged before committing to see what you changed and who calls it.

Or wrap the same checks in a single command:

$ ucn check --staged

Pre-commit Check vs HEAD
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Changed: 3 functions
  parseFlags (cli/index.js:165) [MODIFIED]  2 callers
  ...

ucn check composes diff-impact + verify + affected-tests in one shot โ€” flags ADDED functions with no callers, signature drift across call sites, and recommends which tests to run.

Get the lay of the land in a new repo

One command answers "what is this codebase?" โ€” size and language mix, where the code lives, the most-called production functions, entry points, and how far to trust the index:

$ ucn orient

PROJECT ORIENTATION โ€” /path/to/project
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
169 files ยท 2111 symbols ยท javascript 67%, rust 8%, typescript 8%, java 7%, go 5%, python 5%

TOP DIRS (by symbols):
  core            516 symbols ยท 29 file(s)
  languages       282 symbols ยท 8 file(s)
  test            209 symbols ยท 29 file(s)
  core/output     142 symbols ยท 14 file(s)
  ...

HOT (most-called production functions, top 8 of 1028):
  execute โ€” 1124 call(s) ยท core/execute.js:1608
  ProjectIndex.build โ€” 340 call(s) ยท core/project.js:221
  getParser โ€” 150 call(s) ยท languages/index.js:312
  ...

ENTRY POINTS: 389 โ€” test 284, runtime 72, http 32, di 1
TRUST: MEDIUM โ€” 41 dynamic import(s), 13 eval, 6 reflection  (ucn doctor for detail)

Next: ucn about execute ยท ucn toc --detailed ยท ucn stats --hot --top=20 ยท ucn doctor --deep

Then drill in:

$ ucn brief fetch_user
fetch_user(user_id: int): dict
  svc.py:4-8  (5 lines)
  "Fetch a user from the API."
  async: no  |  side_effects: [fs, network, process]  |  complexity: branches=2, depth=2

brief is the lighter alternative to about โ€” typed signature, first sentence of the docstring, side-effect classification, and complexity, all in one screen. Pair with --git to see who last touched it and how often.

$ ucn doctor

UCN Trust Report โ€” /path/to/project
Index: 169 files, 2104 symbols
Languages: javascript (72%), typescript (14%), java (4%), python (4%), rust (4%), go (3%)
Cache: fresh, 344ms build
...
Trust level: HIGH

doctor reports how much UCN trusts the index โ€” file/symbol counts, blind spots (dynamic imports, eval, reflection), parse failures, and a verdict. Use --deep to also sample resolution coverage.

entrypoints lists detected framework handlers (HTTP routes, DI beans, jobs, tests):

ucn entrypoints --type=http --framework=spring   # narrow to one framework
ucn entrypoints --exclude-tests                  # tests are included by default

Find what to clean up

Which tests should you run after a change? affected-tests walks the blast radius and finds every test that touches the affected functions:

$ ucn affected-tests expandGlob

affected-tests: expandGlob
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
core/discovery.js:191
1 function changed โ†’ 15 functions affected (depth 3)

Test files to run (20):

  test/integration.test.js (covers: expandGlob, build, idx, setupProject)
    L47: index.build(null, { quiet: true });  [call]
    L167: const files = expandGlob('**/*.go', { root: tmpDir });  [call]
    ...

POSSIBLY AFFECTED (1) โ€” reachable only through unverified call edges:
  doctor

Uncovered (12): runGlobCommand, main, isCacheStale, runProjectCommand, runFileCommand, ...
  โš  These affected functions have no test references

Summary: 15 affected โ†’ 20 test files, 3/15 functions covered (20%) ยท 1 possibly affected (unverified chains)

The confirmed closure is what you run; POSSIBLY AFFECTED lists functions reached only through unverified edges โ€” extra tests worth a look, kept separate.

Find unused code

$ ucn deadcode --exclude=test        # run on ripgrep

Dead code: 8 unused symbol(s)

crates/globset/src/serde_impl.rs
  [  38-  42] Glob.deserialize (method)
  [  70-  74] GlobSet.deserialize (method)
crates/matcher/src/lib.rs
  [ 397- 399] Captures.as_match (method)
  [ 669- 678] Matcher.try_find_iter (method) [only self-references โ€” recursive]
  [ 796- 806] Matcher.try_captures_iter (method) [only self-references โ€” recursive]
  ...

921 exported symbol(s) excluded from the audit (public API may have external callers). Use --include-exported to audit them.

Classes, structs, traits, and enums are audited alongside functions. Symbols whose only call sites live inside their own definitions are claimed too, marked [only self-references โ€” recursive]. Deadcode claims are re-derived against compiler/LSP ground truth in CI โ€” a default-audit claim with an oracle-visible reference fails the build.

Find missing-await bugs:

ucn audit-async

Lists async calls inside async functions that lack await (JS/TS/Python).

Map your API surface across languages

UCN can match server routes to client requests across the supported languages โ€” Express/Fastify/Koa/NestJS/Next.js, Flask/FastAPI, Spring/JAX-RS, Go net/http (Gin/Echo/Chi/Fiber), axum/actix-web on the server side; fetch/axios, requests/httpx, RestTemplate/WebClient, reqwest on the client side.

ucn endpoints --bridge

# Filters
ucn endpoints --bridge --unmatched          # routes with no client / clients with no server
ucn endpoints --bridge --method=POST
ucn endpoints --bridge --prefix=/api

Match confidence: EXACT (literal-literal), PARTIAL (server param โ†” client literal), UNCERTAIN (template-literal client). Use --hide-uncertain to drop the noisy tier.

Extract without reading the whole file

$ ucn fn compareNames

core/discovery.js:170
[ 170- 178] compareNames(a, b)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function compareNames(a, b) {
    const aLower = a.toLowerCase();
    const bLower = b.toLowerCase();
    if (aLower < bLower) return -1;
    if (aLower > bLower) return 1;
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
}

Testing and reliability

  • Fast - uses incremental cache for optimal performance
  • Discipline - every bug fix gets a regression test, test code is ~3x the source
  • Coverage - every command, every supported language, every surface (CLI, MCP, interactive)
  • Systematic - a harness exercises all command and flag combinations against real multi-language fixtures
  • Test types - unit, integration, per-language regression, formatter, cache, MCP edge cases, architecture parity guards
  • Ground truth - caller accuracy is measured against ts-morph, pyright, gopls, rust-analyzer, and jdtls on 19 pinned real repos, gated on zero unexplained edges (see Answers you can trust)

Full help

Run ucn --help for the full command list and flags.