Open interactive diagram ↗ · Rendered from docs/images/mcproslyn.architecture.json by archify finalize architecture (Archify.Net).
MCP server exposing C# symbol-level navigation (find-references, goto-definition, find-implementations, semantic-search, rename, etc.) to AI coding agents. Wraps Roslyn's MSBuildWorkspace and serves over stdio.
Status: v1.4, tagged v1.4.0. 20 tools plus echo, 239 tests. Measured timings are in docs/acceptance/, the v1 design in docs/plans/2026-05-15-mcproslyn-design.md, the architecture summary in ARCHITECTURE.md, and open work on the ContextBoard project mcpRoslyn (board-only since 2026-10-03; list_cards).
- Repository review & bug-fix sweep (2026-10-20):
find_dead_code_candidatesno longer flags explicit interface implementations or private helpers they reach (TOOL-015);find_callersseeds the target symbol intoseenand deduplicates multiple call sites in one caller body (TOOL-016);reload_workspacechecksMSBuildFailurebefore touching the workspace (WS-010);find_calleeswalks both halves of partial methods (TOOL-017);SymbolIndex.Refreshexpands the dirty document walk when a file newly joins an existing partial symbol (IDX-006);analyze_symbolandfind_derived_typesdeduplicate multi-reference and partial-declaration results (TOOL-018); removed sync-over-async inComputeConfidenceand build warning inAcceptanceTests(TOOL-019). - The server no longer dies at startup when
global.jsonpins an uninstalled SDK, no solution is found, or--solutionis wrong: it stays up, every tool answersWORKSPACE_NOT_LOADEDnaming the cause, andreload_workspacerecovers (WS-008). - Orphaned
mcpRoslyn.exeprocesses after Codex reviews traced to the Codex plugin's broker lifecycle, not this server; closed without code (WS-009).
Earlier changes are in CHANGELOG.md, and per-session detail in SESSION_HANDOFF.md.
AI coding agents (Claude Code, Cursor, etc.) typically navigate codebases with text search (grep, ripgrep). For C# this loses precision: text search can't distinguish a usage from a definition, can't find implementations of an interface, can't resolve overloads, and can't follow renames. mcpRoslyn gives agents authoritative semantic queries backed by the same engine the C# compiler uses.
- Windows. v1 is Windows-only —
MSBuildLocatorand path-comparison code would need work for Linux/Mac. - .NET 10 SDK (for build) — runtime is bundled into the published self-contained exe.
- A Claude Code installation (or any MCP-compatible client speaking stdio JSON-RPC).
| Category | Tools |
|---|---|
| Navigation | find_references, goto_definition, workspace_symbol, hover |
| Structure | find_implementations, find_derived_types, list_document_symbols |
| Callers / Callees | find_callers, find_callees |
| Diagnostics | get_compilation_errors, get_document_diagnostics |
| Search | semantic_search (patterns: derives-from:, implements:, has-attribute:, returns:, parameter-type:) |
| Composite | analyze_symbol (hover + refs + impls + derived + callers in one call) |
| Architecture | project_overview, find_entrypoints, find_registrations |
| Tests | test_map (production → test heuristic) |
| Cleanup | find_dead_code_candidates |
| Editing | rename_symbol (preview by default; applyEdits: true to write) |
| Lifecycle | reload_workspace |
| Sanity | echo |
Every tool returns structured JSON and accepts format = "structured" | "summary". Navigation tools accept either { filePath, line, column } (cursor style) or { symbolId } (Roslyn's DocumentationCommentId format). rename_symbol is the only path to file writes — default applyEdits: false returns a preview.
find_dead_code_candidates reports private/internal members with no references. On an application solution (a web app plus tests — nothing packaged), pass includePublicTypes: true:
{ "includePublicTypes": true, "includeTests": false }Public members are never candidates — outside callers can reach them in ways a reference scan can't see. Public types in an application are a different matter: nothing outside the solution can reach them, so an unreferenced one is usually dead, and it is the case worth acting on. Left off by default because it is exactly the wrong behaviour for a packable library, where the public surface is the product.
Findings are filtered against the things a reference scan can't see on its own: DI registrations (including hosted services and unrecognised registration calls), framework-reached types (controllers, hubs, middleware, EF migrations), and compiler-generated members. Public-type findings are reported at medium confidence — a type resolved only by assembly scanning (Scrutor) or named in configuration has no code reference anywhere, so confirm before deleting.
reload_workspace and project_overview return WorkspaceLoadDiagnostic { Kind, Message, ProjectName }. Kind distinguishes three situations that MSBuild reports through one channel:
| Kind | Meaning |
|---|---|
Failure |
The project genuinely did not load. |
ProjectLoadedWithWarnings |
It loaded fine; MSBuild grumbled about package pruning, a vulnerability advisory, etc. |
SkippedUnsupportedProject |
Not a C#/VB project (.esproj, .sqlproj, …). Expected in a polyglot solution. |
Edits to files already in the solution are picked up on every call. What needs reload_workspace — a .cs file added or deleted, a .csproj, .sln or Directory.Build.props changed — is detected and reported instead: every tool result then carries a warnings entry naming what changed, e.g. The workspace may be stale: TestLib\AddedAfterLoad.cs added. Answers may miss these changes; call reload_workspace to load them. An answer with that warning may be incomplete (a find_references blind to callers in the new file); reload before trusting it. mcpRoslyn does not reload on its own — a reload costs seconds of warm-up.
mcpRoslyn supports two modes:
Register once:
{
"mcpServers": {
"mcpRoslyn": {
"command": "c:\\projects\\mcpRoslyn\\bin\\publish\\mcpRoslyn.exe"
}
}
}mcpRoslyn discovers the solution by first walking up from Claude Code's CWD looking for *.sln or *.slnx (the closest enclosing solution wins). If none is found up the tree, it then searches down (breadth-first, skipping bin/obj/node_modules/packages and dot-directories) so a solution nested in a subfolder like src/ is still found — the shallowest match wins. When one directory holds several (a lean and a full solution side by side), the one declaring the most C#/VB projects wins, then .sln before .slnx, then name. project_overview shows which solution is loaded; to switch mid-session, call reload_workspace with solutionPath set to the full path of another .sln/.slnx.
Useful when a project contains multiple .sln files and you want to force a specific one:
{
"mcpServers": {
"mcpRoslyn": {
"command": "c:\\projects\\mcpRoslyn\\bin\\publish\\mcpRoslyn.exe",
"args": ["--solution", "c:\\path\\to\\specific.sln"]
}
}
}| Flag | Required | Description |
|---|---|---|
--solution <path> |
No | Path to a .sln or .slnx. When omitted, mcpRoslyn walks up from CWD, then searches down into subfolders, looking for one. |
--log-level <level> |
No | Debug, Information (default), Warning, Error. |
--log-file <path> |
No | Tee ILogger output to a file (append mode). Useful because Claude Code only surfaces MCP stderr during the initialize handler — anything after that (warm-up timings, per-tool diagnostics) is otherwise lost. |
dotnet publish c:\projects\mcpRoslyn\src\mcpRoslyn -c Release -o c:\projects\mcpRoslyn\bin\publishProduces mcpRoslyn.exe as a single-file self-contained win-x64 binary at the publish path. The BuildHost-netcore\ and BuildHost-net472\ directories alongside the exe are required — they contain the Roslyn MSBuild host process that the workspace loader spawns separately at runtime.
On duetGPT.sln (4 loaded projects, 598 .cs files), measured cold-start and query times:
| Metric | v1 | v1.1 | v1.2 |
|---|---|---|---|
Solution load (LoadAsync return) |
~8.5 s | ~2 s | ~2–11 s (env variance; same code path) |
| Warm-up + index build (background) | n/a | ~8 s | ~23 s |
First find_references |
~8.4 s | ~1.9 s | ~640 ms |
find_implementations |
~300 ms | ~830 ms | ~320 ms |
semantic_search has-attribute: |
~11 s | ~7.7 s | ~11 ms (1000× faster) |
All warm-up cost stays in the background — LoadAsync returns at the same time. Full detail in docs/acceptance/.
Index build costs, re-measured 2026-09-13 (published exe cb88412, cold process; BPG over 3 runs, duetGPT over 2):
| Solution | Projects | Warm-up | SymbolIndex |
InvocationIndex |
|---|---|---|---|---|
BPG.sln |
8 | ~2.5 s | 91–125 ms | ~925 ms |
duetGPT.sln |
4 | ~10.9 s | 258–387 ms | ~6 400 ms |
On 2026-08-15 InvocationIndex measured 1 715 ms on BPG and ~12 200 ms on duetGPT. Raw timings on this machine swing up to ~2× with no code change, and BPG has since lost ~1 700 lines of dead code, so read the drop as a trend rather than a result.
SymbolIndex used to dominate this list; it was walking every referenced assembly (the whole BCL, every package) and discarding the result. Fixing the walk root cut it ~20–27× with an identical index — so InvocationIndex is now the only meaningful index cost.
The table above is not comparable to the v1/v1.1/v1.2 columns: those were measured against a duetGPT.sln that has since been restructured, and query timings there were taken on a different solution shape. Treat the version columns as history, not as a baseline to diff against.
MIT — see LICENSE.