Portal Community

Defining a Native Function Class

using Microsoft.SemanticKernel;
using System.ComponentModel;

// SK plugin class containing native functions
public class HRNativePlugin
{
    private readonly ILeaveService _leaveService;

    public HRNativePlugin(ILeaveService leaveService)
    {
        _leaveService = leaveService;
    }

    [KernelFunction("get_leave_balance")]
    [Description("Returns the remaining leave balance for an employee. " +
                 "Use when the user asks how many leave days they have left.")]
    public async Task<string> GetLeaveBalanceAsync(
        [Description("The employee's ID number (e.g. EMP-1234)")]
        string employeeId,
        CancellationToken cancellationToken = default)
    {
        var balance = await _leaveService.GetBalanceAsync(employeeId, cancellationToken);
        return $"Annual: {balance.AnnualRemaining} days, Sick: {balance.SickRemaining} days";
    }

    [KernelFunction("list_team_members")]
    [Description("Lists the direct reports of a manager. Returns a JSON array of employee IDs.")]
    public async Task<string> ListTeamMembersAsync(
        [Description("The manager's employee ID")]
        string managerId,
        CancellationToken cancellationToken = default)
    {
        var team = await _leaveService.GetTeamAsync(managerId, cancellationToken);
        return JsonSerializer.Serialize(team.Select(e => e.EmployeeId));
    }
}

Registering Native Functions in OnStartAsync

public async Task OnStartAsync(IServiceProvider sp, CancellationToken ct)
{
    var kernel = sp.GetRequiredService<ISKKernelRegistry>().Get("default");

    // Scoped services need a scope — capture the factory, not the service
    // The kernel will call these functions at planning time, not at startup
    var scopeFactory = sp.GetRequiredService<IServiceScopeFactory>();

    // Build the native plugin using the DI container
    // Create a scope for the constructor injection
    using var initScope = scopeFactory.CreateScope();
    var leaveService = initScope.ServiceProvider.GetRequiredService<ILeaveService>();
    var hrPlugin     = new HRNativePlugin(leaveService);

    // Register the plugin into the SK kernel
    kernel.ImportPluginFromObject(hrPlugin, "HRPlugin");

    await Task.CompletedTask;
}
Scoped services in native functions. If your native function needs scoped services (e.g., ILeaveService), you must create a scope per call — not at startup. Consider injecting IServiceScopeFactory into the native plugin class and creating a fresh scope in each method.

Native Functions vs MCP Tools

FeatureSK Native FunctionOctopus MCP Tool
RegistrationInto SK kernel via ImportPluginFromObjectInto MCPToolRegistry
Callable by SK PlannerYes — first-class SK citizenRequires bridging (see Planner page)
Callable by Octopus agentVia SK bridge onlyDirectly — native to Octopus
Parameter definition[Description] attributes on method paramsJSON Schema in ToolDefinition.InputSchema
Metadata for LLMAttribute descriptionsRich JSON Schema with types and examples

Calling Native Functions Directly

// Call a native function directly (without the planner)
var result = await kernel.InvokeAsync(
    pluginName:   "HRPlugin",
    functionName: "get_leave_balance",
    arguments:    new KernelArguments { ["employeeId"] = "EMP-1234" });

string balance = result.GetValue<string>()!;
// → "Annual: 15 days, Sick: 8 days"