Attributes & tools

A handful of attributes cover the whole surface: publish, exclude, shape individual inputs, and shape what a tool returns — 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.
[McpToolOutput]controller, actionShapes what the tool returns: hides fields or transforms the response with a converter. Repeatable — see Shaping tool output.

For Minimal APIs the same declarations are made with the .McpTool() / .McpIgnore() / .McpToolOutput() 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

Shaping tool output

An action's response is often wider than what a model should see: internal identifiers, owner fields, audit metadata, payloads sized for a UI rather than a context window. [McpToolOutput] shapes the JSON a tool returns after the pipeline has run — the HTTP API keeps its contract, and only MCP clients get the narrowed view.

[HttpGet]
[McpTool("todos_list")]
[McpTool("todos_list_compact")]
[McpToolOutput(Tool = "todos_list_compact",
    IncludeFields = new[] { "items.id", "items.title", "totalCount" })]
public ActionResult<TodoPage> List(...) => ...
PropertyEffect
IncludeFieldsKeeps only the listed field paths; every other property is removed.
ExcludeFieldsRemoves the listed field paths. Applied after IncludeFields.
ConverterAn IMcpToolOutputConverter type that transforms the (already filtered) JSON.
ToolScopes the occurrence to one tool of a multi-[McpTool] action. Unset applies to all of them.

Field paths are dot-separated JSON property names, matched case-insensitively, and arrays are traversed transparently: items.owner removes owner from every element of items. Both the text content and structuredContent are shaped. The attribute is repeatable, so each variant of an action can publish its own view of the same response; a method-level occurrence overrides a controller-level one, and within one level a Tool-scoped occurrence wins over an unscoped one.

For transformations that field lists cannot express, name a converter:

[McpTool("todos_get_summary")]
[McpToolOutput(Tool = "todos_get_summary",
    ExcludeFields = new[] { "attachments" },
    Converter = typeof(TodoSummaryOutputConverter))]
public ActionResult<TodoItem> GetById(Guid id) => ...

public sealed class TodoSummaryOutputConverter : IMcpToolOutputConverter
{
    public JsonNode? Convert(McpToolOutputContext context, JsonNode? output)
    {
        var item = output!.AsObject();
        return new JsonObject
        {
            ["id"] = item["id"]?.DeepClone(),
            ["summary"] = item["title"]?.GetValue<string>(),
        };
    }
}

Converters run after the field filters, are resolved from the request's services when registered — so they can take dependencies — and are constructed with ActivatorUtilities otherwise. Returning null exposes an empty result.

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.