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:
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.
rg/grep as the default source-navigation reflex for Claude,
Codex, and delegated explorer/reviewer agents in indexed workspaces.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.
find_file and search_text answer path and text
questions with grep-like matching, stable line/byte offsets, workspace-aware
ranking, and response budgets. This is the right layer for config keys, route
strings, error messages, log fragments, comments, and literals: questions
that are genuinely about where exact text appears.outline and syntax-derived spans answer
structural questions inside a file without requiring a whole-file read or
cross-file symbol resolution. This is the right layer for finding containing
types, members, local functions, top-level declarations, and source spans
before reading or editing a large file. The agent-facing surface should
expose syntax-derived summaries and spans, not raw syntax-tree dumps, unless
a later language adapter has a specific, bounded use case for raw nodes.symbol_at, search_symbol, definition,
references, project_graph, projects_containing, and later tools such as
implementations, callers, callees, type_hierarchy, and
related_tests answer program-meaning questions through Roslyn/MSBuild or
the relevant language adapter. This is the right layer for definitions,
exact references, overloads, partial declarations, generated code ownership,
project membership, dependency direction, and impact analysis. Project-graph
facts come from MSBuild/project evaluation rather than Roslyn symbols, but
they belong in the same layer because they describe program structure and
ownership rather than text occurrence.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.
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.
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.
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:
1: namespaces and top-level types2: type members, excluding method bodies3: local functions, top-level statements, and lambda anchors where the
language adapter can report them cheaplyUseful 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:
upstream, downstream, or bothedges or a compact tree summaryprojects_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.
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.
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)
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:
truncated: true means the result set was larger than the response budgetpartial: true means the server hit a deadline, stale dependency, failed
project load, or partial index and is returning an incomplete answernextCursor when more exact results are availablesource_context callTargets 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"
}
]
}
The MCP server should label each result with how it was produced:
exact - produced by the compiler or language serviceindexed - produced by a persisted index that may need freshness checksheuristic - inferred from naming, directory layout, git history, or text
searchstale - produced from an index older than the current file statepartial - produced while the workspace index is still buildingThis 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:
indexVersionindexStatus: fresh, building, partial, or stalecoverage: indexed projects/files compared with discovered totalpartialReason, when partial: truedeadlineMs and elapsedMs for calls that may be slowThe C# adapter should use Roslyn and MSBuild semantics instead of regex parsing.
Recommended index inputs:
.sln, .slnx, .slnf, and discovered .csproj filesDirectory.Build.propsDirectory.Build.targetsglobal.jsonNuGet.configWorkspace 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:
.slnf solution filters and generated solution filesRecommended symbol data:
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.
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.
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.
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 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:
must_include_pathsThe response should distinguish omittedBecauseBudget from partial so agents
know whether the pack is complete within its requested scope.
Large indexes cannot be rebuilt on every keystroke. The MCP server needs clear freshness semantics:
refresh(scope) in later phasesTermAl’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.
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 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:
config.mcp_servers when the workspace
is local and indexable--mcp-configThe 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.
The server should be optimized for fast, bounded answers:
limit and cursorpartial: true with
exact results found so far rather than blocking other agentsWarm-query latency targets should be explicit so replacing rg is measurable:
repo_overview: p95 under 100 ms from the persisted indexfind_file: p95 under 100 mssearch_text: p95 under 250 ms for scoped queriesoutline: p95 under 100 ms from syntax/index datasearch_symbol: p95 under 250 ms from the symbol indexdefinition: p95 under 250 ms when the target project is already loaded or
the definition span is in the persisted indexreferences: p95 under 1 second for scoped, grouped queries; broader queries
should return grouped summaries plus cursors or partial resultsDefault exclusions should be concrete and overridable:
**/.git/****/{bin,obj,packages,node_modules,TestResults}/****/.vs/****/.idea/****/*.{g,designer,generated}.csThe 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.
The feature should be validated as an agent workflow, not only as an indexer. Acceptance tests and benchmarks should cover:
.slnf, overlapping solutions, and solution-less project
discoverynet48;net8.0Success 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.
.sln, .slnx, .slnf, loose .csproj, source roots, test roots,
generated/build-output conventions, and default exclusionsserver_capabilities, repo_overview, find_file, and search_text
for internal testingThis is the first release that should be attached to Claude/Codex as the preferred source-navigation path. It should ship as a coherent slice:
repo_overviewfind_filesearch_text with grep-parity flags and workspace rankingproject_graphprojects_containingoutlinesource_contextsearch_symbolsymbol_atdefinitionreferencesimplementationscallers and callees as on-demand, scoped graph queriestype_hierarchyrelated_testsdependency_path, namespace_layout, projects_under, and
config_lookupcontext_packimpactxml_docdiagnostics reads last build output, live Roslyn analyzers, or
both as separate tools