Skip to main content

Build & Push a Custom Tool

Built-in tools (Read, Glob, WebFetch, …) cover a lot, but eventually your agent needs to do something specific — hit your internal API, transform data a particular way, look something up. That's a custom tool: you write it once, push it to Waxell, and any of the tenant's agents can reference it by name.

This is the companion to Ship a Claude Agent — same wax CLI, one extra artifact.

1. Write the tool

A tool is a folder with a Python file and an optional TOOL.json manifest:

weather/
├── TOOL.json # name + metadata (optional)
└── tool.py # the tool source — this filename matters
The entry file must be tool.py

The loader looks for tool.py, falling back to any .py whose filename contains tool. A file named weather.py is not found — the push succeeds, the tool never registers, and the agent simply runs without it. Name it tool.py.

tool.py — a @tool-decorated function. Type the params; the docstring becomes the description the model sees:

from waxell_runtime import tool


@tool
def get_weather(ctx, city: str, units: str = "celsius") -> dict:
"""Get the current weather for a city. Units: celsius or fahrenheit."""
# Pure-Python logic + ctx.* helpers (domains, secrets, the LLM router).
# Do real I/O through a domain — see the v1 limits below.
return {"city": city, "temp": 21, "units": units, "conditions": "clear"}

TOOL.json (optional — --name and the folder name are fallbacks):

{
"name": "get_weather",
"display_name": "Get Weather",
"description": "Current weather for a city."
}

2. Declare it in your manifest and push

Declare the tool alongside your agent in waxell.yaml and push them together:

version: 1

tools:
- name: get_weather
version: "1.0.0"
description: Current weather for a city.
source: ./weather # the folder containing tool.py

agents:
- name: trip_planner
version: "1.1.0" # bump on every push — versions are immutable
framework: claude_agent_sdk
tools: [Read, WebFetch, get_weather] # built-ins + your custom tool
wax push --dry-run       # validate first
wax push --include-source

--include-source is required — without it the tool is registered without its code and cannot execute.

Use the manifest, not wax push-tool, for adapter agents

wax push-tool writes to a different store than the one the model-facing tool bridge reads. A tool pushed that way is invisible to claude_agent_sdk, pydantic_ai, langgraph, crewai and autogen agents — it registers cleanly and the model never sees it. Declaring the tool in waxell.yaml and pushing the manifest is the path that works for every framework.

Confirm it registered:

wax registry list tool                  # `kind` is positional, not --kind
wax agents info trip_planner # the tool should appear under bindings

What the platform checks on push

  • Obvious dangerous calls are rejectedos.system(, subprocess.Popen, subprocess.run, subprocess.call, eval(, exec(, __import__. This is a substring scan, not a sandbox: treat it as a guardrail against accidents, not a security boundary. Tool source runs in the worker process, so only push code you trust.
  • No third-party dependencies in v1 — keep it stdlib + ctx. For external APIs, call them through a domain (await ctx.domain(...)), which is the governed, per-user-auth boundary. See Domains.

3. How it runs

When the agent runs, the platform surfaces your tool to the model — for claude_agent_sdk agents under the MCP bridge — and executes it host-side, so secrets and domains resolve under governance rather than in the model.

Versioning & updates

Bump the tool's version: in waxell.yaml and push again:

tools:
- name: get_weather
version: "1.0.1" # bumped
source: ./weather
wax push --include-source

Each push is a new immutable version — re-pushing the same version is rejected with 409 semver_conflict. Bump the agent's version too when you change which tools it binds.

Gotchas

  • Bump the agent version: when you re-push the agent after adding a tool.
  • v1 = pure Python, no deps. Need a library or a network call? Put that behind a domain and call it with ctx.domain(...) — that's also where per-end-user auth lives.
  • Don't hardcode the tool name in the system prompt — the platform may expose it as mcp__…__get_weather; tell the agent to "use the weather tool."

Next