Minimal APIs
Route handlers publish the same way controller actions do, through an endpoint convention instead of an attribute.
app.MapGet("/customers/{id}", (int id, ICustomerService customers) => customers.Get(id))
.McpTool();
app.MapPost("/orders", (CreateOrder order) => ...)
.McpTool("orders_create", "Places a new order.");
Everything works exactly as it does for controllers, because the machinery is the same: a tool call
is replayed as a synthetic HTTP request through the whole pipeline, so
RequireAuthorization(), [Authorize], endpoint filters, rate limiting and
validation all keep running, and ToolVisibility reads the endpoint's own authorization
metadata when tailoring tools/list to the caller. An application without any
controllers at all — AddNabuMcp() plus UseNabuMcp() on a plain
WebApplication — is fully supported.
How inputs are inferred
Inputs are inferred with Minimal API binding rules, not MVC's:
| Handler parameter | Becomes |
|---|---|
Route token match, [FromRoute] | A URL segment. |
string, primitives, parsables, and collections of them; [FromQuery] | A query-string entry. |
Complex types (and collections of them), [FromBody] | The JSON request body, flattened like [FromBody]. |
[FromHeader] | A request header (opt in with ExposeHeaderParameters). |
Services (per IServiceProviderIsService), [FromServices], [FromKeyedServices] | Skipped — resolved by the framework. |
HttpContext, CancellationToken, ClaimsPrincipal, Stream, PipeReader, BindAsync types | Skipped — materialized from the request itself. |
One real difference between the stacks is preserved rather than papered over: Minimal APIs answer
400 when a non-nullable parameter without a default value is missing, so such a parameter is
required in the tool schema — where MVC model binding would have silently used the
type's default.
The other [McpTool] surfaces work here too
.McpTool(tool => { ... })exposes the full attribute surface —IncludeParameters,ConstantParameters, behaviour hints — and calling.McpTool(...)repeatedly publishes several tools over one endpoint.- An attribute on the handler itself is equivalent to the extension:
app.MapGet("/ping", [McpTool("ping")] () => ...). .McpIgnore()(or[McpIgnore]on the handler) keeps an endpoint out of discovery, which matters whenExposeAllActionsis on — that option publishes every delegate route handler just as it publishes every controller action, andExcludeFromDescription()is respected the way[ApiExplorerSettings(IgnoreApi = true)]is for controllers.
Tool naming
Without an explicit name, tools are named from the endpoint name (.WithName(...)), the
handler's method name when it is a real method rather than a lambda, or the verb and route —
GET /customers/{id} becomes customers_get_by_id.
NabuMcpOptions.ToolNameFactory sees these through the same
McpToolNamingContext it sees controllers through.
Forms and [AsParameters]
[AsParameters] surface types are expanded member by member — constructor parameters
and settable properties each get the same binding inference they would as a top-level handler
parameter. Form binding works too: an IFormFile becomes a tool argument carrying the
content as base64 (data, plus optional fileName and
contentType), and the invoker replays it as a real multipart/form-data request, so
the endpoint's own form binding, validation and size limits apply unchanged. On .NET 8+, form
endpoints demand antiforgery by default — a tool call cannot carry an antiforgery token, so such
endpoints need .DisableAntiforgery() exactly as any non-browser client does.