MINARA

Adding a Tool

How to add a new tool to the registry

Tools are typed TypeScript functions with JSON-Schema parameters. They live at apps/agent/src/tools/<name>.ts and export a factory that returns a ToolEntry[].

Minimal example

Create apps/agent/src/tools/my-provider.ts:

import { PermissionTier, type ToolEntry } from "../core/tool-registry.js";
import { ok, err, errFromThrow } from "./_shared/result.js";

export function createMyProviderTools(): ToolEntry[] {
  const apiKey = process.env.MY_PROVIDER_API_KEY;
  if (!apiKey) return []; // feature-gate: registry hides missing tools

  return [
    {
      name: "my_provider_search",
      toolSet: "research",
      permissionTier: PermissionTier.READ_ONLY,
      isAsync: true,
      description: "Search My Provider for a query",
      schema: {
        name: "my_provider_search",
        description: "Search My Provider for a query",
        parameters: {
          type: "object",
          properties: {
            query: { type: "string", description: "The search query" },
            limit: { type: "number", description: "Max results", default: 10 },
          },
          required: ["query"],
        },
      },
      handler: async ({ query, limit }) => {
        try {
          const res = await fetch(
            `https://api.myprovider.com/search?q=${encodeURIComponent(query)}&limit=${limit ?? 10}`,
            { headers: { Authorization: `Bearer ${apiKey}` } },
          );
          if (!res.ok) return err(`HTTP ${res.status}`);
          return ok(await res.json());
        } catch (e) {
          return errFromThrow(e);
        }
      },
    },
  ];
}

Register the factory beside the closest capability. Tools owned by a wiring domain belong in its builder under apps/agent/src/app/, such as messaging.ts, institution.ts, or artifacts.ts. A capability without a domain builder stays in apps/agent/src/app.ts next to a related registration.

import { createMyProviderTools } from "./tools/my-provider.js";
// ...
for (const tool of createMyProviderTools()) toolRegistry.register(tool);

Add a tool-set entry in apps/agent/src/core/tool-registry.ts inside BUILTIN_TOOL_SETS:

research: {
  description: "Market and social research tools",
  tools: [
    // ... existing
    "my_provider_search",
  ],
},

If an Agent should discover the tool through a Skill, add its exact name to that Skill's metadata.minara.tool_names. Keep the description specific because deferred tools are found through tool_search.

Permission tiers

Permission tiers are defined in apps/agent/src/core/tool-registry.ts:

TierNameWhen to use
1READ_ONLYprice / balance / search / read_file
2CONFIRM_ONCEanalysis, research, small swaps
3ALWAYS_CONFIRMwrite_file, patch, fund-moving, document generation
4MANUAL_ONLYwithdraws, external-address sends, emergency stop

When in doubt, match the closest existing tool's tier. Fund-moving tools also declare isFundMoving: true and controlPolicy.confirm. The unified tier gate presents the preview and confirms the exact request before the handler runs.

Result envelope

Every handler returns a string via ok({...}) / err("...") from apps/agent/src/tools/_shared/result.ts. Never throw from handlers. Use errFromThrow(e) to convert caught errors. The agent loop parses the envelope and presents structured errors to the LLM.

Sandbox-rooted file tools

If your tool touches the filesystem, resolve every path through resolveInSandbox() from apps/agent/src/tools/_security/sandbox.ts. This is non-negotiable. See Sandbox & Permissions.

Verify and document it

Add a focused test under apps/agent/tests/unit/tools/. Cover the success envelope, provider errors, missing credentials, and the declared safety policy.

pnpm --filter @minara/agent exec vitest run tests/unit/tools/my-provider.test.ts
pnpm --filter @minara/agent typecheck
pnpm --filter @minara/docs generate

If the tool changes a public workflow, update its hand-written documentation in all four languages. Generated tool-reference pages come from BUILTIN_TOOL_SETS; do not edit them directly.

On this page