Skip to content

Tool

Python · package toolnexus · SPEC §1 · python/src/toolnexus/types.py

@dataclass
class Tool:
name: str
description: str
input_schema: JSONSchema
source: ToolSource
execute: ExecuteFn # async def execute(args: dict, ctx: ToolContext | None = None) -> ToolResult

The one type. An MCP server tool, an agent skill, a built-in shell tool, a remote A2A agent, an HTTP endpoint and a plain function of your own are all the same thing to an LLM — a named, described, schema’d callable. Tool is that thing, and every source in toolnexus produces it.

You mostly receive Tools rather than construct them: tk.tools() hands you a list[Tool], and that is what you iterate, filter, and pass to an adapter.

Construct one directly when you are writing a new tool source — something producing tools from a shape toolnexus doesn’t already cover (a table of prompts, an internal RPC registry, a plugin system). For a single ordinary function, don’t hand-build this.

execute is a coroutine functionasync def. The loop awaits it, so a synchronous callable will not work; wrap blocking work in asyncio.to_thread if you need to.

Tool is a dataclass. Nothing is subclassed and nothing is registered — build it and it works.

import asyncio
from toolnexus import Tool, ToolResult
async def _echo(args, ctx=None):
return ToolResult(output=str(args["text"]), is_error=False)
echo = Tool(
name="echo",
description="Return whatever it is given",
input_schema={"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]},
source="custom",
execute=_echo,
)
async def main():
res = await echo.execute({"text": "hello"})
assert res.output == "hello"
assert res.is_error is False
print("ok:", res.output)
asyncio.run(main())

2. Reporting failure, and carrying metadata

Section titled “2. Reporting failure, and carrying metadata”

A tool that fails does not raise — it returns is_error=True. The loop feeds that text back to the model as the tool result, so the model can react. Raising escapes the loop instead.

import asyncio
from toolnexus import Tool, ToolResult
async def _divide(args, ctx=None):
a, b = float(args["a"]), float(args["b"])
if b == 0:
# The model sees this text and can correct itself on the next turn.
return ToolResult(output="Cannot divide by zero", is_error=True)
return ToolResult(
output=str(a / b),
is_error=False,
metadata={"title": "divide", "operands": [a, b]},
)
divide = Tool(
name="divide",
description="Divide two numbers",
input_schema={
"type": "object",
"properties": {"a": {"type": "number"}, "b": {"type": "number"}},
"required": ["a", "b"],
},
source="custom",
execute=_divide,
)
async def main():
ok = await divide.execute({"a": 10, "b": 4})
assert ok.output == "2.5"
assert ok.metadata["operands"] == [10.0, 4.0]
bad = await divide.execute({"a": 1, "b": 0})
assert bad.is_error is True
print("ok:", ok.output, "| error path:", bad.output)
asyncio.run(main())

3. A generated tool source — the real reason this type is public

Section titled “3. A generated tool source — the real reason this type is public”

Producing many tools from data is where you build Tool directly. Here one row of config becomes one tool, and sanitize makes each name schema-safe.

import asyncio
from toolnexus import Tool, ToolResult, sanitize
endpoints = [
{"key": "get user", "path": "/users/:id"},
{"key": "list orders", "path": "/orders"},
]
def make_tool(endpoint):
async def _execute(args, ctx=None):
# ctx is optional — always guard it.
if ctx is not None and getattr(ctx, "signal", None) is not None:
pass
return ToolResult(output=f"{endpoint['path']} <- {args}", is_error=False)
return Tool(
# Names must match [a-zA-Z0-9_-]; sanitize does exactly that.
name=sanitize(endpoint["key"]),
description=f"Call {endpoint['path']}",
input_schema={"type": "object", "properties": {"id": {"type": "string"}}},
source="custom",
execute=_execute,
)
tools = [make_tool(e) for e in endpoints]
async def main():
assert [t.name for t in tools] == ["get_user", "list_orders"]
res = await tools[0].execute({"id": "42"})
assert res.output == "/users/:id <- {'id': '42'}"
print("ok:", ", ".join(t.name for t in tools))
asyncio.run(main())
Field Type What it is
name str The name the model calls. Must match [a-zA-Z0-9_-] — run it through sanitize.
description str What the model reads to decide whether to call it.
input_schema JSONSchema A JSON-Schema object — a plain dict.
source ToolSource One of mcp, skill, native, http, builtin, a2a, custom.
execute ExecuteFn async def execute(args, ctx=None) -> ToolResult. Must be a coroutine function.