Skip to content

reference

The eight tools

Six retrieval tools and two symbol-scoped edits.

Each one names the native call it is meant to be reached for instead of, because a tool a model does not reach for is a tool that costs its description and returns nothing. The eight descriptions and schemas are 2,000 approximate tokens, paid once per session — five structural questions, at the median saving, before the introduction has paid for itself. Run 5 cut them 18% by tightening the wording and the schemas; no tool was merged, split or removed, so no call sequence changed.

every response

What a response promises

says what it did not see
Every retrieval response states how many files it scanned and how many it could not index, scoped to the path searched. On an unfamiliar TypeScript repository that read 120 files scanned, 127 unindexed — Markdown, JSON, CSS and everything else the grammars do not cover. Run 5 added a third count for what the walk itself skipped, so a response can read 45 files scanned, 3 unindexed, 2 paths not walked; the clause is omitted when the count is zero.
never truncates silently
A bounded response says how many results exist and what offset fetches the rest. A limit that quietly fails to bind is worse than none, because the caller believes the answer is complete.
fails loudly
No fallback that returns a partial result as if it were whole. A missing symbol is an error, not an empty list, and an ambiguous name is a refusal that lists the candidates rather than a guess.
reads a snapshot
A refresh builds its replacement to the side and publishes with one assignment, so it cannot change what a reader is already holding. The index is keyed on path plus mtime plus size and is never trusted, only checked.
Two drawings of one twelve-line source fragment side by side. On the left, what Read returns: all twelve lines, indented bodies and closing braces included. On the right, what outline returns: the three declaration lines only, in the same positions, with the nine body lines blank. Nine of these twelve lines are bodies. On the benchmark's 605-line module the same substitution answers 'what is the last declaration in this file' for 91 approximate tokens, against the 1,068 the scripted native arm spent on the same question.

retrieval

outline

Instead of Read on a whole file, or a directory of them.

Every declaration with its exact line range and its signature, bodies omitted. Paged, and from: "end" pages backwards.

The one thing on this list no native sequence can do at all: listing a long file's exports with their return types is where the benchmark's whole margin over the native floor comes from. from: "end" is the cheap answer to 'what is at the bottom of this file' — the tail, rather than everything above it.

retrieval

find_symbol

Instead of a bare-name Grep.

Every declaration of a name, exact matches first, name-contains matches marked ~. Signatures verbatim, so export, private and async are visible.

On the fixture, formatCurrency appears on 64 lines across 21 of 48 files and two of those are decoys. find_symbol returns the three declarations and marks the near-miss; the grep returns all 64 lines with the decoys unlabelled.

{"name": "find_symbol", "arguments": {"root": "…/bench/fixture",  "name": "formatCurrency"}}formatCurrency — 2 definitions, 1 name-contains (marked ~).45 files scanned, 3 unindexedsrc/utils/currency.ts:58-68  export function formatCurrency(    value: Money, locale = 'en-US'): string    # Render money for human display, e.g. `$1,234.50`.src/notifications/receipt-renderer.ts:16-20  [class ReceiptRenderer]    private formatCurrency(value: Money): string~ src/utils/display.ts:9-27  export function formatCurrencyCompact(    value: Money, precision = 1): string

Reproducible from a clone against the committed fixture, with no MCP client — the README pipes the three JSON-RPC lines straight into the built server. root is optional on every tool since run 5, and is spelled out here only because these calls serve the fixture rather than the repository the server was started in.

retrieval

read_symbol

Instead of Read with an offset you had to guess.

The source of exactly one declaration, with its line numbers. symbol is a bare name or ClassName.method.

A name declared twice in scope is refused with the candidates listed rather than guessed at. On an unfamiliar repository that refusal fired on the first try — two declarations named fail — and it costs a round trip every time. RESULTS.md calls that the common case in real code, not the exception.

retrieval

search_code

Instead of Grep.

Each hit inside its structure: the matching line arrives under the declaration that encloses it, with that declaration's signature and line range.

Literal matching only — for a regular expression, Grep is still the tool. within scopes the search to one named declaration and context gives the lines either side, which together answer 'what happens in this branch' without paying for the whole function.

retrieval

find_callers

Instead of Grep for name(.

Call sites grouped under the declaration each one sits in, with a count of the imports, re-exports and same-named mentions it excluded.

Run 5 resolves each site through the calling file's own imports — aliases, barrel re-exports and tsconfig-style @/ paths, matched against the tree by unique suffix because tsconfig is not read — so a same-named function in another module is excluded and counted rather than merged in, and a name with several declarations is grouped under each. On a real aliased Next.js repository, all nine call sites of a function imported as @/lib/format were attributed and none was left unresolved. Still static: a call through a property, a namespace import, a require and an alias matching more than one file arrive under ?.

{"name": "find_callers", "arguments": {"root": "…/bench/fixture",  "symbol": "postLedgerEntry"}}postLedgerEntry — 5 call sites in 3 files, 5 non-call mentionsexcluded (static, name-based). 45 files scanned, 3 unindexedsrc/billing/payment-gateway.ts    48-68  export async function chargeCard(             gateway, request, logger): Promise<ChargeResult>     61    await postLedgerEntry({src/billing/refunds.ts     9-28  export async function issueRefund(             orderId, captured, requested, logger): Promise<Money>     20    await postLedgerEntry({src/orders/order-processor.ts    57-121 export async function processOrder(             order, logger): Promise<ProcessedOrder>    100    await postLedgerEntry({    106    await postLedgerEntry({  566-605  export async function reconcileSettlement(             batchId, orders, logger): Promise<SettlementReport>    583      await postLedgerEntry({

Reproducible from a clone against the committed fixture, with no MCP client — the README pipes the three JSON-RPC lines straight into the built server. root is optional on every tool since run 5, and is spelled out here only because these calls serve the fixture rather than the repository the server was started in.

retrieval

find_references

Instead of Grep for the name.

Every occurrence, each labelled with what the syntax says it is: def, call, import, re-export or ref.

The classification is the thing a grep cannot give you. It carries the same static-matching limit as find_callers, and an empty result is never proof that a symbol is unused.

write

replace_symbol

Instead of Edit or Write on a whole file.

Rewrites one declaration in place. The line range comes from the parsed index, so nothing has to be quoted back to locate the edit.

The rewritten file is re-parsed in memory before anything reaches disk. Source that would not parse is refused with the offending line named and the file left exactly as it was; so is source that parses but declares nothing.

write

insert_symbol

Instead of Edit on a whole file.

Adds a declaration immediately before or after a named anchor, separated by exactly one blank line.

before inserts above the anchor's first line, which sits below any doc comment attached to it — to land above a documented symbol, anchor after the declaration that precedes it. Same re-parse-then-write contract as replace_symbol.

the two writes

A write that would not parse is refused, not repaired

parse before write
The prospective file is re-parsed in memory first. Source that would not parse is refused with the offending line named; source that parses but declares nothing is refused too. The file is left exactly as it was.
inside the root
A file argument that resolves outside the served root is refused, checked both lexically and through realpath, so neither a .. nor a symlink escapes.
the line range is parsed, not matched
Nothing has to be quoted back to locate the edit, which is the whole cost of an Edit on a long file. source is the complete new declaration including its signature, not the body alone.

No refactoring, no formatting, no rename-across-files. Both write tools move exactly the declaration they were pointed at, and insert_symbol puts a new one before or after a named anchor separated by exactly one blank line.

Write tools were a non-goal for the first two runs and the operator lifted it for the third, on the argument that retrieval alone does not close the loop: the model still has to write the code back, and rewriting a whole file to change one function is where the token cost returns.