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
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
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
(agent, thread) = await CreateChatClientAgent(
"default",
threadId: config.Handle,
tools: [
AIFunctionFactory.Create(plugin.GetWeather),
AIFunctionFactory.Create(plugin.GetForecast)
]
);
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
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
| PluginAlias | ToolAlias | |
|---|---|---|
| What it is | A class with a constructor, state, and one or more tool methods | A standalone static method |
| Has state? | Yes — can hold injected services, config, caches | No — pure function, no instance |
| DI support | Yes — constructor receives AgentConfiguration and IServiceProvider | No — static, no constructor |
| Use when | Tools need services (HTTP clients, DB access, config) or share state between methods | Simple, self-contained operations with no dependencies |
| Example | API integrations, database queries, stateful workflows | String 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.
[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.
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";
}
}
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:
| Service | Description |
|---|---|
IFabrCoreAgentHost | The agent host — always non-null through the standard flow |
AgentConfiguration | The agent's configuration |
| All registered DI services | HttpClientFactory, loggers, your custom services, etc. |
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;
}
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:
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:
// Read a single setting
var rootPath = config.GetPluginSetting("FileSystem", "RootPath");
// Read all settings for a plugin
var settings = config.GetPluginSettings("FileSystem");
{
"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:
[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:
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:
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()
};
}
}
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.