Skip to content

tool

JavaScript · package toolnexus · SPEC §6 · js/src/native.ts

function tool(
description: string,
opts?: { name?: string; inputSchema?: JSONSchema },
): (value: Function, context: { name?: PropertyKey; addInitializer?: (fn: () => void) => void }) => Function

A TC39 method decorator: tag a method or function with tool(description, opts) right where it is declared, and it becomes collectible as a Tool by collectTools later. In a project with decorator syntax enabled, that looks like this:

class Weather {
@tool("Current weather for a city")
getWeather(args: { city: string }) {
return `sunny in ${args.city}`
}
}
  • A whole class or object is your tool surface. Annotate each method once, at its declaration, instead of writing a separate defineTool({...}) block per function somewhere else.
  • You want the schema/description physically next to the implementation, so a reviewer sees “this is a tool, here’s what it does” without jumping to a registration file.
  • You’re porting from a framework with the same shape — decorator-annotated tools are a common pattern (FastMCP, several agent SDKs); tool gives you the equivalent without adopting a whole framework.

tool itself does not return a Tool — it returns the original function, marked. Nothing is collected until you call collectTools on the object those methods live on. That two-step split (mark now, sweep later) is what lets you decorate a class definition once and reuse it across many instances.

1. The smallest useful call — one standalone function

Section titled “1. The smallest useful call — one standalone function”
import assert from "node:assert"
import { tool, collectTools } from "toolnexus"
function greet(args: { name: string }) {
return `Hello, ${args.name}!`
}
// Equivalent to `@tool("Greet someone by name") function greet(...) {...}`.
tool("Greet someone by name")(greet, { name: "greet" })
const tools = collectTools({ greet })
assert.equal(tools.length, 1)
assert.equal(tools[0].name, "greet")
assert.equal(tools[0].source, "native")
const res = await tools[0].execute({ name: "Muthu" })
assert.equal(res.output, "Hello, Muthu!")
console.log("ok:", tools[0].name, "->", res.output)

2. A service object with several annotated methods

Section titled “2. A service object with several annotated methods”

opts.name overrides the property name when the model-facing name should differ from the JavaScript identifier; opts.inputSchema attaches real schema instead of the empty-object default. Omitting opts.name falls back to the decorator context’s name — the property key itself.

import assert from "node:assert"
import { tool, collectTools } from "toolnexus"
const orders = {
lookupOrder(args: { id: string }) {
return { id: args.id, status: "shipped" }
},
cancelOrder(args: { id: string; reason: string }) {
return `cancelled ${args.id}: ${args.reason}`
},
}
// name omitted ⇒ falls back to the property key ("lookupOrder").
tool("Fetch an order by id")(orders.lookupOrder, { name: "lookupOrder" })
// name + inputSchema both supplied ⇒ the model-facing name can differ from the JS identifier.
tool("Cancel an order", {
name: "cancel_order",
inputSchema: {
type: "object",
properties: { id: { type: "string" }, reason: { type: "string" } },
required: ["id", "reason"],
},
})(orders.cancelOrder, { name: "cancelOrder" })
const tools = collectTools(orders)
assert.deepEqual(tools.map((t) => t.name).sort(), ["cancel_order", "lookupOrder"])
const cancel = tools.find((t) => t.name === "cancel_order")!
assert.deepEqual(cancel.inputSchema.required, ["id", "reason"])
const res = await cancel.execute({ id: "A-1", reason: "customer request" })
assert.equal(res.output, "cancelled A-1: customer request")
console.log("ok:", tools.map((t) => t.name).join(", "))

3. The full surface — class instances, addInitializer, and ctx

Section titled “3. The full surface — class instances, addInitializer, and ctx”

On a class, the decorator context carries addInitializer — called once per instance at construction, which is how collectTools finds tools keyed to a specific instance (not just to functions with a marked property). run’s second argument is the same ToolContext defineTool receives.

import assert from "node:assert"
import { tool, collectTools, type ToolContext } from "toolnexus"
class Weather {
// A genuine prototype method — not an own instance property — so `tool()`'s
// `addInitializer` path (REGISTRY, keyed per instance) is the ONLY way
// `collectTools` discovers it. `run` calls the function with no bound
// receiver, so reach for `ctx` rather than `this` for anything you need.
getWeather(args: { city: string }, ctx?: ToolContext) {
if (ctx?.signal?.aborted) return { output: "cancelled", isError: true }
return `sunny in ${args.city}`
}
}
// A minimal stand-in for what a real `@tool(...)` method decorator's context
// provides: `name` and `addInitializer`, normally called once per instance, at
// construction.
const initializers: Array<(instance: object) => void> = []
Weather.prototype.getWeather = tool("Current weather for a city")(Weather.prototype.getWeather, {
name: "getWeather",
addInitializer(fn: () => void) {
initializers.push(fn as unknown as (instance: object) => void)
},
}) as typeof Weather.prototype.getWeather
const w = new Weather()
for (const init of initializers) init.call(w) // what a real constructor runs for you
const tools = collectTools(w)
assert.equal(tools.length, 1, "found via REGISTRY only — getWeather is not an own property of w")
assert.equal(tools[0].name, "getWeather")
const res = await tools[0].execute({ city: "Chennai" })
assert.equal(res.output, "sunny in Chennai")
const ac = new AbortController()
ac.abort()
const cancelled = await tools[0].execute({ city: "Chennai" }, { signal: ac.signal })
assert.equal(cancelled.isError, true)
// A second instance gets its OWN registry entry too — addInitializer runs per
// instance, keyed off `this` in the WeakMap-backed REGISTRY, not off the class.
const w2 = new Weather()
for (const init of initializers) init.call(w2)
assert.equal(collectTools(w2).length, 1)
assert.equal(collectTools(w).length, 1, "w's own entry is unaffected by decorating w2")
console.log("ok:", tools[0].name, "->", res.output)
Option Type Required What it does
description string yes What the model reads to decide whether to call this tool.
opts.name string no Overrides the exposed tool name. Defaults to the decorator context’s name (the method/property key), then the function’s own .name.
opts.inputSchema JSONSchema no Defaults to an empty-object schema, same as defineTool.
  • defineTool — Wrap a plain function with a name, description and schema — the shortest path from code you have to a tool the LLM can call.
  • collectTools — Sweep a module or class and collect every function marked as a tool.