Home / Docs / Tools

Tools

Tools

Tools give your agents the ability to take actions — call APIs, query databases, update state, and interact with the outside world. FabrCore uses Microsoft.Extensions.AI's AIFunctionFactory for function calling.

Function Calling

Define tool methods using standard C# with [Description] attributes. The LLM sees the method name, parameter names, descriptions, and types — then decides when to call each tool.

Defining a Tool Plugin

WeatherPlugin.cs
public class WeatherPlugin
{
    [Description("Get the current weather for a location")]
    public async Task<string> GetWeather(
        [Description("City name")] string city,
        [Description("Country code (e.g., US, UK)")] string country)
    {
        return await Task.FromResult(
            $"Weather in {city}, {country}: Sunny, 72F");
    }
}

AIFunctionFactory

Register tools with your agent using AIFunctionFactory.Create():

With GetChatClient

Manual Tool Registration
private readonly WeatherPlugin plugin = new();

public override async Task OnInitialize()
{
    var chatClient = await GetChatClient("default");

    agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions
    {
        ChatOptions = new ChatOptions
        {
            Instructions = config.SystemPrompt,
            Tools = [
                AIFunctionFactory.Create(plugin.GetWeather)
            ]
        },
        Name = fabrAgentHost.GetHandle()
    })
    .AsBuilder()
    .UseOpenTelemetry(null, cfg => cfg.EnableSensitiveData = true)
    .Build(serviceProvider);

    thread = agent.GetNewThread();
}

With CreateChatClientAgent

Simplified Tool Registration
(agent, thread) = await CreateChatClientAgent(
    "default",
    threadId: config.Handle,
    tools: [
        AIFunctionFactory.Create(plugin.GetWeather),
        AIFunctionFactory.Create(plugin.GetForecast)
    ]
);
Tool Execution

When the LLM decides to call a tool, the Microsoft Agent Framework automatically invokes the C# method, passes the return value back to the LLM, and continues the conversation. This loop runs until the LLM responds without a tool call.

Multiple Tools

Multiple Tool Classes
private readonly WeatherPlugin weather = new();
private readonly CalendarPlugin calendar = new();
private readonly FilePlugin files = new();

(agent, thread) = await CreateChatClientAgent(
    "default",
    threadId: config.Handle,
    tools: [
        AIFunctionFactory.Create(weather.GetWeather),
        AIFunctionFactory.Create(calendar.GetEvents),
        AIFunctionFactory.Create(calendar.CreateEvent),
        AIFunctionFactory.Create(files.ReadFile),
        AIFunctionFactory.Create(files.WriteFile)
    ]
);

Plugins & Tools

FabrCore's registry can automatically discover reusable plugins and tools via alias attributes. Use these when you want tools that are shared across agents rather than defined inline.

When to Use Which

PluginAliasToolAlias
What it isA class with a constructor, state, and one or more tool methodsA standalone static method
Has state?Yes — can hold injected services, config, cachesNo — pure function, no instance
DI supportYes — constructor receives AgentConfiguration and IServiceProviderNo — static, no constructor
Use whenTools need services (HTTP clients, DB access, config) or share state between methodsSimple, self-contained operations with no dependencies
ExampleAPI integrations, database queries, stateful workflowsString formatting, math, date calculations

Building a Plugin

Plugins are classes that implement IFabrCorePlugin. They receive configuration and services via InitializeAsync, making them ideal for tools that need dependencies.

WeatherPlugin.cs
[PluginAlias("weather")]
public class WeatherPlugin : IFabrCorePlugin
{
    private HttpClient? _httpClient;
    private string? _apiKey;

    public Task InitializeAsync(
        AgentConfiguration config,
        IServiceProvider serviceProvider)
    {
        _httpClient = serviceProvider
            .GetRequiredService<IHttpClientFactory>()
            .CreateClient("weather");
        _apiKey = config.AdditionalArgs?
            .GetValueOrDefault("weatherApiKey");
        return Task.CompletedTask;
    }

    [Description("Get the current weather for a city")]
    public async Task<string> GetWeather(
        [Description("City name")] string city)
    {
        var response = await _httpClient!.GetStringAsync(
            $"?q={city}&appid={_apiKey}");
        return response;
    }

    [Description("Get the 5-day forecast for a city")]
    public async Task<string> GetForecast(
        [Description("City name")] string city)
    {
        var response = await _httpClient!.GetStringAsync(
            $"forecast?q={city}&appid={_apiKey}");
        return response;
    }
}

Building a Tool

Tools are static methods — no class instance, no state. Use them for simple, self-contained operations.

MathTools.cs
public static class MathTools
{
    [ToolAlias("calculate-percentage")]
    [Description("Calculate a percentage of a value")]
    public static string CalculatePercentage(
        [Description("The value")] double value,
        [Description("The percentage")] double percentage)
    {
        var result = value * (percentage / 100);
        return $"{percentage}% of {value} = {result}";
    }

    [ToolAlias("convert-temperature")]
    [Description("Convert temperature between Celsius and Fahrenheit")]
    public static string ConvertTemperature(
        [Description("Temperature value")] double temp,
        [Description("Source unit: C or F")] string from)
    {
        return from.ToUpper() == "C"
            ? $"{temp}C = {temp * 9 / 5 + 32}F"
            : $"{temp}F = {(temp - 32) * 5 / 9}C";
    }
}
Discovery

Both [PluginAlias] and [ToolAlias] are picked up automatically by FabrCoreRegistry at startup. Verify your registrations via GET /fabrcoreapi/Discovery. See Server > FabrCoreRegistry for details.

Plugin Lifecycle

Understanding the plugin lifecycle helps you avoid common pitfalls with service resolution and resource management.

Services Available During InitializeAsync

The IServiceProvider passed to InitializeAsync is a PluginServiceProvider wrapper that includes the following services in addition to your application's DI container:

ServiceDescription
IFabrCoreAgentHostThe agent host — always non-null through the standard flow
AgentConfigurationThe agent's configuration
All registered DI servicesHttpClientFactory, loggers, your custom services, etc.
Correct Service Resolution
public Task InitializeAsync(
    AgentConfiguration config,
    IServiceProvider serviceProvider)
{
    // Use GetRequiredService — throws immediately if unavailable
    var host = serviceProvider.GetRequiredService<IFabrCoreAgentHost>();

    // IMPORTANT: Always use the serviceProvider parameter passed to this method.
    // Do NOT use a cached or different IServiceProvider instance.
    return Task.CompletedTask;
}
Common Pitfall

If IFabrCoreAgentHost resolves to null, check that you're using the IServiceProvider parameter passed directly to InitializeAsync — not a different provider cached elsewhere. The host is guaranteed non-null through the standard FabrCoreAgentProxy flow.

Optional Base Class Pattern

Reduce boilerplate across plugins with an abstract base class:

FabrCorePluginBase.cs
public abstract class FabrCorePluginBase : IFabrCorePlugin
{
    protected IFabrCoreAgentHost AgentHost { get; private set; } = null!;
    protected AgentConfiguration Config { get; private set; } = null!;

    public Task InitializeAsync(
        AgentConfiguration config,
        IServiceProvider serviceProvider)
    {
        Config = config;
        AgentHost = serviceProvider.GetRequiredService<IFabrCoreAgentHost>();
        return OnInitializeAsync(config, serviceProvider);
    }

    protected virtual Task OnInitializeAsync(
        AgentConfiguration config, IServiceProvider sp)
        => Task.CompletedTask;
}

Plugin Settings Convention

Plugin settings are stored in AgentConfiguration.Args using a PluginAlias:Key naming convention:

Reading Plugin Settings
// Read a single setting
var rootPath = config.GetPluginSetting("FileSystem", "RootPath");

// Read all settings for a plugin
var settings = config.GetPluginSettings("FileSystem");
Agent Configuration JSON
{
    "Handle": "my-agent",
    "Args": {
        "FileSystem:RootPath": "/tmp/files",
        "FileSystem:MaxFileSize": "10485760",
        "WebBrowser:TimeoutMs": "30000"
    }
}

Resource Management

Plugins that hold resources (file watchers, timers, HTTP clients) should implement IDisposable or IAsyncDisposable:

Plugin with Resource Cleanup
[PluginAlias("watcher")]
public class FileWatcherPlugin : IFabrCorePlugin, IDisposable
{
    private readonly List<FileSystemWatcher> _watchers = [];

    public Task InitializeAsync(
        AgentConfiguration config,
        IServiceProvider serviceProvider)
    {
        // Set up file watchers...
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        foreach (var watcher in _watchers)
        {
            watcher.EnableRaisingEvents = false;
            watcher.Dispose();
        }
        _watchers.Clear();
    }
}

Security Best Practices

When building plugins that execute system commands or interact with external resources:

  • Use allowlists, not blocklists. Define what IS permitted rather than trying to block every dangerous pattern. Blocklists are trivially bypassable.
  • Sandbox at the OS level. Use containers, restricted users, or seccomp profiles for defense in depth.
  • Validate at boundaries. Check input at the plugin's API boundary. Trust internal code paths and framework guarantees.
  • Scope permissions narrowly. Each plugin should have the minimum permissions needed for its function.

Structured Output

Use IChatClient.GetResponseAsync<T>() for structured output extraction. This is useful in AIContextProvider implementations:

Structured Output Example
public class UserInfo
{
    public string? UserName { get; set; }
    public int? UserAge { get; set; }
}

// Extract structured data from conversation
var result = await chatClient.GetResponseAsync<UserInfo>(
    context.RequestMessages,
    new ChatOptions
    {
        Instructions = "Extract the user's name if present."
    });

Dynamic Tools via AIContextProvider

Inject tools dynamically per-invocation based on user permissions or context:

DynamicToolProvider.cs
public class DynamicToolProvider : AIContextProvider
{
    private readonly IToolRegistry _toolRegistry;

    public override async ValueTask<AIContext> InvokingAsync(
        InvokingContext context,
        CancellationToken cancellationToken = default)
    {
        var tools = await _toolRegistry
            .GetToolsForUserAsync(_userId, cancellationToken);

        return new AIContext
        {
            Tools = tools.Select(t =>
                AIFunctionFactory.Create(t.Method)).ToList()
        };
    }
}
Best Practice

Keep tool descriptions clear and specific. The LLM uses the [Description] attribute text to decide when to call each tool. Vague descriptions lead to incorrect tool selections.

Documentation