From a Question to Evidence: Navigating the Graph
deusdata/codebase-memory-mcp becomes useful only when an agent can turn a human question into the right sequence of graph operations. This chapter focuses on that sequence: how to orient within an indexed project, find a candidate, follow the relationship that matters, retrieve exact source, and check whether the result is bounded or incomplete. The practical judgment is not “Which tool is fastest?” but “Which tool produces the evidence this question actually requires?”
The distinction matters because a graph exposes many possible paths through the same repository. A developer asking where a function is defined needs a different answer from someone asking what calls it, which route reaches it, or which files changed around it. Codebase-memory-mcp separates those questions into query surfaces rather than forcing every investigation through one broad search. That separation reduces blind exploration, but it also makes the agent responsible for choosing a meaningful path.
Begin with orientation, not a blind symbol search
The first call should establish the project and its current shape. In codebase-memory-mcp, project status identifies whether an index exists, how many nodes and edges it contains, where its source root is, and whether coverage records report skipped or partial material. Architecture queries provide a compact view of languages, packages, entry points, routes, boundaries, layers, clusters, or a selected directory. Schema inspection shows which node labels and relationship types actually exist in the indexed graph.
This orientation step prevents a common error: asking a precise question of an imprecise mental model. If the project has not been indexed, a symbol search cannot distinguish “no match” from “no graph.” If a question concerns routes but the graph has no route evidence for the relevant scope, a call trace alone may answer a different question. If a directory was intentionally excluded, a graph that looks internally consistent can still be incomplete relative to the working tree.
Orientation also makes names interpretable. A short name such as run, Config, or Handler can occur in many files. Architecture and schema information reveal the broad organization; the eventual search can then be narrowed by a label, a qualified-name pattern, a file pattern, or a relationship. The aim is not to inspect every node. It is to establish enough context that the next query has a defined scope.
Once the scope is clear, search_graph handles discovery. It can use a BM25 full-text query, regular-expression filters over names or qualified names, file patterns, labels, relationship types, and degree constraints. The result rows retain the identity a follow-up call needs: qualified name, file location, source-line range, and relationship degrees. A search for a function is therefore not merely a list of strings. It is a ranked or filtered set of candidates that can be connected to later evidence.
The full-text path is useful when the question uses words that do not exactly match an identifier. Codebase-memory-mcp's store indexes names, qualified names, labels, paths, and—when available—docstrings. It also indexes a CamelCase identifier both as the original spelling and as split words. A human phrase can therefore find a symbol such as updateCloudClient through its component words, while an exact identifier remains searchable. That is a retrieval aid, not semantic proof: a text match still needs the right label, scope, and source context.
Regular-expression and structural filters answer a different need. If the reader knows that the target is a route handler, wants symbols in one package, or needs high-degree functions, those constraints reduce the candidate set without asking the agent to read unrelated source. Degree is especially useful as orientation: a highly connected function may be a useful focus point, while a node with no relevant relationships may be a leaf or an extraction boundary. Neither degree alone proves importance or correctness, but it helps choose what to inspect next.
After discovery, trace_path follows relationships. In its ordinary call mode it can move inbound toward callers, outbound toward callees, or in both directions. It can also follow argument-aware data flow or service-shaped edges. That distinction changes the question being answered. An inbound CALLS trace asks who invokes a local callable. An outbound trace asks what the callable invokes. A data-flow trace asks how an argument or value is carried. A service trace follows relationships that represent HTTP or asynchronous boundaries rather than pretending that a remote operation is an in-process function call.
query_graph is the surface for questions that are not naturally one rooted traversal. It handles read-only multi-hop patterns and aggregation, so an agent can ask how two kinds of nodes are connected, which relationship types cross a boundary, or which structures meet a condition. A schema query can reveal the graph's vocabulary before a more specialized query is attempted. The important design choice is that a multi-hop query remains explicitly a graph query; it does not hide a large, unbounded repository crawl inside a natural-language convenience function.
The division becomes clearer with a concrete question. Imagine that an agent must explain how a request reaches a particular handler. Architecture or status first establishes the project and route-bearing scope. search_graph finds the handler and related route or service candidates. trace_path follows inbound calls or service edges. get_code_snippet retrieves the handler's exact source, and a coverage check tests the relevant files or directory. If the answer concerns only a literal configuration string, search_code may be more appropriate than a graph traversal. The question determines the path.
The interaction can be summarized without treating any one call as sufficient:
The source participant in this diagram is not an alternative to the graph. It is the place where a graph candidate becomes an explanation grounded in actual code. A graph can point to a range; the source reveals the statements, conditions, and surrounding context that the relationship summary cannot carry by itself.
Boundaries are part of the answer
The second half of graph navigation is response control. A highly connected codebase can produce more callers, callees, or multi-hop rows than an agent can usefully read in one response. Codebase-memory-mcp therefore treats output size, ordering, pagination, and omission metadata as part of the query contract.
Search and trace responses can use a grouped tree representation that factors repeated qualified-name and file prefixes. The purpose is not to make output look clever. Repeated context consumes space without adding new identity, while grouping related rows can make the structure easier to scan. The server can compare a direct representation with a grouped one and choose the smaller complete form. JSON remains available when a machine-oriented consumer needs stable direct fields.
The word complete in that description has a narrow meaning: the selected representation should not cut an identifier, path, or row in half. It does not mean that every matching row in the graph fits into the response. A page can still be limited by a requested row count, a traversal ceiling, or an output budget. Good bounded output preserves the rows most useful for identification and reports what remains instead of disguising omission as an empty result.
This is why lower-priority enrichment yields before core identity. A response should keep the qualified name and path of a candidate before it spends the remaining budget on optional connected names, extra properties, or additional lower-ranked rows. A source line or relationship label that is cut in the middle would be worse than one that is omitted and clearly marked. The server's byte-oriented ceiling also avoids pretending that all clients tokenize text in the same way.
Pagination has the same dual role. An offset can continue a stable search order, but a multi-hop query is vulnerable to change: the graph may be reindexed, a result may have been reordered, or an aggregation may now produce different rows. For that reason, query_graph cursors bind more than an integer position. They carry the project and query shape, an opaque graph generation, and a digest of the materialized result. If the underlying result changes, the cursor is rejected as stale and the agent must rerun the original query. Continuing against a new result would silently mix two graph states.
The stale-cursor behavior is easy to interpret as friction until the alternative is made explicit. Suppose the first page says a function has callers A and B, then a reindex removes or changes one relationship before the second page is requested. If the second page is read as though it belonged to the old materialization, the final explanation may contain a combination that never existed in one graph state. Failing closed makes the state transition visible. The agent can then rerun the query and explain the new result consistently.
Relationship types are another boundary that should survive compression. CALLS represents a source-level invocation relationship. HTTP_CALLS and ASYNC_CALLS represent service-shaped operations with route or topic evidence. USAGE can record identifier use when a unique callable target is not proven. CALL_REFERENCE is more specific: it represents a supported reference to one exact callable value. INHERITS and IMPLEMENTS describe type relationships rather than execution. Flattening these into a single “connected to” column would make a compact response shorter but less truthful.
A route example shows why. A local handler may be connected to a route registration, and a client function may make an HTTP call to a path-shaped target. Those edges can be related when investigating a service boundary, but they do not mean that the client directly calls the handler as a local function. The graph gives the agent a way to preserve that distinction. The final explanation can say that a request crosses a route boundary and identify the handler on the other side, rather than inventing an ordinary call edge between two symbols.
get_code_snippet completes targeted retrieval. It accepts the qualified name discovered by search, can outline a large container instead of returning an unbounded body, and can page source or members. This is a deliberate compromise between two bad extremes: returning too little to understand a symbol and dumping an entire large class into the agent's context. A declaration outline can establish what a file contains; an exact source range can establish what a chosen definition does.
When graph coverage is incomplete, search_code or an ordinary text search remains a legitimate fallback. Literal searches are often the right tool for non-code text, unusual strings, or a flagged source range. The fallback is not an admission that graph-first exploration failed. It is the consequence of assigning different evidence to different tools. A graph is strongest when the question concerns identity and relationships; source text is indispensable when the answer depends on details the graph does not model or could not parse.
check_index_coverage makes that decision more explicit. It can inspect exact repository-relative paths or directory scopes and report freshness against the index's recorded metadata. It distinguishes a path outside the project, a stale or unavailable record, a partial parse, an unusable parse, a skipped file, and a deliberate exclusion. The recommended action changes with the state: read flagged ranges, read the source directly, change ignore rules and reindex, or use the graph with a best-effort caveat.
The most important phrase in this contract is the caveat: no recorded issue is not proof of completeness. Coverage metadata records known problems; it cannot establish that an unflagged parser construct was understood perfectly. This matters most for negative conclusions. A positive search result gives the agent a candidate to verify. A zero-result search may reflect a spelling, scope, label, stale index, excluded path, unsupported construct, or a genuinely absent symbol. The more consequential the absence claim, the more important it is to inspect scope and source.
Consider a question such as “Does this function have any callers?” A disciplined path finds the exact qualified symbol, traces inbound CALLS relationships, checks whether the trace is truncated, and checks coverage for the defining and relevant dependent scope. If the graph is clean and the query is not truncated, the result is useful evidence. If the defining file was partially parsed or the relevant callers lie in an excluded subtree, “no callers” is not established. The correct answer may instead be “no recorded callers in the indexed scope; direct source review is required.”
The same discipline applies to change impact. A change-oriented query can map Git differences to affected files and symbols, then a trace can examine callers, callees, and tests. But the impact result still inherits the index's scope and coverage. A compact blast-radius list is a starting point for review, not a proof that every dynamic or unindexed dependency has been found.
Graph navigation therefore has a simple but demanding shape: orient before narrowing, preserve relationship meaning, retrieve exact source for material claims, and treat pagination and coverage state as evidence about the answer. The interface is efficient because it lets the agent reuse structural work. It remains trustworthy only when the agent does not confuse a well-formed page with an exhaustive model.
The resulting question is larger than tool selection. A graph answer is supported by a chain: the relevant source was in scope, the symbol identity was resolved, the relationship type matched the question, the response was not silently truncated, and the exact source or coverage state did not contradict the interpretation. The next step is to make that chain explicit and decide how confidence should change when one of its links is missing.