DEV Community

Cover image for I built an interactive HTML export for .NET dependency graphs, because my AI agent kept guessing wrong
Mahmoud Nabil
Mahmoud Nabil

Posted on

I built an interactive HTML export for .NET dependency graphs, because my AI agent kept guessing wrong

Every time I asked Claude Code or Copilot something like "what breaks if I rename this method" on a mid-sized .NET solution, it would grep through files, miss call sites that go through interfaces, and burn a lot of tokens getting a partial answer. On a solution with a few dozen projects, that's slow and often just wrong: the agent doesn't see the whole picture, so it can't reason about it correctly.

That's the problem Slnmap is built to solve. It uses Roslyn (the actual C# compiler) to build a semantic graph of your solution, stores it locally in SQLite, and exposes it over MCP so any compatible agent can query it directly instead of guessing from whatever files happen to be open.

The part I actually use the most: slnmap viz

Tool calls are great for agents. They're not great for me, sitting there wondering what an unfamiliar codebase actually looks like before I start asking an agent to change it.

So Slnmap also ships a viz command that exports the whole graph as a single self-contained HTML file. No server, no account, no CDN calls. Open it in a browser, pan and zoom, click a symbol, and see everything connected to it collapse into view.

slnmap viz --project MyProject.csproj
Enter fullscreen mode Exit fullscreen mode

The reason this matters more than it sounds: it's the one artifact in this whole workflow that isn't just for the agent. You can drop the HTML file in a PR description, or send it to a teammate who doesn't have the repo checked out, and they get the same interactive view you do. Every other Roslyn-based MCP server I looked at while building this outputs data for an agent to consume. None of them give you something to look at.

slnmap viz interactive HTML graph export showing a .NET solution's dependency structure

A concrete example

Say you're about to touch IBasketService. Instead of grepping five files and hoping you found every caller, you ask the agent (or run the query yourself):

"What breaks if I change IBasketService?"

Slnmap follows both the interface's callers and its concrete implementations, across every project in the solution, in one query. On eShopOnWeb (10 projects), querying an interface with 18 dependents comes back in about 270ms end-to-end over MCP (median of 3 runs — full methodology is in BENCHMARKS.md if you want to check the setup yourself).

Why the tool surface is small on purpose

If you go looking, there are a few other Roslyn-based MCP servers out there, some with a much larger surface of tools than Slnmap's. I went the other way on purpose. Every tool Slnmap exposes is read-only and narrow — find a symbol, trace its callers, check impact, list implementations — on the bet that an agent (or a person) gets more value from a small set of tools that answer a specific question well than from a large API surface that tries to do everything.

I'll be honest: I don't know yet whether that bet holds up as codebases and usage scale. It's a real open question, not a marketing line.

What it doesn't do

It doesn't call out anywhere. Slnmap reads your source with Roslyn and writes one local SQLite file — nothing else. It's open source now, so that's not just a claim: you can read the code or watch the process yourself and confirm nothing leaves the machine.

It also doesn't guess. Every answer comes from the compiler's own understanding of your code, not from string matching or heuristics on the source text — which is the same reason it catches call sites that go through an interface instead of missing them like grep does.

Try it

dotnet tool install --global Slnmap
slnmap analyze path/to/YourSolution.sln
Enter fullscreen mode Exit fullscreen mode

Then point any MCP-compatible client at it (Claude Code, Cursor, etc.) — setup instructions are in the README.

It's MIT licensed. If you've hit the "grep vs. semantic index" tradeoff yourself, or tried the narrow-tools-vs-many-tools design in a different direction, I'd genuinely like to hear how you approached it.

Top comments (1)

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

Using Roslyn instead of grep is the right foundation. The next thing I’d want every impact result to expose is exactly which compilation the graph represents: commit, target framework, configuration, language version, conditional symbols, package lock hash, and index timestamp. If the solution has compilation diagnostics or the index is older than the source tree, the MCP result should say incomplete or stale rather than present a confident dependency list.

Edge provenance would help too. Tag relationships as compiler-confirmed, generated-source, or runtime-inferred. Reflection, convention-based DI, serializers, plugins, and configuration-driven type loading are real dependencies that a static graph cannot prove; making that boundary visible is more trustworthy than implying “everything connected.”

I’d also add a CI fixture with interfaces, source generators, conditional compilation, and runtime registration, then snapshot impact results across supported target frameworks. For shared HTML exports, a metadata-only mode that strips absolute paths and source snippets would make PR sharing safer for private repositories.