Octopus
Custom Tools
Custom tools let you extend any Octopus agent with domain-specific capabilities — querying your ERP, calling your internal APIs, updating your CRM, or any other action the agent should be able to take. Custom tools are registered via plugins or the AI Function mechanism.
Complete Custom Tool Example
This example adds an inventory query tool to an agent:
// 1. Define the service interface
public interface IInventoryService
{
Task<InventoryItem?> GetItemAsync(string sku, Guid tenantId, CancellationToken ct);
Task<int> GetStockLevelAsync(string sku, Guid tenantId, CancellationToken ct);
}
// 2. Create the plugin that registers the tool
public class InventoryPlugin : IOctopusPlugin
{
public void OnRegister(IServiceCollection services, OctopusConfig config)
{
services.AddScoped<IInventoryService, InventoryService>();
}
public Task OnStartAsync(IServiceProvider sp, CancellationToken ct)
{
var registry = sp.GetRequiredService<MCPToolRegistry>();
var inventory = sp.GetRequiredService<IInventoryService>();
registry.Register(new MCPTool
{
Schema = new ToolDefinition
{
Name = "check_inventory",
Description = "Returns the current stock level for a product SKU. " +
"Call this when the user asks about stock levels, availability, or " +
"how many units are in stock.",
InputSchema = JsonDocument.Parse("""
{
"type": "object",
"properties": {
"sku": {
"type": "string",
"description": "Product SKU (e.g. 'AX-2240')"
}
},
"required": ["sku"]
}
""")
},
Handler = async (input, ctx, ct) =>
{
string sku = input.GetProperty("sku").GetString()
?? return JsonSerializer.Serialize(new { error = "sku is required" });
var item = await inventory.GetItemAsync(sku, ctx.TenantId, ct);
if (item == null)
return JsonSerializer.Serialize(new
{
error = "not_found",
message = $"Product '{sku}' not found in inventory."
});
int stock = await inventory.GetStockLevelAsync(sku, ctx.TenantId, ct);
return JsonSerializer.Serialize(new
{
sku = sku,
productName = item.Name,
stockLevel = stock,
unit = item.StockUnit,
reorderPoint= item.ReorderPoint
});
}
});
return Task.CompletedTask;
}
public Task OnStopAsync(CancellationToken ct) => Task.CompletedTask;
}
AI Functions as Dynamic Custom Tools
For lightweight tools that don't need a full C# service, AI Functions provide a code-in-database approach — the tool logic is JavaScript stored in SQL, executed in a sandboxed interpreter:
// AI Function stored in SQL:
// Name: "convert_currency"
// Language: JavaScript
// Code:
function execute(input) {
const rates = { "USD": 1, "EUR": 0.92, "GBP": 0.79, "AUD": 1.53 };
const from = input.fromCurrency;
const to = input.toCurrency;
if (!rates[from] || !rates[to])
return { error: "Unsupported currency" };
const converted = input.amount / rates[from] * rates[to];
return { fromCurrency: from, toCurrency: to, originalAmount: input.amount, convertedAmount: Math.round(converted * 100) / 100 };
}
// This function is automatically registered as an MCP tool
// when the agent has AI Functions enabled.
Tool Checklist Before Deploying
| Check | Requirement |
|---|---|
| Description quality | Clearly states when to call the tool and what it returns |
| Error handling | Returns JSON error objects, not exceptions |
| Result size | Returns <2000 tokens; large results truncated |
| Tenant isolation | Uses ctx.TenantId in all database/service calls |
| Credential pattern | Secrets accessed via ICredentialResolver, not hardcoded |
| No PII in results | Sensitive fields (SSN, salary) excluded from returned data |
| Idempotency | Tool can be called twice without adverse side effects (or documents non-idempotent behaviour clearly) |