collectTools
JavaScript · package toolnexus · SPEC §6 · js/src/native.ts
function collectTools(obj: object): Tool[]The other half of tool: sweep an object — a plain object of
functions, a class instance, a module namespace — and return every Tool that was marked on it.
Decorating marks; collectTools is what actually turns those marks into the Tool[] you hand to
createToolkit or a provider adapter.
When to use it
Section titled “When to use it”- Right after applying
tool(...)to a class instance or a plain object — this is the call that produces theTool[]. - Building a tool surface from a module namespace (
import * as api from "./service.js"): every exported function that was decorated is onecollectTools(api)call away from aTool. - Assembling
extraToolsforcreateToolkitout of several decorated services at once —collectToolsis per-object, so you call it once per service and concatenate.
Why this and not the alternative
Section titled “Why this and not the alternative”collectTools reads from two independent sources and concatenates them — understanding both is
the whole API surface here:
- Per-instance registry. A decorated class field/method with a real decorator context
(one that calls
addInitializer) registers itself into aWeakMapkeyed on the constructed instance.collectTools(instance)reads that instance’s entries. - Own-property duck-typing. Any function that is an own, enumerable property of
objand carries a.__tool(set bytool(...)on every function it touches, decorator context or not) is picked up by scanningObject.keys(obj).
Examples
Section titled “Examples”1. The smallest useful call — a plain object of decorated functions
Section titled “1. The smallest useful call — a plain object of decorated functions”The duck-typing path: no class, no addInitializer — just functions marked with tool(...) and
assigned as an object’s own properties.
import assert from "node:assert"import { tool, collectTools } from "toolnexus"
function ping() { return "pong"}tool("Health check")(ping, { name: "ping" })
// A key with no .__tool (an ordinary function, or a non-function value) is silently skipped.const api = { ping, version: "1.0.0", helper: () => "not a tool" }
const tools = collectTools(api)assert.equal(tools.length, 1)assert.equal(tools[0].name, "ping")assert.equal((await tools[0].execute({})).output, "pong")
console.log("ok:", tools.map((t) => t.name).join(", "))2. A module’s worth of decorated exports
Section titled “2. A module’s worth of decorated exports”The realistic shape: several functions in a “module” (here, an object standing in for one),
decorated once, swept in one call — the pattern for turning an existing service into tools without
writing a defineTool({...}) per function.
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 }) { return `cancelled ${args.id}` }, // Not every export needs to be a tool — undecorated functions are just ignored. _internalHelper() { return "not exposed" },}
tool("Fetch an order by id")(orders.lookupOrder, { name: "lookupOrder" })tool("Cancel an order")(orders.cancelOrder, { name: "cancelOrder" })
const tools = collectTools(orders)assert.deepEqual(tools.map((t) => t.name).sort(), ["cancelOrder", "lookupOrder"])assert.ok(!tools.some((t) => t.name === "_internalHelper"))
const res = await tools.find((t) => t.name === "lookupOrder")!.execute({ id: "A-1" })assert.deepEqual(JSON.parse(res.output), { id: "A-1", status: "shipped" })
console.log("ok:", tools.map((t) => t.name).join(", "))3. The full surface — both sources on one object, and independence across instances
Section titled “3. The full surface — both sources on one object, and independence across instances”A class instance can carry tools from both sources at once: a real, addInitializer-backed
method decoration (registry) alongside an own-property function marked with tool(...) directly
(duck-typing) — collectTools returns both, and each class instance keeps its own registry
entries.
import assert from "node:assert"import { tool, collectTools } from "toolnexus"
class Service { // An own property, decorated directly — found via duck-typing. ping = tool("Health check")(function ping() { return "pong" }, { name: "ping" })}
// A real prototype method, decorated with a context that supplies addInitializer —// found via the per-instance registry, same mechanism a genuine `@tool(...)` uses.const initializers: Array<(instance: object) => void> = []Service.prototype.status = tool("Report status")(function status() { return "ok"}, { name: "status", addInitializer(fn: () => void) { initializers.push(fn as unknown as (instance: object) => void) },})
const a = new Service()const b = new Service()for (const init of initializers) { init.call(a) init.call(b)}
const toolsA = collectTools(a)assert.deepEqual(toolsA.map((t) => t.name).sort(), ["ping", "status"], "both sources contribute")
// Sweeping an object with neither source present ⇒ an empty list, not an error.assert.deepEqual(collectTools({}), [])assert.deepEqual(collectTools(new (class Empty {})()), [])
console.log("ok:", toolsA.map((t) => t.name).join(", "))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.tool— Derive the schema from the function signature or annotation instead of writing it by hand.