SwaggerMcp
Expose ASP.NET Core API actions as an MCP (Model Context Protocol) server. Tag actions with [McpTool], add AddSwaggerMcp() and MapSwaggerMcp() for a POST /mcp endpoint.
Ask AI about SwaggerMcp
Powered by Claude Β· Grounded in docs
I know everything about SwaggerMcp. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
ZeroMcp
** Migrated to zeroMcp/ZeroMcp.net **
This is the repository (GitLab/project) README β full documentation, build, contributing, and project structure. The NuGet package ships with a shorter, consumer-focused README in ZeroMCP/README.md.
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.
How It Works
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
Quick Start
1. Install
<PackageReference Include="ZeroMcp" Version="1.*" />
2. Register services
// Program.cs
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. Tag your actions
[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 [McpTool] β 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.
For versioning and breaking-change policy, see VERSIONING.md.
Configuration
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.
});
Observability (Phase 1)
- 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.
Governance & tool control (Phase 1)
You can control which tools appear in tools/list per request:
- Role-based exposure β On
[McpTool]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 .WithMcpTool("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.
Custom route
app.MapZeroMcp("/api/mcp"); // overrides options.RoutePrefix
Using controllers and minimal APIs together
If you expose both controller actions (with [McpTool]) and minimal API endpoints (with .WithMcpTool(...)), 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 .WithMcpTool(...)
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.
The [McpTool] Attribute
[McpTool(
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.
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.
)]
Placement rules
- Per-action only β
[McpTool]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.
How Parameters Are Mapped
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")]
[McpTool("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"]
}
In-Process Dispatch
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
Minimal API endpoints
You can expose minimal API endpoints as MCP tools by calling .WithMcpTool(...) when mapping:
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" }))
.WithMcpTool("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.
Connecting MCP Clients
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"my-api": {
"type": "http",
"url": "http://localhost:5000/mcp"
}
}
}
Claude.ai (remote 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");
Two READMEs
| File | Purpose |
|---|---|
| README.md (this file) | Repository / GitLab: full docs, build, tests, contributing, project layout. |
| MCPSwagger/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 MCPSwagger/README.md.
Project Structure
mcpAPI/
βββ MCPSwagger/ β Library (NuGet package ZeroMcp)
β βββ README.md β Package README (NuGet)
β βββ Attributes/ β [McpTool]
β βββ 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, WithMcpTool
β βββ Options/ β ZeroMcpOptions
β βββ MCPSwagger.csproj (PackageId: ZeroMcp, Version: 1.0.2)
βββ MCPSwagger.Sample/ β Sample (Orders API, health minimal endpoint, optional auth)
βββ nupkgs/ β dotnet pack -o nupkgs
βββ progress.md
βββ README.md
Known Limitations
- Streamable HTTP only β stdio and SSE transports are not supported
- Minimal APIs β supported via
WithMcpTool; 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
Build
- Targets: .NET 9.0 and .NET 10.0 (library); sample and tests may target a single framework.
- Library:
dotnet build MCPSwagger\MCPSwagger.csproj - Sample:
dotnet build MCPSwagger.Sample\MCPSwagger.Sample.csproj - Tests:
dotnet build MCPSwagger.Tests\MCPSwagger.Tests.csprojthendotnet test MCPSwagger.Tests\MCPSwagger.Tests.csproj - TestService:
dotnet build TestService\TestService.csproj
Test coverage
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).
Contributing
PRs welcome. The most impactful next additions would be:
- SSE transport support
- Richer minimal API parameter binding (query/body from route delegate)
