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.
On This Page
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,delegatemethod,constructor,propertyevent,field
Pass 2 — Relationships
A second walk resolves edges between symbols using the semantic model:
| Relationship | Meaning |
|---|---|
calls | A method/property/constructor invokes another (includes object creation). |
inherits | A type derives from a base class. |
implements | A type implements an interface. |
overrides | A method overrides a base method. |
uses_type | A field, property, parameter, or return type references another type. |
subscribes | A member subscribes to an event (+=). |
handles | A 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
.csfiles. 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, orPOST /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:
| Variable | Default | Effect |
|---|---|---|
MULTITERMINAL_CODEGRAPH_WATCH | on | Set to 0/false/off to disable auto-indexing entirely (manual indexing still works). |
MULTITERMINAL_CODEGRAPH_DEBOUNCE_MS | 12000 | Quiet window after the last .cs change before a re-index fires (clamped to a 3–600s range). |
MULTITERMINAL_CODEGRAPH_MIN_INTERVAL_MS | 300000 | Per-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:
| Tool | Purpose |
|---|---|
index_code_graph | Force an index (or re-index) of a directory on demand. Registered projects are also indexed automatically by the watcher (see §2). |
search_code_graph | Find symbols by name, with an optional kind filter. |
get_symbol_callers | Direct callers of a symbol. |
get_symbol_callees | Direct callees of a symbol. |
get_impact_analysis | Transitive callers (blast radius). |
get_inheritance_tree | Inheritance / implementation tree. |
get_dead_code | Unreferenced public/internal members. |
get_file_symbols | All symbols defined in a file. |
5. REST API
The same operations are exposed under /api/code-graph (the MCP tools call these):
| Method & path | Purpose |
|---|---|
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/index | Index a directory: body { directory, projectName }. |
GET /api/code-graph/stats | Index 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:
| Table | Holds |
|---|---|
cg_projects | One row per indexed project (name + optional .csproj path). |
cg_symbols | Every extracted symbol with its metadata. |
cg_relationships | The 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.