Skip to main content
Tools are the whole of the agent’s access to the world. It cannot read state, enumerate collections, or reach around them — if it should be able to do something, a tool exists for it, and if no tool exposes it, the agent cannot know it. That makes the tool surface a design decision, not a formality. It is the API of the world you are simulating.

Anatomy

tools/opportunities.ts

Descriptions are prompt engineering

The model chooses tools by reading descriptions. A description that only restates the name wastes the one signal you have:
Say what it is for, when to reach for it, and what must be true first. CRM’s get_current_date description explicitly warns that every staleness judgement is relative to the simulated date, not the real calendar — because a model that assumes otherwise gets the task wrong.

Validation happens before your code runs

Arguments are checked against inputSchema in bindTools. A call with a hallucinated property never reaches run:
The message names both the mistake and the real parameter, which is what gives the model a chance to self-correct. Before this existed, unknown arguments were silently dropped and tools answered questions nobody had asked.

Failing usefully

Throw toolError with a code the agent can act on. It is returned as a value, not raised — a rejected call is an observation, not the end of the rollout.
Any string is accepted, so a domain can add its own vocabulary — but prefer these when they fit. Business rules belong in the tool, not in the verifier. CRM refuses to assign work to a deactivated user inside assertAssignable, which is why “reassign the departed rep’s pipeline” is a real task rather than a formality.

Registration is explicit

The scaffold writes a stub and adds it to the barrel. The stub throws on purpose:
An unimplemented tool that silently returned nothing would look like a working tool that found nothing. A file appearing in tools/ does not become callable until its export is in tools/index.ts. That asymmetry with data/ is deliberate: a dataset landing on disk is inert, a tool landing on disk would widen what the agent can do.
tools/index.ts

How many tools?

More than feels necessary. The count follows from the rule that the agent sees nothing but tools: every collection it must enumerate needs a way to list, search and fetch, before a single mutation exists. CRM needs 42 across nine collections. ERP has 185. A surface small enough to feel tidy is usually one that hides state the agent needs.

Sharing a contract

Templates pin defineTool to their own state type once, so every tool file imports one thing:
tools/contract.ts
Alongside it live the schema builders (S.string, S.enumeration), input readers (readString, readOptionalNumber) and paging helpers every tool shares. Lookup and mutation helpers go in tools/helpers.ts, so a rule like “only an active user may act” is written once rather than re-implemented per tool.