<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>MCP on Welcome to Christophe Nasarre's Blog</title><link>https://chrisnas.github.io/tags/mcp/</link><description>Recent content in MCP on Welcome to Christophe Nasarre's Blog</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 08 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://chrisnas.github.io/tags/mcp/index.xml" rel="self" type="application/rss+xml"/><item><title>.NET CLI tools in the AI fury or how to guide agents during production investigations</title><link>https://chrisnas.github.io/posts/2026-06-08_dotnet-cli-tools-in-the-ai-fury/</link><pubDate>Mon, 08 Jun 2026 00:00:00 +0000</pubDate><guid>https://chrisnas.github.io/posts/2026-06-08_dotnet-cli-tools-in-the-ai-fury/</guid><description>How to let an AI coding agent investigate dev and production issues by driving your .NET diagnostics CLI tools — via MCP servers, SKILL.md files, or even by controlling a GUI application.</description><content:encoded><![CDATA[<p>Since the end of last year, I&rsquo;ve been heavily using AI coding agents such as Cursor, VS Code + Copilot, or Claude Code to brainstorm, plan and generate code for me. But writing code is only half of a developer&rsquo;s life. The other half is spent <strong>investigating issues</strong>: a service that leaks memory in production, an application that freezes on a deadlock, a thread pool that starves under load, or a request that takes way too long for no obvious reason.</p>
<p>These investigations rely on a familiar toolbox of command-line diagnostic tools. In .NET, it usually means <code>dotnet-dump</code>, <code>dotnet-counters</code>, <code>dotnet-trace</code>, <code>dotnet-pstacks</code>, <code>dotnet-dstrings</code>, and other CLI tools. They all share one property that turns out to be very convenient: they are <strong>text-in / text-out</strong> console programs. You give them a process ID or a dump file, they print a wall of text that you do your best to understand what is going on, and you reason about what to do next.</p>
<p>Reasoning about a wall of text to decide the next command is exactly what AI agents are good at. So, in addition to using an agent to write the next feature, why not let it <em>run the investigation</em> — capture a dump, look at the heap, follow the references from a GC root, and tell you which static field is keeping your cache alive? The agent runs the commands, reads the output, and decides the next step, just like a senior developer would.</p>
<p>In this post, I&rsquo;ll show <strong>two main ways</strong> to wire your .NET diagnostics tools into an AI agent — and point you to a third, more advanced pattern at the end for when the tool you want to drive is not a CLI at all but a GUI application.</p>
<h2 id="two-three-paths-to-ai-integration"><del>Two</del> Three paths to AI integration</h2>
<p>The first path is to do nothing special and believe in the power of models. However, how to be sure that the model will know which tool to call for what? And what if you have your own tools with no chance for the model to have learnt during its training?</p>
<p>When I started to work on building an AI-based way to automate investigations, I realized that the models are good at starting an investigation but often (1) stick to a lead or (2) become less and less imaginative as the context grows.</p>
<p>The following diagram is a very very high level view of the interactions between the user (= you), the two black boxes being the models and the Agent harness you use such as Cursor, Copilot, Claude or your own. The last element is the environment that the harness allows the model to access via tools.</p>
<p><img alt="AgentHarness" loading="lazy" src="/posts/2026-06-08_dotnet-cli-tools-in-the-ai-fury/AgentHarness.png"></p>
<p>You can:</p>
<ul>
<li>
<p>register an MCP Server that lists <em>tools</em> (i.e. functions) and <em>prompts</em> to be used by the model</p>
</li>
<li>
<p>define <em>skill</em> markdown files to &ldquo;drive the model&rdquo; to use certain tools in a certain order</p>
</li>
</ul>
<p>So, there are two practical ways to help an agent to be more focused and make a not-so-bad diagnostic based on available tools. Here are the pros/cons of each possibility even though your choice most often depends on a single question: <strong>do you own the tool&rsquo;s source code?</strong></p>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th>When to use</th>
          <th>Code changes?</th>
          <th>How the agent uses it</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>MCP Server</strong> (stdio)</td>
          <td>You own the tool and want structured tool discovery</td>
          <td>Yes — add <code>ModelContextProtocol</code> + <code>Microsoft.Extensions.Hosting</code> references and command line processing change</td>
          <td>Agent calls typed tools, works autonomously</td>
      </tr>
      <tr>
          <td><strong>Skill</strong> (<code>SKILL.md</code>)</td>
          <td>You want to reuse existing CLI tools as-is</td>
          <td>None — just a markdown file</td>
          <td>Agent executes shell commands, works autonomously</td>
      </tr>
  </tbody>
</table>
<p>The <strong>MCP server</strong> approach is the most structured: the agent discovers strongly-typed tools (with named parameters) and even pre-built workflow <em>prompts</em>, with no shell parsing involved. The price is that you have to modify the tool&rsquo;s source code, so this only works for <strong>tools you author</strong>. In terms of performance, it also means that each instance of the agent harness will spawn an instance of each registered MCP server.</p>
<p>The <strong>SKILL.md</strong> approach is the opposite trade-off: zero friction and zero code. A single markdown file teaches the agent how to drive <strong>the right</strong> CLI tool at the right time — including Microsoft&rsquo;s <code>dotnet-dump</code> and <code>dotnet-counters</code>, which you obviously cannot recompile. The cost is that the agent works with raw text output instead of typed JSON.</p>
<p>The two approaches are complementary: use MCP for the tools you write, and Skills for everything else. Let&rsquo;s look at each one with a concrete example.</p>
<h2 id="mcp-server-turning-dotnet-dstrings-into-a-dual-climcp-tool">MCP server: turning <code>dotnet-dstrings</code> into a dual CLI/MCP tool</h2>
<h3 id="what-is-mcp">What is MCP?</h3>
<p>The <a href="https://modelcontextprotocol.io/">Model Context Protocol</a> (MCP) is an open standard that lets an agent harness discover and call tools. An MCP server exposes two kinds of capabilities over a <strong>transport</strong> (stdio for local tools, HTTP for remote ones):</p>
<ul>
<li><strong>Tools</strong> — functions the agent can call to <em>get data</em> (with typed, documented parameters).</li>
<li><strong>Prompts</strong> — pre-built workflow templates that tell the agent <em>what to do</em> with those tools.</li>
</ul>
<p>In .NET, turning a console app into a stdio MCP server requires only two NuGet packages:</p>
<ul>
<li><code>ModelContextProtocol</code> (v1.1.0) for the hosting, dependency injection, and attribute-based discovery of tools and prompts,</li>
<li><code>Microsoft.Extensions.Hosting</code> for the generic host.</li>
</ul>
<p>If you are starting from scratch, the .NET 10 SDK even ships a dedicated <code>dotnet new mcpserver</code> template. But here I want to show something more realistic: <strong>upgrading an existing CLI tool</strong> into an MCP server without losing its command-line personality. I will also show one difference that might be a problem for you.</p>
<h3 id="the---mcp-dual-mode-pattern">The <code>--mcp</code> dual-mode pattern</h3>
<p>The key idea is to generate a <strong>single binary</strong> that behaves as a normal CLI tool <em>or</em> as an MCP server, depending on a <code>--mcp</code> flag on the command line. My <code>dotnet-dstrings</code> tool (which finds duplicated strings in a process or memory dump) has been upgraded exactly this way: one NuGet package, one <code>dotnet tool install</code>, two audiences.</p>
<p>The entire decision happens at the top of <code>Program.cs</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">System.Text</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">dstrings</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">Microsoft.Extensions.DependencyInjection</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">Microsoft.Extensions.Hosting</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">Microsoft.Extensions.Logging</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">ModelContextProtocol.Server</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">Contains</span><span class="p">(</span><span class="s">&#34;--mcp&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">var</span> <span class="n">mcpArgs</span> <span class="p">=</span> <span class="n">args</span><span class="p">.</span><span class="n">Where</span><span class="p">(</span><span class="n">a</span> <span class="p">=&gt;</span> <span class="n">a</span> <span class="p">!=</span> <span class="s">&#34;--mcp&#34;</span><span class="p">).</span><span class="n">ToArray</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="kt">var</span> <span class="n">host</span> <span class="p">=</span> <span class="n">Host</span><span class="p">.</span><span class="n">CreateDefaultBuilder</span><span class="p">(</span><span class="n">mcpArgs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">.</span><span class="n">ConfigureLogging</span><span class="p">(</span><span class="n">logging</span> <span class="p">=&gt;</span> <span class="n">logging</span><span class="p">.</span><span class="n">SetMinimumLevel</span><span class="p">(</span><span class="n">LogLevel</span><span class="p">.</span><span class="n">Warning</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="p">.</span><span class="n">ConfigureServices</span><span class="p">(</span><span class="n">services</span> <span class="p">=&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">services</span>
</span></span><span class="line"><span class="cl">                <span class="p">.</span><span class="n">AddMcpServer</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                <span class="p">.</span><span class="n">WithStdioServerTransport</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                <span class="p">.</span><span class="n">WithToolsFromAssembly</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">                <span class="p">.</span><span class="n">WithPromptsFromAssembly</span><span class="p">();</span>   <span class="c1">// &lt;-- registers the prompts</span>
</span></span><span class="line"><span class="cl">        <span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="p">.</span><span class="n">Build</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">host</span><span class="p">.</span><span class="n">RunAsync</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// CLI mode — manual argument parsing (same options as the original dstrings)</span>
</span></span><span class="line"><span class="cl"><span class="p">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Notice that both branches end up calling the same <code>StringAnalyzer.Analyze()</code> method that computes the duplicates.</p>
<h3 id="mcp-tools-data-access">MCP tools: data access</h3>
<p>The MCP tools are thin wrappers around the shared analyzer, decorated with attributes so the referenced library can discover them and expose their parameters to the agent. A static class decorated with the <strong><code>McpServerToolType</code></strong> attribute defines the <em>tools</em> (= functions) decorated with an <strong><code>McpServerTool</code></strong> attribute where the <strong><code>Description</code></strong> property provides enough details to be picked by the agent for a matching prompt. Each parameter of the tool/function is also detailed by a <strong><code>Description</code></strong> attribute.</p>
<p>Here is the first one, <code>GetGenerationStats</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="na">[McpServerToolType]</span>
</span></span><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">static</span> <span class="k">class</span> <span class="nc">GenerationStatsTool</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="na">    [McpServerTool, Description(&#34;Shows per-generation heap statistics including string size, &#34; +
</span></span></span><span class="line"><span class="cl"><span class="na">        &#34;duplication ratios, and object counts for Gen0, Gen1, Gen2, LOH, POH, and Frozen heaps. &#34; +
</span></span></span><span class="line"><span class="cl"><span class="na">        &#34;Provide either a process ID or a dump file path (not both).&#34;)]</span>
</span></span><span class="line"><span class="cl">    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">string</span> <span class="n">GetGenerationStats</span><span class="p">(</span>
</span></span><span class="line"><span class="cl"><span class="na">        [Description(&#34;Process ID to attach to (mutually exclusive with dumpPath)&#34;)]</span> <span class="kt">int?</span> <span class="n">pid</span> <span class="p">=</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="na">        [Description(&#34;Path to a memory dump file (mutually exclusive with pid)&#34;)]</span> <span class="kt">string?</span> <span class="n">dumpPath</span> <span class="p">=</span> <span class="kc">null</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">pid</span><span class="p">.</span><span class="n">HasValue</span> <span class="p">==</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="n">IsNullOrEmpty</span><span class="p">(</span><span class="n">dumpPath</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="s">&#34;Error: provide either a process ID or a dump file path, but not both.&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="kt">var</span> <span class="n">analyzer</span> <span class="p">=</span> <span class="n">StringAnalyzer</span><span class="p">.</span><span class="n">Analyze</span><span class="p">(</span><span class="n">pid</span><span class="p">,</span> <span class="n">dumpPath</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kt">var</span> <span class="n">sb</span> <span class="p">=</span> <span class="k">new</span> <span class="n">StringBuilder</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="n">OutputFormatter</span><span class="p">.</span><span class="n">FormatGenerationStats</span><span class="p">(</span><span class="n">sb</span><span class="p">,</span> <span class="n">analyzer</span><span class="p">.</span><span class="n">GenerationStats</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">sb</span><span class="p">.</span><span class="n">ToString</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>A second tool, <code>GetDuplicatedStrings</code>, follows the exact same shape but adds the <code>countThreshold</code>, <code>sizeThresholdKB</code>, and <code>stringLengthLimit</code> parameters so the agent can tune how aggressively it surfaces duplicates. The <code>[Description]</code> attributes are not cosmetic: they are what the agent reads to understand when and how to call each tool. Tools provide <strong>data access</strong> — the agent calls one, gets structured text back, and moves on.</p>
<h3 id="mcp-prompts-domain-expertise">MCP prompts: domain expertise</h3>
<p>An MCP Server tells the agent <em>what it can call and with which parameters</em>. <strong>Prompts</strong> tell it <em>what to do with them</em>. An MCP prompt is a pre-built template that encodes the workflow knowledge a human expert would apply — the order of operations, which columns to read, how to adjust thresholds based on what was just observed.</p>
<p><code>dotnet-dstrings</code> ships three of them. The most complete, <code>analyze_string_memory</code>, orchestrates both tools in sequence:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">Microsoft.Extensions.AI</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="na">
</span></span></span><span class="line"><span class="cl"><span class="na">[McpServerPromptType]</span>
</span></span><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">static</span> <span class="k">class</span> <span class="nc">DstringsPrompts</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"><span class="na">    [McpServerPrompt(Name = &#34;analyze_string_memory&#34;)]</span>
</span></span><span class="line"><span class="cl"><span class="na">    [Description(&#34;Full analysis workflow: get generation stats overview then find the worst duplicated strings&#34;)]</span>
</span></span><span class="line"><span class="cl">    <span class="kd">public</span> <span class="kd">static</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">ChatMessage</span><span class="p">&gt;</span> <span class="n">AnalyzeStringMemory</span><span class="p">(</span>
</span></span><span class="line"><span class="cl"><span class="na">        [Description(&#34;Process ID to analyze&#34;)]</span> <span class="kt">int?</span> <span class="n">pid</span> <span class="p">=</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="na">        [Description(&#34;Path to a memory dump file&#34;)]</span> <span class="kt">string?</span> <span class="n">dumpPath</span> <span class="p">=</span> <span class="kc">null</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kt">var</span> <span class="n">target</span> <span class="p">=</span> <span class="n">FormatTarget</span><span class="p">(</span><span class="n">pid</span><span class="p">,</span> <span class="n">dumpPath</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="k">new</span><span class="p">[]</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">new</span> <span class="n">ChatMessage</span><span class="p">(</span><span class="n">ChatRole</span><span class="p">.</span><span class="n">User</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s">$&#34;Analyze string memory usage in {target}.\n\n&#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;Follow these steps:\n&#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">$&#34;1. Call the GetGenerationStats tool ({target}) to get per-generation heap statistics.\n&#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;2. Review the DupSize% and HeapSize% columns to identify which generations &#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;have the most string duplication.\n&#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">$&#34;3. Call the GetDuplicatedStrings tool ({target}) to get the list of duplicated strings. &#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;If the generation stats show low duplication, use a lower countThreshold (e.g. 32) &#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;and sizeThresholdKB (e.g. 10) to surface smaller issues.\n&#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;4. Summarize the findings: overall duplication ratio, which generations are most affected, &#34;</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;and the top duplicated strings by wasted memory.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// check_generation_stats — focused interpretation of per-generation stats</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// find_duplicate_strings — find duplicates and suggest remediation (intern, cache, dedup)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Again, a static class decorated with a <strong><code>McpServerPromptType</code></strong> attribute lists the available prompts as static methods decorated with a <strong><code>Description</code></strong> property. The same <strong><code>Description</code></strong> attribute is used to describe the parameters (here, either a process id or a memory dump file path). The core of the static method builds the prompt as a <strong><code>ChatMessage</code></strong>.</p>
<p>With <strong>2 tools and 3 prompts</strong>, the agent gets both the <em>data</em> (tools) and the <em>workflow knowledge</em> (prompts). The user can pick the <code>analyze_string_memory</code> prompt and the agent will know to &ldquo;get the generation stats first, then adjust thresholds based on the duplication ratios it just saw&rdquo; — without the user having to spell any of that out.</p>
<h3 id="the-project-file">The project file</h3>
<p>A single <code>.csproj</code> produces the dual-mode tool. It targets <code>net6.0</code> with <code>RollForward=Major</code> so the tool runs on any .NET 6+ runtime (including .NET 10), and pulls in the three packages it needs:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;Project</span> <span class="na">Sdk=</span><span class="s">&#34;Microsoft.NET.Sdk&#34;</span><span class="nt">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;PropertyGroup&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;OutputType&gt;</span>Exe<span class="nt">&lt;/OutputType&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;TargetFramework&gt;</span>net6.0<span class="nt">&lt;/TargetFramework&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;RollForward&gt;</span>Major<span class="nt">&lt;/RollForward&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;RootNamespace&gt;</span>dstrings<span class="nt">&lt;/RootNamespace&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;ImplicitUsings&gt;</span>enable<span class="nt">&lt;/ImplicitUsings&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;Nullable&gt;</span>enable<span class="nt">&lt;/Nullable&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;PackAsTool&gt;</span>true<span class="nt">&lt;/PackAsTool&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;ToolCommandName&gt;</span>dotnet-dstrings<span class="nt">&lt;/ToolCommandName&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;PackageOutputPath&gt;</span>./nupkg<span class="nt">&lt;/PackageOutputPath&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;GeneratePackageOnBuild&gt;</span>true<span class="nt">&lt;/GeneratePackageOnBuild&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/PropertyGroup&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;ItemGroup&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">&#34;Microsoft.Diagnostics.Runtime&#34;</span> <span class="na">Version=</span><span class="s">&#34;3.1.512801&#34;</span> <span class="nt">/&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">&#34;Microsoft.Extensions.Hosting&#34;</span> <span class="na">Version=</span><span class="s">&#34;6.0.1&#34;</span> <span class="nt">/&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">&#34;ModelContextProtocol&#34;</span> <span class="na">Version=</span><span class="s">&#34;1.1.0&#34;</span> <span class="nt">/&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&lt;/ItemGroup&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/Project&gt;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>CLI argument parsing is done by hand to keep the dependency list minimal, and the <code>ModelContextProtocol</code> package transitively brings in <code>Microsoft.Extensions.AI.Abstractions</code> for the <code>ChatMessage</code> type used by the prompts.</p>
<p>This is where a template-generated MCP Server would be different. The following new element would have been added:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl">   <span class="nt">&lt;PackageType&gt;</span>McpServer<span class="nt">&lt;/PackageType&gt;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>When the package is uploaded to nuget.org, this information is used to identify as an MCP Server and NOT as a CLI tool! It is not possible to be both. For compatibility&rsquo;s sake, I kept the CLI tool identity for both <strong><code>dotnet-dstrings</code></strong> and <strong><code>dotnet-pstacks</code></strong> even though they can behave as MCP servers.</p>
<h3 id="wiring-it-into-your-ai-client">Wiring it into your AI client</h3>
<p>Once the tool is installed globally (<code>dotnet tool install -g dotnet-dstrings</code>), any MCP-compatible client can launch it in MCP mode by passing <code>--mcp</code> on the command line. The configuration is a small JSON snippet; the file location and the top-level key differ slightly per client. Note that the VS template generates a server.json file under the .mcp project sub-folder.</p>
<p>Here are the three most common ones.</p>
<p><strong>VS Code</strong> — <code>C:\Users\&lt;user&gt;\AppData\Roaming\Code\User\mcp.json</code> (key: <code>servers</code>):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;servers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;dstrings&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;stdio&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;dotnet-dstrings&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;--mcp&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>Cursor</strong> — <code>C:\Users\&lt;user&gt;\.cursor\mcp.json</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;mcpServers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;dstrings&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;dotnet-dstrings&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;--mcp&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>Claude Code</strong> — <code>C:\Users\&lt;user&gt;\.mcp.json</code> (key: <code>mcpServers</code> but also understand <code>servers</code>; remember to restart after editing):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;mcpServers&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;dstrings&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;dotnet-dstrings&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;--mcp&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Visual Studio, and the GitHub Copilot CLI work too. The only things that change are the config file location and the JSON key:</p>
<table>
  <thead>
      <tr>
          <th>Client</th>
          <th>JSON key</th>
          <th>Config file / location</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Visual Studio</td>
          <td><code>servers</code></td>
          <td><code>.mcp.json</code> (solution root or <code>%USERPROFILE%</code>); in the same per user file as Claude</td>
      </tr>
      <tr>
          <td>Copilot CLI</td>
          <td><code>mcpServers</code></td>
          <td><code>C:\Users\&lt;user&gt;\.copilot\mcp-config.json</code></td>
      </tr>
  </tbody>
</table>
<p>All of them discover the same 2 tools and 3 prompts from the same binary.</p>
<h3 id="key-takeaway-for-mcp-servers">Key takeaway for MCP servers</h3>
<p>Same binary, same NuGet package, same <code>dotnet tool install</code> — but two audiences. A human types <code>dotnet-dstrings app.dmp</code>; an agent launches <code>dotnet-dstrings --mcp</code> and discovers typed tools and workflow prompts. There is no shell command parsing on the agent side, because MCP handles the JSON serialization. Also remember that such a binary cannot be registered in nuget.org as both a CLI tool and an MCP server.</p>
<p>The catch is that it requires modifying the tool&rsquo;s source code, so it works for <strong>your own tools</strong> but not for Microsoft&rsquo;s <code>dotnet-dump</code>, <code>dotnet-counters</code>, and the like. That is exactly where the second approach comes in.</p>
<h2 id="skillmd-zero-code-ai-integration">SKILL.md: zero-code AI integration</h2>
<h3 id="what-is-a-skill">What is a skill?</h3>
<p>A <code>SKILL.md</code> file is just a markdown document that teaches an AI agent a workflow built on <strong>existing CLI tools, unchanged</strong>. No NuGet packages, no recompilation — it works with Microsoft&rsquo;s <code>dotnet-dump</code>, <code>dotnet-counters</code>, <code>dotnet-trace</code> out of the box. The agent reads the skill, runs the shell commands it describes, and interprets the text output.</p>
<p>The format started in the Claude/Cursor ecosystem but is quickly becoming a cross-agent standard. The same file can be picked up by Cursor, VS Code + Copilot, Visual Studio, Claude Code, and the GitHub Copilot CLI.</p>
<h3 id="where-to-store-a-skill">Where to store a skill</h3>
<p>Each tool looks for skills in its own directory, with a project scope (shared via source control) and a personal scope (local to you):</p>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>Project scope (shared)</th>
          <th>Personal scope (local)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Cursor</strong></td>
          <td><code>.cursor/skills/&lt;name&gt;/SKILL.md</code></td>
          <td><code>~/.cursor/skills/&lt;name&gt;/SKILL.md</code></td>
      </tr>
      <tr>
          <td><strong>VS Code</strong> (Copilot)</td>
          <td><code>.github/skills/&lt;name&gt;/SKILL.md</code></td>
          <td><code>~/.copilot/skills/&lt;name&gt;/SKILL.md</code></td>
      </tr>
      <tr>
          <td><strong>Visual Studio</strong> (Copilot)</td>
          <td><code>.github/skills/&lt;name&gt;/SKILL.md</code></td>
          <td><code>~/.copilot/skills/&lt;name&gt;/SKILL.md</code></td>
      </tr>
      <tr>
          <td><strong>GitHub Copilot CLI</strong></td>
          <td><code>.github/skills/&lt;name&gt;/SKILL.md</code></td>
          <td><code>~/.copilot/skills/&lt;name&gt;/SKILL.md</code></td>
      </tr>
      <tr>
          <td><strong>Claude Code</strong></td>
          <td><code>.claude/skills/&lt;name&gt;/SKILL.md</code></td>
          <td><code>~/.claude/skills/&lt;name&gt;/SKILL.md</code></td>
      </tr>
  </tbody>
</table>
<p>To maximize portability you can drop the same skill in several of these folders, or use the emerging cross-agent path <code>.agents/skills/</code>. Agents discover skills automatically based on the <code>description</code> field in the YAML frontmatter — no manual enablement needed.</p>
<p>A quick note for Claude Code users: <code>CLAUDE.md</code> and <code>SKILL.md</code> are complementary. <code>CLAUDE.md</code> is <strong>always-on</strong> context injected into every conversation (project layout, conventions, build commands). A <code>SKILL.md</code> is loaded <strong>on demand</strong>, only when the user&rsquo;s prompt matches its description. Use <code>CLAUDE.md</code> for what the agent should always know, and <code>SKILL.md</code> for specialized workflows like memory dump analysis that would be wasteful to inject every time.</p>
<h3 id="anatomy-of-a-diagnostics-skill">Anatomy of a diagnostics skill</h3>
<p>I have built two skills:</p>
<ul>
<li>
<p><code>dotnet-memory-analysis</code> that teaches the agent a full memory-leak investigation workflow using <code>dotnet-dump</code>, <code>dotnet-gcdump</code>, and <code>dotnet-dstrings</code></p>
</li>
<li>
<p><code>dotnet-thread-analysis</code> that look for thread-related latency issues using <code>dotnet-pstacks</code> and <code>dotnet-dump</code></p>
</li>
</ul>
<p>Each starts with a <strong>tool selection</strong> table that tells the agent to prefer an MCP server when one is available, and to fall back to the CLI otherwise:</p>
<table>
  <thead>
      <tr>
          <th>Goal</th>
          <th>Prefer (MCP)</th>
          <th>Fallback (CLI)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Duplicate strings</td>
          <td><code>get_duplicated_strings</code> (server: <code>user-dotnet-dstrings</code>)</td>
          <td><code>dotnet-dstrings &lt;dump&gt;</code></td>
      </tr>
      <tr>
          <td>Heap stats</td>
          <td>—</td>
          <td><code>dotnet-dump analyze -c &quot;dumpheap -stat&quot;</code></td>
      </tr>
      <tr>
          <td>GC root</td>
          <td>—</td>
          <td><code>dotnet-dump analyze -c &quot;gcroot &lt;addr&gt;&quot;</code></td>
      </tr>
      <tr>
          <td>Heap overview</td>
          <td>—</td>
          <td><code>dotnet-dump analyze -c &quot;eeheap -gc&quot;</code></td>
      </tr>
  </tbody>
</table>
<p>Then comes the single most important technique for agent-driven debugging: <strong>non-interactive SOS commands</strong>. <code>dotnet-dump analyze</code> is interactive by default, which traps an agent in a prompt it cannot escape. The fix is to pass commands with <code>-c</code> and always end with <code>exit</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dotnet-dump analyze &lt;dump&gt; -c <span class="s2">&#34;dumpheap -stat&#34;</span> -c <span class="s2">&#34;exit&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The body of the skill is a <strong>decision tree</strong> rather than prose. The agent runs a triage command, then routes itself based on what it sees:</p>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>Next step</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>One application type dominates the heap</td>
          <td><code>dumpheap -mt &lt;MT&gt;</code> then <code>gcroot</code> on 2-3 instances</td>
      </tr>
      <tr>
          <td><code>System.String</code> dominates</td>
          <td><code>get_duplicated_strings</code> (or <code>dotnet-dstrings</code>)</td>
      </tr>
      <tr>
          <td>Growth spread across many types</td>
          <td>Look for a holding collection, then <code>gcroot</code> on it</td>
      </tr>
      <tr>
          <td>Large finalizer queue</td>
          <td><code>finalizequeue</code>, check &ldquo;Ready for finalization&rdquo;</td>
      </tr>
      <tr>
          <td>Heap small but process memory large</td>
          <td>Native leak — inspect the <code>eeheap</code> gap</td>
      </tr>
  </tbody>
</table>
<p>It closes with explicit <strong>stop conditions</strong> (when is the investigation actually done?), a <strong>common errors</strong> table (DAC version mismatch, access denied, empty MCP results), and a set of <strong>safety guardrails</strong>.</p>
<p>A summary document should be generated to list the investigation&rsquo;s step and MCP tools/CLI command used. After the first version, I iterated on these files using Claude code and Cursor with different models asking for feedback about how to make them better. For example, one of the changes was to extract the summary template to limit the impact on the context consumption.</p>
<h3 id="the-design-principles-that-matter">The design principles that matter</h3>
<p>Three principles make a skill reliable instead of a loose pile of commands:</p>
<ul>
<li><strong>Progressive analysis</strong> — start with cheap summaries (<code>dumpheap -stat</code>) before drilling into individual instances. Don&rsquo;t dump the whole heap when a histogram will do. This is also good to minimize the impact on the context usage.</li>
<li><strong>Decision-driven</strong> — every step states what to look for <em>before</em> choosing the next command, so the agent reasons instead of guessing.</li>
<li><strong>Safety guardrails</strong> — warn before freezing a process with <code>dotnet-dump collect</code>, and never kill a process without the user&rsquo;s explicit consent.</li>
</ul>
<p>With this skill installed, you can tell the agent &ldquo;PID 12345 seems to be leaking memory, can you investigate?&rdquo; and watch it capture a dump, find the dominant type, trace the GC root to the static field that holds it, and recommend a fix — all with Microsoft&rsquo;s stock tools and not a single line of code on your side.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Here are the two things to remember from this post:</p>
<ol>
<li><strong>MCP servers</strong> give you structured tool discovery for the tools <strong>you own</strong> — typed parameters and workflow prompts, no shell parsing, working identically across Visual Studio, VS Code, Cursor, Claude, and the Copilot CLI.</li>
<li><strong>SKILL.md files</strong> give you <strong>zero-code</strong> integration with <strong>any</strong> existing CLI tool — a markdown decision tree that teaches the agent your diagnostic workflow, ideal for Microsoft&rsquo;s <code>dotnet-dump</code> and <code>dotnet-counters</code>.</li>
</ol>
<h3 id="going-further-controlling-a-gui-application">Going further: controlling a GUI application</h3>
<p>This idea is not even limited to CLI tools. My friend Kevin Gosse&rsquo;s <a href="https://github.com/kevingosse/windbg-bridge">windbg-bridge</a> shows how an AI agent can <strong>drive a GUI application</strong> — WinDbg — through a named-pipe bridge. The user and the agent share the <em>same</em> WinDbg instance: the user can type a question in the harness, the agent runs the right commands, and the answer comes back inline. It is also possible to type your own command in WinDbg. It is a glimpse of AI-assisted workflows going well beyond command line tools.</p>
<h3 id="references">References</h3>
<ul>
<li>Skills and model feedbacks are available in my repository from my session at  <a href="https://github.com/chrisnas/UpdateKrakow2026/tree/main/Skills">Update Conference Krakow 2026</a></li>
<li>Tools: <code>dotnet-dump</code>, <code>dotnet-counters</code>, <a href="https://github.com/chrisnas/DebuggingExtensions"><code>dotnet-pstacks</code>, <code>dotnet-dstrings</code></a></li>
<li><a href="https://github.com/chrisnas/DebuggingExtensions">chrisnas/DebuggingExtensions</a> — home of <code>dotnet-dstrings</code> and <code>dotnet-pstacks</code></li>
<li>MCP C# SDK: <a href="https://github.com/modelcontextprotocol/csharp-sdk">modelcontextprotocol/csharp-sdk</a></li>
<li>Build an MCP server in C#: <a href="https://devblogs.microsoft.com/dotnet/build-a-model-context-protocol-mcp-server-in-csharp/">devblogs.microsoft.com</a></li>
<li>MCP project template: <code>dotnet new mcpserver</code> (<a href="https://www.nuget.org/packages/Microsoft.McpServer.ProjectTemplates">Microsoft.McpServer.ProjectTemplates</a>)</li>
<li>Controlling a GUI application: <a href="https://github.com/kevingosse/windbg-bridge">kevingosse/windbg-bridge</a></li>
</ul>
]]></content:encoded></item></channel></rss>