Tool
JavaScript · package toolnexus · SPEC §1 · js/src/types.ts
interface Tool { name: string description: string inputSchema: JSONSchema source: ToolSource execute(args: Record<string, unknown>, ctx?: ToolContext): Promise<ToolResult>}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 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.
When to use it
Section titled “When to use it”You mostly receive Tools rather than construct them: tk.tools() hands you a Tool[], and
that is the type you iterate, filter, and pass to an adapter.
Construct one directly when you are writing a new tool source — something that produces tools from a shape toolnexus doesn’t already cover (a database of prompts, an internal RPC registry, a plugin system). For a single ordinary function, don’t hand-build this; see below.
Why this and not the alternative
Section titled “Why this and not the alternative”The fields are fixed because they are what every adapter reads. name, description and
inputSchema become the provider’s function schema; source is toolnexus bookkeeping that tells
you where a tool came from after aggregation flattens everything into one list.
Examples
Section titled “Examples”1. The smallest possible tool
Section titled “1. The smallest possible tool”A Tool is a plain object. Nothing is subclassed and nothing is registered — build it and it works.
import assert from "node:assert"import type { Tool } from "toolnexus"
const echo: Tool = { name: "echo", description: "Return whatever it is given", inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] }, source: "custom", async execute(args) { return { output: String(args.text), isError: false } },}
const res = await echo.execute({ text: "hello" })assert.equal(res.output, "hello")assert.equal(res.isError, false)console.log("ok:", res.output)2. Reporting failure, and carrying metadata
Section titled “2. Reporting failure, and carrying metadata”A tool that fails does not throw — it returns isError: true. The loop feeds that text back to
the model as the tool result, so the model can react to it. Throwing escapes the loop instead.
import assert from "node:assert"import type { Tool } from "toolnexus"
const divide: Tool = { name: "divide", description: "Divide two numbers", inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"], }, source: "custom", async execute(args) { const a = Number(args.a) const b = Number(args.b) if (b === 0) { // The model sees this text and can correct itself on the next turn. return { output: "Cannot divide by zero", isError: true } } return { output: String(a / b), isError: false, metadata: { title: "divide", operands: [a, b] }, } },}
const ok = await divide.execute({ a: 10, b: 4 })assert.equal(ok.output, "2.5")assert.deepEqual(ok.metadata?.operands, [10, 4])
const bad = await divide.execute({ a: 1, b: 0 })assert.equal(bad.isError, true)console.log("ok:", ok.output, "| error path:", bad.output)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 build Tool directly. Here one row of config becomes
one tool, and sanitize makes each name schema-safe.
import assert from "node:assert"import { sanitize } from "toolnexus"import type { Tool } from "toolnexus"
const endpoints = [ { key: "get user", path: "/users/:id" }, { key: "list orders", path: "/orders" },]
const tools: Tool[] = endpoints.map((e) => ({ // Names must match [a-zA-Z0-9_-]; sanitize does exactly that (same rule as opencode). name: sanitize(e.key), description: `Call ${e.path}`, inputSchema: { type: "object", properties: { id: { type: "string" } } }, source: "custom", async execute(args, ctx) { // ctx is optional — always guard it. if (ctx?.signal?.aborted) return { output: "cancelled", isError: true } return { output: `${e.path} <- ${JSON.stringify(args)}`, isError: false } },}))
assert.deepEqual(tools.map((t) => t.name), ["get_user", "list_orders"])
const res = await tools[0].execute({ id: "42" })assert.equal(res.output, '/users/:id <- {"id":"42"}')console.log("ok:", tools.map((t) => t.name).join(", "))Fields
Section titled “Fields”| Field | Type | What it is |
|---|---|---|
name |
string |
The name the model calls. Must match [a-zA-Z0-9_-] — run it through sanitize. |
description |
string |
What the model reads to decide whether to call it. |
inputSchema |
JSONSchema |
A JSON-Schema object — type: "object", plus properties and required. |
source |
ToolSource |
One of mcp, skill, builtin, native, http, a2a, custom. |
execute |
(args, ctx?) => Promise<ToolResult> |
Runs the tool. ctx is optional — guard it. |
See also
Section titled “See also”ToolResult— whatexecutereturnsToolContext— whatexecutereceivesdefineTool— build aToolfrom a functioncreateToolkit— aggregate tools from every source