The tool system
Tools are how the engine's capabilities become named, discoverable workflow operations. A tool composes a multi-step job — resolve an asset, spawn an entity, add a Model, add a collider, position it…
What a tool is for
A tool earns its place by doing one of three things:
- Makes a one-off easy — one call that would otherwise be several (resolve an asset, spawn an entity, add a Model, add a collider, position it → one
spawnModel(...)). - Packs a whole workflow into one call — "convert every material in this scene to the ps1 shader", "bake every probe in the scene", "run this input sequence". Brute force is fine: tools run on the authoring path, not the gameplay hot path.
- Hands the agent context an API wouldn't — spawn a model and get back its position, bounds, and neighbouring entities in one shot.
Its inputs must be settable by an agent blind to how the engine works underneath — words, intents, nil meaning "all", proxies already in hand. If using a tool requires understanding the system beneath it, it's the wrong tool.
A tool composes over a capability — the underlying engine API the tool body itself calls: the entity / component / asset globals, the modules.api.engine.* modules, and the rest of the engine surface. Gameplay logic lives in those capabilities (or a module built on them) and is called directly; a tool calls the same capability and wraps it in a schema an agent can discover and invoke. That split is what lets a tool stay freely renamable — gameplay code reaches for the capability, the agent reaches for the tool.
Two surfaces — pick the one that matches where you are
A tool's identity is <toolbox>.<name> — a tool inside a toolbox. There are exactly two ways to invoke it, one per context:
use_tool — the MCP tool call, when you're driving the engine from outside. The executing sibling of search_tools: search_tools finds the tool, use_tool runs it, with no execute() snippet to hand-write. Pass the toolbox, the tool, and POSITIONAL args in signature order. It returns the tool's ZmToolResult envelope ({ ok, value | error, durationMs, tool }):
search_tools { query = "spawn asset" } -- find it: returns toolbox "entityOps", tool "fromAsset", its signature
use_tool { toolbox = "entityOps", tool = "fromAsset",
args = ["cube", { name = "ragdoll", position = [0, 0, 0] }] }
-- → { ok = true, value = { id = "<entity id>", ... }, durationMs = 1.8, tool = "entityOps.fromAsset" }
This is the front door for agent-driven authoring: search_tools → use_tool.
tools.use(toolbox, tool, ...) — from Luau, when you're authoring in code. Inside an execute() call or an editor/authoring script, this runs a tool and unwraps the envelope: the raw value on success, a raised error on failure — so you write against the return value directly, no .ok / .value threading:
local r = tools.use("entityOps", "fromAsset", "cube", { name = "ragdoll" })
-- r.id is the entity id; a failing tool raises
Because toolbox and tool are asset identities, a tools.use(...) call written with literal names is statically resolved — the LSP checks the toolbox and tool exist, and the call is captured as a dependency edge so content that ships with a tools.use call carries its tool deps with it. Library toolboxes resolve by root: tools.use("@somelib::toolbox", "name", ...).
Where a tool may run
Tools write the VFS, create persistent assets, make HTTP calls, and queue heavy jobs — authoring work, not per-frame work. That is the line: a tool belongs in an authoring context (an execute() call, an editor tool or panel), never in runtime code. Runtime code — a component, a scene entrypoint, anything that ticks each frame or runs on load — reaches for the underlying capability, not a tool. The engine enforces this statically:
- A component reaches for a capability, not a tool. Calling a tool from a component is an error. A tool can opt in with
declare { componentSafe = true }at the top of itsinit.luau— but that switch is an explicit "I acknowledge I'm using this against its intent": it's heavily frowned upon, there only for the rare case where you genuinely can't avoid duplicating the logic. The right move is almost always to extract the operation into a module the component calls directly. - A tool reaches for a module, not another tool. Using
tools.useinside a.tool/.toolboxsource is an error; shared logic goes in arequire'd module.
So when a guide shows a component spawning an entity, driving a light, or reading input, it uses the capability (entity.spawn, modules.api.engine.lighting, zinput) — the tool form is only for the interactive authoring you do from execute().
Toolboxes — the namespace
Tools live inside a toolbox, which gives each tool its <toolbox>.<name> identity. A toolbox can hold a shared.module for helpers more than one of its tools reuse, reached with require(".shared") from inside the toolbox. Browse the built-in toolboxes with asset.list("toolbox"), or search_tools with no arguments for a grouped overview of every toolbox and what each is for.
The result contract is system-enforced
A tool body returns a raw value (or calls error(msg) to fail). The asset type wraps every call into the { ok, value | error, durationMs, tool } envelope and times it — the contract lives in the type, not in each tool, so every tool behaves the same way: returning a value is success, error(...) is failure, and the envelope is added for you. use_tool hands you that envelope; tools.use unwraps it.
Anatomy, and creating one
A tool is an ordinary asset folder — <toolbox>.toolbox/<name>.tool/ with an init.luau. The schema lives entirely in that Luau: the typed function signature is the tool's signature, and the --!desc / --!arg / --!return / --!example annotations above it are its per-function docs; tags live in the sibling .metadata. The function whose name matches the .tool folder stem is the canonical entry. Edit the file like any source and it hot-reloads.
To make tools programmatically, tools.createToolbox and tools.create write the folder for you (lsp.describe gives their exact signatures). Whatever the route, the live surface is the reference:
tools.list() -- toolboxes and their tools
tools.get("entityOps.spawn") -- a tool's full schema
Reading a built-in tool's init.luau is the best way to see how one is built, and asset.inspect("<toolbox>.<name>") shows any tool's schema and docs. The asset system behind tools (identities, hot-reload, sharing) is in the assets guide.