Skip to content

A Roslyn MCP server is the cheat code for AI on .NET — here's what to put in one

Why every C# coding agent needs compiler-grade context, the nine tool methods worth exposing, and the design rules that keep a Roslyn MCP server fast.

Vukasin Vulovic 9 min read
A Roslyn MCP server is the cheat code for AI on .NET — here's what to put in one

A million-line .NET solution does not fit in any agent’s context window — and it does not need to. The compiler already holds a complete, correct model of every symbol, reference, and dependency in it. The trick is giving the agent a phone line to that model: a Model Context Protocol server wrapping Roslyn, so that “what implements IPaymentProvider?” is a tool call returning compiler truth instead of a grep returning guesses.

We run one in production on every .NET engagement. This post covers why C# specifically needs it, what the public landscape offers, the nine tool methods ours exposes (with the actual code), and the design rules that keep the thing fast enough that the agent prefers it to grepping — because a semantic tool the agent routes around is just latency.

The context asymmetry nobody planned

Agents working on TypeScript inherit a lucky accident: the entire JS tooling ecosystem — tsserver, ESLint’s parsers, tree-sitter grammars — is lightweight, ubiquitous, and embedded in every editor. An agent in a TS repo gets decent structural answers essentially for free, and the language’s tooling culture means those answers are always a subprocess away.

C# has better ground truth available — Roslyn’s semantic model is compiler-grade, not parser-grade — but it is heavyweight to stand up. You need MSBuild located, the workspace loaded, compilations built and cached. No agent ships that by default, so out of the box, an agent on your million-line solution falls back to what it can do: read files and match text. The result is the absurd asymmetry we kept hitting: the platform with the strongest semantic oracle gets the weakest semantic context in practice.

A Roslyn MCP server closes that gap once, for every session afterward.

The public landscape

You do not have to build from zero. At the time of writing, the public Roslyn MCP field includes egorpavlikhin/roslyn-mcp, carquiza/RoslynMCP, kooshi/SharpToolsMCP, and sailro/RoslynMcpExtension, plus the tooling in codewithmukesh’s .NET Claude Kit. They differ in maturity and ambition, but they cluster around the same core surface — symbol search, reference finding, diagnostics — which tells you something useful: independent builders keep converging on the same first tools, because those are the questions agents actually ask. Each project’s README will tell you quickly which subset it covers and how it loads your solution; the pattern below is what we converged on ourselves, and it overlaps that public core deliberately.

Bootstrap: the part that bites first

Two production realities before any tools exist. First, MSBuild must be located before any Roslyn type is touchedMSBuildLocator.RegisterDefaults() on line one, because the moment a Microsoft.CodeAnalysis type loads without it, workspace loading fails in ways the error messages will not explain. Second, the server should find the solution itself:

using Microsoft.Build.Locator;

// MSBuild must be located before any Roslyn types are used
MSBuildLocator.RegisterDefaults();

var slnPath = FindSolutionFile();   // walk up from cwd until a .sln / .slnx appears
var workspace = new WorkspaceManager(slnPath);
var server = new McpServer(workspace);

await server.RunAsync(CancellationToken.None);

The walk-up solution discovery matters because the agent launches the server from wherever the session happens to be rooted — hardcoding paths is how MCP servers die on the second repo. WorkspaceManager owns the third essential: compilation caching. Building a compilation for a large project takes seconds; a server that rebuilds per call trains the agent to stop calling it. Cache per project, invalidate on file change, and calls after the first come back in milliseconds.

The nine tools ours exposes

ToolAnswersWhy the agent needs it
find_symbolWhere is anything named X?The entry point — replaces grep for identifiers
find_referencesWho uses this symbol?Blast-radius before any edit
get_implementationsWhat implements this interface / overrides this member?The question text search is worst at
get_type_hierarchyBase types and derived types of XInheritance-aware planning
get_compilation_errorsWhat is broken right now, per project?Ground truth between builds
find_unused_codeWhat is dead?Safe-to-delete answers with evidence
analyze_usingsWhich usings are redundant / missing?Cheap hygiene the agent otherwise guesses at
get_dependency_graphWhat does this type/namespace depend on?Layering violations caught at plan time
get_project_dependenciesWhich project references which?Architecture questions without opening .csproj files

The shape of a tool, from our find_symbol — the details here are the difference between a demo and a daily driver:

var types = compilation.GetSymbolsWithName(
    n => n.Contains(name, StringComparison.OrdinalIgnoreCase),
    SymbolFilter.Type, ct);

foreach (var sym in types.Take(20))          // cap — context is the scarce resource
{
    var loc = sym.Locations.FirstOrDefault(l => l.IsInSource);
    if (loc == null) continue;
    results.Add(new
    {
        name = sym.Name,
        kind = sym.Kind.ToString(),
        project = project.Name,
        file = loc.SourceTree?.FilePath,     // always file:line — the agent Reads next
        line = loc.GetLineSpan().StartLinePosition.Line + 1,
        @namespace = sym.ContainingNamespace?.ToString(),
        signature = sym.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
    });
}

The same task, with and without

To make the “cheat code” claim concrete, here is a routine task — add a new implementation of IPaymentProvider and register it — on a large solution, both ways.

Without the server, the agent greps for IPaymentProvider, gets 61 text hits including comments, tests, and a stale interface with a similar name in a legacy project. It reads four files to disambiguate (three of them the wrong ones), misses the existing MockPaymentProvider registered only in the test host, and produces an implementation whose CaptureAsync signature matches an older overload it found in an outdated call site. The compiler catches the signature; nothing catches that the DI registration duplicates a scenario the mock already covered. Cost: ~15 minutes, six file reads, one review comment that a human had to notice.

With the server, the sequence is four tool calls: find_symbol IPaymentProvider (one authoritative hit, file:line), get_implementations (three implementations, including the mock and where each lives), find_references on the registration extension (one composition root), get_type_hierarchy to confirm the base class the other providers share. The agent then reads exactly two files — the interface and one sibling implementation as a pattern reference — and writes code that lands in review looking like the existing providers. Cost: ~2 minutes, and the mock duplication never happens because the mock was in the answer.

The delta is not intelligence. Same model, same prompt. The delta is that the second agent asked a compiler and the first one asked a string-matcher.

Wiring it into Claude Code

The registration in .mcp.json — including the two settings that matter:

{
  "mcpServers": {
    "roslyn": {
      "command": "dotnet",
      "args": ["run", "--project", "RoslynMcp", "--no-launch-profile"],
      "timeout": 30000,
      "env": { "DOTNET_ENVIRONMENT": "Development" }
    }
  }
}

The 30-second timeout is not decoration: first contact with a big solution pays MSBuild workspace loading plus the first compilation, and a default timeout will kill the server mid-warmup — after which the agent silently falls back to grep for the whole session and you wonder why quality regressed. --no-launch-profile keeps the server from inheriting whatever ASP.NET ports and environment your launch profiles configure. Operationally: cold start on our five-application solution is ~15 seconds, cached tool calls run 10–100 ms, and the workspace goes stale only on project-file changes — file edits are picked up per-call. When MSBuild gets into a mood (it will), the server is stateless enough that a session restart clears it.

Design rules that survived contact with real sessions

Cap every result set. Take(20) is not laziness — it is the recognition that the agent’s context window is the scarce resource the whole server exists to protect. A reference query on a popular symbol can return four hundred hits; dumping them all costs more context than the grep the server was supposed to replace. Cap, count, and let the agent narrow with a project filter.

Always return file:line. Every result is a place the agent can follow up with a targeted Read. The server’s job is not to replace reading code — it is to make every read the right read. This is also what lets the investigation-gate hook compose with the server: symbol claims come back pre-verified with the evidence location attached.

Minimally-qualified signatures. ToDisplayString(MinimallyQualifiedFormat) instead of fully-qualified monsters. CancelAsync(CancelCommand) reads at a glance; global::Api.Features.Billing.Subscriptions.CancelAsync(...) burns tokens to say less.

Case-insensitive contains-matching on names. The agent half-remembers identifiers — it will ask for paymentprovider when the type is IPaymentProvider. Exact-match search punishes the exact failure mode the server exists to absorb.

A project filter on everything. On a five-application solution, unscoped queries are noise generators. The optional project argument turned out to be the most-used parameter in our logs.

Stateless tools, stateful workspace. Tools take arguments and return JSON; all memory lives in the workspace cache. This keeps the server restartable mid-session — and restarts happen, because MSBuild workspaces have moods.

What is still missing — everywhere, including ours

The spec-complete Roslyn MCP server does not exist yet, and honesty about the gaps is part of why this series exists. Three capabilities we consider unsolved in the public field:

Live diagnostics propagation. Every server (ours included) answers “what are the errors?” when asked. The better design pushes compiler diagnostics into the session as they change — the agent finds out its last edit broke three call sites without spending a tool call wondering. We approximate this today with the PostToolUse build gate; true incremental push is on our roadmap.

Refactoring with rollback. Roslyn can execute renames and code fixes solution-wide, not just report them. Nobody exposes that under agent control properly, because the failure story is terrifying without transactional rollback — which is exactly the missing piece: apply, verify with the build gate, commit or revert as one unit, under hook governance.

SourceLink + decompilation for dependencies. Agents hit walls at NuGet boundaries: “what does this library method actually do?” A server that answers via SourceLink fetch or decompilation — with the results flowing through the same evidence-annotation discipline as first-party code — would remove the last routine reason agents guess.

If you build any of these three well, publish it; this corner of the ecosystem is small enough that good work gets found fast.

Where it sits

The Roslyn MCP server is layer six of the twelve-layer system — the ground-truth supplier that the investigation gate, the code-existence annotations, and the planning passes all lean on. The broader argument for compiler-verifiable stacks in the agent era is in our field guide, and the practical companion posts cover the CLAUDE.md that tells the agent when to use these tools and the inventories that cover what symbol search cannot.

The minimal starter is open source: Code-Majesty-Tech/dotnet-mcp-starter — the bootstrap, the workspace caching, and three of the nine tools, build-verified and MIT-licensed, with the remaining six described above as the extension path. Standing it up against your own solution is most of a Sprint Zero afternoon, and it is the afternoon with the single highest payoff-per-hour we know of in AI-augmented .NET work.