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
| Attribute | Target | Purpose |
|---|---|---|
[McpTool] | action | Publishes the action as a tool. Repeatable — see One action, several tools. |
[McpTool] | controller | Publishes every action on the controller. |
[McpIgnore] | controller, action, parameter, property | Excludes it. Always wins. |
[McpParameter] | parameter, property | Overrides 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.
| Property | Effect |
|---|---|
IncludeParameters | Whitelist. Only these inputs are exposed; everything else is left unset. |
ExcludeParameters | Hides inputs. They are left unset, so the action's own defaults apply. |
ConstantParameters | name=value pairs. The input disappears from the schema and the value is always sent. |
RequiredParameters | Marks inputs required for this tool even if the action treats them as optional. |
OptionalParameters | The reverse. Route tokens the URL cannot be built without stay required. |
Notes
- Names match either the tool input name (camelCase, as it appears in the schema) or the underlying binding name, case-insensitively. A name that matches nothing is logged as a warning.
- Constant values are converted to the parameter's CLR type:
pageSize=100becomes a JSON number,isCompleted=falsea JSON boolean, a complex parameter accepts a JSON literal such astags=["docs","ops"], and everything else is sent as a string. On a non-string parameter an empty value ornullmeans "send nothing". - A constant always wins over an argument that happens to target the same place, so a pinned value cannot be talked out of by the model.
- Route tokens can be pinned too, which is how an endpoint collapses into a zero-argument tool:
Hiding a route token without pinning it would produce a tool whose URL cannot be built, so Nabu logs a warning and skips that variant rather than publishing something uncallable.[HttpGet("{city}")] [McpTool] [McpTool("weather_get_yerevan_week", ConstantParameters = new[] { "city=Yerevan", "days=7" })] public ActionResult<IEnumerable<Forecast>> GetForecast(string city, [FromQuery] int days = 3) => ... - Give every extra variant an explicit
Name. Variants without one fall back to the generatedcontroller_actionname and collide, and all but the first end up with a_2,_3, ... suffix. - Variants declared on an action replace a controller-wide
[McpTool]rather than adding to it.
How arguments are mapped
Nabu reads MVC's own binding metadata, so it maps arguments the same way your API already binds them.
| Binding source | Becomes |
|---|---|
[FromRoute] / route template token | A 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, HttpContext | Skipped — 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:
- primitives,
Guid,DateTime/DateTimeOffset/DateOnly/TimeOnly/TimeSpan,Uri,byte[] Nullable<T>and nullable reference types (string?is optional,stringis required)- collections,
string-keyed dictionaries, nested models, with cycle and depth protection [Required],[Range],[StringLength],[MinLength],[MaxLength],[RegularExpression],[EmailAddress],[Url],[DefaultValue],[Description],[Display][JsonPropertyName],[JsonIgnore]- XML documentation
<summary>on models and properties
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.