<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mahmoud Nabil</title>
    <description>The latest articles on DEV Community by Mahmoud Nabil (@emahmoudnabil).</description>
    <link>https://dev.to/emahmoudnabil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4049816%2Fc004ee48-481f-449f-904e-f281b2154521.jpg</url>
      <title>DEV Community: Mahmoud Nabil</title>
      <link>https://dev.to/emahmoudnabil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emahmoudnabil"/>
    <language>en</language>
    <item>
      <title>My AI agent ignored my MCP server and grepped anyway — three things that actually fixed it</title>
      <dc:creator>Mahmoud Nabil</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:25:46 +0000</pubDate>
      <link>https://dev.to/emahmoudnabil/my-ai-agent-ignored-my-mcp-server-and-grepped-anyway-three-things-that-actually-fixed-it-193j</link>
      <guid>https://dev.to/emahmoudnabil/my-ai-agent-ignored-my-mcp-server-and-grepped-anyway-three-things-that-actually-fixed-it-193j</guid>
      <description>&lt;p&gt;I built an MCP server that gives coding agents a compiler-accurate map of a .NET codebase. Thirteen tools at the time — find_usages, impact_analysis, the works. Then I watched Claude Code, with the server connected and healthy, answer a "who calls this?" question by running ripgrep. The tools were right there. It grepped anyway.&lt;br&gt;
Someone on Reddit asked me the exact question I'd been asking myself: how do you make/force/convince the LLM to actually use the CPG MCP? That question got 10 upvotes, which tells me a lot of people are watching their agents do the same thing. Here's what actually moved the needle for me — measured by watching sessions, not vibes.&lt;br&gt;
First: a concrete example invocation inside every tool description. Not prose describing the parameters — an actual call: find_usages(fqn: "MyApp.Services.VendorService"). The model's first guess at how to call your tool comes entirely from the schema plus the description, and a real invocation beats three paragraphs about parameter semantics. A good chunk of wrong-parameter calls disappeared the day I shipped this, and wrong-parameter failures are exactly what teaches an agent "this tool doesn't work, back to grep."&lt;br&gt;
Second: corrective errors instead of generic ones. My server used to return "An error occurred invoking 'find_usages'" when the agent guessed a wrong parameter name — the real cause went to stderr, which no client surfaces to the model. The agent would retry the same broken call once, give up, and grep. Now a failure comes back as a normal result with a machine-checkable shape: the bad parameter name, the list of valid ones, and a message that says what to do ("this tool takes fqn (required), e.g. Namespace.Class.Method"). The agent fixes itself on the next call about as often as you'd hope. This design came out of an r/mcp thread where three engineers who run production MCP servers basically wrote the spec in the comments.&lt;br&gt;
Third — and this is the unglamorous one that matters most: a standing instruction in the project's agent rules. One short paragraph in the repo's agent-facing docs: "This project has a code graph. For usages, impact, or architecture questions, query the slnmap tools first; use grep only as a fallback for things the graph doesn't model." Blunt. Works. The model reads the project context before it reads your tool descriptions, and a sentence there outweighs a paragraph in the schema.&lt;br&gt;
The honest summary: you can't force it. There's no config flag for "prefer my tools." What you can do is make your tool the path of least resistance — discoverable (the standing rule), learnable (the example call), and forgiving (the corrective error). After those three, tool usage in my sessions went from occasional to default.&lt;br&gt;
The meta-lesson generalizes beyond code graphs: an MCP server is an API whose only consumer has no documentation, no Stack Overflow, and no memory of ever using it before. Every session it learns your tool from scratch, from exactly two things — the description and the error messages. Spend your effort there.&lt;br&gt;
The server in question is free and MIT if you want to see the patterns in place: github.com/EMahmoudNabil/slnmap&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
    </item>
    <item>
      <title>My code graph now answers "which React screens break if I change this C# handler?" — the whole journey</title>
      <dc:creator>Mahmoud Nabil</dc:creator>
      <pubDate>Mon, 24 Aug 2026 15:46:14 +0000</pubDate>
      <link>https://dev.to/emahmoudnabil/my-code-graph-now-answers-which-react-screens-break-if-i-change-this-c-handler-the-whole-54nb</link>
      <guid>https://dev.to/emahmoudnabil/my-code-graph-now-answers-which-react-screens-break-if-i-change-this-c-handler-the-whole-54nb</guid>
      <description>&lt;p&gt;So a month ago I posted an experiment here: can you link a React frontend to a .NET backend with compiler accuracy? The answer back then was "probably" — a one-day throwaway script resolved 96.6% of frontend HTTP calls on a real codebase to backend route templates. This is the follow-up. The thing is built and shipped now, and it caught real production bugs on the way. It also caught three rounds of bugs in my own tool before launch, which is honestly the part I find more interesting.&lt;/p&gt;

&lt;p&gt;Quick context if you missed the first post. I maintain slnmap, a Roslyn-based code graph for .NET. It exists because AI coding agents kept confidently lying to me about my own codebase — one told me a class had "zero usages, safe to delete" and that class was load-bearing middleware. The rule that came out of that pain: if the tool can't resolve something statically, it counts it and says so. It never guesses. Everything below follows from that one rule.&lt;/p&gt;

&lt;p&gt;The goal was one graph an agent can walk end to end. The C# half already existed — HTTP endpoints as graph nodes, resolved through MapGroup prefixes, [controller] tokens, const patterns, overload resolution. What was missing was the frontend half: which React call site hits which endpoint.&lt;/p&gt;

&lt;p&gt;Extractor first. slnmap-ts (on npm) walks a React/Next.js project with the real TypeScript Compiler API — the type checker, not regex. The checker folds literal types deterministically, so apiClient.get(API_ROUTES.VENDORS), where the URL is buried behind a barrel file three imports away, resolves to the exact string the same way the compiler would resolve it. On my reference codebase: 743 call sites, 91% resolved to exact route templates. The other 9% get counted with reasons — runtime-computed segments, dynamic imports, stuff that genuinely can't be known statically.&lt;/p&gt;

&lt;p&gt;Then the linker. Frontend templates matched against backend endpoint templates, verb-exact, with precedence rules that mirror what ASP.NET actually does (a literal segment beats a parameter hole). When one call site can genuinely hit multiple endpoints at runtime, you get an honest "set edge" instead of a fake single answer. On the reference codebase, 716 of 743 call sites linked deterministically. The remaining 27 each get disclosed with a reason.&lt;/p&gt;

&lt;p&gt;And here's the output the whole project exists for — impact_analysis on a C# command handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Depth 1: HandledBy → POST api/TaskCenter/compliances/{taskId}/reminder
- Depth 2 (React callers of that endpoint):
   POST src/.../taskCenterService.ts:33
   POST src/.../useUserTaskCenter.ts:323  (shared hook — also targets 3 sibling endpoints)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the handler, see which React screens break. One graph, zero guessing. The shared generic hook shows up correctly as a multi-target caller instead of getting silently dropped or falsely pinned to one route.&lt;/p&gt;

&lt;p&gt;The part I didn't expect: it finds bugs grep structurally can't. During the very first matching experiment, the harness flagged a live production dialog POSTing to /organizationusers. That endpoint doesn't exist — the backend only has a GET at that path. A real screen, quietly sending requests into the void, and nobody noticed because the two halves of the bug live in two different languages. That check is now a first-class tool, find_orphan_calls, and it splits results into "nothing exists at this path" vs "path exists but under a different verb" — the second usually means someone typed the wrong verb, which is the sharper signal. It found two more of these on my codebase that I didn't know about.&lt;/p&gt;

&lt;p&gt;Now the part that kept me honest. Before announcing anything I ran the tool the way a stranger would — fresh machine, fresh install, real codebase. Three separate times. Every round caught a real bug.&lt;/p&gt;

&lt;p&gt;Round one: false ambiguity in the route matcher. A call site's parameter hole was absorbing an endpoint's literal in one position, while the endpoint's hole absorbed the call site's literal in another — two unrelated routes "matching" through a criss-cross coincidence ASP.NET's runtime could never produce. impact_analysis was over-reporting blast radius on six call sites. Fixed before anyone saw it.&lt;/p&gt;

&lt;p&gt;Round two: I pointed the tool at codebases nothing like mine — official Microsoft Blazor samples, a Turborepo monorepo, an Angular app. Found four cases where it silently dropped data while reporting clean success. Blazor markup composition hidden behind a "0 skipped" message. A call-site counter that could disagree with what actually got persisted. String-concatenated URLs vanishing without a trace. All four are exactly the defect class this project exists to kill — the confident lie — sitting inside my own tool. All four fixed or honestly disclosed within 48 hours, verified on both machines before this post.&lt;/p&gt;

&lt;p&gt;I keep relearning the same lesson: the moment a static-analysis tool guesses to make its numbers look better, it's worthless, because someone will trust it. So now the tool tells you it can't see Blazor markup. It tells you a URL was built by concatenation and couldn't be resolved. Its reported counts provably equal what's in the graph. That's not modesty for marketing — a graph that guesses is worse than no graph.&lt;/p&gt;

&lt;p&gt;What it doesn't do, so nobody's surprised: Blazor and Razor Pages are detected and disclosed but not modeled yet (issues are open). MediatR's sender.Send dispatch isn't traced — the chain stops at the handler. Angular/Vue detection is honest but mostly unresolved. Monorepo tsconfigs need per-package targeting.&lt;/p&gt;

&lt;p&gt;Everything's free and MIT. Three commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;- dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; slnmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;- slnmap analyze MySolution.sln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;- slnmap analyze-ts ./frontend &lt;span class="nt"&gt;--db&lt;/span&gt; slnmap.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;- slnmap &lt;span class="nb"&gt;link&lt;/span&gt; &lt;span class="nt"&gt;--db&lt;/span&gt; slnmap.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point any MCP client (Claude Code etc.) at slnmap serve — 15 tools including the cross-stack ones. Repo: github.com/EMahmoudNabil/slnmap&lt;/p&gt;

&lt;p&gt;If you run it on your codebase, I'd genuinely like to hear what the orphan report finds. And if the linker refuses a routing shape you have, paste the --verbose output in an issue — that's immediately actionable.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>typescript</category>
      <category>react</category>
      <category>staticanalysis</category>
    </item>
    <item>
      <title>Can you link a React frontend to a .NET backend with compiler accuracy? I ran the experiment</title>
      <dc:creator>Mahmoud Nabil</dc:creator>
      <pubDate>Fri, 21 Aug 2026 19:45:29 +0000</pubDate>
      <link>https://dev.to/emahmoudnabil/can-you-link-a-react-frontend-to-a-net-backend-with-compiler-accuracy-i-ran-the-experiment-3m68</link>
      <guid>https://dev.to/emahmoudnabil/can-you-link-a-react-frontend-to-a-net-backend-with-compiler-accuracy-i-ran-the-experiment-3m68</guid>
      <description>&lt;p&gt;I maintain slnmap, a tool that gives AI coding agents a compiler-accurate graph of .NET codebases. It's built on Roslyn, so if the compiler sees a reference, the graph has the edge. But it stops at the C# boundary. The question I kept getting was: can it cross into the frontend? Can "what breaks if I change this handler" end with "these three React screens"?&lt;/p&gt;

&lt;p&gt;I used to think the honest answer was no. I actually had evidence — an earlier prototype of mine tried exactly this with heuristic matching. Name-equality joins, filename-as-truth, take-first guesses written into the graph as real edges. Its own docs ended up flagging the results as unreliable. A graph that guesses is worse than no graph, because someone eventually trusts it.&lt;/p&gt;

&lt;p&gt;So before building anything this time, I gave myself one day and one question: is cross-stack linking deterministic, or is it doomed to heuristics no matter what?&lt;/p&gt;

&lt;p&gt;The setup was a ~200-line throwaway script using the TypeScript Compiler API — ts.createProgram plus the type checker, the real one, not a regex parser. I pointed it at a production React/Next.js frontend, 2,570 files, and matched what it found against the route data my Roslyn side already extracts.&lt;/p&gt;

&lt;p&gt;Three things came out of it.&lt;/p&gt;

&lt;p&gt;The type checker solves the exact thing that killed the old prototype. The old attempt gave up on 23% of call sites as "variable URLs" — stuff like apiClient.get(API_ROUTES.VENDORS) where the URL lives in a constant behind a barrel file, three imports away. The checker's literal types fold those constants. Not "probably this string" — this string, resolved the same way the compiler resolves it. Most of that 23% bucket just evaporated.&lt;/p&gt;

&lt;p&gt;The numbers held up. 675 frontend HTTP call sites, 96.6% resolved to a deterministic route template. The remaining 3.4% (runtime-computed segments, env-dependent bases) are detectable and countable, so the tool can say "these 23 sites I can't resolve, here's why" instead of guessing. That's already the design rule for the whole tool: anything not statically resolvable gets counted and disclosed, never guessed.&lt;/p&gt;

&lt;p&gt;And my favorite part — the experiment caught a real bug before the feature even exists. While matching frontend calls against backend routes, the script found a live dialog POSTing to /organizationusers. That endpoint doesn't exist on the backend. A production screen quietly sending requests into the void. grep would never surface this, because the two halves of the bug live in different languages.&lt;/p&gt;

&lt;p&gt;Looking back, the old prototype wasn't wrong to try. It was wrong to disable the type checker (for speed) and then paper over the gap with heuristics. That lesson generalizes, I think: in static analysis, the moment you start guessing to improve coverage, you've traded away the only thing that made the tool worth trusting.&lt;/p&gt;

&lt;p&gt;So it's buildable. The C# half already shipped — HTTP endpoints are graph nodes as of v0.7/v0.8, Minimal APIs and attribute-routed controllers, 658/658 registrations resolved on my reference codebase. The TypeScript extractor is what I'm building now. When the two halves join, impact_analysis on a C# handler will end its chain at the React components that break.&lt;/p&gt;

&lt;p&gt;slnmap is free and MIT: dotnet tool install --global slnmap — repo at &lt;a href="https://github.com/EMahmoudNabil/slnmap" rel="noopener noreferrer"&gt;github.com/EMahmoudNabil/slnmap&lt;/a&gt;. If you've tried cross-stack analysis and hit the heuristics wall, I'd like to hear how far you got.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>typescript</category>
      <category>staticanalysis</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built an interactive HTML export for .NET dependency graphs, because my AI agent kept guessing wrong</title>
      <dc:creator>Mahmoud Nabil</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:10:29 +0000</pubDate>
      <link>https://dev.to/emahmoudnabil/i-built-an-interactive-html-export-for-net-dependency-graphs-because-my-ai-agent-kept-guessing-2nd1</link>
      <guid>https://dev.to/emahmoudnabil/i-built-an-interactive-html-export-for-net-dependency-graphs-because-my-ai-agent-kept-guessing-2nd1</guid>
      <description>&lt;p&gt;Every time I asked Claude Code or Copilot something like &lt;em&gt;"what breaks if I rename this method"&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;That's the problem &lt;a href="https://github.com/EMahmoudNabil/slnmap" rel="noopener noreferrer"&gt;Slnmap&lt;/a&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I actually use the most: &lt;code&gt;slnmap viz&lt;/code&gt;
&lt;/h2&gt;

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

&lt;p&gt;So Slnmap also ships a &lt;code&gt;viz&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;slnmap viz &lt;span class="nt"&gt;--project&lt;/span&gt; MyProject.csproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw649c0laozawp6uobss4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw649c0laozawp6uobss4.png" alt="slnmap viz interactive HTML graph export showing a .NET solution's dependency structure" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

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

&lt;blockquote&gt;
&lt;p&gt;"What breaks if I change IBasketService?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Slnmap follows both the interface's callers &lt;strong&gt;and&lt;/strong&gt; its concrete implementations, across every project in the solution, in one query. On &lt;a href="https://github.com/dotnet-architecture/eShopOnWeb" rel="noopener noreferrer"&gt;eShopOnWeb&lt;/a&gt; (10 projects), querying an interface with 18 dependents comes back in about &lt;strong&gt;270ms end-to-end over MCP&lt;/strong&gt; (median of 3 runs — full methodology is in &lt;a href="https://github.com/EMahmoudNabil/slnmap/blob/main/BENCHMARKS.md" rel="noopener noreferrer"&gt;BENCHMARKS.md&lt;/a&gt; if you want to check the setup yourself).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the tool surface is small on purpose
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; Slnmap
slnmap analyze path/to/YourSolution.sln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point any MCP-compatible client at it (Claude Code, Cursor, etc.) — setup instructions are in the &lt;a href="https://github.com/EMahmoudNabil/slnmap" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/EMahmoudNabil/slnmap" rel="noopener noreferrer"&gt;https://github.com/EMahmoudNabil/slnmap&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Slnmap" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/Slnmap&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://slnmap.dev" rel="noopener noreferrer"&gt;https://slnmap.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
