Every tool returns compact path:line text designed for token economy. Outputs below are
real (trimmed) results from indexing gson,
ripgrep, django,
and godot-demo-projects.
Symbols carry stable ids (#N) usable wherever a tool accepts symbol_id. A trailing * marks a
symbol that is not exported. A symbol line is its own declaration with the qualified name spliced in
(function renderTree(nodes: Map<string, DirNode>): string[] (src/tools/repomap.ts:170) #1482),
falling back to kind name — signature when the signature does not declare the name.
Provenance is explicit everywhere, but stated once where it is shared rather than repeated on every
row. When most rows of an answer come from the same source at the same confidence, a header line
says so — 49 usage(s): resolved 0.90 unless a row says otherwise, edges: all lsp 1.00 — and only
the rows that differ carry their own tag. That header is repeated on every page of a paginated
answer, so provenance never gets lost in a continuation. Where rows genuinely disagree, each keeps
its own (lsp) / (resolved 0.85) / [index 0.70].
List-shaped tools accept max_tokens (default 2500) and a cursor. When output is clamped, the
footer carries a continuation cursor — pass it back with the same arguments to get the next page
instead of re-asking with a bigger budget:
… 312 of 480 lines omitted at ~2500 tokens
continue: cursor="c2VhcmNoX3N5bWJvbHN8MTY4fDQ4MHwzYTlmMmJ8ZDRlMTA3" (or raise max_tokens)
Cursors are stateless, so a restarted server still honours one. A cursor from a different tool, or
replayed against different arguments, restarts from the beginning and says so rather than erroring.
When a list of symbols does not fit, signatures are dropped from every row before any row is
dropped — N complete results beat 0.6N annotated ones. batch_symbols resolves up to 50 #N ids in
one call.
First call in an unfamiliar repo. The annotated architecture map: directory tree with per-directory
file/symbol counts, dominant language, role, size and import fan-in/fan-out, plus entry points, the
highest-fan-in symbols with the reason each ranks, and the third-party dependency inventory. One
budgeted call in place of a grep sweep. path_prefix + depth zoom; sections picks one part.
Trimmed, from Unreal Engine's Engine/Source/Runtime (19,062 files, 622k symbols — 2.5 s cold,
160 ms warm, ~2,400 tokens):
repo_map .../Engine/Source/Runtime
19062 files · 622,000 symbols · cpp 98%, c_sharp 2% · index ready
ranking: structural — distinct-file fan-in over resolved imports and call/type edges; heuristic, not exact
ref counts omitted: 8,972,887 occurrences over the 2M guard — ranking uses edges + imports only
--- entry points (5) ---
Launch/Private/Linux/LaunchLinux.cpp:14 #400284 [main-symbol 0.90] cpp main
--- tree (depth 2; counts are subtree totals, in/out = distinct sibling dirs) ---
Core/Public/ 1290 files 43679 sym cpp in 445 out 8 9.8 MB
CoreUObject/Public/ 476 files 20275 sym cpp in 193 out 6 4.3 MB
Engine/Public/ 1303 files 46449 sym cpp in 126 out 46 7.7 MB
(+594 more directories at depth 2 of 634 total — pass path_prefix to zoom)
foundation (imported, imports nothing local): AutoRTFM/Public · CorePreciseFP/Internal
mutual imports: Core/Public <-> TraceLog/Public · Core/Public <-> CoreUObject/Public
--- key symbols (top 25 by structural importance) ---
method MoveTemp (Core/Public/Templates/UnrealTemplate.h:530) #107934 called from 2248 files · file imported by 366 · exported
function EnumHasAnyFlags (Core/Public/Misc/EnumClassFlags.h:37) #96686 called from 545 files · file imported by 200 · exported
Ranking is a composite (call fan-in counted in distinct files, type fan-in, module import fan-in,
exportedness, entry/route membership, docs, minus a test-file penalty) rather than PageRank, which on a
call graph concentrates on sinks like log and toString. Known biases — ubiquitous helpers outranking
domain types, languages whose resolver emits few edges ranking low, reflection and DI being invisible —
are documented in src/analysis/importance.ts and worked around with path_prefix.
First call of a session — languages, sizes, engines, freshness.
workspace: D:\dev\godot-demos
index: ready
totals: 492 files, 4719 symbols, 216 imports, 47205 occurrences, 1136 graph edges
languages:
gdscript: 456 files, 4280 symbols
engine assets:
godot: 138 project, 136 resource, 392 scene
search_content — {query, regex?, case_sensitive?, path_prefix?, ext?, max_per_file?, limit?, offset?}
Literal text, for everything that is not a definition name: string literals, error messages, env var
names, config keys, TODO comments. It is the only tool that sees non-code files — .md, .json,
.yaml, .toml, .sql, Dockerfile, CI workflows — so it answers "where is this config key set?"
and "which manifest names this handler?".
The index holds no file text: an FTS match proposes candidate files and each one is then read from disk and scanned for real. A stale index row therefore costs one wasted read, never a wrong line. The header says which tier answered, because they differ in how much a miss means:
"DATABASE_URL": 3 hit(s) in 2 file(s) (indexed candidates, verified on disk)
src/net/client.ts (2)
14: const url = process.env.DATABASE_URL;
88: // TODO: honour DATABASE_URL from the environment
config/retry.yaml (1)
3: url: postgres://x
Tiers: indexed (whole-word match) → prefix match (an unfinished last word) → direct scan, used
for regex=true, for queries with no indexable words (->), or when the index found nothing and a
fragment inside a longer identifier is still possible. A scan is budgeted and reports its coverage
rather than quietly returning "no matches". Lock files and minified blobs are never indexed.
Name/doc search (FTS + fuzzy). search_symbols {query: "Player", lang: "gdscript", kind: "class"}:
class Player — class_name Player (2d/physics_platformer/player/player.gd:1) #318
class Player — class_name Player (2d/platformer/player/player.gd:1) #802
Unreal Blueprints are searchable too — lang: "uasset" scopes to binary editor packages, and the
blueprint_function / event_dispatcher kinds are Blueprint-only:
search_symbols {query: "Damage", lang: "uasset", kind: "blueprint_function"}
blueprint_function BP_Player.ApplyDamage (Content/Player/BP_Player.uasset:12) #4471
event_dispatcher BP_Player.OnDied (Content/Player/BP_Player.uasset:31) #4488
Describe behavior in plain language; hybrid embedding+keyword ranking, fully local.
semantic_search {query: "where are uploaded files validated for malicious names"} on django:
[vec cos=0.73] function validate_file_name — def validate_file_name(name, allow_relative_path=False) (django/core/files/utils.py:7) #7588
[vec cos=0.67] method Storage.get_valid_name — def get_valid_name(self, name): (django/core/files/storage/base.py:60) #7451
First-ever call downloads the model (~150 MB, one-time); until coverage completes results are keyword-weighted and say so.
Read a file's structure without reading the file.
src/math.ts (typescript, 12 symbols)
5: function add(a: number, b: number): number #3
20: class Circle #7
26: method area(): number #9
Over budget it sheds nesting depth rather than tail lines, so every top-level symbol survives and
the shape stays honest — tail-truncating an outline returns the first 40% of a file and looks
complete. src/db/store.ts (168 symbols, ~3.9k tokens unbudgeted) at the default budget:
src/db/store.ts (typescript, 168 symbols)
depth limited to 0 to fit ~2500 tokens — 150 nested members collapsed; raise max_tokens or pass max_depth for them
48: class Store #177 (+79 members)
1379: interface SymbolLite #265 (+11 members)
1459: function normalize(rows: SymbolRow[]): SymbolRow[] [private] #327
Pass a directory instead of a file to get its public API surface in one call rather than one call
per file (exported_only defaults to true for directories, false for files; recursive for subdirs).
Works on Unreal .uasset/.umap too — Blueprint members are indexed as symbols at synthetic line
numbers, so the outline is the asset's member list. There is no text to read back: source-showing
tools return binary asset — open <path> in Unreal Editor instead of a snippet.
Signature, docs, container, LSP hover when available, optional bounded source snippet.
LSP-exact with index fallback:
definition of JsonReader:
gson/src/main/java/com/google/gson/stream/JsonReader.java:211:13 (lsp)
Raw tree-sitter S-expressions for structural searches regex can't express:
(call_expression function: (identifier) @fn (#eq? @fn "eval")).
LSP-first, provenance-tagged, resolved usages before name-match candidates.
callers of constructor Gson.Gson (…/Gson.java:247) #214
method create() : Gson (…/GsonBuilder.java:922) #325 [lsp 1.00]
method setUp() : void (metrics/src/…/SerializationBenchmark.java:42) #4879 [lsp 1.00]
Extends/implements edges in either direction.
File import graph: what this file imports / who imports it.
Shortest call chain: how does the request handler reach the DB layer?
call path (2 hops):
function report (src/report.ts:12) -> function calculate (src/calc.ts:8) -> function add (src/math.ts:5)
change_impact — {symbol_id | path+line | name | files[] | (no args = git diff), max_depth?, min_confidence?, tests_only?}
Blast radius of a change: reverse BFS over call/type edges plus transitive import reachability, affected TEST files first. With no arguments it shells out to git and analyzes the uncommitted diff — hunk-level, so only symbols you actually touched seed the traversal (untracked files seed whole, deleted files seed their importers). Warns when the index is stale against the working tree. Route handlers in the result carry their route.
seeds: git: 2 modified, 1 untracked
impact: 14 files affected (5 TEST) within depth 6, 41 symbols via call/type edges
TEST test/resolver.test.ts — via calls→resolveWorkspace [index 0.81] at resolveScope (depth 2)
TEST test/store.test.ts — via import chain (depth 1)
src/indexer/indexer.ts — via calls→replaceFile [index 0.95] at Indexer.indexOne (depth 1)
src/tools/routes.ts — via calls→listRoutes [index 0.90] at handler (depth 2) [ROUTE GET /users/:id]
Web-framework routes (Express, Fastify, NestJS, FastAPI, Flask, Django) with handler symbols:
GET /items/{item_id} → function read_item — def read_item(item_id: int) (api/items.py:6) #212
GET /users → function listUsers (api/users.ts:1) #37
USE /api → "apiRouter" (unresolved) (api/server.ts:6)
Which code serves this URL? Matches a concrete path against indexed patterns —
:id, {id}, <int:pk> segments are wildcards; find_route {url: "GET /items/42"} returns
the read_item line above. Handler #ids chain into call_hierarchy and change_impact.
The graph tools rendered as Mermaid instead of text — the output is a ```mermaid fence ready to
paste into GitHub markdown, docs, or any Mermaid viewer. imports draws the workspace import graph
(file-level with directory subgraphs, or collapsed to directories via granularity; scope with
path_prefix); calls draws the call graph around one symbol (direction: in|out|both, depth);
types draws inheritance around one type; call_path draws the shortest call chain between two
symbols. Dotted arrows mark low-confidence structural edges (< 0.70).
call graph around function calculate (src/calculator.ts:4), arrows caller -> callee:
```mermaid
flowchart LR
n0["calculate"]
n1["report"]
n2["add"]
n1 --> n0
n0 --> n2
classDef focus stroke-width:3px
class n0 focus
```
One call, one briefing: header + signature + docs, definition source, the rest of the file's
outline, type context (LSP hover if a server is already running), top callers and callees,
route (if a handler), related tests, and — given a task string — possibly relevant symbols.
Sections are included in priority order until the token budget (default 4000) is spent; what
didn't fit is named in an omitted: footer so you can ask for it specifically.
Post-edit structural check of the uncommitted working tree against git HEAD (the HEAD blob is re-extracted per changed file — no snapshots). Waits for the index to catch up first.
verified 3 changed file(s) against HEAD: 1 broken, 1 to check, 0 informational
BROKEN src/lib.ts: exported function add was removed and is still referenced — src/calc.ts:1 (import), src/calc.ts:3 (call)
CHECK src/api.ts: signature of function fetchUser changed
old: function fetchUser(id: string): User
new: function fetchUser(id: string, opts: FetchOpts): User
callers: loadProfile (src/profile.ts:14)
change_impact predicts breakage before an edit; this confirms what actually broke after.
Which tests exercise this symbol? Reverse walk over call/type edges to test files (names the test-case symbol, its depth, and the edge route), plus test files that only reach it through imports, reported separately as a weaker signal.
"Does a helper for this already exist?" Cosine over the local embedding vectors when ready
([cos 0.91]), degrading to token-shingle text similarity ([jaccard 0.42], labeled as the
weaker signal) while embedding coverage builds.
Compact one-line info (plus optional 20-line snippets) for up to 50 ids — resolve a screenful
of #N references from call_hierarchy / change_impact / find_dead_code output in one call.
Symbols with zero incoming references after excluding entry points, route handlers, and
engine/framework lifecycle callbacks. Every claim is hedged: dead (high confidence) means no
same-name usage exists anywhere; possibly dead — N same-name usage(s) flags dynamic-dispatch
risk. Test-only helpers and internal-only exports are bucketed separately. A structural index
cannot see reflection, DI, or external consumers — treat this as a review list.
Churn-ranked files from one git log --numstat pass: score = commits × log2(lines).
hotspots over the last 90 days (30 commits scanned):
src/db/store.ts 12 commits +1165/-15 ~1262 lines score 124
src/tools/register.ts 14 commits +406/-54 ~354 lines score 119
Answered from the cached commit history when it is warm, with a live git log fallback.
Which files historically change together with this one — the coupling a structural index cannot
see. Directional: →other is "when I touch this, that changes N% of the time", →this is the
reverse, and the asymmetry is the signal. Commits touching more than 50 files are excluded as
refactors. Run on this repo:
change coupling for src/tools/register.ts
19 commits touched it in the last 12 months (1 commits over 50 files excluded as refactors)
file together →other →this import edge
test/mcp.test.ts 14 74% 88% none ←
src/db/store.ts 13 68% 65% none ←
README.md 10 53% 67% none ←
src/tools/graph.ts 8 42% 100% yes
← no import edge in either direction: the structural graph cannot see this coupling
(support >= 2 commits; co-change is correlation — release and formatting commits couple unrelated files)
The top hit is the point: test/mcp.test.ts asserts the exact tool list, so it changes with
register.ts 88% of the time, and no import edge exists between them. Same for README.md.
Neither would ever appear in change_impact.
Why does this look like this, and who do I ask? git log -L over the symbol's line span:
history of function estimateTokens (src/tools/tokens.ts:27-43) #1466
git log -L 27,43 — follows the code as it moves within the file
8d36a8b0 2026-07-25 Lusiem Token primitives: structural estimator, resumable cursors, depth-collapsed outlines
0175849d 2026-07-16 Lusiem Token budgets: estimator, shared clamp, max_tokens on list-shaped tools
2 commit(s) shown, 1 author(s), 2026-07-16 to 2026-07-25
Line-range history is capped at 10 s; on a deep history it falls back to file-level commits and says so.
Unreal .uasset/.umap packages are parsed natively — no Unreal installation needed. Uncooked
(editor) assets are the target; cooked packages are detected and skipped with a recorded reason.
Blueprint functions, event dispatchers, and member variables become searchable symbols, and
Blueprint→C++ links (parent classes, called UFUNCTIONs) become confidence-scored
provenance: engine edges, so type_hierarchy, call_hierarchy, and change_impact cross the
Blueprint/native boundary.
Godot scene tree with scripts, instanced sub-scenes, and signal connections (handlers resolved to symbols):
2d/pong/pong.tscn (godot scene, 21 nodes)
Pong (Node2D)
Left (Area2D) script=res://logic/paddle.gd
Ball (Area2D) script=res://logic/ball.gd
connections:
area_entered: LeftWall -> LeftWall :: _on_wall_area_entered (2d/pong/logic/wall.gd:3 #839)
Reverse lookup across assets — a script path, res:// path, Unity-mapped path (GUIDs resolved
via .meta), handler method, or Unreal module name:
2d/pong/pong.tscn (godot scene) script: res://logic/paddle.gd — Pong/Left
Assets/Player.prefab (unity prefab) script: Assets/PlayerController.cs (guid aaaa1111…) — Player
Unreal Blueprints answer the same question. Content/**.uasset is mapped onto the /Game/ mount
per .uproject, so either spelling of the target works, and reference kinds cover the Blueprint
graph: parent_class (what it derives from), call_function (native UFUNCTIONs it calls),
handles_event (who implements ReceiveBeginPlay and friends), and asset (hard object refs).
find_asset_references {target: "ReceiveBeginPlay"}:
Content/Player/BP_Player.uasset (unreal uasset) handles_event: ReceiveBeginPlay — EventGraph
Content/AI/BP_Minion.uasset (unreal uasset) handles_event: ReceiveBeginPlay — EventGraph
Engine reflection markers: UPROPERTY, BlueprintCallable, [SerializeField], @export, signal.
Unreal macros (multi-line specifiers included) are captured onto the annotated symbol at index
time, so answers come from the index with symbol ids:
Source/Game/MyActor.h:19 UFUNCTION(BlueprintCallable, Category = "Combat") void Fire(); #212
2d/dodge_the_creeps/player.gd:5 [gdscript] variable speed: @export var speed = 400 #64
Index freshness, per-language LSP server state, embedding coverage, watcher health;
reindex forces a hash-checked re-scan.