Custom Tools
Tools in JD.AI are Semantic Kernel plugins — plain C# classes with [KernelFunction] attributes. The AI agent discovers and invokes them automatically based on their descriptions and parameter schemas.
How tools work
Every tool call flows through the Semantic Kernel function invocation pipeline:
LLM decides to call a tool
→ SK deserializes parameters from the tool_call JSON
→ IFunctionInvocationFilter chain runs (including ToolConfirmationFilter)
→ User confirms (or auto-approved via /autorun)
→ Your [KernelFunction] method executes
→ Return value is serialized and sent back to the LLM
Creating a tool
Create a class in src/JD.AI.Core/Tools/ with [KernelFunction] methods:
using System.ComponentModel;
using Microsoft.SemanticKernel;
public class DatabaseTools
{
private readonly string _connectionString;
public DatabaseTools(string connectionString)
{
_connectionString = connectionString;
}
[KernelFunction("query_database")]
[Description("Execute a read-only SQL query and return results as a table")]
public async Task<string> QueryDatabaseAsync(
[Description("SQL SELECT query to execute")] string query,
[Description("Maximum rows to return")] int maxRows = 50,
CancellationToken ct = default)
{
if (!query.TrimStart().StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
return "Error: Only SELECT queries are allowed.";
// Execute query and format results...
return formattedResults;
}
[KernelFunction("list_tables")]
[Description("List all tables in the database")]
public async Task<string> ListTablesAsync(CancellationToken ct = default)
{
// Implementation...
return tableList;
}
}
Key rules
- Every public method that should be a tool needs
[KernelFunction("tool_name")] - Every method and parameter needs
[Description("...")]— the LLM uses these to decide when and how to call the tool - Return
string— the LLM reads the return value as text. For complex data, format it as a table, JSON, or markdown - Use
snake_casefor function names — this matches the convention used by all built-in tools - Accept
CancellationTokenfor async operations — SK passes it automatically
Parameter types
Semantic Kernel handles parameter deserialization from the LLM's JSON tool call:
| C# Type | JSON Type | Notes |
|---|---|---|
string |
string |
Most common |
int, long |
number |
Integers |
double, float |
number |
Floating point |
bool |
boolean |
Flags |
int?, string? |
nullable | Optional parameters — use default values |
enum |
string |
SK maps string values to enum members |
[KernelFunction("search_logs")]
[Description("Search application logs")]
public string SearchLogs(
[Description("Search pattern (regex)")] string pattern,
[Description("Log level filter")] LogLevel? level = null,
[Description("Maximum results")] int maxResults = 20,
[Description("Include timestamps")] bool includeTimestamps = true)
{
// Parameters with defaults are optional for the LLM
}
public enum LogLevel { Debug, Info, Warning, Error, Critical }
Tool safety
JD.AI uses a ToolConfirmationFilter (IFunctionInvocationFilter) that prompts the user before tool execution. Safety is managed at the execution level:
- Read-only tools (e.g.,
read_file,grep,glob) — safe, can be auto-approved via/autorun - Write tools (e.g.,
write_file,edit_file) — require confirmation by default - Destructive tools (e.g.,
run_command) — always require confirmation unless explicitly overridden
When writing tools, design with safety in mind:
// GOOD: Separate read and write operations
[KernelFunction("list_deployments")]
[Description("List all active deployments (read-only)")]
public async Task<string> ListDeploymentsAsync(CancellationToken ct = default)
{
// Safe read-only operation
}
[KernelFunction("restart_deployment")]
[Description("Restart a deployment by name — this causes downtime")]
public async Task<string> RestartDeploymentAsync(
[Description("Deployment name")] string name,
CancellationToken ct = default)
{
// Destructive — user will be prompted to confirm
}
Registering tools
Register your tools with the Semantic Kernel instance:
// In the kernel setup (e.g., Program.cs or a DI registration)
kernel.Plugins.AddFromObject(new DatabaseTools(connectionString), "DatabaseTools");
The plugin name ("DatabaseTools") is used for namespacing — the LLM sees functions as DatabaseTools-query_database.
Registering with constructor dependencies
If your tool needs services from DI, resolve them during registration:
var dbTools = new DatabaseTools(
configuration.GetConnectionString("Default")!);
kernel.Plugins.AddFromObject(dbTools, "Database");
Patterns from built-in tools
FileTools pattern — working directory scoping
public class FileTools
{
private readonly string _workingDirectory;
public FileTools(string workingDirectory)
{
_workingDirectory = workingDirectory;
}
[KernelFunction("read_file")]
[Description("Read file contents with an optional line range")]
public string ReadFile(
[Description("File path (relative to working directory)")] string path,
[Description("Start line (1-based, optional)")] int? startLine = null,
[Description("End line (-1 for end of file, optional)")] int? endLine = null)
{
var fullPath = Path.GetFullPath(path, _workingDirectory);
// Validate path is within working directory
// Read and return contents
}
}
GitTools pattern — async shell execution
public class GitTools
{
private readonly string _workingDirectory;
public GitTools(string workingDirectory)
{
_workingDirectory = workingDirectory;
}
[KernelFunction("git_status")]
[Description("Show the working tree status")]
public async Task<string> GitStatusAsync(
[Description("Repository path")] string? path = null,
CancellationToken ct = default)
{
var repoPath = path ?? _workingDirectory;
// Execute git command and return output
}
[KernelFunction("git_diff")]
[Description("Show changes between commits, working tree, etc.")]
public async Task<string> GitDiffAsync(
[Description("Diff target (branch, commit, or 'staged')")] string? target = null,
[Description("File path filter")] string? path = null,
CancellationToken ct = default)
{
// Execute git diff and return output
}
}
SubagentTools pattern — complex orchestration
public class SubagentTools
{
[KernelFunction("spawn_agent")]
[Description("Spawn a specialized subagent for a scoped task")]
public async Task<string> SpawnAgentAsync(
[Description("Agent type: explore, task, plan, review, general")] string type,
[Description("Task prompt for the subagent")] string prompt,
[Description("Execution mode: single or multi")] string mode = "single")
{
// Build scoped kernel, run agent, return result
}
}
Error handling
Return error messages as strings rather than throwing exceptions. The LLM reads the return value and can recover:
[KernelFunction("read_config")]
[Description("Read a configuration value")]
public string ReadConfig(
[Description("Configuration key")] string key)
{
try
{
var value = _configStore.Get(key);
return value ?? $"Configuration key '{key}' not found.";
}
catch (Exception ex)
{
return $"Error reading configuration: {ex.Message}";
}
}
Testing tools
Test tools as plain C# classes — no Semantic Kernel infrastructure needed:
public class DatabaseToolsTests
{
[Fact]
public async Task QueryDatabase_RejectsNonSelectQueries()
{
var tools = new DatabaseTools("Data Source=:memory:");
var result = await tools.QueryDatabaseAsync("DROP TABLE users");
Assert.Contains("Only SELECT queries", result);
}
[Fact]
public async Task ListTables_ReturnsFormattedList()
{
var tools = new DatabaseTools(TestDb.ConnectionString);
var result = await tools.ListTablesAsync();
Assert.Contains("users", result);
}
}
For integration tests that verify the LLM can discover and call your tool, register it with a test kernel:
[Fact]
public void Tool_IsDiscoverableByKernel()
{
var kernel = Kernel.CreateBuilder().Build();
kernel.Plugins.AddFromObject(new DatabaseTools(":memory:"), "Database");
var functions = kernel.Plugins.GetFunctionsMetadata();
Assert.Contains(functions, f => f.Name == "query_database");
Assert.Contains(functions, f => f.Name == "list_tables");
}
See also
- Architecture Overview — tool pipeline and agent lifecycle
- Extending JD.AI — project layout and coding standards
- Tool Loadouts — expose only relevant tools to each agent
- Tools Reference — all built-in tools
- Plugin SDK — distributable plugins with tool registration