# Adding a tool

> Tracon documentation. Tracon is a .NET package family that adds a control
> plane on Microsoft Agent Framework, published as NuGet packages that run
> inside your own ASP.NET Core application.
>
> Page: https://tracon.dev/getting-started/tools/
> Summary: Register a tool so an agent can do something, and understand the one rule that trips people up.

An agent without tools can only talk. A tool is a method in your codebase that the
model may call.

## Register one

Mark the method and let the source generator find it:

```csharp title="Tools/OrderTools.cs"
using Tracon;

internal class OrderTools
{
    /// <summary>Returns the shipping status of an order.</summary>
    /// <param name="orderId">The order number.</param>
    [TraconTool("get_order_status", "Returns the shipping status of an order.")]
    public static string GetOrderStatus(string orderId)
        => $"Order {orderId} has shipped. Estimated delivery: 2 days.";
}
```

```csharp title="Program.cs"
builder.AddTracon()
       .AddGeneratedTools();
```

`AddGeneratedTools()` is filled in at compile time — no reflection, no trimming or
AOT warning. A method without the attribute is not a tool, so adding a helper to the
class does not quietly expose it to a model.

Then name it on the agent:

```csharp
tracon.AddAgent(new AgentDefinition
{
    Name = "support",
    // …
    ToolNames = ["get_order_status"],
});
```

An unknown tool name fails **definition compilation**, not C# compilation. A database
definition is rejected when you validate or save it. A code definition is compiled
when the catalog first resolves it and does not resolve until the registration is
fixed. In either case, the model never receives a tool name Tracon cannot bind.

### The other two ways

```csharp
.AddTool(GetOrderStatus)      // from a delegate — uses reflection
.AddToolsFrom<OrderTools>()   // scans a type — uses reflection
```

Both work and both carry `[RequiresUnreferencedCode]` and `[RequiresDynamicCode]`.
The warning is handed to you rather than suppressed. Use `AddGeneratedTools()` unless
your tools live in an assembly you do not compile.

### Tools in a class library

`AddGeneratedTools()` is generated into every assembly that has a tool, and it is
`internal`: it registers that assembly's tools only. A library hands its tools to a
host through its own extension, which calls the generated one from inside:

```csharp title="ContosoToolsExtensions.cs (in the library)"
public static class ContosoToolsExtensions
{
    public static ITraconBuilder AddContosoTools(this ITraconBuilder builder)
        => builder.AddGeneratedTools();
}
```

The host calls `AddContosoTools()` next to its own `AddGeneratedTools()`. Nothing is
scanned at run time, so the trimming and AOT guarantee holds across the two
assemblies.

```mermaid
flowchart LR
    accTitle: Each assembly registers its own generated tools
    accDescr: The host's AddGeneratedTools registers the host's tools. The host also calls the library's public AddContosoTools, which calls the library's own internal AddGeneratedTools from inside the library.
    HOST["Host Program.cs"] -->|"AddGeneratedTools()"| HT["Host tools<br/>internal, generated in the host"]
    HOST -->|"AddContosoTools()"| LIB["Library extension<br/>public, written by the library"]
    LIB -->|"AddGeneratedTools()"| LT["Library tools<br/>internal, generated in the library"]
```

## The rule that trips people up

:::danger[A tool sees an empty service provider]
`AddTool(delegate)` and `AddToolsFrom<T>()` build the tool with
`AIFunctionFactory.Create`, which binds an `IServiceProvider` parameter from
`AIFunctionArguments.Services`. MAF always supplies that as an **empty** provider.
Resolving a dependency inside a tool body fails — and it fails at run time, in a model
call, not at startup.

Take dependencies at **registration**:

```csharp
// Wrong — services is empty when the model calls this
public static string GetOrderStatus(string orderId, IServiceProvider services)
    => services.GetRequiredService<IOrderRepository>().Find(orderId).Status;

tracon.AddTool(GetOrderStatus);

// Right — the dependency is captured when the tool is registered
var repository = app.Services.GetRequiredService<IOrderRepository>();
tracon.AddTool(AIFunctionFactory.Create(
    (string orderId) => repository.Find(orderId).Status,
    "get_order_status",
    "Returns the shipping status of an order."));
```

`[TraconTool]` catches the same mistake earlier: the source generator does not
support an `IServiceProvider` parameter, so the method fails to compile instead of
failing on the first call.

The same problem reaches instance methods picked up by `AddToolsFrom<T>()` — the
instance has to come from somewhere, and MAF cannot supply it. Tracon rejects an
instance method there too, but at scan time, when `AddToolsFrom<T>()` runs at startup,
not on the first call.
:::

Taking a dependency at registration only works for a **singleton**. When the
dependency has to be fresh per call — a repository, a `DbContext` — use
`AddScopedTool` instead of `AddTool`:

```csharp
tracon.AddScopedTool(AIFunctionFactory.Create(
    (string orderId, AIFunctionArguments arguments) =>
        arguments.Services!.GetRequiredService<IOrderRepository>().Find(orderId).Status,
    "get_order_status",
    "Returns the shipping status of an order."));
```

`AddScopedTool` opens a real dependency-injection scope for every call and exposes
it through `AIFunctionArguments.Services` — the empty-provider trap above does not
apply to a tool registered this way. See
[Write your own tool](/guides/write-your-own-tool/#scoped-dependencies) for the
full mechanism.

## Tools that need a human

Some tools should not run unattended:

```csharp
[TraconTool("issue_refund", "Refunds an order.", RequiresApproval = true)]
public static string IssueRefund(string orderId) => /* … */;

// or, for a tool registered from a delegate:
tracon.AddTool(IssueRefund, configure: options => options.RequiresApproval = true);
```

For the full singleton, scoped-dependency, timeout, and output contract, see
[Write your own tool](/guides/write-your-own-tool/).

The tool is wrapped in the registry — the single place where "an agent may only point
at a registered tool" is enforced, so there is no code path that skips the wrapper.
When the model calls it, the run pauses and an approval request appears in the
console. What happens next depends on how the run was started: a streaming run
carries the approval in its next turn, while a queued run stops at
`AwaitingApproval` and waits in the approval mailbox.

Standing decisions are possible too — an approval rule pre-approves a tool, or one
exact set of arguments, so it stops asking. Those rules are visible and revocable
under **Governance**.

## Try it

```bash
curl -N -X POST http://localhost:5081/tracon/api/agents/support/run \
     -H 'Content-Type: application/json' \
     -d '{"message":"Where is order 4182?"}'
```

The stream carries `ToolInvoking` and `ToolInvoked` events around the model's reply,
and each call is written to the run with its arguments, its result, its duration, and
its error if it had one.

## Read next

- [Persistence](/getting-started/persistence/) — keep the history
- [Tools, skills, and MCP](/concepts/tools/) — the whole picture, including
  the two exceptions to "code only"
