ITool
C# · package Toolnexus · SPEC §1 · ITool.cs
public interface ITool{ string Name { get; } string Description { get; } IDictionary<string, object?> InputSchema { get; } string Source { get; }
Task<ToolResult> ExecuteAsync(IDictionary<string, object?> args, ToolContext? ctx = null);}The one interface. An MCP server tool, an agent skill, a built-in shell tool, a remote A2A agent, an
HTTP endpoint and a plain method of your own are all the same thing to an LLM — a named,
described, schema’d callable. ITool is that thing, and every source produces it.
When to use it
Section titled “When to use it”You mostly receive tools rather than implement them: tk.Tools() hands you a list, and that is
what you filter and pass to an adapter.
Implement it directly when you are writing a new tool source — something producing tools from a
shape toolnexus doesn’t already cover. For a single ordinary method, use
NativeTool.Of or the [ToolMethod] attribute instead.
Why an interface, and why ctx is optional
Section titled “Why an interface, and why ctx is optional”ExecuteAsync takes ToolContext? ctx = null — the parameter has a default, so callers may omit it
entirely. Inside your implementation always treat it as nullable.
Examples
Section titled “Examples”1. The smallest possible tool
Section titled “1. The smallest possible tool”using Toolnexus;
var echo = new EchoTool();
var res = await echo.ExecuteAsync(new Dictionary<string, object?> { ["text"] = "hello" });if (res.Output != "hello" || res.IsError) throw new Exception($"unexpected: {res.Output}");
Console.WriteLine($"ok: {res.Output}");
sealed class EchoTool : ITool{ public string Name => "echo"; public string Description => "Return whatever it is given"; public string Source => "custom";
public IDictionary<string, object?> InputSchema => new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["text"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new[] { "text" }, };
public Task<ToolResult> ExecuteAsync(IDictionary<string, object?> args, ToolContext? ctx = null) => Task.FromResult(ToolResult.Ok(args["text"]?.ToString() ?? ""));}Source is not free-form — it is one of mcp, skill, native, http, a2a, custom. Use
custom for tools you implement yourself.
2. Reporting failure, and carrying metadata
Section titled “2. Reporting failure, and carrying metadata”A tool that fails does not throw — it returns an error result. ToolResult.Ok and
ToolResult.Error are the shorthand, each taking optional metadata.
using Toolnexus;
var divide = new DivideTool();
var ok = await divide.ExecuteAsync(new Dictionary<string, object?> { ["a"] = 10.0, ["b"] = 4.0 });if (ok.Output != "2.5") throw new Exception($"got {ok.Output}");if (ok.Metadata?["title"] as string != "divide") throw new Exception("metadata");
var bad = await divide.ExecuteAsync(new Dictionary<string, object?> { ["a"] = 1.0, ["b"] = 0.0 });if (!bad.IsError) throw new Exception("expected IsError");
Console.WriteLine($"ok: {ok.Output} | error path: {bad.Output}");
sealed class DivideTool : ITool{ public string Name => "divide"; public string Description => "Divide two numbers"; public string Source => "custom";
public IDictionary<string, object?> InputSchema => new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["a"] = new Dictionary<string, object?> { ["type"] = "number" }, ["b"] = new Dictionary<string, object?> { ["type"] = "number" }, }, ["required"] = new[] { "a", "b" }, };
public Task<ToolResult> ExecuteAsync(IDictionary<string, object?> args, ToolContext? ctx = null) { var a = Convert.ToDouble(args["a"]); var b = Convert.ToDouble(args["b"]);
if (b == 0) { // The model sees this text and can correct itself on the next turn. return Task.FromResult(ToolResult.Error("Cannot divide by zero")); }
return Task.FromResult(ToolResult.Ok( (a / b).ToString(System.Globalization.CultureInfo.InvariantCulture), new Dictionary<string, object?> { ["title"] = "divide", ["operands"] = new[] { a, b } })); }}3. A generated tool source — the real reason this interface is public
Section titled “3. A generated tool source — the real reason this interface is public”Producing many tools from data is where you implement ITool directly. Tools.Sanitize makes each
name schema-safe.
using Toolnexus;
var endpoints = new[]{ ("get user", "/users/:id"), ("list orders", "/orders"),};
var tools = endpoints.Select(e => new EndpointTool(e.Item1, e.Item2)).ToList();
if (tools[0].Name != "get_user" || tools[1].Name != "list_orders") throw new Exception($"names: {tools[0].Name},{tools[1].Name}");
var res = await tools[0].ExecuteAsync(new Dictionary<string, object?> { ["id"] = "42" });if (res.Output != "/users/:id <- 42") throw new Exception(res.Output);
Console.WriteLine($"ok: {string.Join(", ", tools.Select(t => t.Name))}");
sealed class EndpointTool : ITool{ private readonly string _path;
public EndpointTool(string key, string path) { // Names must match [a-zA-Z0-9_-]; Sanitize does exactly that. Name = Tools.Sanitize(key); _path = path; }
public string Name { get; } public string Description => $"Call {_path}"; public string Source => "custom";
public IDictionary<string, object?> InputSchema => new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["id"] = new Dictionary<string, object?> { ["type"] = "string" } }, };
public Task<ToolResult> ExecuteAsync(IDictionary<string, object?> args, ToolContext? ctx = null) { // ctx is nullable — always guard it. if (ctx?.IsCancelled == true) return Task.FromResult(ToolResult.Error("cancelled")); return Task.FromResult(ToolResult.Ok($"{_path} <- {args["id"]}")); }}Members
Section titled “Members”| Member | Type | What it is |
|---|---|---|
Name |
string |
The name the model calls. Must match [a-zA-Z0-9_-]. |
Description |
string |
What the model reads to decide whether to call it. |
InputSchema |
IDictionary<string, object?> |
A JSON-Schema object as a plain dictionary. |
Source |
string |
One of mcp, skill, native, http, a2a, custom. |
ExecuteAsync(args, ctx?) |
Task<ToolResult> |
Runs the tool. ctx defaults to null. |
See also
Section titled “See also”ToolResult— whatExecuteAsyncreturnsToolContext— whatExecuteAsyncreceivesNativeTool.Of— build a tool from a methodToolkit.CreateAsync— aggregate tools from every source