Skip to content

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.

  • Right after applying tool(...) to a class instance or a plain object — this is the call that produces the Tool[].
  • Building a tool surface from a module namespace (import * as api from "./service.js"): every exported function that was decorated is one collectTools(api) call away from a Tool.
  • Assembling extraTools for createToolkit out of several decorated services at once — collectTools is per-object, so you call it once per service and concatenate.

collectTools reads from two independent sources and concatenates them — understanding both is the whole API surface here:

  1. Per-instance registry. A decorated class field/method with a real decorator context (one that calls addInitializer) registers itself into a WeakMap keyed on the constructed instance. collectTools(instance) reads that instance’s entries.
  2. Own-property duck-typing. Any function that is an own, enumerable property of obj and carries a .__tool (set by tool(...) on every function it touches, decorator context or not) is picked up by scanning Object.keys(obj).

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(", "))
  • 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.