Find All Callers of a Function Across Repositories

Finding one caller takes a search box. Finding all callers of a function across repositories takes symbol identity, interface resolution, and an honest confidence value, because the name you type is not the thing you mean. getUser may appear in forty files and name four different functions, and the call you care about may never contain the name at all.

This page covers what a complete caller list requires, how to query one over MCP, and which callers a static graph cannot promise to find.

Why a name is not a caller list

A text search for getUser returns every line that contains the string: the declaration, the doc comment above it, a test name, a local variable, and three unrelated functions that happen to share the name in other modules. It also misses the call that goes through an interface method named find, because that call site never mentions getUser. And it stops at the repository boundary.

A caller list has to be built from resolved symbols. A function is identified by its file and its position in the declaration tree, not by its name alone, so two functions named getUser in two packages are two nodes with two separate sets of inbound edges. The callers of one are not the callers of the other.

Symbol identity, overloads, and interfaces

find_node resolves a selector to a node and returns the file path, line bounds, and signature. When a name is ambiguous it returns { resolved: false, candidates } instead of picking one, and the caller selects the right node by nodeId or path. That step is what makes the caller list trustworthy: resolve first, then traverse.

  • Overloads. One name can be several declarations with different signatures. Where the language and the index can tell them apart, an edge attaches to the declaration the call resolves to. Where they cannot be told apart, the edge attaches to the shared name and the row's confidence drops, which is the signal to verify by reading the call site.
  • Interfaces. A call written as repository.find(id) creates an edge to the interface method, while the code that runs is one of several implementations. relate with kind: "callers" resolves call sites through implementations, kind: "implementers" lists the classes behind an interface, and kind: "heritage" returns the full type hierarchy when dispatch passes through several levels.
  • Inheritance and aliases. A subclass that inherits a method, an import renaming a symbol, or a re-export that forwards a function: each hop is followed through heritage and import edges, so the caller list lands on the declaration rather than on a name that merely matches.
  • Multiple selectors. Every graph tool accepts one to ten selectors per call, so a rename that touches three helpers can resolve all three in a single round trip.

Cross-repository caller lookup

Inside one repository, callers are a local question with a local answer. The question that breaks text search is the cross-repo one: a shared library with consumer repositories, a frontend calling an API defined in another repository, a worker invoking a handler that lives in a service repository.

When repositories are linked in the same project, the graph holds edges that cross the boundary. It matches HTTP call sites to the route definitions they hit, SQL access to the ORM model that owns the table, and queue producers to consumers on the same channel, alongside package imports and calls joined on package identity. A cross-repo row can name the package identity that joined the edge, so you can see why the two repositories are connected rather than guessing from an import path.

Pass includeCrossRepo: true on relate to include those rows in a caller or dependency answer. The same query then returns consumer repositories alongside local call sites, with each row carrying its own file path and line bounds.

One boundary matters for the word "all": a repository that was never attached to the project is not indexed, and the graph reports it as not indexed rather than returning a partial list without saying so. A caller list is complete against the repositories in the project, not against the world. index_health shows which repositories produced no graph and why. Linking repositories into one project, including the transport edges above, is covered in code dependency mapping.

Confidence on every caller

Caller rows are not equally certain, so Symvanta ships a confidence tier with each one.

Tier What it means How to treat it
high A compiler-grade index resolved the symbol Edit against it
medium A framework convention or heuristic matched it Review the call site, then act
low A string heuristic matched it Treat it as a lead, not a call
correlational The symbols keep changing together in git history Investigate before trusting it

A missing tier means the row predates tiering, not that the edge is certain. Confidence applies at two levels, and both matter. The edge tier says how the caller was resolved. The node match says how well the selector resolved: a high-confidence match can still be the wrong kind of node, for example a property whose name matches the class you asked for. Check the node kind before treating a result as the caller of a function.

For a rename, filter to high rows and edit those. Read the medium rows by hand, because framework conventions produce real calls that no call edge states explicitly. Never present low or correlational rows as calls; they are search results that survived a relevance filter.

Query callers over MCP

The whole sequence is a handful of calls against the hosted endpoint. This example resolves an ambiguous name, then asks for callers across repositories.

find_node({ selectors: [{ symbol: "getUser" }] })
// -> ambiguous name: { resolved: false, candidates: [...] } with filePath and signature per candidate

relate({
  kind: "callers",
  selectors: [{ nodeId: "src/users/get-user.ts:getUser" }],
  includeCrossRepo: true
})
// -> rows with filePath, line bounds, confidence, and package on cross-repo edges

relate({ kind: "implementers", selectors: [{ symbol: "UserRepository" }] })
relate({ kind: "chain", selectors: [{ nodeId: "src/users/user-repository.ts:UserRepository:find" }] })
locate({ mode: "text", queries: ["getUser", "get_user_by_id"] })

locate with mode: "text" takes up to ten terms in one call and replaces a grep sweep across the indexed repositories, which is the right fallback when the symbol name itself is uncertain. Resolve what you find with find_node, then ask for its callers.

Add the endpoint once and every MCP client on the team can run the same queries:

claude mcp add --transport http symvanta https://mcp.symvanta.com/mcp

The first connection opens an OAuth 2.0 (PKCE) sign-in, so there is no key to paste. The queries work from Claude Code, Amp, Cursor, Zed, and any other MCP client; the integration guides cover setup, and the MCP server overview describes what the endpoint exposes.

Callers a static graph cannot see

A parsed graph sees the source, so it cannot promise a caller list that matches runtime behavior. The cases below are the ones that matter in practice.

  • Container resolution. A method chosen from a dependency-injection container by string key at boot time has no call edge naming the class that runs.
  • Reflection and computed names. When a handler name is assembled at runtime, there is no static call site to attach an edge to.
  • Plugin and registry loading. A module loaded because it sits in a scanned directory is a runtime relationship unless the registration code is indexed.
  • Callbacks stored as values. A function placed in a map or handed to an event bus may appear as a reference rather than a call, or not at all once the value is read back by key.
  • Dynamic languages. Monkey patching and metaprogramming rewrite behavior after parse time. Edges in those regions come back with low or correlational confidence, and reading the call site is the only way to settle them.
  • Raw SQL. The graph links SQL access to the ORM model that owns a table, not every string-built query. locate with mode: "config" finds the remaining writers.
  • Code outside the index. Generated code, vendored code, a repository nobody attached, and languages outside the eleven that parse today (TypeScript, JavaScript, Python, Java, Kotlin, C#, Go, Rust, Swift, PHP, and Ruby) are reachable through text search only, which finds mentions rather than callers.
  • Index lag. A caller list describes an indexed revision. freshness reports how far that revision sits behind the remote head, and a commit that landed after the last index is not in the answer. Uncommitted edits in a sibling repository are invisible unless you overlay them in your session with ref.

The fix for the first four cases is runtime signal, traces and logs merged into the static graph. Until that signal exists, the useful contract is narrower and testable: regular calls, imports, implementations, and inheritance come back resolved, and every row states how strongly it was resolved.

A task to run in your agent

Caller lookup stops being a tool demo once it sits inside a task. This prompt works in any MCP client:

Before you touch getUser in the users repository:
1. Resolve the symbol with find_node and confirm its file and line bounds.
2. List every caller with relate, kind callers, includeCrossRepo true.
3. Group the rows by confidence and by repository.
4. Update the high-confidence call sites, then run the same query again.
5. Finish with diff_impact on the branch and report any impacted file you did not change.

Step 4 is the part that catches an agent that edited from memory: the second query either returns the same rows with new line bounds or it returns rows the first pass missed. Step 5 turns "done" into a list of files the agent did not touch, which is what a reviewer actually needs.

Run that task against a repository you own rather than a sample. Start a free trial, connect GitHub, and point your agent at a function whose callers you already know by heart; the first query tells you whether the caller list is ready to work from. Every plan starts with a 7-day trial that takes no credit card, and self-serve plans start at $19 per month (pricing).

For the wider question the caller list feeds into, read blast radius analysis and what breaks if I change this function. To see how callers, dependencies, and modules fit together, see code dependency mapping and the MCP server overview.

See Symvanta on your own codebase →