The core API
createAgentToolsis a thin convenience over three exported building blocks:resolveConfig,dispatch, andlistTools. Use them directly when you want to own the transport — an HTTP endpoint, a CLI, or a test harness.
What the factory is
createAgentTools is essentially this:
import { resolveConfig, dispatch, listTools } from "@clarvis/agent-tools";
function createAgentTools(options) {
const config = resolveConfig(options); // validate + freeze the config once
return {
config,
listTools: () => listTools(config),
callTool: (name, args = {}) => dispatch(name, args, config),
};
}So working with the core is the same three moves, unwrapped: build a ServerConfig once, then call listTools(config) and dispatch(name, args, config) as needed.
Build a config
resolveConfig(options) takes the same options as the factory and returns a fully-resolved, validated ServerConfig (it probes for ripgrep and verifies the workspace exists). Reuse it across every call — it is immutable.
import { resolveConfig, dispatch, listTools } from "@clarvis/agent-tools";
const config = resolveConfig({ workspaceRoot: process.cwd() });
const surface = listTools(config); // ToolInfo[]
const { isError, text } = await dispatch("grep", { pattern: "TODO" }, config);For a process that reads its config from argv and environment instead of an options object, use buildConfig(argv, env) — it parses --workspace / WORKSPACE_ROOT, --read-only / READ_ONLY, --allow-outside-workspace / ALLOW_OUTSIDE_WORKSPACE, and the MAX_* / BASH_TIMEOUT_* env vars, then delegates to resolveConfig. See Configuration → argv & environment.
import { buildConfig } from "@clarvis/agent-tools";
const config = buildConfig(process.argv.slice(2), process.env);A minimal transport
Everything a transport needs is listTools (to advertise) and dispatch (to execute). Here is a tiny JSON-over-HTTP adapter:
import { createServer } from "node:http";
import { buildConfig, dispatch, listTools } from "@clarvis/agent-tools";
const config = buildConfig(process.argv.slice(2), process.env);
createServer(async (req, res) => {
if (req.url === "/tools") {
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(listTools(config)));
return;
}
if (req.url === "/call" && req.method === "POST") {
const body = JSON.parse(await readBody(req)) as { name: string; args?: Record<string, unknown> };
const result = await dispatch(body.name, body.args ?? {}, config);
res.statusCode = result.isError ? 400 : 200;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(result));
return;
}
res.statusCode = 404;
res.end();
}).listen(8080);dispatch does the validation (ajv, against each tool's JSON Schema), output bounding, and error serialization for you — the transport just moves { isError, text } across the wire.
The raw registry
If you need the tools themselves — to filter the surface, look one up, or introspect a schema — the registry is exported too:
tools— the full array ofToolDef(all nine tools).readOnlyTools— the non-mutating subset (read_file,list_dir,glob,grep).selectSurface(readOnly)— returnsreadOnlyToolswhentrue, elsetools. This is whatlistTools/dispatchuse to honorconfig.readOnly.getTool(name, surface)— look up one tool by name within a surface, orundefined.
import { tools, selectSurface, getTool } from "@clarvis/agent-tools";
const surface = selectSurface(config.readOnly);
const grep = getTool("grep", surface);
console.log(grep?.inputSchema);See Core API reference for the exact signatures and the ToolDef shape.
See also
- Core API reference — signatures for
dispatch,listTools, the registry, and error helpers - Configuration —
resolveConfig/buildConfigoptions, env, and argv - How it works — what
dispatchdoes step by step