TermAl

Feature Brief: Code Navigation MCP

Status

Proposed.

This brief defines an MCP server that gives agent sessions a fast, semantic way to navigate large source trees. The first target is very large C# workspaces with a few thousand projects, but the design should leave room for language adapters beyond C#.

Related:

Problem

Agents can already read files, run shell commands, and use text search. That is enough for small repositories, but it becomes inefficient in a large enterprise codebase:

For a C# workspace with a few thousand projects, the missing capability is a compact, structured code intelligence layer that can answer source-navigation questions without dumping large files into the transcript.

Goals

Non-goals for v1

Core Idea

Add a Code Navigation MCP server for each active local workspace:

workspace files
  -> lightweight workspace discovery
  -> persisted path/text/project/symbol index
  -> bounded language-service cache
  -> search and ranking layer
  -> MCP tools consumed by agents

The MCP server should be read-only by default. Agents still use their normal file-editing and terminal tools to make changes, while the navigation MCP gives them the shortest reliable path to the right files, symbols, tests, and dependency edges.

TermAl should supervise one shared navigation server per workspace root, not one per agent process. Parent sessions and delegated Claude/Codex/Cursor/Gemini sessions should attach to that shared server and reuse its persisted index and warm caches. Remote workspace support is separate because index placement and path mapping are different problems.

The primary design rule is:

Return the smallest precise context that lets an agent take the next step.

The MCP should expose three distinct layers of source understanding. Agents should use the cheapest layer that can answer the question, while preferring compiler-backed answers for code facts.

Text finds bytes. Syntax finds source shape. The semantic model finds program meaning. search_symbol may use name matching to locate candidates, but its results should still carry semantic identity: symbol id, kind, project, target framework, and declaration spans.

The default preference should be:

semantic model for code facts
syntax / AST for file structure and spans
indexed text lookup for literal text questions
shell rg/grep only for unavailable, unindexed, or non-source scopes

Agents should still use shell rg or grep when the MCP server is unavailable, the scope is outside the workspace index, the query targets binary-adjacent logs or transient build/generated output, or the question is outside source navigation. In an indexed workspace, shell text search should not be the first step for code facts unless server_capabilities() or index metadata shows that the relevant semantic layer is unavailable, partial, or stale.

Agent instructions should be scoped to the layers and tools reported by server_capabilities(). During partial rollout, unsupported languages, failed project loads, dirty or broken files, missing generated source, linked files, multi-targeting differences, reflection, dependency-injection conventions, and string-based dispatch should be treated as explicit degraded states rather than silently downgraded to exact semantic facts.

Agent Navigation Workflow

The MCP should make the semantic layer the agent’s default reflex for code facts, not a fallback after broad text search.

When the task already names a symbol, stack frame, build error, route, or file, the agent should follow a surgical flow:

repo_overview()
  -> search_symbol(...) or symbol_at(path, line, column?)
  -> definition(target)
  -> outline(path) for large files before any whole-file read
  -> references(target, group_by = "project")
  -> related_tests(target) before behavior changes
  -> source_context(path, spans, contextLines, maxBytes)

When the task is exploratory, the agent should first find the project cluster:

repo_overview()
  -> project_graph(project_or_prefix, depth = 2)
  -> search_symbol(...) for entrypoints such as controllers, handlers, services
  -> context_pack(target, budget)
  -> drill into specific symbols with the surgical flow

The rule to encode in agent instructions is:

Never read a large file before outline or definition identifies the relevant span.

For agent instructions, a “large file” should mean any source file above roughly 400 lines or 24 KB, unless the server reports a lower language-specific threshold. Agents may still use shell rg for unindexed generated output, binary-adjacent logs, or when the MCP server is unavailable, but the default path for source, project, and symbol navigation should be the MCP.

Tool Surface

The surface should be divided into phase-gated capability levels so agents know which workflows are safe. The server should expose its effective capabilities through normal MCP tool discovery and, optionally, a small server_capabilities() tool that reports supported languages, index status, available tools, response budgets, and disabled features.

For implementation planning, core tools are required for the source-navigation MVP, higher-level tools should follow once the core signals are reliable, and expanded tools are optional or later-phase capabilities.

Core tools

These tools should ship together because they cover the minimum useful large-repository navigation loop and are the minimum agent-usable default path for indexed source navigation.

server_capabilities()

Returns supported languages, enabled tools, index status, response budgets, latency/deadline defaults, and feature flags for partial phase availability. Agents can use normal MCP tool discovery too, but a compact capability summary helps them branch without probing broad queries.

repo_overview()

Returns a compact workspace map:

find_file(name_or_glob, filters, limit, cursor)

Cheap path-only lookup for cases where symbol semantics are unnecessary or the agent only has a filename from a log, build output, or human prompt.

search_text(query, filters, limit, cursor)

Fast text search with ranking and filters. This is the indexed text lookup layer: it should behave like rg, but with structured output, stable line/byte offsets, result budgets, and workspace-aware ranking. For code facts, agents should use the semantic layer instead: search_symbol, definition, and references. A search_text hit is evidence that bytes matched; it is not proof of symbol identity, ownership, or reference semantics.

Useful filters:

Ranking should prefer exact-token matches, handwritten source, project-local matches, recently changed files when relevant, and production/test scope matching the query. It should demote build output, vendored dependencies, generated files, designer files, and broad package/cache directories by default.

outline(path, depth)

Returns a syntactic file map with names and spans, but without method bodies or cross-file resolution. This is the primary token-saver for large C# files and should be cheap enough to call before any whole-file read.

depth should be bounded and explicit:

Useful outline fields:

source_context(path, spans, contextLines, maxBytes)

Reads bounded live source around one or more returned spans. This is the bridge between navigation and editing: outline, definition, references, and symbol_at identify spans; source_context returns only the selected live source with line numbers, byte limits, truncation metadata, and freshness metadata. Batch span reads should be supported for common fan-out workflows.

search_symbol(query, kinds, filters, limit, cursor)

Finds symbols by name, partial name, fully qualified name, prefix, containing type/namespace, or regex/fuzzy query. The match mode should be explicit so the server does not have to guess whether a query is exact or fuzzy.

Useful symbol kinds:

attribute usage is not a Roslyn symbol kind; it should be treated as an adapter-level search category over attributes attached to symbols.

symbol_at(path, line, column)

Reverse lookup for stack traces, build errors, grep hits, diagnostics, and diff hunks. Returns the smallest containing symbol plus any enclosing type, namespace, project, and stable symbol id.

definition(target)

Returns the exact definition for a symbol or source location, including all declaration spans for partial types/members, path, line span, signature, containing type, containing project, and a small snippet.

references(target, filters, group_by, limit, cursor)

Returns exact references for a symbol or source location. For C#, this should be Roslyn-backed rather than raw text search.

Because this is the most explosive query in a workspace with a few thousand projects, it should default to grouped summaries and bounded locations rather than a global flat dump. Filters should include project glob, production/test, generated/handwritten, reference kind, caller type, target framework, and whether to include interface or override expansion. Reflection, string-based lookup, DI container wiring, and expression-tree-only edges should be reported as heuristic or excluded by default.

Useful grouping:

project_graph(project, depth, direction)

Returns project references, package references, target frameworks, solution membership, and dependency direction.

Useful options:

projects_containing(path)

Returns all projects that compile or link a file. This matters for shared files and linked <Compile Include="..."> entries and is needed before interpreting diagnostics or symbol identity for files included by multiple projects.

Higher-level tools

context_pack(target, budget)

Builds a compact bundle of relevant source context for an agent turn. This should be built on top of reliable definitions, references, outlines, project graph data, and related-test signals rather than forwarding raw top-N matches. The request should allow a task hint such as investigate_bug, rename, add_feature, review, or explore, plus must_include_paths and exclude_paths for steering.

Expanded tools

implementations(target, filters, limit, cursor)

Finds implementations of interfaces, abstract members, virtual members, partial types, and overridden methods.

callers(target, depth, filters, limit, cursor)
callees(target, depth, filters, limit, cursor)

Walks the call graph around a method, constructor, property accessor, or delegate invocation. These tools should be on-demand, scope-limited, and confidence-labeled; precomputing whole-repo call graphs is not required for the core source-navigation workflow.

type_hierarchy(target, direction, depth)

Returns base types, derived types, implemented interfaces, and implementing types.

related_tests(target, limit)

Finds likely tests through exact references, project relationships, naming patterns, test framework metadata, and recent git history.

namespace_layout(prefix, filters, limit, cursor)
projects_under(prefix, filters, limit, cursor)

Lists namespaces, folders, and projects under a product/team prefix for cold-start exploration.

dependency_path(from_project, to_project, limit)

Returns the shortest project-reference or package-reference paths explaining why one project depends on another.

config_lookup(key, filters, limit, cursor)

First-class lookup across configuration files such as appsettings*.json, Directory.Build.props, Directory.Build.targets, global.json, and NuGet.config.

generated_for(target)

Explains generated symbols or files, including the producing source, attribute, source generator, or MSBuild target where detectable.

recent_changes(scope, since, limit)

Git-aware navigation for current work, returning recent files, symbols, and projects related to a scope.

xml_doc(target)

Returns XML documentation summaries without requiring a source snippet read.

diagnostics(scope)

Returns current build, analyzer, nullable, or test diagnostics for a file, project, solution, or changed-file set.

impact(target, change_kind, budget)

Summarizes likely blast radius before a change:

Batch forms should exist for common fan-out calls:

batch_definition(targets)
batch_references(targets, filters, group_by, limit)
batch_outline(paths, depth)
batch_source_context(requests)

Result Shape

Every source result should be stable, line-addressable, and small. All list responses should include a short summary, cursor metadata, and index freshness metadata so the agent can decide whether to tighten filters, page, or trust the answer.

All tool responses should enforce both item and byte/token budgets. Defaults should be tuned for agent turns rather than human IDE panes:

Targets should be accepted in any of these forms:

{ "symbolId": "roslyn:Billing.Application:net8.0:M:Billing.Application.InvoiceService.CreateInvoiceAsync(...)" }
{ "path": "src/Billing/Application/InvoiceService.cs", "line": 42, "column": 17 }
{ "qualifiedName": "Billing.Application.InvoiceService.CreateInvoiceAsync" }
{
  "projectPath": "src/Billing/Application/Billing.Application.csproj",
  "targetFramework": "net8.0",
  "documentationCommentId": "M:Billing.Application.InvoiceService.CreateInvoiceAsync(...)"
}

Symbol ids are stable within an indexVersion. If an index refresh invalidates an id, tools should return a clear stale-id error and, where possible, a replacement candidate. For C#, the stable identity should be based on Roslyn’s documentation comment id or metadata name plus project identity and target framework, not a display name alone.

{
  "path": "src/Billing/Application/InvoiceService.cs",
  "line": 42,
  "endLine": 88,
  "snippetLines": [38, 54],
  "project": "Billing.Application",
  "projectPath": "src/Billing/Application/Billing.Application.csproj",
  "assemblyName": "Billing.Application",
  "targetFramework": "net8.0",
  "configuration": "Debug",
  "symbolId": "roslyn:Billing.Application:net8.0:M:Billing.Application.InvoiceService.CreateInvoiceAsync(...)",
  "documentationCommentId": "M:Billing.Application.InvoiceService.CreateInvoiceAsync(...)",
  "kind": "method",
  "accessibility": "public",
  "containingNamespace": "Billing.Application",
  "containingType": "InvoiceService",
  "signature": "Task<Invoice> CreateInvoiceAsync(CreateInvoiceCommand command)",
  "isGenerated": false,
  "isTest": false,
  "score": 0.93,
  "confidence": "exact",
  "partial": false,
  "truncated": false,
  "snippet": "public async Task<Invoice> CreateInvoiceAsync(...)",
  "indexVersion": "workspace-sha-or-index-id",
  "indexStatus": "fresh"
}

References should default to grouped output, not a flat global dump:

{
  "summary": "47 references across 12 projects (8 production, 4 test).",
  "totalReferences": 47,
  "groupBy": "project",
  "groups": [
    {
      "key": "Billing.Api",
      "count": 4,
      "summary": "4 callers in InvoicesController and BillingWebhook.",
      "locations": []
    }
  ],
  "truncated": true,
  "partial": false,
  "nextCursor": "...",
  "indexVersion": "workspace-sha-or-index-id",
  "indexStatus": "fresh",
  "coverage": {
    "indexedProjects": 1510,
    "totalProjects": 2400
  }
}

For graph results, return edges rather than prose:

{
  "nodes": [
    {
      "id": "project:Billing.Application",
      "kind": "project",
      "name": "Billing.Application"
    }
  ],
  "edges": [
    {
      "from": "project:Billing.Api",
      "to": "project:Billing.Application",
      "kind": "projectReference"
    }
  ]
}

Confidence Model

The MCP server should label each result with how it was produced:

This matters because agents should treat compiler-backed references differently from best-effort guesses.

Confidence is separate from the navigation layer. For example, indexed text lookup can return fresh or stale results, and semantic tools can return exact, partial, or stale results depending on project load and index state. If exposed as a field, navigationLayer should describe the abstraction used: text, syntax, or semantic.

Every tool response should carry index metadata when freshness could affect the answer:

C# First Index

The C# adapter should use Roslyn and MSBuild semantics instead of regex parsing.

Recommended index inputs:

Workspace discovery must not assume one canonical solution. Large enterprise repos often contain many overlapping solutions, generated solution filters, or no useful solution file at all. The indexer should support:

Recommended symbol data:

Symbol Identity

C# symbol identity must handle overloads, partials, linked files, generated source, and multi-targeted projects. A symbol id should include:

For multi-targeted projects, the default policy should choose a canonical target framework for broad search results while preserving the exact TFM in symbol ids. The canonical TFM should be configurable per workspace. When behavior differs by TFM, tools should return separate results rather than collapsing them.

Partial types and members should share one logical symbol id with multiple declaration spans. Extension methods should be anchored to their declaring static type and method symbol, while search/ranking may expose the extended type as a convenience field.

Storage Model

The first implementation should use a persisted local index rather than rebuilding navigation state inside each agent session. SQLite with FTS5 is the default pragmatic storage choice unless benchmarks show it is insufficient. The schema should include at least:

The index should support single-writer/multi-reader access so multiple agents can query while a background refresh updates stale scopes. The spec should track a rough size budget per million lines of code once benchmark data exists.

Memory And Process Model

A few-thousand-project C# repo cannot assume a retained whole-repo Roslyn compilation. The indexer should separate:

MSBuildWorkspace.OpenSolutionAsync over the entire repo should not be the required startup path. The server should be useful during partial readiness, load project clusters lazily, and never block ordinary TermAl session startup on full semantic indexing.

Source Generators

Generated source must be explicit in both coverage and confidence. The indexer should record whether generated outputs are indexed, which generator or MSBuild target produced them when detectable, and whether a symbol came from generated or handwritten syntax. Generator execution should be cached per project snapshot and should not run per query. When generated output is unavailable, tools should return partial or heuristic instead of silently missing generated symbols.

Recommended semantic edges:

Context Pack Strategy

context_pack is the tool that should feel agent-native. It should synthesize a small navigation result, not merely forward raw search hits.

Example request:

{
  "target": {
    "kind": "symbol",
    "name": "InvoiceService.CreateInvoiceAsync"
  },
  "budget": {
    "maxFiles": 8,
    "maxSnippets": 20,
    "maxTokens": 12000
  }
}

Example response:

{
  "summary": "Invoice creation is owned by Billing.Application and exposed via Billing.Api.",
  "primary": [
    {
      "path": "src/Billing/Application/InvoiceService.cs",
      "line": 42,
      "reason": "Target method definition"
    }
  ],
  "supporting": [
    {
      "path": "src/Billing/Api/InvoicesController.cs",
      "line": 31,
      "reason": "Primary API caller"
    },
    {
      "path": "tests/Billing.Application.Tests/InvoiceServiceTests.cs",
      "line": 18,
      "reason": "Closest direct tests"
    }
  ],
  "risks": [
    "Method is part of API request flow.",
    "Five tests assert current validation behavior."
  ]
}

Ranking should prefer:

The budget algorithm should be deterministic. A hard maxTokens or maxBytes must never be exceeded. If the full pack does not fit, include content in this order and report omitted categories:

The response should distinguish omittedBecauseBudget from partial so agents know whether the pack is complete within its requested scope.

Freshness And Dirty Workspaces

Large indexes cannot be rebuilt on every keystroke. The MCP server needs clear freshness semantics:

TermAl’s file-change awareness can feed the same invalidation model, but the navigation server should still work as a standalone MCP process.

For large C# repositories, Roslyn cold-load can take minutes. Partial readiness is expected. repo_overview should expose which projects are loaded, loading, failed, stale, or not yet indexed so agents can state uncertainty instead of treating partial answers as complete.

Agent Instructions

Workspace setup should include compact instructions similar to:

## Code Navigation MCP

This repository is too large for broad grep-based C# navigation. Use the
read-only Code Navigation MCP before reading files. Prefer MCP search and
symbol tools over shell `rg`/`grep` for source navigation.

Default flow:
1. Call `repo_overview()` before code work and check `indexStatus`.
2. Use the semantic layer for code facts: `search_symbol`, `definition`,
   `references`, and `symbol_at`.
3. Use `search_text` for config keys, route strings, error messages, log
   fragments, comments, and literals.
4. If starting from a stack trace, build error, diagnostic, grep hit, or diff
   hunk, call `symbol_at(path, line, column?)`.
5. Before reading a large file, call `outline(path)`, then `source_context` for
   only the needed spans.
6. Before changing behavior, call `references(target, group_by="project")` and
   `related_tests(target)`.
7. Trust `confidence: exact`; treat `heuristic`, `partial`, and `stale` as
   leads.
8. Keep limits small. Tighten filters before paging broad result sets.
9. Use shell `rg` only when the MCP is unavailable, the relevant layer is
   unavailable or stale, the scope is unindexed, or the query is outside source
   navigation.

Codex and Claude prompts can share the same rules, but TermAl should inject agent-specific wording where needed. In particular, delegated explorer and reviewer sessions should be told that MCP navigation is mandatory before broad file reads, because their value comes from finding the right source slice without spending the parent transcript on exploration.

TermAl Integration

TermAl should supervise the initial implementation as a workspace-scoped MCP server process. A concrete command shape could be:

termal code-nav-mcp --workspace-root <path> --index-root <path> --workspace-id <id>

The backend should own workspace path mapping, process lifecycle, index root selection, and server restart behavior. Agent processes should receive only the MCP descriptor and compact instructions.

Recommended user-facing integration points:

Recommended agent integration:

The integration should fail soft for ordinary chat but fail visible for navigation-heavy code work. If a large C# workspace is detected and the MCP is not attached, the session instructions should say that source navigation is degraded and shell search may be noisy.

Safety And Performance

The server should be optimized for fast, bounded answers:

Warm-query latency targets should be explicit so replacing rg is measurable:

Default exclusions should be concrete and overridable:

The server should avoid hidden writes. Future write-capable tools, if any, must be separate from navigation tools and go through TermAl’s normal approval and file-change protection paths.

Evaluation And Testing

The feature should be validated as an agent workflow, not only as an indexer. Acceptance tests and benchmarks should cover:

Success metrics should include p95 tool latency, index size, memory ceiling, percentage of agent file reads preceded by MCP span navigation, percentage of shell rg calls avoided in large C# sessions, and click/open rates for returned source locations in the human UI.

Phased Plan

Phase 0: Index substrate and workspace discovery

Phase 1: Agent source-navigation MVP

This is the first release that should be attached to Claude/Codex as the preferred source-navigation path. It should ship as a coherent slice:

Phase 2: C# scale hardening

Phase 3: Expanded semantic navigation

Phase 4: Context packs and impact summaries

Phase 5: Human inspection surface

Open Questions