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);
}
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) { ... }
}
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.*" />
2. Register services
builder.Services.AddZeroMcp(options =>
{
options.ServerName = "My Orders API";
options.ServerVersion = "1.0.0";
});
3. Map the endpoint
app.MapZeroMcp(); // registers GET and POST /mcp
4. Connect your MCP client
Add this to your claude_desktop_config.json:
{
"mcpServers": {
"my-api": {
"type": "http",
"url": "http://localhost:5000/mcp"
}
}
}
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 -
ModelStatevalidation 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; }
}
Produces this MCP input schema automatically:
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"status": { "type": "string" },
"reason": { "type": "string" }
},
"required": ["id", "status"]
}
Minimal APIs are supported too
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" }))
.AsMcp("health_check", "Returns API health status.");
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"];
});
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() { ... }
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_");
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
- NuGet: nuget.org/packages/ZeroMcp
- GitHub: github.com/ZeroMcp/ZeroMCP.net
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)
Nice! Works well.