Code Graph

The code graph is a Roslyn-indexed map of the C# codebase — every class, method, property, and constructor, plus the relationships between them. Agents and users query it to find a symbol, trace who calls what, run an impact analysis before a change, and spot dead code, without reading every file.

1. What the index captures

The indexer (CSharpCodeGraphIndexer) parses .cs files with Roslyn (Microsoft's official C# compiler API — it resolves real types and bindings, so call, override, and inheritance edges reflect what the compiler sees rather than what the text looks like) using a two-pass strategy — symbols first, then the relationships between them:

Why it beats grep: a text search for a method name matches every string that happens to share it — comments, unrelated overloads, same-named members on other types. Because the graph resolves invocations through the semantic model, an edge like calls points at the actual method that runs, correctly distinguishing overloads and following overrides and inheritance — not a string match.

Pass 1 — Symbols

Each declaration becomes a symbol row capturing its accessibility, static/async/abstract modifiers, generic parameters, return type, parameter list, the type it belongs to, line number, and a one-line source preview. Symbol kinds:

  • class, interface, struct, enum, delegate
  • method, constructor, property
  • event, field

Pass 2 — Relationships

A second walk resolves edges between symbols using the semantic model:

RelationshipMeaning
callsA method/property/constructor invokes another (includes object creation).
inheritsA type derives from a base class.
implementsA type implements an interface.
overridesA method overrides a base method.
uses_typeA field, property, parameter, or return type references another type.
subscribesA member subscribes to an event (+=).
handlesA handler method is wired to an event.

Generated and build output is skipped: obj/, bin/, node_modules/, and .g.cs / .designer.cs / .AssemblyInfo.cs files.

2. Building & keeping the index fresh

Indexing runs per directory. The indexer registers (or clears and rebuilds) a project, parses every non-excluded .cs file into a Roslyn compilation, runs the two passes inside a transaction, and records statistics (file count, symbol count, relationship count, duration). Re-running re-indexes that project in place. After any index, GET /api/code-graph/stats reports per-kind counts and the last-indexed timestamp.

Indexing is automatic — the graph keeps itself fresh

A background service, CodeGraphWatcher, keeps the graph current so you don't have to remember to re-index. It does three things:

  • Heals on startup. When the app launches it sweeps every registered C# project and re-indexes any whose graph is stale — so a graph that sat untouched for weeks is brought up to date automatically.
  • Re-indexes on edit. While the app runs it watches each registered project's .cs files. After a short quiet window (a debounce, ~12s by default) it re-indexes the project that changed. Rapid saves coalesce into a single re-index; build output (obj/, bin/) and generated files are ignored, so a build doesn't trigger churn.
  • Picks up new projects mid-session. A project created or registered while the app is running (new-project wizard, create_project, or POST /api/projects) is indexed straight away — no restart needed.

A per-project freshness floor (5 min by default) suppresses redundant re-indexes, and a single coordinator (CodeGraphIndexCoordinator) serializes every index — automatic and manual alike — so two runs can never overlap and corrupt the graph.

Manual indexing (still available)

You rarely need to index by hand now, but you can force a refresh on demand with the index_code_graph MCP tool or POST /api/code-graph/index (see below) — useful to index a directory that isn't a registered project, or to refresh immediately rather than waiting for the debounce. Both paths run through the same coordinator as the watcher.

Configuration

The watcher is on by default and tuned with environment variables:

VariableDefaultEffect
MULTITERMINAL_CODEGRAPH_WATCHonSet to 0/false/off to disable auto-indexing entirely (manual indexing still works).
MULTITERMINAL_CODEGRAPH_DEBOUNCE_MS12000Quiet window after the last .cs change before a re-index fires (clamped to a 3–600s range).
MULTITERMINAL_CODEGRAPH_MIN_INTERVAL_MS300000Per-project freshness floor — a project re-indexed more recently than this is skipped.

3. Practical workflows

Worked example: who creates the MessageBroker?

Find a symbol, then trace its callers. The watcher keeps the graph fresh, so step 1 (an explicit index) is usually unnecessary — it's shown here to force an immediate refresh and to demonstrate the tool. The MCP calls and representative (elided) results:

// 1. (optional) Force an immediate index — normally the watcher handles this
index_code_graph(directory: "H:\\DevLaptop\\...\\MultiTerminal",
                 projectName: "MultiTerminal")
   → { "success": true, "projectName": "MultiTerminal",
        "fileCount": 156, "symbolCount": 7841,
        "relationshipCount": 24193, "durationMs": 9120 }

// 2. Find the symbol (exact matches first, then substring)
search_code_graph(query: "MessageBroker", type: "class")
   → results: [
        { "id": 1422, "name": "MessageBroker", "type": "class",
          "file_path": "MCPServer/Services/MessageBroker.cs",
          "line_number": 19, "accessibility": "public" }
      ]

// 3. Trace its callers ("who creates / depends on this?")
get_symbol_callers(symbolName: "MessageBroker")   // or symbolId: 1422
   → results: [
        { "name": "MessageBroker", "member_of": "MultiTerminalRestServer",
          "call_file": "API/MultiTerminalRestServer.cs", "call_line": 94 }
      ]

The caller row points at the exact construction site — API/MultiTerminalRestServer.cs:94, where the REST host does _broker = new MessageBroker(); — so you can jump straight to where the dependency lives (object creation is a calls edge). Resolve callees the same way with get_symbol_callees, or widen to the full transitive set with get_impact_analysis. (The numeric ids and counts above are representative — your exact values depend on the current index.)

Find a symbol

Search by name (exact matches first, then substring), optionally filtered by kind. This returns the symbol's id, file, line, accessibility, signature, and source preview — the starting point for every other query.

Trace callers and callees

Given a symbol, list its direct callers ("who depends on this?") and direct callees ("what does this reach?"). Each result includes the call site's file and line.

Impact analysis before a change

Run a transitive blast-radius query — the recursive set of everything that (in)directly calls a symbol, with cycle detection and a depth cap (default 10). Do this before refactoring a hot method to see what you might break.

Find dead code

List public/internal methods and properties with no incoming calls edge — candidates for removal. Scope it to a single project if you like.

Map a type's hierarchy

Walk the inheritance/implementation tree to see everything that derives from or implements a class or interface.

4. MCP tools

Agents reach the graph through these MCP tools:

ToolPurpose
index_code_graphForce an index (or re-index) of a directory on demand. Registered projects are also indexed automatically by the watcher (see §2).
search_code_graphFind symbols by name, with an optional kind filter.
get_symbol_callersDirect callers of a symbol.
get_symbol_calleesDirect callees of a symbol.
get_impact_analysisTransitive callers (blast radius).
get_inheritance_treeInheritance / implementation tree.
get_dead_codeUnreferenced public/internal members.
get_file_symbolsAll symbols defined in a file.

5. REST API

The same operations are exposed under /api/code-graph (the MCP tools call these):

Method & pathPurpose
GET /api/code-graph/search?query=&type=Search symbols by name (optional kind filter).
GET /api/code-graph/callers?symbolId=&symbolName=Direct callers (resolve by id or name).
GET /api/code-graph/callees?symbolId=&symbolName=Direct callees.
GET /api/code-graph/impact?symbolId=&maxDepth=Transitive impact (default depth 10).
GET /api/code-graph/inheritance?symbolId=Inheritance / implementation tree.
GET /api/code-graph/dead-code?projectId=Unreferenced public/internal members.
GET /api/code-graph/file-symbols?filePath=All symbols in a file.
POST /api/code-graph/indexIndex a directory: body { directory, projectName }.
GET /api/code-graph/statsIndex statistics and last-indexed time.

Callers/callees/impact/inheritance accept either a numeric symbolId or a symbolName (resolved to an id). When the graph hasn't been built yet the endpoints return 503.

6. Storage

The graph lives in the same SQLite database as the rest of MultiTerminal, in three core tables managed by CodeGraphDatabase and queried by CodeGraphQuery:

TableHolds
cg_projectsOne row per indexed project (name + optional .csproj path).
cg_symbolsEvery extracted symbol with its metadata.
cg_relationshipsThe typed edges between symbols, with the source file and line of each edge.

Callers, callees, impact, inheritance, and dead-code queries are SQL over these tables — the transitive ones use recursive CTEs with cycle detection — so results are fast and reproducible.