Expose your existing ASP.NET Core API as an MCP (Model Context Protocol) server with a single attribute and two lines of setup. No separate process. No code duplication.
Tag controller actions with [Mcp] or minimal APIs with .AsMcp(...). ZeroMcp will:
- Discover tools at startup from controller API descriptions (same source as Swagger) and from minimal API endpoints that use
AsMcp - Generate a JSON Schema for each tool's inputs (route, query, and body merged)
- Expose a single endpoint (GET and POST
/mcp) that speaks the MCP Streamable HTTP transport - Dispatch tool calls in-process through your real action or endpoint pipeline — filters, validation, and authorization run normally
MCP Client (Claude Desktop, Claude.ai, etc.)
│
│ GET /mcp (info) or POST /mcp (JSON-RPC 2.0)
▼
ZeroMcp Endpoint
│
│ in-process dispatch (controller or minimal endpoint)
▼
Your Action / Endpoint ← [Mcp] or .AsMcp(...)
│
│ real response
▼
MCP Client gets structured result
<PackageReference Include="ZeroMcp" Version="1.*" />// Program.cs
builder.Services.AddZeroMcp(options =>
{
options.ServerName = "My Orders API";
options.ServerVersion = "1.0.0";
});app.MapZeroMcp(); // registers GET and POST /mcp[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. Returns the created order.")]
public ActionResult<Order> CreateOrder([FromBody] CreateOrderRequest request) { ... }
[HttpDelete("{id}")]
// No [Mcp] — invisible to MCP clients
public IActionResult Delete(int id) { ... }
}Point any MCP client at your app's /mcp URL; it will see your tagged controller actions and minimal endpoints as tools.
- GET /mcp — Server info and example JSON-RPC payload.
- GET /mcp/tools — (Phase 3) JSON list of all registered tools and their schemas (when EnableToolInspector is true). Use for debugging or tooling.
- GET /mcp/ui — (Phase 3) Swagger-like test invocation UI: list tools, view schemas, invoke tools from the browser (when EnableToolInspectorUI is true).
- POST /mcp — JSON-RPC (
initialize,tools/list,tools/call).
For versioning and breaking-change policy, see VERSIONING.md.
builder.Services.AddZeroMcp(options =>
{
options.ServerName = "My API"; // shown during MCP handshake
options.ServerVersion = "2.0.0"; // shown during MCP handshake
options.RoutePrefix = "/mcp"; // where the endpoint is mounted
options.IncludeInputSchemas = true; // attach JSON Schema to tools (helps LLM)
options.ForwardHeaders = ["Authorization"]; // copy these from MCP request to tool dispatch
// Optional: filter which tagged tools are exposed at discovery time (by name)
options.ToolFilter = name => !name.StartsWith("admin_");
// Optional: filter which tools appear in tools/list per request (e.g. by user, headers)
options.ToolVisibilityFilter = (name, ctx) => ctx.Request.Headers.TryGetValue("X-Show-Admin", out _) || !name.StartsWith("admin_");
// Observability (Phase 1)
options.CorrelationIdHeader = "X-Correlation-ID"; // read from request, echo in response and logs; default
options.EnableOpenTelemetryEnrichment = true; // tag Activity.Current with mcp.tool, mcp.duration_ms, etc.
// Phase 2: result enrichment and streaming (all optional, default off)
options.EnableResultEnrichment = true; // tools/call result includes metadata (statusCode, durationMs, correlationId) and optional hints
options.EnableSuggestedFollowUps = true; // when SuggestedFollowUpsProvider is set, result includes suggested next tools
options.EnableStreamingToolResults = false; // when true, content is returned as chunks (chunkIndex, isFinal, text)
options.StreamingChunkSize = 4096;
// Phase 3: XML Doc and Inspector (defaults)
options.EnableXMLDocAnalysis = true; // when true, use XML doc <summary> as tool description if [Mcp] Description is blank
options.EnableToolInspector = true; // GET {RoutePrefix}/tools returns full tool list as JSON
options.EnableToolInspectorUI = true; // GET {RoutePrefix}/ui serves Swagger-like test invocation UI
});- Structured logging — Each MCP request is logged with a scope containing
CorrelationId,JsonRpcId, andMethod. Tool invocations logToolName,StatusCode,IsError,DurationMs, andCorrelationId. - Execution timing — Request duration and per-tool duration are recorded and included in log messages.
- Correlation ID — Send
X-Correlation-ID(or the header name inCorrelationIdHeader) on the request; the same value is echoed in the response and propagated to the synthetic request (TraceIdentifierandHttpContext.Items). If omitted, a new GUID is generated. - Metrics sink — Implement
IMcpMetricsSinkand register it afterAddZeroMcp()to record tool invocations (tool name, status code, success/failure, duration). The default is a no-op. - OpenTelemetry — Set
EnableOpenTelemetryEnrichment = trueto tag the currentActivitywithmcp.tool,mcp.status_code,mcp.is_error,mcp.duration_ms, andmcp.correlation_idwhen present.
You can control which tools appear in tools/list per request:
- Role-based exposure — On
[Mcp]setRoles = new[] { "Admin" }. The tool is only listed if the current user is in at least one of the roles. RequiresAddAuthentication()andAddAuthorization(). - Policy-based exposure — Set
Policy = "RequireEditor"(or any policy name). The tool is only listed ifIAuthorizationService.AuthorizeAsync(user, null, policy)succeeds. - Environment / custom filter — Use
ToolFilterfor discovery-time filtering by name (e.g. excludeadmin_*in non-production). UseToolVisibilityFilterfor per-request filtering:(toolName, httpContext) => bool(e.g. hide tools based on user, headers, or feature flags).
Minimal APIs support the same via .AsMcp("name", "description", tags: null, roles: new[] { "Admin" }, policy: "RequireEditor").
Tools that are hidden from tools/list are also not callable: a direct tools/call for that tool name will still be rejected (unknown tool). Authorization on the underlying action/endpoint is still enforced when the tool is invoked.
app.MapZeroMcp("/api/mcp"); // overrides options.RoutePrefixIf you expose both controller actions (with [Mcp]) and minimal API endpoints (with .AsMcp(...)), you must register the API explorer so controller actions are discovered:
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); // required for controller tool discovery
// ... AddZeroMcp(...) ...
app.MapControllers();
// minimal APIs with .AsMcp(...)
app.MapZeroMcp();Without AddEndpointsApiExplorer(), only minimal API tools will appear in tools/list; controller actions will be missing because they are discovered from the same API description source as Swagger.
When EnableToolInspector is true (default), GET {RoutePrefix}/tools returns a JSON payload with serverName, serverVersion, protocolVersion, toolCount, and a tools array. Each tool entry includes name, description, httpMethod, route, inputSchema, and optional category, tags, examples, hints, requiredRoles, requiredPolicy. Use it for debugging or to build tooling.
When EnableToolInspectorUI is also true (default), GET {RoutePrefix}/ui serves a Swagger-like test invocation UI: you can browse tools, view input schemas, and invoke tools/call from the browser with editable JSON arguments.
Set EnableToolInspector or EnableToolInspectorUI to false to disable the JSON endpoint or the UI (e.g. in production if sensitive). The sample app (ZeroMCP.Sample) enables them only when builder.Environment.IsDevelopment() is true. See wiki/Configuration and wiki/Enterprise-Usage.
The examples/ folder contains five standalone projects:
| Example | Description |
|---|---|
| Minimal | Bare-minimum: one controller action, one minimal API, no auth |
| WithAuth | API-key auth, role-based tool visibility, [Authorize] |
| WithEnrichment | Phase 2 result enrichment, suggested follow-ups, streaming options |
| WithRateLimiting | Phase 4 (Option A): ASP.NET Core rate limiting on the MCP endpoint, 429 + JSON-RPC error |
| Enterprise | Auth, enrichment, observability, ToolFilter, ToolVisibilityFilter |
Run any example with dotnet run from its folder. See each project's README.md for details.
[Mcp(
name: "create_order", // Required. Snake_case tool name for the LLM.
Description = "Creates an order.", // Shown to the LLM. Be descriptive.
Tags = ["write", "orders"], // Optional. For grouping/filtering.
Category = "orders", // Optional (Phase 2). Primary category for tools/list.
Examples = ["Create order for Alice, 2 Widgets"], // Optional (Phase 2). Usage examples.
Hints = ["idempotent", "cost=low"], // Optional (Phase 2). AI-facing hints.
Roles = ["Editor", "Admin"], // Optional. Tool only in tools/list if user in one of these roles.
Policy = "RequireEditor" // Optional. Tool only in tools/list if user satisfies this policy.
)]- Per-action only —
[Mcp]goes on individual action methods, not controllers - One name per application — duplicate names are logged as warnings and skipped
- Any HTTP method — GET, POST, PATCH, DELETE all work
- Description — If you omit
Description, ZeroMcp uses the method's XML doc<summary>when available.
ZeroMcp merges all parameter sources into a single flat JSON Schema object that the LLM fills in:
| Parameter source | MCP mapping |
|---|---|
Route params ({id}) |
Always required properties |
Query params (?status=) |
Optional (or required if [Required]) |
[FromBody] object |
Properties expanded inline from JSON Schema |
Example:
[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:
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"status": { "type": "string" },
"reason": { "type": "string" }
},
"required": ["id", "status"]
}When the MCP client calls a tool, ZeroMcp:
- Creates a fresh DI scope (same as a real request)
- Builds a synthetic
HttpContextwith route values (including ambientcontroller/actionfor link generation), query string, and body from the JSON arguments - Sets the matched endpoint on the context so
CreatedAtActionandLinkGeneratorwork - Invokes the controller action via
IActionInvokerFactoryor the minimal endpoint'sRequestDelegate - Captures the response body and forwards it as the MCP result
This means:
[Authorize]works — set up auth on the MCP endpoint and your action filters enforce it- Auth forwarding — Headers in
ForwardHeaders(e.g.Authorization) are copied from the MCP request to the synthetic request - CreatedAtAction works — synthetic request has endpoint and controller/action route values so link generation succeeds
[ValidateModel]/ModelStateworks — validation errors return as MCP error results- Exception filters work — unhandled exceptions are caught and returned gracefully
- Your existing DI services, repositories, and business logic are called as-is
You can expose minimal API endpoints as MCP tools by calling .AsMcp(...) when mapping:
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" }))
.AsMcp("health_check", "Returns API health status.", tags: new[] { "system" });- Name (required) — snake_case tool name for the LLM
- Description (optional) — shown to the LLM
- Tags (optional) — for grouping/filtering
Discovery includes both controller actions (from API descriptions) and minimal endpoints (from EndpointDataSource). Route parameters on minimal APIs are supported; query/body binding is limited to what the route pattern exposes.
Add to claude_desktop_config.json:
{
"mcpServers": {
"my-api": {
"type": "http",
"url": "http://localhost:5000/mcp"
}
}
}Point at your deployed API's /mcp endpoint. For production, add authentication — ZeroMcp doesn't impose any auth on the /mcp route itself, so you can apply standard ASP.NET Core auth middleware or .RequireAuthorization() as needed:
app.MapZeroMcp().RequireAuthorization("McpPolicy");| File | Purpose |
|---|---|
| README.md (this file) | Repository / GitLab: full docs, build, tests, contributing, project layout. |
| ZeroMcp/README.md | NuGet package: install, quick start, config summary. Shipped inside the package; keep it consumer-focused. |
When you add features or options, update both: details and examples here, short summary and link in ZeroMcp/README.md.
mcpAPI/
├── ZeroMcp/ ← Library (NuGet package ZeroMcp)
│ ├── README.md ← Package README (NuGet)
│ ├── Attributes/ ← [Mcp]
│ ├── Discovery/ ← Controller + minimal API tool discovery
│ ├── Schema/ ← JSON Schema for tool inputs (NJsonSchema)
│ ├── Dispatch/ ← Synthetic HttpContext, controller/minimal invoke
│ ├── Metadata/ ← McpToolEndpointMetadata for minimal APIs
│ ├── Extensions/ ← AddZeroMcp, MapZeroMcp, AsMcp
│ ├── Options/ ← ZeroMcpOptions
│ └── ZeroMCP.csproj (PackageId: ZeroMcp, Version: 1.0.2)
├── ZeroMCP.Sample/ ← Sample (Orders, Customer, Product APIs; nested route Customer/{id}/orders; health minimal endpoint, optional auth)
├── examples/ ← Minimal, WithAuth, WithEnrichment, WithRateLimiting, Enterprise
├── ZeroMCP.Tests/ ← Integration + schema tests
├── wiki/ ← Wiki documentation (linked Markdown pages)
├── nupkgs/ ← dotnet pack -o nupkgs
├── progress.md
└── README.md
Wiki: Detailed documentation can be found on Our Wiki pages.
- Streamable HTTP only — stdio and SSE transports are not supported
- Minimal APIs — supported via
AsMcp; route params are bound; query/body binding is limited - [FromForm] and file uploads — not supported; JSON-only body binding
- Streaming responses —
IAsyncEnumerable<T>and SSE action results are not captured correctly - If CreatedAtAction or link generation ever fails in your environment, use
return Created(Url.Action(nameof(OtherAction), new { id = entity.Id })!, entity);as a fallback
- Targets: .NET 9.0 and .NET 10.0 (library); sample and tests may target a single framework.
- Library:
dotnet build ZeroMcp\ZeroMCP.csproj - Sample:
dotnet build ZeroMCP.Sample\ZeroMCP.Sample.csproj - Tests:
dotnet build ZeroMCP.Tests\ZeroMCP.Tests.csprojthendotnet test ZeroMCP.Tests\ZeroMCP.Tests.csproj - TestService:
dotnet build TestService\TestService.csproj
Integration and schema tests cover JSON-RPC validation and errors, model binding failures, wrong/empty arguments, unauthorized [Authorize] tool calls, tools/list schema shape, and schema edge cases (nested objects, arrays, enums, route+body merging).
PRs welcome. The most impactful next additions would be:
- SSE transport support
- Richer minimal API parameter binding (query/body from route delegate)