DEV Community

norequest
norequest

Posted on

Turn your existing ASP.NET Core API into MCP tools at build time

You already shipped a Web API. Now an AI agent wants to use it. With the official MCP C# SDK, the path from "I have endpoints" to "an agent can call them" runs straight through a pile of hand-written boilerplate. McpIt removes that pile. You add one attribute, and the tools are generated for you at compile time.

The boilerplate problem

The official MCP C# SDK is excellent, Microsoft-backed, and the right foundation. But it expects you to hand-write an [McpServerTool] class for every operation you want an agent to reach. You already described that operation once: the route, the HTTP verb, the parameters, the validation, the business logic all live in your controller action. Writing a second parallel class that mirrors all of it, then keeping the two in sync forever, is exactly the kind of duplication that rots.

If you have ten endpoints you want to expose, you write and maintain ten extra classes. Rename a parameter in the controller, and the tool drifts until you remember to update it by hand. That is the tax McpIt is built to remove.

Before and after

Here is a normal ASP.NET Core controller action:

[ApiController]
[Route("orders")]
public class OrdersController : ControllerBase
{
    [HttpGet("{id}")]
    public string GetOrder(int id) => $"order-{id}";
}
Enter fullscreen mode Exit fullscreen mode

Here is the same action exposed to AI agents. Add one attribute and a <summary> for the description:

using McpIt;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("orders")]
public class OrdersController : ControllerBase
{
    /// <summary>Gets an order by its id.</summary>
    [HttpGet("{id}")]
    [McpTool(Name = "getOrder")]
    public string GetOrder(int id) => $"order-{id}";
}
Enter fullscreen mode Exit fullscreen mode

That is the whole change. At compile time McpIt generates the MCP tool class for getOrder. It reads the action's HTTP verb, route template, parameters, and XML summary, then builds the tool's input schema and safety hints from them. The id route parameter becomes a typed tool argument. The <summary> becomes the tool description.

To an MCP client, a tools/list call now returns:

{
  "tools": [
    {
      "name": "getOrder",
      "description": "Gets an order by its id.",
      "inputSchema": {
        "type": "object",
        "properties": { "id": { "type": "integer" } },
        "required": ["id"]
      },
      "annotations": { "readOnlyHint": true, "idempotentHint": true }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Notice the annotations. Because GetOrder is a GET, McpIt marks the tool read-only and idempotent automatically. No hand-written class, no duplicated schema.

How it works

Three things make McpIt different from the alternatives.

It is a source generator, not a runtime layer. The tool classes exist at build time. There is no reflection scanning your assembly at startup and no proxy object built on the fly. That means it is AOT-safe: it works under Native AOT and trimming, where reflection-heavy approaches break.

It invokes your action in-process. When an agent calls getOrder, McpIt calls your real GetOrder method directly. Your routing, model binding, validation, and business logic all run exactly as they do for an HTTP caller. There is no second HTTP request looping back into your own server. The only comparable library, Api.ToMcp, makes an internal HTTP self-call for every tool invocation. McpIt skips the network entirely.

It covers controllers and minimal APIs. Mark a controller action or a minimal-API endpoint with [McpTool]. Both styles work. Api.ToMcp is controllers-only.

Why this over writing servers by hand

The official SDK gives you the transport, the protocol, and the serving infrastructure. That is real value, and McpIt sits on top of it: it layers on ModelContextProtocol.AspNetCore and lets the official SDK serve the tools. What McpIt adds is the generation step, so you never hand-write the tool classes that mirror endpoints you already wrote.

Exposure is opt-in. Only endpoints you annotate with [McpTool] become tools, so you are never accidentally handing an agent your entire API surface. And safety hints come from the verb: GET and HEAD are read-only and idempotent, while POST, PUT, PATCH, and DELETE are flagged destructive. Exposing a destructive operation raises a build warning until you acknowledge it with [McpTool(AllowDestructive = true)]. Build-time diagnostics (MCPGEN001 for a missing description, MCPGEN002 for an unacknowledged destructive op) keep the tool surface honest before it ships.

Install and quickstart

dotnet add package McpIt
Enter fullscreen mode Exit fullscreen mode

McpIt brings in the official MCP SDK transitively, so you do not add ModelContextProtocol.AspNetCore yourself. The [McpTool] and [McpToolOutput] attributes ship in the small McpIt.Abstractions package, which also comes in transitively.

Wire it up in Program.cs:

using McpIt;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

builder.Services.AddMcpServer()
    .WithHttpTransport(o => o.Stateless = true)
    .WithToolsFromAssembly();   // discovers the tools McpIt generated

builder.Services.AddMcpEndpoints();   // in-process invoker for the generated tools

var app = builder.Build();
app.MapControllers();
app.MapMcp("/mcp");   // MCP server at /mcp; your API stays where it is
app.Run();
Enter fullscreen mode Exit fullscreen mode

Your REST API runs unchanged, and an MCP server is now served at /mcp.

Keep the tool surface lean

Two extras help responses stay cheap. [McpToolOutput] shapes what comes back: Fields projects a response down to the top-level JSON properties you list, and MaxLength truncates the result.

[HttpGet("{id}")]
[McpTool]
[McpToolOutput(Fields = new[] { "id", "status" }, MaxLength = 500)]
public Order GetOrder(int id) { ... }
Enter fullscreen mode Exit fullscreen mode

The second extra is the token-report tool. Agents load every tool's name, description, and input schema into context before the user asks anything, so a fat tool surface is a real, recurring context cost. mcp-token-report measures it:

dotnet tool install -g McpIt.TokenReport.Tool

mcp-token-report http://localhost:5199/mcp             # live server or a saved tools-list.json
mcp-token-report http://localhost:5199/mcp --markdown  # Markdown table for CI artifacts
mcp-token-report http://localhost:5199/mcp --budget 2000   # exit 1 if over budget
Enter fullscreen mode Exit fullscreen mode

It is fully offline and deterministic, so it is safe to gate a CI build with --budget. The counts come from an offline heuristic tokenizer: estimates for comparing tools and catching bloat, not exact billing.

Honest current limitations

McpIt is v1.0.0, and there are edges worth knowing up front:

  • It targets .NET 10 and builds on ModelContextProtocol.AspNetCore 1.4.0.
  • It is controllers-focused, with minimal-API support alongside. The richest behavior lives in the controller path.
  • Output shaping is best-effort: malformed JSON passes through untouched rather than being reshaped.
  • Token counts are heuristic estimates, not provider-exact billing numbers.

None of these block the core use case, but you should know them before you adopt.

Try it

MCP in .NET is having a moment: Visual Studio 2026 ships with built-in MCP support, Aspire is leaning in, and the SDK hit 1.0. If you already have an ASP.NET Core API, this is the shortest path from "I have endpoints" to "an agent can use them."

One [McpTool] attribute, zero reflection, zero proxy, zero hand-written server. That is the pitch.

Top comments (0)