Portal Community

MCPTool Class

public class MCPTool
{
    public ToolDefinition Schema  { get; init; } = new();

    // Handler receives raw JSON input from the LLM + execution context
    public Func<JsonElement, ConversationContext, CancellationToken, Task<string>>
        Handler { get; init; } = (_, _, _) => Task.FromResult("{}");
}

public class ConversationContext
{
    public Guid   AgentId        { get; init; }
    public Guid   ConversationId { get; init; }
    public string UserId         { get; init; } = string.Empty;
    public Guid   TenantId       { get; init; }
    public IServiceProvider Services { get; init; } = default!;
}

Pattern 1: Plugin-Level Registration (Global)

Tools registered in a plugin's OnRegister are available to all agents that have that plugin active:

public class HRToolsPlugin : IOctopusPlugin
{
    public void OnRegister(IServiceCollection services, OctopusConfig config)
    {
        services.AddSingleton<ILeaveService, LeaveService>();
    }

    public Task OnStartAsync(IServiceProvider sp, CancellationToken ct)
    {
        var registry = sp.GetRequiredService<MCPToolRegistry>();
        var leave    = sp.GetRequiredService<ILeaveService>();

        registry.Register(new MCPTool
        {
            Schema = new ToolDefinition
            {
                Name        = "get_leave_balance",
                Description = "Returns remaining annual leave days for an employee.",
                InputSchema = JsonDocument.Parse("""
                {
                  "type": "object",
                  "properties": {
                    "employeeId": { "type": "string", "description": "Employee ID" }
                  },
                  "required": ["employeeId"]
                }
                """)
            },
            Handler = async (input, ctx, ct) =>
            {
                string empId = input.GetProperty("employeeId").GetString()!;
                int days = await leave.GetBalanceAsync(empId, ctx.TenantId, ct);
                return JsonSerializer.Serialize(new { employeeId = empId, remainingDays = days });
            }
        });

        return Task.CompletedTask;
    }
}

Pattern 2: Per-Agent Registration via Admin UI

In the agents-app, administrators assign tool groups to specific agents. Tool group assignments are stored in Octopus_AgentToolGroups and resolved at agent load time:

// Agent tool group assignment (stored in SQL)
POST /api/octopus/agents/{agentId}/tool-groups
{
  "toolGroupId": "tg_hr_leave",
  "enabled":      true
}

// At runtime, AgentComposite.ToolRegistry contains only the tools
// assigned to that agent — not all globally registered tools.

Pattern 3: Dynamic Registration

Tools can be registered or unregistered while the application is running. This supports scenarios like feature flags or per-tenant tool availability:

// Register a tool at runtime (e.g. when a feature is enabled)
public class FeatureFlagToolRegistrar
{
    private readonly MCPToolRegistry _registry;
    private readonly IFeatureFlags   _flags;

    public async Task SyncToolsForAgentAsync(Guid agentId, CancellationToken ct)
    {
        if (await _flags.IsEnabledAsync("BetaExpenseReports", agentId, ct))
        {
            _registry.RegisterForAgent(agentId, new MCPTool
            {
                Schema  = ExpenseReportToolDefinition,
                Handler = ExpenseReportHandler
            });
        }
        else
        {
            _registry.UnregisterForAgent(agentId, "submit_expense_report");
        }
    }
}

Registration Scope Summary

ScopeWho Sees the ToolWhen to Use
Global (plugin-level)All agents with that pluginCore tools used by most agents
Per-agent (admin UI)Only the configured agentSpecialist tools for specific agent roles
Dynamic (runtime)Controlled at runtime per agentFeature flags, per-tenant capabilities, A/B testing