Portal Community

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

CheckRequirement
Description qualityClearly states when to call the tool and what it returns
Error handlingReturns JSON error objects, not exceptions
Result sizeReturns <2000 tokens; large results truncated
Tenant isolationUses ctx.TenantId in all database/service calls
Credential patternSecrets accessed via ICredentialResolver, not hardcoded
No PII in resultsSensitive fields (SSN, salary) excluded from returned data
IdempotencyTool can be called twice without adverse side effects (or documents non-idempotent behaviour clearly)