Attributes & tools

Four attributes cover the whole surface: publish, exclude, and shape individual inputs — plus the ability to publish one action as several differently shaped tools.

The attribute surface

AttributeTargetPurpose
[McpTool]actionPublishes the action as a tool. Repeatable — see One action, several tools.
[McpTool]controllerPublishes every action on the controller.
[McpIgnore]controller, action, parameter, propertyExcludes it. Always wins.
[McpParameter]parameter, propertyOverrides the name, description, requiredness or example of one input.

For Minimal APIs the same declarations are made with the .McpTool() / .McpIgnore() endpoint conventions (or the same attributes on the handler delegate) — see Minimal APIs.

[McpTool] also accepts Name, Title, Description, Enabled, and the four MCP behaviour hints ReadOnly, Destructive, Idempotent and OpenWorld. The hints default to the HTTP semantics of the verb — GET is read-only and idempotent, DELETE and PUT are destructive — and any hint you set explicitly overrides that default.

[HttpPost("{id:guid}/publish")]
[McpTool(
    Name = "publish_article",
    Description = "Publishes a draft article so it becomes visible to readers.",
    Idempotent = false,
    Destructive = true)]
public IActionResult Publish(Guid id) => ...

One action, several tools

A single endpoint is often the wrong shape for a model. GET /api/todos with five optional filters is easy for a client that already knows what it wants and awkward for a model that has to guess. Apply [McpTool] more than once and the same action is published as several tools, each with its own name, description and parameter set — without adding controller actions, and with the same pipeline replay behind every one of them.

[HttpGet]
[McpTool]                                                       // the full endpoint, unchanged
[McpTool("todos_list_open",
    Title = "List open todos",
    Description = "Lists the todo items that are still open.",
    ExcludeParameters = new[] { "search", "priority" },
    ConstantParameters = new[] { "isCompleted=false" })]
[McpTool("todos_search",
    Title = "Search todos",
    Description = "Searches the signed-in user's todo items by title and notes.",
    IncludeParameters = new[] { "search", "page", "pageSize" },
    RequiredParameters = new[] { "search" })]
public ActionResult<TodoPage> List(
    [FromQuery] bool? isCompleted,
    [FromQuery] TodoPriority? priority,
    [FromQuery] string? search,
    [FromQuery] int page = 0,
    [FromQuery] int pageSize = 20) => ...

tools/list now advertises three tools over one action: todos_list takes all five filters, todos_list_open takes only page and pageSize and can never return completed items, and todos_search takes a mandatory search plus paging.

PropertyEffect
IncludeParametersWhitelist. Only these inputs are exposed; everything else is left unset.
ExcludeParametersHides inputs. They are left unset, so the action's own defaults apply.
ConstantParametersname=value pairs. The input disappears from the schema and the value is always sent.
RequiredParametersMarks inputs required for this tool even if the action treats them as optional.
OptionalParametersThe reverse. Route tokens the URL cannot be built without stay required.

Notes

How arguments are mapped

Nabu reads MVC's own binding metadata, so it maps arguments the same way your API already binds them.

Binding sourceBecomes
[FromRoute] / route template tokenA URL segment, URL-encoded.
[FromQuery]A query-string entry. Arrays repeat the key; objects use key.property.
[FromBody]The JSON request body.
[FromHeader]A request header (opt in with ExposeHeaderParameters).
[FromServices], CancellationToken, HttpContextSkipped — resolved by the framework.

When no explicit attribute is present, Nabu infers the source exactly as [ApiController] does: route tokens first, then body for complex types on POST/PUT/PATCH, then query string.

Body flattening

A single complex [FromBody] parameter is flattened into the top level of the tool schema, so a model fills one flat object instead of a nested wrapper:

public ActionResult<TodoItem> Create([FromBody] CreateTodoRequest request)

// arguments: {"title": "...", "priority": "High", "tags": ["a"]}   not  {"request": {...}}

Set FlattenBodyParameter = false to keep the wrapper. Types that are not objects — a [FromBody] int[], for example — are always sent as the whole body under their parameter name.

Schema generation

Input schemas are generated from the CLR types and honour:

Enums are always described to the model by name, because names are what a model can reason about. If your API serializes enums as numbers — the default for both System.Text.Json and Newtonsoft.Json — Nabu detects that and converts the names back to their numeric values while building the request body, including inside nested objects and arrays. Nothing to configure; override it with StringEnumsInRequestBody if the detection is ever wrong.