<?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: Samuel Sidor</title>
    <description>The latest articles on DEV Community by Samuel Sidor (@kebechet).</description>
    <link>https://dev.to/kebechet</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2273907%2Fe039d19b-58f3-418d-9d85-83d7bdff2947.jpg</url>
      <title>DEV Community: Samuel Sidor</title>
      <link>https://dev.to/kebechet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kebechet"/>
    <language>en</language>
    <item>
      <title>Your ASP.NET API Already Speaks MCP - It Just Doesn't Know It Yet</title>
      <dc:creator>Samuel Sidor</dc:creator>
      <pubDate>Fri, 27 Mar 2026 15:10:41 +0000</pubDate>
      <link>https://dev.to/kebechet/your-aspnet-api-already-speaks-mcp-it-just-doesnt-know-it-yet-2870</link>
      <guid>https://dev.to/kebechet/your-aspnet-api-already-speaks-mcp-it-just-doesnt-know-it-yet-2870</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;code&gt;dotnet add package Kebechet.Api.ToMcp&lt;/code&gt; + 3 lines in Program.cs = your ASP.NET controllers become AI-callable MCP tools. No separate server, no OpenAPI spec, compile-time generated. &lt;a href="https://github.com/Kebechet/Api.ToMcp" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three lines of code. That's what it took to let Claude talk to my ASP.NET API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMcpTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetExecutingAssembly&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseMcpLoopPrevention&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapMcpEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hand-written tool classes. No separate proxy server. No OpenAPI spec to maintain. The controllers I already had became AI-callable tools automatically.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/Kebechet/Api.ToMcp" rel="noopener noreferrer"&gt;Api.ToMcp&lt;/a&gt; - a C# source generator that reads your existing controllers at compile time and generates MCP-compatible tool classes. This is the story of how it went from an idea to a published NuGet package in a single day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use OpenAPI?
&lt;/h2&gt;

&lt;p&gt;Before diving in, let's address the elephant in the room. There are already ways to bridge REST APIs to MCP, and most of them work through OpenAPI/Swagger specs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAPI/Swagger JSON approach&lt;/strong&gt; - Tools like &lt;a href="https://github.com/janwilmake/openapi-mcp-server" rel="noopener noreferrer"&gt;openapi-mcp-server&lt;/a&gt; (887 stars) and &lt;a href="https://github.com/automation-ai-labs/mcp-link" rel="noopener noreferrer"&gt;mcp-link&lt;/a&gt; (605 stars) read your &lt;code&gt;swagger.json&lt;/code&gt; and dynamically generate MCP tools at runtime. If your API already produces a Swagger document (and most ASP.NET Core APIs do), you point the tool at the JSON and it creates MCP endpoints on the fly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API gateway approach&lt;/strong&gt; - &lt;a href="https://github.com/TykTechnologies/api-to-mcp" rel="noopener noreferrer"&gt;Tyk's api-to-mcp&lt;/a&gt; works at the gateway level. Language-agnostic - doesn't matter if your backend is .NET, Node, Python, or Go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framework-specific code transforms&lt;/strong&gt; - &lt;a href="https://github.com/addozhang/spring-rest-to-mcp" rel="noopener noreferrer"&gt;spring-rest-to-mcp&lt;/a&gt; uses OpenRewrite to transform Spring REST controllers into MCP servers. Similar idea to Api.ToMcp but for the Java ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual HTTP configuration&lt;/strong&gt; - &lt;a href="https://github.com/Tght1211/http-4-mcp" rel="noopener noreferrer"&gt;http-4-mcp&lt;/a&gt; provides a visual interface where you configure HTTP-to-MCP mappings without writing code.&lt;/p&gt;

&lt;p&gt;These all work. But they share common trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime overhead&lt;/strong&gt; - they parse specs and create tools dynamically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loose typing&lt;/strong&gt; - they work with JSON schemas, not your actual C# types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No compile-time safety&lt;/strong&gt; - if your API changes, mismatches surface at runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate process&lt;/strong&gt; - they run as a standalone server or proxy between the AI client and your API. That's an extra service to deploy, monitor, and keep running&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something that felt native to .NET - compile-time generated, type-safe, and deployed as part of the same application. One service, one deployment, one process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: MCP is great, bridging is not
&lt;/h2&gt;

&lt;p&gt;If you haven't heard of &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;, it's an open standard that lets AI assistants (like Claude, Cursor, etc.) call tools - essentially functions - in a structured way. Think of it as a universal plugin system for AI.&lt;/p&gt;

&lt;p&gt;The .NET ecosystem already has great MCP support through the official &lt;code&gt;ModelContextProtocol&lt;/code&gt; package. You define tool classes, decorate them with attributes, and you're live. But here's the catch - each tool is a separate class that you write manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerToolType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Products_GetByIdTool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Products_GetById"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Gets a product by its unique identifier."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IMcpHttpInvoker&lt;/span&gt; &lt;span class="n"&gt;invoker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Parameter: id"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"/api/products/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EscapeDataString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;invoker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now multiply that by every endpoint you want to expose. It's boilerplate. Pure, mechanical, error-prone boilerplate.&lt;/p&gt;

&lt;p&gt;I was building a .NET MAUI fitness app with a standard ASP.NET Core backend. Dozens of controllers, hundreds of endpoints. The routes were defined. The parameters were typed. The XML docs were written. All the information was &lt;em&gt;right there&lt;/em&gt; in the source code - I just needed something to read it and generate the glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "what if" moment
&lt;/h2&gt;

&lt;p&gt;C# source generators can inspect your code at compile time and emit new source files. They see your classes, methods, attributes, parameters - everything Roslyn knows, you know.&lt;/p&gt;

&lt;p&gt;So the idea was simple: scan controllers, find HTTP action methods, and for each one, generate an MCP tool class that calls the original endpoint via HTTP internally. The API stays untouched. The MCP layer is purely additive.&lt;/p&gt;

&lt;h2&gt;
  
  
  From zero to NuGet in one day
&lt;/h2&gt;

&lt;p&gt;I started on January 16th. The commit history tells the story:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Morning&lt;/strong&gt; - scaffolded the project structure and implemented the basic generator. Three projects: &lt;code&gt;Abstractions&lt;/code&gt; (attributes and config), &lt;code&gt;Generator&lt;/code&gt; (the source generator itself), and &lt;code&gt;Runtime&lt;/code&gt; (HTTP invoker, middleware, DI extensions).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Midday&lt;/strong&gt; - hit the first real bugs. Parsing controller routes with constraints like &lt;code&gt;{id:guid}&lt;/code&gt; was trickier than expected. Tool registration wasn't wiring up correctly. Fixed both, added Swagger to the demo project so I could verify the API side independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Afternoon&lt;/strong&gt; - added tests, linked everything to a solution, set up GitHub Actions for build and NuGet publish. The first preview landed on NuGet that same evening.&lt;/p&gt;

&lt;p&gt;By end of day, you could do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Kebechet.Api.ToMcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;generator.json&lt;/code&gt;, three lines in &lt;code&gt;Program.cs&lt;/code&gt;, and your API was speaking MCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;The generator reads a &lt;code&gt;generator.json&lt;/code&gt; config that controls which endpoints to expose (allowlist via &lt;code&gt;SelectedOnly&lt;/code&gt; or blocklist via &lt;code&gt;AllExceptExcluded&lt;/code&gt;), scans your controllers for HTTP action methods, and emits MCP tool classes at compile time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SelectedOnly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ProductsController.GetAll"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ProductsController.GetById"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"naming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"toolNameFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{Controller}_{Action}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"removeControllerSuffix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attributes always win over config - &lt;code&gt;[McpExpose]&lt;/code&gt; forces inclusion, &lt;code&gt;[McpIgnore]&lt;/code&gt; forces exclusion. This gives you granular control over what AI can and can't touch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id:guid}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpIgnore&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;// AI should never delete products&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what the generator produces for &lt;code&gt;ProductsController.GetAll&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;McpServerToolType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController_GetAllTool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;McpServerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Products_GetAll"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Gets all products with optional category filter."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IMcpHttpInvoker&lt;/span&gt; &lt;span class="n"&gt;invoker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Parameter: category"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queryParts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;queryParts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"category=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EscapeDataString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/api/products"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryParts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"?"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;amp;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queryParts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;invoker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeforeInvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;McpScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;invoker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the XML doc comment &lt;code&gt;/// &amp;lt;summary&amp;gt;Gets all products...&amp;lt;/summary&amp;gt;&lt;/code&gt; became the &lt;code&gt;[Description]&lt;/code&gt;. The AI assistant sees a well-described tool, not a cryptic endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop prevention problem
&lt;/h2&gt;

&lt;p&gt;Here's something I didn't anticipate until it happened: what if the AI calls an MCP tool, which calls the API, which somehow triggers another MCP call? Infinite loop.&lt;/p&gt;

&lt;p&gt;The solution is a simple middleware + header combo. Every internal HTTP call from the MCP invoker adds an &lt;code&gt;X-MCP-Internal-Call&lt;/code&gt; header. The middleware checks for this on MCP endpoints and returns &lt;code&gt;400 Bad Request&lt;/code&gt; if present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWithSegments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/mcp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ContainsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-MCP-Internal-Call"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"MCP endpoints cannot be called internally to prevent loops."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, effective, zero configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication forwarding
&lt;/h2&gt;

&lt;p&gt;If your API uses JWT authentication, the MCP tools need to forward those credentials. The &lt;code&gt;McpHttpInvoker&lt;/code&gt; automatically grabs the &lt;code&gt;Authorization&lt;/code&gt; header from the incoming MCP request and attaches it to the internal API call. Your &lt;code&gt;[Authorize]&lt;/code&gt; attributes just work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope-based access control
&lt;/h2&gt;

&lt;p&gt;After the initial release, I added a feature I hadn't planned but quickly realized was necessary: not every AI session should have access to every tool.&lt;/p&gt;

&lt;p&gt;A read-only analytics dashboard shouldn't be able to call &lt;code&gt;POST /api/products&lt;/code&gt;. So I mapped HTTP methods to scopes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;HTTP Methods&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;GET, HEAD, OPTIONS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write&lt;/td&gt;
&lt;td&gt;POST, PUT, PATCH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You configure a JWT claim mapper, and the invoker validates scopes before each call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMcpTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetExecutingAssembly&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClaimName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClaimToScopeMapper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claimValue&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;McpScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claimValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;McpScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claimValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"write"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;McpScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the scope doesn't match, the tool returns an &lt;code&gt;UnauthorizedAccessException&lt;/code&gt; before any HTTP call is made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons from shipping fast
&lt;/h2&gt;

&lt;p&gt;Looking back at the commit log, a few things stand out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source generators are powerful but unforgiving.&lt;/strong&gt; Debugging is painful - you're writing code that writes code, and errors show up as compile-time diagnostics, not runtime exceptions. Snapshot testing (comparing generated output against expected files) saved me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "just ship it" approach works.&lt;/strong&gt; The first version had rough edges. Parameterless method generation was broken (fixed next day). The URL handling had issues in production behind reverse proxies (fixed by a community PR a month later). But having a working package on NuGet meant people could try it and report real problems instead of theoretical ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community feedback matters immediately.&lt;/strong&gt; Within a month, I got a PR from &lt;a href="https://github.com/Saurus119" rel="noopener noreferrer"&gt;Saurus119&lt;/a&gt; fixing URL normalization for production environments. I had been testing locally - they were running it behind a load balancer. That's the kind of bug you only find with real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;There are still &lt;a href="https://github.com/Kebechet/Api.ToMcp/issues" rel="noopener noreferrer"&gt;open issues&lt;/a&gt; I'm thinking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should the &lt;code&gt;isError&lt;/code&gt; flag in MCP responses reflect HTTP status codes automatically?&lt;/li&gt;
&lt;li&gt;How should cancellation tokens be handled across the MCP-to-HTTP boundary?&lt;/li&gt;
&lt;li&gt;Extracting XML doc comments for richer tool descriptions (issue #1 - one of the first things I filed)&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Kebechet.Api.ToMcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/Kebechet/Api.ToMcp" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; has a full demo project you can run. Add the package, create a &lt;code&gt;generator.json&lt;/code&gt;, add three lines to &lt;code&gt;Program.cs&lt;/code&gt;, and your API speaks MCP.&lt;/p&gt;

&lt;p&gt;If you're building something with it, or if you have ideas for improvement, &lt;a href="https://github.com/Kebechet/Api.ToMcp/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; or submit a PR. The codebase is intentionally small - the entire generator is under 500 lines.&lt;/p&gt;

&lt;p&gt;Every ASP.NET API already has everything MCP needs - typed endpoints, route metadata, XML docs. The only question is whether you extract that information yourself, or let the compiler do it for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this was useful, feel free to connect with me on &lt;a href="https://x.com/samuel_sidor" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt; or check out my other .NET open-source work on &lt;a href="https://github.com/Kebechet" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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