Choose which endpoints become tools

Selection decides which endpoints enter the catalog at all. It is not visibility and not authorization: an unselected endpoint does not exist for any agent, for every caller.

The default is opt-in. With no annotations anywhere, search_tools returns nothing.

Opt in a controller or a method

Mark the class and every endpoint on it comes through:

[ApiController]
[McpTool]
public sealed class OrdersController : ControllerBase { }

Mark a method to bring in one endpoint from an unmarked class:

[HttpGet("/orders/{id:int}")]
[McpTool]
[Description("Fetches one order by id.")]
public IActionResult GetOrder(int id) => ...;

On ASP.NET Core the description comes from the framework's own metadata: [Description], XML doc comments or EndpointDescriptionAttribute. NestJS has no equivalent, which is why @McpTool({ description }) exists.

Opt out instead, for a large backend

Above a few dozen endpoints, flip the default and exclude the exceptions. Anything new is then published by default.

builder.Services.AddSezzlee(options =>
{
    options.Selection.Default = SelectionDefault.Include;
});

Then remove an endpoint or a whole controller:

[HttpPost("/internal/reindex")]
[McpIgnore]
public IActionResult Reindex() => ...;

Withhold a route subtree from configuration

For a subtree that is categorically off limits, put the decision in configuration instead of an attribute:

builder.Services.AddSezzlee(options =>
{
    options.Selection.Default = SelectionDefault.Include;
    options.Selection.Rules.Add(new SelectionRule(SelectionDefault.Exclude, Route: "/admin/**"));
    options.Selection.Rules.Add(new SelectionRule(SelectionDefault.Exclude, Method: "POST"));
});

A rule matches the route template. * stays inside one path segment and ** crosses them. Matching is case-sensitive, except for method. Leave a field out to match everything. A blank string is rejected at startup.

Opt in a minimal API

A minimal-API endpoint has no class to decorate, so the marker goes on as metadata:

csharp
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }))
    .AllowAnonymous()
    .WithMetadata(
        new McpToolAttribute(),
        new EndpointDescriptionAttribute("Service health status; requires no identity."));

Verify the result

Selection is reported at startup, before any agent connects:

text
info: Sezzlee.AspNetCore.SezzleeCatalogProvider[0]
      sezzlee catalog: 16 discovered, 9 selected, 9 tools, 0 diagnostic(s)

discovered is what the framework knows about, selected is what survived selection, and tools is what became callable. A selected of 0 means no marker was found. A tools count below selected means endpoints were dropped afterwards: Find out why a tool is missing.

Selection is global, not per caller. What differs per caller is visibility. Neither is a security boundary: an unselected endpoint is still reachable over plain HTTP by anyone your backend allows.

Which marker wins

Decisions apply at four levels: the global default, the configured rules, the controller and the method. The most specific wins. A method marker beats a controller marker, which beats a rule, which beats the default. So Selection.Default = Include plus [McpIgnore] on a controller plus [McpTool] on one of its methods publishes exactly that one method.

Attributes outrank rules because an attribute sits on the endpoint and a rule sits in your startup file. Among rules, the one naming more fields is sharper.

Two conflicting markers at the same level are an error, not a resolution. The catalog fails with ambiguous_selection. That is why /admin/** excluded alongside /admin/health included is an error: both name one field, so neither is sharper. Carve the exception out with [McpTool] on the endpoint instead. The normative rules are in packages/http/spec/selection-hierarchy.md.

McpToolAttribute implements the public IMcpSelectionMetadata interface. A host with its own convention can implement that interface on its own type.