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 }) => FunctionA 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}` }}When to use it
Section titled “When to use it”- 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);
toolgives you the equivalent without adopting a whole framework.
Why this and not the alternative
Section titled “Why this and not the alternative”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.
Examples
Section titled “Examples”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)Options
Section titled “Options”| 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. |
See also
Section titled “See also”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.