DEV Community

Matt Anderson
Matt Anderson

Posted on

Your Existing ASP.NET Core API is Already an MCP Server — You Just Don't Know It Yet

If you've been following the AI tooling space lately, you've probably heard about MCP — the Model Context Protocol. It's the standard that lets AI clients like Claude connect to external tools and APIs. And if you're a .NET developer, you've probably also had the thought: "How do I expose my existing API as MCP tools without rewriting everything?"

The answer most tutorials give you looks something like this:

[McpServerToolType]
public class OrderTools
{
    private readonly OrderService _orders;

    public OrderTools(OrderService orders) => _orders = orders;

    [McpServerTool, Description("Retrieves a single order by ID.")]
    public async Task<Order> GetOrder(int id) => await _orders.GetByIdAsync(id);

    [McpServerTool, Description("Creates a new order.")]
    public async Task<Order> CreateOrder(string customerName, string product, int quantity)
        => await _orders.CreateAsync(customerName, product, quantity);
}
Enter fullscreen mode Exit fullscreen mode

You're duplicating your controller logic. Your auth filters don't run. Your ModelState validation doesn't run. Your existing DI pipeline is bypassed. You're maintaining two surfaces for the same functionality.

There's a better way.


Introducing ZeroMCP

ZeroMCP is a .NET library that exposes your existing ASP.NET Core API as an MCP server with a single attribute and two lines of setup. No separate process. No code duplication. No rewriting.

Here's what it looks like:

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet("{id}")]
    [Mcp("get_order", Description = "Retrieves a single order by ID.")]
    public ActionResult<Order> GetOrder(int id) { ... }

    [HttpPost]
    [Mcp("create_order", Description = "Creates a new order.")]
    public ActionResult<Order> CreateOrder([FromBody] CreateOrderRequest request) { ... }

    [HttpDelete("{id}")]
    // No [Mcp] — invisible to MCP clients
    public IActionResult Delete(int id) { ... }
}
Enter fullscreen mode Exit fullscreen mode

That's it. Your existing controller, your existing logic, your existing pipeline — now also an MCP server.


Quick Start

1. Install

<PackageReference Include="ZeroMcp" Version="1.*" />
Enter fullscreen mode Exit fullscreen mode

2. Register services

builder.Services.AddZeroMcp(options =>
{
    options.ServerName = "My Orders API";
    options.ServerVersion = "1.0.0";
});
Enter fullscreen mode Exit fullscreen mode

3. Map the endpoint

app.MapZeroMcp(); // registers GET and POST /mcp
Enter fullscreen mode Exit fullscreen mode

4. Connect your MCP client

Add this to your claude_desktop_config.json:

{
  "mcpServers": {
    "my-api": {
      "type": "http",
      "url": "http://localhost:5000/mcp"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That's the entire setup. Point Claude at your /mcp endpoint and it will see your tagged actions as tools.


Why This Approach Is Different

Your entire pipeline runs

When an MCP client calls one of your tools, ZeroMCP doesn't call your method directly. It creates a fresh DI scope, builds a synthetic HttpContext with the correct route values, query string, and body, and dispatches through IActionInvokerFactory — the same pipeline a real HTTP request uses.

This means:

  • [Authorize] works — auth filters run normally
  • ModelState validation works — validation errors return as proper MCP errors
  • Exception filters work — unhandled exceptions are caught and returned gracefully
  • Your DI services, repositories, and business logic are called as-is

Parameters are merged automatically

ZeroMCP merges route params, query params, and body properties into a single flat JSON Schema that the LLM fills in:

[HttpPatch("{id}/status")]
[Mcp("update_order_status", Description = "Updates an order's status.")]
public IActionResult UpdateStatus(int id, [FromBody] UpdateStatusRequest req) { ... }

public class UpdateStatusRequest
{
    [Required] public string Status { get; set; }
    public string? Reason { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Produces this MCP input schema automatically:

{
  "type": "object",
  "properties": {
    "id":     { "type": "integer" },
    "status": { "type": "string" },
    "reason": { "type": "string" }
  },
  "required": ["id", "status"]
}
Enter fullscreen mode Exit fullscreen mode

Minimal APIs are supported too

app.MapGet("/api/health", () => Results.Ok(new { status = "ok" }))
   .AsMcp("health_check", "Returns API health status.");
Enter fullscreen mode Exit fullscreen mode

Built for Production

ZeroMCP isn't just a quick demo library. It ships with features that enterprise teams actually need.

Auth forwarding

builder.Services.AddZeroMcp(options =>
{
    options.ForwardHeaders = ["Authorization"];
});
Enter fullscreen mode Exit fullscreen mode

Headers are copied from the MCP request to the synthetic dispatch request, so your existing JWT or API key auth just works.

Role and policy-based tool visibility

[Mcp("admin_report", Description = "Runs admin report.", Roles = ["Admin"])]
public ActionResult<Report> GetAdminReport() { ... }
Enter fullscreen mode Exit fullscreen mode

Tools not visible to the current user won't appear in tools/list at all — and direct calls to hidden tools are also rejected.

Per-request tool filtering

options.ToolVisibilityFilter = (name, ctx) =>
    ctx.Request.Headers.TryGetValue("X-Beta-Features", out _) || !name.StartsWith("beta_");
Enter fullscreen mode Exit fullscreen mode

Observability out of the box

  • Structured logging with correlation IDs, tool name, status code, and duration
  • OpenTelemetry enrichment via EnableOpenTelemetryEnrichment = true
  • Pluggable metrics sink via IMcpMetricsSink

The Brownfield Story

This is where ZeroMCP really shines. If you have a 5-year-old ASP.NET Core API with hundreds of endpoints, complex auth, custom filters, and business logic that's been battle-tested in production — you don't need to rewrite any of it to participate in the MCP ecosystem.

You add a NuGet package, two lines of setup, and [Mcp] attributes to whichever endpoints make sense to expose. Your existing tests still pass. Your existing auth still works. Your existing monitoring still fires.

That's the zero in ZeroMCP.


Get Started

PRs and issues welcome. The MCP ecosystem for .NET is still early — if you try it and hit something that doesn't work, open an issue.

Top comments (1)

Collapse
 
laura_russell_27b0a92cf18 profile image
Laura

Nice! Works well.