A proof-of-concept tool that gives AI coding assistants semantic understanding of .NET solutions via Roslyn. It provides twelve semantic analysis commands, two workspace-free discovery commands, plus an optional MCP stdio server that keeps one refreshable workspace warm across repeated calls.
This is a prototype, not a production framework. See
docs/prototype.md for scope, non-goals, and known
limitations.
| Command | Purpose |
|---|---|
find-symbol <name> |
Where a type/method/property is defined. |
find-references <name> |
All usages of a symbol. |
find-implementations <name> |
Types implementing an interface / deriving a base class. |
find-callers <method> |
Methods that call a method. |
find-callees <method> |
Methods and constructors called by a method. |
find-overrides <method> |
Overrides of a virtual/abstract method. |
get-type-hierarchy <type> |
Base types, interfaces, derived types. |
get-public-api <type> |
Public members of a type. |
get-symbol-detail <name> |
Signature, parameters, return type, XML doc. |
get-symbol-context <name> |
Bounded detail, references, and relationships for one symbol. |
get-project-graph |
Solution project dependency tree. |
get-diagnostics |
Compiler diagnostics for a file/project/solution. |
list-commands |
Machine-readable command catalog and global option schema. |
describe-command <command> |
Argument and option schema for one CLI or MCP command. |
list-commands and describe-command do not discover or load a solution. Use
them when an agent needs the exact command contract rather than parsing help
text.
Target and candidate payloads include dnv2 symbolId values built from
assembly identity, documentation ID, kind, fully qualified signature, and a
deterministic metadata/source location fallback. Pass them back with
--symbol-id to resolve same-named symbols exactly across source projects and
metadata assemblies.
Run dotnet-navigator mcp to start a stdio MCP server. By default the server
starts immediately, exposes tools/list, and loads the Roslyn workspace in the
background. Semantic tools return schema-valid status: "loading" payloads until
the first generation is ready. The coordinator tracks workspace states
not-started, loading, ready, refreshing, stale, failed, and
stopping, plus active generation, timings, failures, and retry metadata.
Workspace refreshes build a fresh candidate workspace with empty caches and then
publish it as a new generation. In atomic swap mode, calls already using an old
generation finish safely and calls during refresh continue using the previous
generation. In blocking swap mode, semantic calls during refresh return
structured loading. A failed candidate load leaves the active generation in
place and reports stale/failed status instead of crashing MCP startup.
# Small default surface with common navigation, context, graph, status, and refresh.
dotnet-navigator mcp --solution ./MyApp.slnx
# Every tool except the expensive compiler-diagnostics tool.
dotnet-navigator mcp --profile heavy
# Start from one or more categories, add tools, then remove tools last.
dotnet-navigator mcp \
--categories navigation \
--include-tools get-project-graph \
--exclude-tools find-overrides
# Old startup behavior for deterministic local checks.
dotnet-navigator mcp --workspace-load blocking --refresh-strategy manual| Profile | Exposed tools |
|---|---|
light (default) |
find-symbol, find-references, get-symbol-detail, get-symbol-context, get-project-graph, refresh-workspace, get-workspace-status |
heavy |
All navigation/context tools, get-project-graph, refresh-workspace, and get-workspace-status; excludes expensive diagnostics. |
full |
All twelve analysis tools plus refresh-workspace and get-workspace-status. |
Categories are navigation (symbol/type/call/context tools) and analysis
(get-project-graph, get-diagnostics); both include refresh-workspace and
get-workspace-status.
--categories replaces the profile
baseline. --include-tools adds named tools, then --exclude-tools removes
named tools and therefore wins conflicts. Lists are comma-separated; unknown
profiles, categories, or tool names are usage errors.
MCP lifecycle options are --workspace-load <background|blocking> (default
background), --refresh-strategy <polling|manual> (default polling),
--refresh-interval <seconds> (default 30), and
--refresh-swap <atomic|blocking> (default atomic). Polling fingerprints
solution/project/source inputs, SDK-globbed source/project files, common ancestor
MSBuild/NuGet/editorconfig/global markers (Directory.Build.*,
Directory.Packages.props, NuGet.config, global.json, .editorconfig) from
project folders up to the nearest repository boundary or filesystem root, and
does nothing when unchanged. Explicit custom MSBuild imports outside those
bounded roots are not exhaustively discovered; put shared imports under the repo
root or run a forced refresh after changing them.
refresh-workspace accepts force (default false), wait (default false),
and optional timeoutMilliseconds. Its status is one of accepted,
already-running, unchanged, completed, failed, or cooldown.
wait: true joins an existing refresh; refreshes are coalesced so only one load
runs at a time.
The MCP tool is annotated read-only because it mutates only dotnet-navigator's in-process Roslyn workspace cache. It does not write files, change projects, call external services, or mutate user/application state.
| Option | Meaning |
|---|---|
--solution <sln|slnx|dir> |
Solution file or directory (default: discover from CWD, 3 levels deep). |
--project <name> |
Restrict analysis to a project by exact name. Restricts returned locations (references, callers, implementations, overrides, derived types, diagnostics) to that project; target symbols declared elsewhere are still resolved. Unknown project => exit 2. |
--format <json|text> |
Output format (default: json). |
--max-results <1..1000> |
Cap list-producing commands, including get-type-hierarchy derived types and get-public-api members (default 50). |
--brief |
Omit nonessential fields from symbol, reference, public API, symbol-detail, and project-graph results. |
Command options are validated against a per-command allowlist: an unknown option,
or an option that is only valid on a different command (e.g. --kind on
get-project-graph), is a usage error (exit 2).
| --help, -h | Show help. Follow with a command name for command help. |
| --version | Show version. |
- stdout carries exactly one result document (a single-line JSON object) or human text for one-shot commands. In MCP mode stdout is reserved exclusively for the JSON-RPC transport.
- JSON result contracts are versioned with
schemaVersion. Symbol-targeting commands includestatus,message,target,candidates, and workspace metadata where applicable. Status values distinguishresolved,resolved-no-results,ambiguous,not-found,dependency-not-indexed,loading,failed, andstale. - Ambiguous target resolution is never silently satisfied by the first match.
Retry with supported disambiguators returned by the contract:
--symbol-id,--file/--line,--type,--signature,--arity,--kind, or--metadata true. - All logs, workspace warnings, and errors go to stderr. In normal mode there are no success logs.
- List results are bounded deterministically; responses that support it include
a
totalFoundfield reporting the unbounded count.get-type-hierarchyreportsderivedTypesCount/derivedTypesTotalFoundfor its boundedderivedTypes;get-public-apireportscount/totalFoundfor its boundedmembers. - Brief results preserve identities, solution-relative locations, counts, and
totals. Full output remains the default. MCP exposes the same optional
briefparameter on tools whose payload changes. - File paths are solution-relative and forward-slashed.
--file/--pathmatching is separator-anchored, soOrder.csdoes not matchPriorityOrder.cs. find-symbol --kind typematches any named type;--kind classis class-only.- Symbol matching normalizes attribute suffixes, generic arity/query syntax, records, constructors, extension/reduced methods, operators, indexers, and partial declarations where Roslyn exposes a stable symbol identity.
- References/call relationships carry target identity and can resolve source
symbols plus referenced-assembly metadata symbols when Roslyn binds them from
the loaded project compilations. Source candidates win ordinary unqualified
lookup; broad metadata candidates are added only when source has no exact
candidate or when the query uses a qualified name,
--symbol-id, or--metadata true. Metadata results expose origin and assembly; transitive NuGet package provenance is not reconstructed. - MCP analysis calls lease the current workspace generation; freshness checks are
handled by polling or explicit refresh, not by every semantic call. Added or
deleted SDK-globbed source files and common project inputs trigger polling
refresh when
--refresh-strategy pollingis active. If refresh fails, calls keep using the last valid generation and reportworkspace.status: "stale"withlastRefreshFailure. get-workspace-statusreports cache and timing metrics: cached compilations, indexed metadata assemblies, latest refresh-check milliseconds, refresh milliseconds, and metadata-index milliseconds. These are direct instrumentation fields used by the test harness rather than a future placeholder.get-project-graphincludes project references plus package/framework dependency metadata from restoredproject.assets.json; when assets are unavailable, the project reportsdependency-not-indexedinstead of silently returning empty dependency lists.get-diagnosticsomits generated/intermediate outputs (obj/,*.g.cs,AssemblyInfo.cs,*.GlobalUsings.g.cs, …) so source diagnostics dominate.
| Code | Meaning |
|---|---|
0 |
Success, including empty result sets. |
1 |
Internal failure. |
2 |
Usage error: unknown command, invalid option, or unknown --project. |
3 |
Solution discovery or workspace load failure. |
130 |
Cancelled (Ctrl+C). |
Repository-local evals live under evals/.
make eval-scriptedruns deterministic black-box MCP protocol and semantic cases with no model access.make eval-agentkeeps the legacy OpenAI-compatible Chat Completions loop for comparison.make eval-piruns the Pi SDK-backed agent eval. It buildsevals/pi-runner, starts a fresh dotnet-navigator MCP subprocess per case, exposes only MCP-discovered tools to a fresh in-memory Pi session, and scores the same strict final JSON contract asagentmode.
Pi evals use existing Pi auth/subscription state. They do not accept API keys on argv and do not install or load Pi extensions, skills, context files, or built-in file/shell tools. Default model is EVAL_PI_MODEL=openai-codex/gpt-5.4-mini, which is OpenAI Codex through Pi, not OpenCode, and avoids GitHub Copilot credits. Users can override it with EVAL_PI_MODEL; optional knobs are EVAL_PI_PROVIDER, EVAL_PI_THINKING, EVAL_PI_REPEATS, EVAL_PI_MAX_CALLS, and --case-timeout-seconds.
The Pi SDK host uses exact local npm package pins at @earendil-works/pi-coding-agent@0.82.1; it does not execute a globally installed pi CLI, so a local CLI at 0.80.6 can coexist without changing eval runtime behavior.
Build the package and install it into an isolated tool path:
dotnet tool restore
dotnet pack -c Release -o artifacts/packages
# Install into a local tool manifest (recommended) ...
dotnet new tool-manifest # if you do not have one
dotnet tool install DotNetNavigator.Tool \
--version 0.1.0-alpha.2 \
--add-source ./artifacts/packages
# ... or into an isolated global tool path:
dotnet tool install DotNetNavigator.Tool \
--version 0.1.0-alpha.2 \
--add-source ./artifacts/packages \
--tool-path /tmp/dnnav-tools
/tmp/dnnav-tools/dotnet-navigator --version# Discover the solution from the current directory and list projects (JSON).
dotnet-navigator get-project-graph
# Point at a specific solution and find implementations of an interface.
dotnet-navigator find-implementations IOrderRepository --solution ./MyApp.slnx
# Human-readable output.
dotnet-navigator find-symbol OrderService --format text
# Bound a large result set and read the true total from `totalFound`.
dotnet-navigator find-references GetByIdAsync --max-results 20
# Omit snippets for a smaller reference payload.
dotnet-navigator find-references GetByIdAsync --brief --max-results 20
# Scope analysis to a single project.
dotnet-navigator get-public-api Order --project MyApp.Domain
# Symbol detail without opening the file.
dotnet-navigator get-symbol-detail CancelOrderAsync --type OrderService
# Reuse an ambiguity candidate exactly.
dotnet-navigator get-symbol-detail Append --symbol-id '<candidate symbolId>' --metadata true
# Resolve a framework metadata type by qualified name.
dotnet-navigator get-symbol-detail System.Text.StringBuilder
# Project graph dependency lists come from restored NuGet assets; missing assets
# are reported per project as dependency-not-indexed.
dotnet-navigator get-project-graph
# One bounded call for detail, references, callers/callees, and relationship metadata.
dotnet-navigator get-symbol-context CancelOrderAsync --type OrderService --max-results 10
# Compiler diagnostics for one project.
dotnet-navigator get-diagnostics --scope project --path MyApp.Domain --severity warningmake help # list targets
make build # dotnet build -c Release
make test # dotnet test -c Release
make format # csharpier format .
make check # csharpier check .
make pack # dotnet pack -c Release -o artifacts/packagesThe repository includes a black-box MCP eval harness in
evals/DotNetNavigator.Evals. It launches the built tool as a stdio MCP
subprocess, performs real initialize/tool discovery/tool calls through the
official MCP client, scores curated fixture oracles from
tests/DotNetNavigator.Tests/TestData/SampleSolution, and writes JSON reports
under artifacts/evals/. The eval project is not referenced by the NuGet tool
package.
make eval-scripted
# Agent mode uses a fresh OpenAI-compatible Chat Completions conversation per case.
EVAL_BASE_URL=https://example.test \
EVAL_API_KEY=... \
EVAL_MODEL=... \
make eval-agent
# Inspect the MCP server manually with the official inspector.
npx --yes @modelcontextprotocol/inspector --cli \
dotnet src/DotNetNavigator/bin/Release/net10.0/DotNetNavigator.dll \
mcp --solution tests/DotNetNavigator.Tests/TestData/SampleSolution/SampleSolution.slnx \
--profile full --workspace-load blockingScripted evals require no model access and are intended for CI. Agent evals may
spend money; configure repeats with EVAL_REPEATS and deterministic sampling
with EVAL_TEMPERATURE=0 when the provider supports it. Add cases by editing
evals/DotNetNavigator.Evals/cases.json; derive expected facts manually from
the fixture source, not from dotnet-navigator output.
Agent eval API keys are accepted only through EVAL_API_KEY, not CLI arguments.
EVAL_BASE_URL must use HTTPS except for localhost/loopback development endpoints.
Or directly:
dotnet tool restore
dotnet restore
dotnet build -c Release
dotnet test -c Release
dotnet format --verify-no-changes
dotnet csharpier check .
dotnet pack -c Release -o artifacts/packagesCI also runs dotnet list DotNetNavigator.slnx package --vulnerable --include-transitive after restore to surface vulnerable direct or transitive packages without changing the repository NuGet audit policy.