Developers
Adding tools
Define a schema-validated tool, register it, and it becomes available to the UI, CLI, agents, and plugins at once.
A tool is a typed unit of behavior callable by the renderer, CLI, agents, plugins, and internal code through the same ToolRegistry. Built-in tools live in packages/desktop/src/main/tools/.
Every tool should be small, schema-validated, honest about its capabilities, and implemented through a service rather than reaching across app boundaries.
1. Pick the owning service
Tools should expose behavior from a main-process service. Prefer adding logic to the service that owns the domain (for example BrowserTabService for browser tools, WorkspaceFileService for file tools), then add a thin tool wrapper. If the feature needs a new durable domain, add a service and wire it in bootstrap.ts first.
2. Define the tool
Use defineTool from @meith/protocol and a Zod input schema. Tool names are snake_case.
import { defineTool } from "@meith/protocol"
import { ToolError, okResult } from "@meith/shared"
import { z } from "zod"
import type { ToolDeps } from "./deps.js"
export function createExampleTools(deps: ToolDeps) {
return [
defineTool({
name: "example_echo",
description: "Return the supplied message.",
capabilities: ["read-only"],
inputSchema: z.object({
message: z.string().min(1).describe("Message to echo."),
}),
execute: async (ctx, input) => {
if (ctx.signal?.aborted) {
throw new ToolError("CANCELLED", "Call was cancelled.")
}
ctx.emit?.({ kind: "progress", message: "echoing", fraction: 1 })
return okResult({ message: input.message })
},
}),
]
}3. Register the tool
Register the tool factory in packages/desktop/src/main/bootstrap.ts:
import { createExampleTools } from "./tools/exampleTools.js"
// after deps are available
registry.registerAll(createExampleTools(deps))Once registered, the tool is available through:
- renderer IPC via
bridge.tools.call("example_echo", args), - CLI via
meith call example_echo --message hello, - agents through the generated live tool catalog,
- plugins through
window.meithPlugin.tools.call()if granted the required capabilities, - the debug tool runner.
4. Declare capabilities correctly
Capabilities drive permission decisions and plugin grants. Be conservative and declare every meaningful effect — writes-files, controls-browser, starts-process, accesses-network, and destructive. Renderer and internal callers are trusted in-process but still audited; CLI, agent, and plugin callers need explicit grants for privileged capabilities.
5. Handle errors and cancellation
- Reject invalid input with the Zod schema where possible.
- Throw
ToolError("VALIDATION_ERROR", message)for domain validation, andToolError("PERMISSION_DENIED", ...)for ownership failures. - Long-running tools should observe
ctx.signaland usectx.emitforprogress,log,partial_text, andartifactevents.
6. Add a friendly CLI command when useful
Any tool is already reachable with meith call. For common workflows, add a command mapping in packages/cli/src/commands.ts:
export const commands = {
"example-echo": {
tool: "example_echo",
positionals: ["message"],
summary: "Echo <message>",
},
}Checklist
- The service owns the actual behavior.
- The tool has a Zod input schema with useful descriptions.
- The tool name is
snake_caseand capabilities cover every side effect. - Errors map to the right
ToolErrorCode; long work observesctx.signaland streams withctx.emit. - The tool is registered in
bootstrap.ts, and tests cover the service and registry behavior.
Verify your change
pnpm --filter @meith/desktop test and pnpm typecheck at a minimum. For broad or shared changes, run pnpm check.