Skip to Content
ReferenceWithAgentTool()

WithAgentTool() Reference

WithAgentTool() marks a minimal-API endpoint as an MCP-callable tool. It is the minimal-API counterpart to the [AgentTool] attribute: the attribute is read by the source generator at compile time, while WithAgentTool() attaches metadata to the endpoint that Ithil reads back from the routing table once the app has started.

Both paths produce the same tool schema. Nothing downstream — the gateway, the MCP session, the dashboard — can tell which one a tool came from.

Namespace and version

using Ithil.Hosting;

Requires Ithil.Hosting 0.3.0 or later.

Quick example

app.MapGet("/api/inventory/{sku}", (string sku, bool? includeReserved) => ...) .WithAgentTool("GetInventory", "Returns current stock on hand for a SKU.", category: "Inventory"); app.MapPost("/api/inventory/adjust", (StockAdjustment adjustment) => ...) .WithAgentTool("AdjustInventory", "Adjusts stock on hand for a SKU by a signed delta.", allowWrite: true, category: "Inventory"); // Not marked, so agents never see it. app.MapGet("/healthz", () => Results.Ok("healthy")); // includeMappedEndpoints: true is what pulls WithAgentTool() endpoints into the schema. app.MapIthilSchema(SchemaRegistry.Tools, includeMappedEndpoints: true);

WithAgentTool() on its own is not enough. Mapped tools only appear in GET /ithil/schema when you call MapIthilSchema(..., includeMappedEndpoints: true). The original MapIthilSchema(SchemaRegistry.Tools) overload still returns attribute-based tools only.

Signature

public static TBuilder WithAgentTool<TBuilder>( this TBuilder builder, string name, string description, bool allowWrite = false, int maxResponseTokens = 2000, string? category = null, string[]? requiredScopes = null, string? httpMethod = null) where TBuilder : IEndpointConventionBuilder

Call it on the builder returned by MapGet, MapPost, MapPut, MapPatch, MapDelete, MapMethods or Map — one call per endpoint, one endpoint per tool.

Parameter reference

ParameterTypeDefaultDescription
namestring(required)Tool identifier the agent calls, e.g. GetInventory. Must be non-empty.
descriptionstring(required)What the tool does, shown to the agent in the MCP tool manifest. Must be non-empty. See writing good descriptions.
allowWriteboolfalseWhen false, the gateway rejects calls that would mutate state. Set to true for intentional write tools.
maxResponseTokensint2000Token ceiling on the response body.
categorystring?nullGrouping label shown in the Ithil Dashboard’s tool library.
requiredScopesstring[]?nullOAuth scopes the calling agent’s JWT must contain. The array is copied at registration, so changing it afterwards has no effect.
httpMethodstring?nullThe verb the agent should use. Only needed when the endpoint does not resolve to exactly one verb — see Choosing a verb.

These map one-to-one onto the [AgentTool] properties. The only difference is name.

Why the name is required

With [AgentTool], the tool is named after the controller method. Minimal-API handlers are usually lambdas, whose compiler-generated names (<<Main>$>b__0_1) are neither stable nor meaningful to an agent. An explicit name is also refactor-safe: renaming a handler cannot silently rename a tool agents already call.

Opt-in by design

Only endpoints you mark are exposed. Adding a new route to your app can never silently widen what agents can reach — it stays invisible until you chain WithAgentTool() onto it.

Registering the schema endpoint

MapIthilSchema has three forms:

CallTools in /ithil/schema
app.MapIthilSchema(SchemaRegistry.Tools)[AgentTool] controller actions only
app.MapIthilSchema(SchemaRegistry.Tools, includeMappedEndpoints: true)[AgentTool] actions and WithAgentTool() endpoints, merged
app.MapIthilSchema(SchemaRegistry.Tools, includeMappedEndpoints: false)Same as the first form

With includeMappedEndpoints: true the routing table is read on the first request to /ithil/schema rather than at registration, because ASP.NET Core does not finish building its endpoint list until the host has started. The result is cached, so the routing table is walked at most once.

When both paths find the same route

Tools are keyed on HTTP verb plus route. If a [AgentTool] controller action and a WithAgentTool() endpoint share both, the [AgentTool] entry wins and the mapped one is dropped.

How parameters become the input schema

Route templates, verbs and binding are read from ASP.NET Core’s routing table, so MapGroup prefixes, constraints like {id:int}, optional {id?} and catch-all {**path} segments all resolve correctly.

Each handler parameter is classified the same way the source generator classifies controller parameters:

ParameterSent by the agent as
Has [FromRoute], or its name matches a route segmentroute
Has [FromQuery], or is a simple type (string, number, bool, Guid, dates…)query
Has [FromBody], or is a complex type (a class or record)body — its public properties are expanded into individual camelCase inputs

Framework-supplied parameters never reach the agent: CancellationToken, HttpContext, HttpRequest, HttpResponse, ClaimsPrincipal, Stream, PipeReader, and anything marked [FromServices] or [FromHeader].

If a route or query parameter shares a name with a body property, the route or query parameter wins (route › query › body).

JSON Schema types are mapped as: int, long, short, byte → integer; float, double, decimal → number; bool → boolean; everything else → string. Nullable value types map like their underlying type, so int? is still integer.

Required and optional inputs

An input is optional — listed in properties but left out of required — when its parameter is nullable (int?, string?) or has a default value (int page = 1). A nullable property inside a body type is optional on its own, and a nullable body parameter makes all of its properties optional. Route parameters are always required, because leaving one out changes the URL being called. This matches what ASP.NET Core itself will accept.

string? only counts as optional in projects with nullable reference types enabled (<Nullable>enable</Nullable>, the default for new .NET projects). Without it the compiler records no nullability, so reference-type inputs are treated as required.

The GetInventory example above produces:

{ "name": "GetInventory", "description": "Returns current stock on hand for a SKU.", "allowWrite": false, "maxResponseTokens": 2000, "category": "Inventory", "requiredScopes": [], "httpMethod": "GET", "routePattern": "api/inventory/{sku}", "parameterSources": { "sku": "route", "includeReserved": "query" }, "inputSchema": { "properties": { "sku": { "type": "string", "description": null }, "includeReserved": { "type": "boolean", "description": null } }, "required": ["sku"] } }

includeReserved is a bool?, so the agent may leave it out.

Choosing a verb

A tool carries exactly one HTTP method. MapGet, MapPost and friends resolve to one on their own (the HEAD that ASP.NET Core adds alongside GET is ignored). Two cases need httpMethod:

  • a verbless app.Map(...), which serves every verb
  • a multi-verb app.MapMethods(..., ["GET", "POST"], ...)
app.MapMethods("/api/things", ["GET", "POST"], handler) .WithAgentTool("CreateThing", "Creates a thing", allowWrite: true, httpMethod: "POST");

An explicit httpMethod must be one the endpoint actually serves — declaring "DELETE" on a MapGet is an error, because the schema would advertise a call that always returns 405.

Errors

Ithil refuses to publish a tool an agent cannot use. Problems are reported as exceptions whose message names the tool, the route and the fix.

ProblemWhen it surfaces
Empty name or descriptionAt startup, when the endpoint is registered (ArgumentException)
Ambiguous verb — verbless Map or multi-verb MapMethods without httpMethodOn the first request to /ithil/schema, which returns 500 (InvalidOperationException)
httpMethod names a verb the endpoint doesn’t serveOn the first request to /ithil/schema, which returns 500 (InvalidOperationException)

Verb problems surface on the first schema request rather than at startup because the routing table is not complete until then. After deploying, request GET /ithil/schema once yourself — it is the quickest way to confirm every mapped tool is valid.

Last updated on