Skip to main content
Glama

corbel

Project status: maintenance mode. corbel is feature-complete as of 1.0.0. The API is stable and the tool remains installable.

In scope: dependency and toolchain changes that break the build; security advisories (cargo audit); clear malfunctions in existing functionality.

Out of scope: new features, new language support, performance work, accuracy work.

Best-effort only; no response time is promised. Feature request issues will be closed.

corbel is a local MCP server that performs static analysis to build a resolved call graph of your codebase, so a coding agent can ask "what calls this?" and "what breaks if I change this?" without guessing.

A corbel (/ˈkɔːrbəl/) is the bracket built into a wall that carries the load above it — corbel maps what carries what in your code. (Unrelated to the Microsoft font of the same name.)

The problem, in one real query

"What calls format_duration_unit?" — ripgrep and corbel, run against sharkdp/hyperfine at f12f3d9f (pinned so these numbers don't drift — clone it yourself to reproduce):

$ rg -n 'format_duration_unit\(' src/
src/output/format.rs:6:    let (duration_fmt, _) = format_duration_unit(duration, unit);
src/output/format.rs:11:pub fn format_duration_unit(duration: Second, unit: Option<Unit>) -> (String, Unit) {
src/output/format.rs:30:    let (out_str, out_unit) = format_duration_unit(1.3, None);
src/output/format.rs:35:    let (out_str, out_unit) = format_duration_unit(1.0, None);
...8 more lines, each a bare file:line with no indication of which function the call is inside

ripgrep finds every text occurrence of format_duration_unit( — 12 lines (the declaration plus 11 real calls), unlabeled. Telling which caller is which, and which are duplicates from the same test, means opening the file and counting by hand. corbel's get_symbol, called on the same function, resolves each hit to the function it's actually inside:

{
  "callers": [
    { "file": "src/benchmark/mod.rs", "line": 141, "name": "Benchmark::run", "resolution": "scoped" },
    { "file": "src/output/format.rs", "line": 5, "name": "format_duration", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 29, "name": "test_format_duration_unit_basic", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 29, "name": "test_format_duration_unit_basic", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 29, "name": "test_format_duration_unit_basic", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 29, "name": "test_format_duration_unit_basic", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 29, "name": "test_format_duration_unit_basic", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 29, "name": "test_format_duration_unit_basic", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 62, "name": "test_format_duration_unit_with_unit", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 62, "name": "test_format_duration_unit_with_unit", "resolution": "same-file" },
    { "file": "src/output/format.rs", "line": 62, "name": "test_format_duration_unit_with_unit", "resolution": "same-file" }
  ]
}

Six of those 11 calls are inside test_format_duration_unit_basic (one assertion per call), three inside test_format_duration_unit_with_unit — get_symbol tells you that directly; grep leaves you to work it out by reading the file. That's the gap corbel closes: not finding text, but naming the caller. This exact case (hyperfine-10 in the golden set below) is hand-verified — corbel's answer here matches ground truth exactly, precision and recall both 1.0.

Related MCP server: code-analyze-mcp

Install

corbel ships as a single static binary with no runtime dependencies, and your code never leaves the machine.

cargo install corbel

Pre-built binaries are produced by cargo-dist shell and PowerShell installers on tagged releases:

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/BETAER-08/corbel/releases/latest/download/corbel-installer.sh | sh
powershell -ExecutionPolicy ByPass -c "irm https://github.com/BETAER-08/corbel/releases/latest/download/corbel-installer.ps1 | iex"

Supported platforms (per dist-workspace.toml, each built and tested in CI): aarch64-apple-darwin, x86_64-apple-darwin, aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc.

Claude Code, in three steps

  1. Index the repo:

    corbel index .
  2. Register corbel as an MCP server:

    claude mcp add corbel -- corbel serve
  3. Ask a refactoring question in plain language — the agent calls get_symbol/impact/find on its own:

    "If I change resolve_all, what else needs to change?"

For other MCP clients, add corbel directly to the server config:

{
  "mcpServers": {
    "corbel": { "command": "corbel", "args": ["serve"] }
  }
}

The three tools

Examples below are all real responses against the same pinned repo as above (sharkdp/hyperfine at f12f3d9f) — clone it and run these yourself to check.

get_symbol looks up a symbol by name and returns its definition (file, line, signature) plus everything that calls it and everything it calls. Every edge carries a resolution field naming which lookup stage matched it to a specific definition (see docs/mcp-tools.md for what each value does and doesn't guarantee). Real response, get_symbol("format_duration_unit"):

{
  "results": [{
    "name": "format_duration_unit",
    "file": "src/output/format.rs",
    "line": 11,
    "signature": "pub fn format_duration_unit(duration: Second, unit: Option<Unit>) -> (String, Unit)",
    "callers": [
      { "file": "src/benchmark/mod.rs", "line": 141, "name": "Benchmark::run", "resolution": "scoped" }
      /* ...10 more, see above */
    ],
    "callees": [
      { "file": "src/output/format.rs", "name": "format_duration_value", "resolution": "same-file" }
    ],
    "truncated": false
  }]
}

impact is the flagship tool: it walks the reverse call graph from a symbol across multiple hops and returns every affected symbol tagged with depth and resolution — the multi-hop trace a single grep or a one-hop "find references" cannot do. Real response, impact("compute_relative_speeds") (6 affected symbols total):

{
  "results": [{
    "target_name": "compute_relative_speeds",
    "affected": [
      { "depth": 1, "file": "src/benchmark/relative_speed.rs", "line": 86, "name": "compute_with_check_from_reference", "resolution": "same-file" },
      { "depth": 1, "file": "src/benchmark/relative_speed.rs", "line": 98, "name": "compute_with_check", "resolution": "same-file" },
      { "depth": 2, "file": "src/benchmark/relative_speed.rs", "line": 143, "name": "test_compute_relative_speed", "resolution": "same-file" }
    ],
    "affected_count": 6,
    "max_depth_reached": 2,
    "truncated": false
  }]
}

find is a name search over the index, for when the exact name to hand get_symbol isn't known yet. It does not resolve call relationships. Real response, find("duration", limit=3) — 5 symbols match, 3 are returned:

{
  "results": [
    { "name": "format_duration", "file": "src/output/format.rs", "line": 5, "kind": "function" },
    { "name": "format_duration_unit", "file": "src/output/format.rs", "line": 11, "kind": "function" },
    { "name": "format_duration_value", "file": "src/output/format.rs", "line": 18, "kind": "function" }
  ],
  "total_matches": 5,
  "truncated": true,
  "truncated_count": 2
}

Supported languages

Language

Level

Notes

Rust

full

Own scope walker; all five resolution stages exercised.

Python

full

Own scope walker; all five resolution stages exercised.

TypeScript

full

Own scope walker; all five resolution stages exercised.

TSX

full

Adds JSX-tag references on top of TypeScript's resolution.

JavaScript

full

Shares TypeScript's resolution machinery. CommonJS require(...) produces no import entry — only ES-module import/export is scope-aware.

Every language above resolves to the same five outcomes, via logic implemented once and shared by all of them: same-file, scoped, global-unique, external, unresolved (scoped and global-unique are the same index-wide-uniqueness check with different labels, not sequential stages — see docs/mcp-tools.md). See docs/language-support.md for the promotion criteria new languages must clear.

Measured accuracy

Text search doesn't just miss things — it over-reports, confidently. A real, reproducible example from this benchmark's own repos:

$ rg -n '\.iter\(' --type rust src/    # inside hyperfine's own source tree
31 matches

Only one of those 31 hits is a call to the specific Commands::iter method a caller-graph query is actually asking about; the other 30 are .iter() on unrelated Vecs and slices — one of the most common method names in any Rust codebase. That gap between "what text search finds" and "what's actually being asked" is what a resolved call graph closes, and why we score it rather than just describe it.

Measured against a 120-entry hand-verified golden set (callers + definition tasks; see Methodology), precision / recall / F1, split by language rather than averaged away:

Language

corbel

grep / ripgrep¹

ripgrep+ctags²

TypeScript

0.868 / 0.820 / 0.844

0.387 / 0.424 / 0.404

0.783 / 0.880 / 0.829

Rust

0.586 / 0.488 / 0.532

0.512 / 0.714 / 0.597

0.530 / 0.739 / 0.617

Python

0.618 / 0.920 / 0.740

0.524 / 1.000 / 0.688

0.524 / 1.000 / 0.688

Overall

0.709 / 0.705 / 0.707

0.472 / 0.640 / 0.543

0.617 / 0.844 / 0.713

(precision / recall / F1)

corbel loses to ripgrep+ctags overall (F1 0.707 vs 0.713) and on Rust specifically (0.532 vs 0.617) — left in the table as measured.

¹ grep and ripgrep score byte-identically here — shown as one column; see Reproducibility below. ² a hybrid, not plain ctags: ripgrep finds call sites, ctags supplies the enclosing scope for each hit. Plain ctags has no call-site index, so the callers task is structurally impossible for it alone — this scores the hybrid a real developer would actually reach for, not a strawman zero.

Scoring caveats, disclosed rather than tuned away:

  • T2 (callees) is excluded from this table — ~90% of its golden-set ground truth is empty, so scoring it would grade "did you correctly return nothing," not tool capability.

  • The automatic classifier used to categorize corbel's misses doesn't account for call-count multiplicity: corbel's callers list is one row per caller symbol, not one row per call site, so a symbol calling the target twice scores as a miss even when corbel names the right function. 18 of 19 failures this classifier tags unqualified_symbol_name are this artifact, not a remaining qualification bug (one of the 19 is real — see Known limitations). This was not changed after seeing what it produced.

Full per-entry breakdown and adversarial-case detail: benchmarks/results/.

Reproducibility

grep and ripgrep's numbers above are byte-identical to a run taken before the fix that moved corbel's own F1 from 0.395 to 0.707 — direct evidence the harness and golden set were not adjusted to move corbel's number:

  • before — corbel F1 0.395 (analysis)

  • after — corbel F1 0.707 (analysis)

  • 1.0.0 re-run — corbel F1 0.707, byte-identical TP/FP/FN to the run above (analysis): confirms the 1.0.0 index-schema and impact-depth changes moved no accuracy number in either direction.

python3 benchmarks/harness/run_benchmark.py

Performance at scale

Measured on real open-source repositories, not accuracy-scored. Full methodology: benchmarks/results/perf-20260904.md.

Symbols

Repo

Cold index

find p50 / p99

Peak RSS

8,145

tokio

9.6s

1.1 / 1.4ms

8.3 MB

31,849

bevy

27.7s

4.9 / 5.8ms

8.2 MB

112,940

TypeScript compiler

282s

15.0 / 16.9ms

12.1 MB

116,870

servo

566s

31.7 / 122ms

8.4 MB

  • Cold-index time is super-linear: exponent ≈2.3 between the 32K and 110K+ tiers.

  • The driver is name collisions, not symbol count. servo and the TypeScript compiler have almost the same symbol count, but servo takes 2x longer to index because it has 5.8x more name-collision call sites (164,043 vs 28,282) — bare-name resolution, not indexing, is the bottleneck.

  • find does two full-table scans per call (LIKE '%query%' can't use the name index): negligible under ~32K symbols, 15-32ms typical past 110K.

  • Call frequency matters more than symbol count for find: a workflow issuing several find calls per task feels this before any single call does.

  • Peak memory is flat regardless of repo size (see table above) — time and tail latency are the scaling constraint, not memory.

  • impact accepts an optional depth parameter (capped at its internal maximum of 10); omitting it preserves the pre-1.0 default of walking to depth 10 or budget exhaustion, so depth-3-specific latency isn't measurable and isn't approximated here.

  • Only one cold-index run, not three, at the 100K+ tier: a single run cost 9-10 minutes, making repeated averaging impractical. rust-lang/rust was not attempted.

Methodology

  • corbel wasn't used to build the golden set. candidate_scanner.py selects candidate symbols without importing corbel — a structural guarantee, not a policy.

  • 120 entries, one AI verifier, no human review. Every entry was checked by a single model (Claude Sonnet 5), not a person — disclosed because it matters, not because it's flattering.

  • Cross-checked three ways: ripgrep-enumerated candidates, an LSP server's draft answer, and direct reading of the source.

  • The LSP cross-check surfaced 6 distinct classes of wrong answers, catalogued rather than trusted blindly: LSP_ERROR_TYPES.md.

  • Text search overcounts by up to 31x in this benchmark's own repos (the .iter( example above and others), catalogued the same way: TEXT_SEARCH_LIMITATIONS.md.

  • The 12 hardest ("adversarial") entries got a second pass: a context-isolated subagent re-verified them independently, without seeing the first pass's reasoning.

Full methodology, including what the single-verifier limitation does and doesn't compensate for: benchmarks/README.md.

Known limitations

corbel resolves what static analysis can prove and refuses to guess at the rest. On its own source (796 symbols, 5,481 references at time of writing), 93.3% of internal calls resolve. The rest are name collisions with nothing in scope to disambiguate, marked unresolved (ambiguous) rather than guessed.

Structurally out of reach for static analysis, by design, in every supported language:

Limitation

Why

How corbel handles it

Dynamic dispatch (trait objects, duck typing, interface-typed calls)

No statically-determined target exists

Reported as external or unresolved, never a fabricated edge

Macro-generated code (Rust macro_rules!/derive, call-site-rewriting decorators)

Invisible to tree-sitter extraction if the expansion isn't present in source form

Silently absent from the call graph — no edge is created, fabricated or otherwise

JavaScript/TypeScript CommonJS require(...)

Doesn't populate an import entry

A call reached only via require resolves less precisely than the same call via import

find's substring query (%query%)

Can't use the symbol-name index; full table scan every call

Same result, just slower — no call has ever failed from this, only added latency

Standard library / external crate or package calls

Outside the index entirely

Reported as external; corbel does not resolve into dependencies

This table is about mechanism — why each case can't be resolved and what corbel does instead — not about how often it happens. Two of these are separately measured: macro-generated code (zero of the golden set's measured failures traced to a macro-generated call site) and find's scan cost (negligible under ~32K symbols, 15-32ms typical past 110K — full numbers in Performance). Dynamic dispatch's actual failure rate is measured too, but that number belongs to the table below, which is about frequency, not mechanism:

Measured breakdown of corbel's actual misses (603 classified failures, callers + definition tasks):

Cause

Share

Name collision (a bare-name lookup limit shared by every tool measured, not corbel-specific)

13.4%

Dynamic dispatch

6.8%

Runtime prototype assembly (chevrotain's applyMixins, assigns prototype methods at runtime — invisible to static analysis by construction)

2.3%

Duck typing

0.8%

Full breakdown: analysis.

Name collision, concretely: pallets/itsdangerous at 672971d6 defines __init__ 13 times across its class hierarchy. get_symbol("__init__") with no file/line to disambiguate returns all 13 — corbel narrows by name, not by which class you meant, same as any bare-name index would:

$ corbel index . && echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_symbol","arguments":{"name":"__init__"}}}' | corbel serve .
# 13 results across src/itsdangerous/exc.py, serializer.py, signer.py

Pass file (and line, if the file still has more than one match) — exactly what find's results give you — to get exactly one.

A real, unfixed bug: a Rust method defined as a trait's default method body (trait Foo { fn bar(&self) { ... } }, not inside an impl block) doesn't get an owner-qualified caller name. owner_of_definition walks up looking for impl_item and never checks for an enclosing trait_item. 3 occurrences in the benchmark (MarkupExporter::table_results in hyperfine) — narrow, but real, and listed here rather than folded into the percentages above.

Cases where every tool measured — corbel, grep, ripgrep, and the ripgrep+ctags hybrid — gets the same answer wrong:

  • for x in iter desugars to repeated Iterator::next() calls with no .next() text anywhere in source. No tool here does implicit-desugaring analysis; all four fail identically — a shared ceiling, not a corbel gap.

  • Five chevrotain entries (findEndOfInputAnchor and four siblings) appear to have an incorrect golden-set answer: their real sole caller is validateRegExpPattern, but the golden set records validatePatterns (one level further out). All four tools agree on validateRegExpPattern and are uniformly scored wrong against it. This was not corrected in the golden set — the entries stand as originally verified, flagged here instead, so a scoring artifact doesn't get fixed quietly after the fact.

corbel audit compares the blast radius a changed symbol's callers should have received against what was actually queried. It reads git diff HEAD for uncommitted changes, maps each hunk to the symbol it falls in, calls impact on that symbol, and cross-references the result against .corbel/audit.jsonl (see Privacy) to report which affected callers were checked via get_symbol and which weren't. --since <duration|timestamp> (e.g. --since 2h, --since 30m, or a raw unix timestamp) restricts the query log to events at or after that cutoff, for judging coverage of a specific work session rather than the log's entire history; queries excluded by the window are counted and reported, not silently dropped. Each symbol's "not inspected" list prints at most 5 names before collapsing the rest into a count, so one heavily-called symbol's fan-out can't bury every other symbol's verdict in a multi-symbol report — the full list is always one impact() call away.

audit's known limitations:

audit maps git diff HEAD hunks to symbol definitions using line numbers from the last corbel index run. git diff HEAD always expresses each hunk relative to two versions of a file: HEAD (the "old" side) and the current working tree (the "new" side). audit matches hunks against indexed symbols using the old side, because that's the version the index needs to agree with — comparing indexed line numbers against post-edit ("new" side) line numbers would attribute a hunk to the wrong symbol whenever a change shifted lines (e.g. inserting a line above every symbol shifts everything below it). This is a coordinate-system match, not a content match: it means the index must have been built while the working tree matched HEAD, not that the working tree must currently match the index — editing a file after indexing it is exactly the normal edit-then-audit workflow, and must not be flagged as a problem on its own.

audit verifies this by comparing each changed file's indexed content hash against its HEAD blob hash (via git show HEAD:<path>), not against the file's current on-disk hash. If they match, the index reflects HEAD, old-side coordinates are trustworthy, and analysis proceeds — silently, with no warning, no matter how the working tree has since been edited. If they don't match, the index was built from some third state (most commonly: indexed mid-edit, before that edit was committed) and no coordinate system can be trusted, so audit warns and excludes that file rather than reporting a mapping it can't stand behind. A file changed on disk but never indexed at all is reported separately from this warning — there's no coordinate risk to flag, just nothing indexed yet to check coverage against. A file that's new relative to HEAD (doesn't exist there yet) has no old side to match against by definition, so it's likewise excluded, whether or not it happens to be indexed. The fix in each case is to run corbel index while the working tree matches HEAD — right after a commit, before making further edits — not to re-index after every change.

audit's query log (.corbel/audit.jsonl) is a plain JSON-Lines file; a line that fails to parse (partial write, disk corruption) is indistinguishable from a query that never happened unless it's counted separately. audit counts and reports unparseable lines rather than silently dropping them, so a corrupted log reads as "N lines corrupted," not as a lower, wrong query count.

audit maps a changed line range to the symbol it falls in using each symbol's actual indexed line..=end_line span — end_line comes straight from the tree-sitter definition node's own end position at index time, not an approximation. Appending a brand-new top-level symbol at the end of a file is recognized as touching no existing symbol, not misattributed to whatever symbol used to be last.

Observed, not yet acted on: in one real run against corbel's own repo, resolve_all's coverage came back 1/91 (1%) inspected — technically correct (impact() had been called, and only one of its 91 callers had been separately inspected via get_symbol), but the denominator was dominated by 90 test-function callers, with the one production caller (index_repo) among them. A coverage fraction that's structurally near-zero whenever a function has many test callers risks training users to ignore the number rather than act on it. Whether test callers belong in the denominator at all is an open question, deferred until there's more real usage to judge it against — noted here rather than changed speculatively.

License and boundaries

corbel is licensed under MIT.

Indexing and querying your own codebase — the entire tool as it exists today — is and will remain free for individual use, with no license server, no telemetry, and no phone-home behavior, ever. Organization-level features (fleet-wide indexing, shared indexes, team administration) are the intended boundary for a future commercial offering; nothing in the current codebase is gated, and this line is drawn now, before any such feature exists, rather than moved after the fact.

Contributing

See CONTRIBUTING.md for the development workflow, the language-promotion gates, and the schema-migration rules. Every commit must carry a DCO sign-off (git commit -s).

Privacy

corbel is not AI-based: no model runs inside it, and it makes no probabilistic claims about your code.

corbel never sends your code anywhere. Indexing and querying run entirely offline; the binary contains no network code. What an agent sends to its model is between the agent and its MCP client — corbel itself never touches the network.

With --audit (off by default), corbel also writes a local query log to .corbel/audit.jsonl — symbol names and file paths only, never source code, never leaves the machine. The audit subcommand additionally reads git diff HEAD to see uncommitted working-tree changes — it does not read commit history, logs, or blame.

Non-goals

corbel does not edit code, generate documentation, ship a web UI, scan for secrets, integrate with the Language Server Protocol, or collect telemetry. index and serve never read git at all; the only git interaction anywhere in corbel is audit's working-tree diff described in Privacy above — no mode reads commit history, logs, or blame.

Available Tools

3 tools
findA

Search the local corbel index for symbols whose name matches a query, for when you don't know the exact symbol name to pass to get_symbol. Matching is substring-based and case-insensitive, ranked so exact name matches come first, then names starting with the query, then names merely containing it; within each of those tiers results are ordered by name, then file, then line for a stable, repeatable order. Case-insensitivity only folds ASCII letters (SQLite's default LIKE behavior) — it will not match differently-cased Unicode identifiers (e.g. a Python or JavaScript identifier using non-ASCII letters), so queries against such names must match the identifier's actual case. Each match reports name, file, and line, which together are the exact triple get_symbol needs to pin down that one symbol even when several symbols elsewhere share its name. This tool does not resolve or return call relationships — use get_symbol or impact on a specific match for that. The response can be truncated to fit within limit and a token budget; when it is, truncated is set to true and truncated_count reports how many further matches were left out. Results come from the local corbel index built by corbel index and only reflect the repository as of the last index run.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoOptional cap on the number of matches returned, from 0 up to corbel's hard maximum of 200 (requests above 200 are rejected, not silently reduced). Defaults to corbel's built-in limit if omitted.
queryYesSubstring to search for in symbol names (case-insensitive for ASCII).
token_budgetNoOptional cap on the size of the response, in estimated tokens. Defaults to corbel's built-in budget if omitted.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden, and it delivers: it discloses substring matching, ASCII-only case folding, the ranking scheme, stable ordering, truncation behavior with truncated/truncated_count, and the fact that results depend on the last corbel index run. This is unusually transparent behavioral detail.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every sentence carries useful information that is not derivable from the schema or annotations. It is front-loaded with the core purpose and differentiator before diving into ranking and truncation details. It could be tightened slightly, but the density justifies the length.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given that there is no output schema and no annotations, the description is remarkably complete: it specifies the match fields (name, file, line) and how they map to get_symbol's needs, explains truncation flags, covers index freshness, and clarifies what the tool does not return. An agent has enough context to invoke it correctly and interpret the result.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although the input schema already covers all three parameters (100% coverage), the description adds substantial meaning beyond it: the hard maximum of 200 for limit, rejection behavior above it, the built-in defaults, how token_budget constrains the response, and how query matching and ranking actually behave. This exceeds the baseline 3 for full schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb ('Search'), a specific resource ('the local corbel index'), and a precise condition ('for when you don't know the exact symbol name to pass to get_symbol'). It clearly distinguishes itself from siblings by naming get_symbol and impact and explaining what this tool does not do.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly says when to use the tool ('when you don't know the exact symbol name'), and explicitly defines when not to use it: 'This tool does not resolve or return call relationships — use get_symbol or impact on a specific match for that.' This gives an agent direct routing guidance against sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_symbolA

Look up a single symbol by name in the local corbel index and return where it is defined (file, line, signature), everything that calls it (callers), and everything it calls (callees). Every caller and callee comes with a resolution field (e.g. same-file, scoped, global-unique) naming which index-wide lookup found the definition, rather than a guess from text matching — this is what makes the result trustworthy for navigation and refactoring, unlike a grep/text search which can't tell you if a match is actually the same symbol. scoped and global-unique both mean a single matching definition was found index-wide; the difference is only whether the caller's file imports that name (see docs/mcp-tools.md for details). Use this tool when you need to jump to a function's or type's definition, inspect its signature, or see who calls it and what it calls, before editing it. If name (optionally narrowed by file) still matches more than one symbol — e.g. overloaded declarations in the same file — pass line as well; the find tool's results already carry the exact name/file/line triple needed to pin down one match. The response can be truncated to fit within a token budget; when it is, truncated is set to true and truncated_count reports how many additional callers and callees together were left out. The budget is divided evenly across every matched symbol first (so if name is ambiguous and returns several results, no single match can consume the whole budget and starve the others), then each match's own share is split evenly between its callers and its callees (so a hot function's huge caller list can't crowd out its callees, or vice versa). The index is built ahead of time by corbel index and only reflects the state of the repository as of the last index run.

ParametersJSON Schema
NameRequiredDescriptionDefault
fileNoOptional file path to disambiguate when multiple symbols share this name.
lineNoOptional definition line to disambiguate further, for when `name` and `file` alone still match more than one symbol (e.g. overloaded declarations in the same file). Requires `file` to also be set.
nameYesThe symbol name to look up (function, method, type, etc.).
token_budgetNoOptional cap on the size of the response, in estimated tokens. Divided evenly across every matched symbol first, then each match's share is split evenly between its callers and callees. Defaults to corbel's built-in budget if omitted.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden, and it does so thoroughly. It discloses that the index is built ahead of time by `corbel index` and may be stale, that `resolution` reflects index-wide lookup rather than text matching, explains the difference between `scoped` and `global-unique`, and details truncation behavior (`truncated`, `truncated_count`) and token-budget allocation. This is a very transparent description of behavior and limitations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose and then scaffolds into disambiguation, truncation, budget, and staleness, which is logical. It is longer than average, but every sentence earns its place given there are no annotations and no output schema. A small deduction because the budget explanation is somewhat wordy and could be tightened without losing meaning.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no output schema and no annotations, the description is highly complete: it names the return components (definition, callers, callees, `resolation`), explains how disambiguation works, covers truncation and budget behavior, and notes index staleness. An agent has enough context to invoke the tool correctly and interpret its results without further lookup.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the description adds substantial meaning beyond the schema. It explains that `file` narrows the lookup, that `line` requires `file` and is for overloaded declarations, and it provides a detailed account of how `token_budgets` is divided across matches and then between callers and callees. This goes well beyond the parameter descriptions in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Look up a single symbol by name in the local corbel index and return where it is defined (file, line, signature), everything that calls it (callers), and everything it calls (callees).' It clearly distinguishes itself from grep/text search and implies a difference from sibling `find` by noting the exact name/file/line triple needed to pin down one match. This is more than adequate for an agent to understand what the tool does.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description contains explicit usage guidance: 'Use this tool when you need to jump to a function's or type's definition, inspect its signature, or see who calls it and what it calls, before editing it.' It also advises passing `line` when a name is ambiguous and references the `find` tool's output as a source for the disambiguation triple. It stops short of explicitly stating when NOT to use this tool versus `find` or `impact`, so it earns a 4 rather than a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

impactA

Trace the blast radius of changing a symbol: starting from the given symbol, walk the reverse call graph — direct callers, their callers, and so on across multiple hops — and return every symbol that could be affected by a change to it. Each affected symbol comes with a depth field (how many hops away it is) and a resolution field naming which index-wide lookup resolved that call edge (e.g. same-file, scoped, global-unique — scoped/global-unique both mean a single matching definition was found index-wide, see docs/mcp-tools.md), so results are grounded in real, resolved call relationships rather than a text search for the symbol's name (which cannot follow more than one hop and cannot tell a real call from a coincidental name match). Use this tool before refactoring — e.g. changing a function's signature or behavior — to find every place in the codebase that may need to change as a result, including indirect callers that a single-hop "find references" would miss. The response can be truncated to fit within a token budget and/or a hop-count depth; when either limit is hit, the corresponding flag is set (truncated/truncated_count for the budget, depth_truncated for depth) so a caller can tell which one cut the walk short. Results come from the local corbel index built by corbel index and only reflect the repository as of the last index run.

ParametersJSON Schema
NameRequiredDescriptionDefault
fileNoOptional file path to disambiguate when multiple symbols share this name.
nameYesThe symbol name to start the impact analysis from.
depthNoOptional cap on how many call-graph hops to walk outward (0 returns no affected symbols, 1 returns only direct callers, and so on). Defaults to corbel's internal maximum of 10 if omitted, which is also the hard ceiling — values above 10 are clamped to 10. Whichever of `depth` and `token_budget` is hit first stops the walk; `depth_truncated` in the response is true only if the depth ceiling (not the budget) is what stopped it.
token_budgetNoOptional cap on the size of the response, in estimated tokens. Defaults to corbel's built-in budget if omitted.

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden and does so thoroughly: it discloses that results are grounded in resolved call relationships, that responses can be truncated with corresponding flags set, and that results come from the local corbel index and are only as fresh as the last index run. No contradiction with annotations exists because none are provided.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but not bloated, front-loading the core purpose before usage guidance, truncation flags, and index freshness. Some parenthetical detail could be tightened, but each sentence contributes decision-relevant information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Without an output schema, the description compensates by naming returned fields (`depth`, `resolution`), explaining truncation flags, and noting the index-freshness caveat. It stops short of giving a full response shape or an example, but it is enough for an agent to call the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already documents all four parameters with rich descriptions, giving 100% schema description coverage, so the baseline is 3. The tool description adds context about truncation behavior and depth/token_budget interplay but does not need to re-explain parameter syntax.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('Trace') and a specific resource ('the blast radius of changing a symbol'), and explains the mechanism: walking the reverse call graph across multiple hops. It also contrasts itself with text-search/find-references behavior, which effectively distinguishes it from the sibling find tool even without naming it.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says 'Use this tool before refactoring — e.g. changing a function's signature or behavior' and explains that it catches indirect callers that a single-hop find-references would miss. It provides clear context but does not name the sibling tools directly or list when-not-to-use scenarios.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev1.0.0
    • Changedimpact1 field changed
      • addedInput schema / properties / depth
        Added value: +{
        +  "description": "Optional cap on how many call-graph hops to walk outward (0 returns no affected symbols, 1 returns only direct callers, and so on). Defaults to corbel's internal maximum of 10 if omitted, which is also the hard ceiling — values above 10 are clamped to 10. Whichever of `depth` and `token_budget` is hit first stops the walk; `depth_truncated` in the response is true only if the depth ceiling (not the budget) is what stopped it.",
        +  "type": "number"
        +}
  2. 3 tool updatesv0.1.0
    • First observedfind
    • First observedget_symbol
    • First observedimpact

TDQS

A4.5/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: `find` searches for symbol names, `get_symbol` returns a single symbol's definition with direct callers/callees, and `impact` traces the transitive reverse call graph. There is no overlap or ambiguity between them.

Naming Consistency4/5

Tool names are lowercase, short, and descriptive, but `get_symbol` uses snake_case while `find` and `impact` are single words. The naming is mostly consistent and predictable, with a minor stylistic deviation.

Tool Count5/5

Three tools is a well-scoped set for a code symbol index server: search, single-symbol lookup, and transitive impact analysis. Each tool earns its place and there is no redundancy.

Completeness4/5

The server covers the core workflow of finding and inspecting symbols and analyzing change impact. Minor gaps exist—such as no direct way to list all symbols in a file or inspect index health—but they are workable and do not break the primary use case.

Maintenance

ActivityActive
ResponsivenessUnresponsive

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    CodeGraph — Open-source code intelligence MCP server. Builds a semantic graph of your codebase (functions, classes, imports, call chains) and exposes it through 31 tools. Callers, callees, impact analysis, complexity metrics, unused code detection, AI context assembly, persistent memory, cross-project search. 15 languages via tree-sitter. Single Rust binary, local-first.
    267 npm
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    Standalone MCP server for code structure analysis using tree-sitter. Directory trees, symbol definitions, and call graphs without reading raw source files. Supports Rust, Python, Go, Java, TypeScript, Fortran, JavaScript, C/C++, and C#. Benchmarked up to 68% fewer tokens vs native tools.
    6
    Apache 2.0
  • A
    license
    A
    quality
    C
    maintenance
    Cross-repository code knowledge graph MCP server for Java, Kotlin, JavaScript, and TypeScript. Indexes source code into embedded KuzuDB via tree-sitter and exposes 30+ tools for call-flow tracing, multi-hop taint analysis (OWASP/CWE/PCI/STIG), entry-point reachability filtering, performance hotspot detection, and license compliance — without reading source files. 95% fewer tokens vs source-read
    33
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Multi-language code-graph MCP server with 18 tools for structural code queries — find_symbol, callers, callees, blast_radius, dead_code, and cross-stack dataflow_trace from HTTP request through service layers to SQL. Tree-sitter parsing for Python, TypeScript, JavaScript, and Go; local-first, no API key required.
    18
    MIT