Skip to content
WIP

This guide is a work in progress. Barry is also a work in progress.

Write a tool

Use this when your bag needs a tool an agent can call: something that reads from a service, changes something, or returns an image. You define each tool with Barry.Tool from @barry-rocks/sdk, export it from the bag’s tools.ts, and Barry hosts it for every session that a trait grants it to. The example is octo, a bag that reads and stars GitHub repositories with your token.

  • Create a bag with a package.json that depends on @barry-rocks/sdk and zod 3.25 or zod 4.
  • To try the octo example, have a GitHub token and add it to your identity’s secrets as GITHUB_TOKEN, the way Manage secrets describes.

This is the whole tools module of the octo bag. It defines three tools, one of each kind this guide covers:

import { Barry } from "@barry-rocks/sdk";
import { mcpResult } from "@barry-rocks/sdk/tools";
import { z } from "zod";
import { github } from "./src/github.js";
interface Account {
login: string;
name: string | null;
}
export const whoami = Barry.Tool({
namespace: "octo",
name: "whoami",
access: "read",
description: "Show the GitHub account the token belongs to.",
schema: {},
secrets: ["GITHUB_TOKEN"],
outputSchema: {
type: "object",
properties: { login: { type: "string" }, name: { type: ["string", "null"] } },
required: ["login", "name"],
},
handler: async (_params, context): Promise<Account> => {
const user = (await (await github(context?.secrets.GITHUB_TOKEN, "/user")).json()) as Account;
return { login: user.login, name: user.name };
},
cliFormat: (account) => `${account.login} (${account.name ?? "no display name"})`,
});
export const star = Barry.Tool({
namespace: "octo",
name: "star",
access: "write",
description: "Star a GitHub repository as the token's account.",
schema: {
owner: z.string().describe("The repository's owner"),
repo: z.string().describe("The repository's name"),
},
secrets: ["GITHUB_TOKEN"],
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
handler: async ({ owner, repo }, context) => {
await github(context?.secrets.GITHUB_TOKEN, `/user/starred/${owner}/${repo}`, { method: "PUT" });
return `Starred ${owner}/${repo}.`;
},
});
export const avatar = Barry.Tool({
namespace: "octo",
name: "avatar",
access: "read",
description: "Show a GitHub user's avatar as an image.",
schema: { login: z.string().describe("The GitHub login") },
deferred: true,
handler: async ({ login }) => {
const response = await fetch(`https://github.com/${encodeURIComponent(login)}.png?size=128`);
if (!response.ok) throw new Error(`No avatar for ${login}`);
const data = Buffer.from(await response.arrayBuffer()).toString("base64");
const mimeType = response.headers.get("content-type") ?? "image/png";
return mcpResult({ content: [{ type: "image", data, mimeType }] });
},
});

The helper the tools share lives under src/, which keeps tools.ts a list of tools:

/** Call the GitHub REST API with the token Barry hands the tool. */
export async function github(token: string | undefined, path: string, init: RequestInit = {}): Promise<Response> {
if (!token) throw new Error("GITHUB_TOKEN is not set");
const response = await fetch(`https://api.github.com${path}`, {
...init,
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
"x-github-api-version": "2022-11-28",
},
});
if (!response.ok) throw new Error(`GitHub answered ${response.status} for ${path}`);
return response;
}

Barry.Tool returns the definition you pass it, typed: the handler’s parameters come from schema, and cliFormat receives the handler’s result type. These are the fields:

Field What it does
namespace Required. The namespace a trait grants. Use the bag’s name
name Required. The tool’s name within its namespace
access Required. read or write. A readonly bound removes write tools
description Required. What the model reads when it decides to call the tool
schema Required. The parameters, as a zod raw shape: an object of zod schemas, not z.object(...)
handler Required. async (params, context) => result
secrets Secret names the tool reads from context.secrets
annotations MCP hints: title, readOnlyHint, destructiveHint, idempotentHint, and openWorldHint
deferred Hides the tool until the agent finds it with tool search
outputSchema A JSON Schema the result must match
cliFormat Turns the result into the text the terminal prints

Without annotations, Barry derives readOnlyHint from access. Annotations you write replace that default, so set readOnlyHint yourself when you set any of them.

The handler’s second argument is a ToolContext, which Barry fills in when it hosts the tool. It is optional in the type, so a handler that needs it checks for it:

  • secrets holds the values of the names in the tool’s secrets list, and nothing else. Barry resolves them from the identity, so the value never reaches the model’s context.
  • sessionId names the calling session. Prefer it to an environment variable, because one tool host serves many sessions.
  • availableTools lists the tools the session has, for describing them. It doesn’t authorize anything; the host enforces access.
  • bagNames lists the bags the session’s identity enabled. When it’s absent, Barry doesn’t know, which is different from an empty list.

What the handler returns decides what the agent receives:

The handler The agent receives
Returns a string That text
Returns a JSON value The value as pretty-printed JSON text
Returns undefined A successful, empty result
Returns mcpResult(response) The MCP response as written: images, resources, or an error
Throws An error result, isError: true, whose text is {"error": "<message>"}

An ordinary object is always data, even when it has a content field, so wrap a native MCP response in mcpResult, imported from @barry-rocks/sdk/tools. Barry doesn’t redact error messages, so keep secret values out of them.

Barry caps each text item of a result, and an error message, at 100,000 characters, or at the number in the tool host’s BARRY_TOOL_RESULT_CHAR_LIMIT environment variable. It keeps the start of the text and appends a note that says how much it cut and asks the agent to narrow the request. Images and structured content pass through whole.

When an error message names a credential and says it’s not set, such as GITHUB_TOKEN is not set, or says missing required secret, Barry appends guidance for the agent: the tool authenticates with an API key, the user adds the credential to the identity, and the next session picks it up. Throw that wording for a missing secret, as src/github.ts does, so the agent asks for the key instead of retrying.

With an outputSchema, the handler returns a plain object. The agent receives it as text and as structured content, and the SDK checks it against the schema. A result that doesn’t match becomes an error that says the handler already ran, so the agent checks the outcome before it repeats a write.

Barry identifies a tool by its qualified ID, namespace:name, such as octo:whoami. Two tools with the same qualified ID in one module stop the bag from loading, and an export that has a handler but lacks a valid name, namespace, access, description, or schema stops it too.

Barry loads tools.ts, or src/tools.ts when the bag has no root tools.ts, and registers every exported tool. An exported array of tools counts as well. A tools.ts that re-exports tools from src/ works the same way.

The manifest can override a tool’s visibility by name without changing its code. Each entry is enabled, deferred, or disabled:

name: octo
description: Reads and stars GitHub repositories with your GitHub token.
tools:
- star: deferred

A deferred tool stays callable but out of the session’s tool list until tool search finds it. A tool set to disabled doesn’t load at all. An override that names a tool the module doesn’t export fails the bag.

Install, pack, and grant the bag as Create a bag describes, then call a tool from the terminal:

Terminal
barry install ~/repos/octo
bonanza pack octo
bonanza identity traits add octo
bonanza tool octo whoami

The command prints your GitHub login and display name, formatted by cliFormat. Add --json to print the raw result instead. A tool’s only required string parameter becomes a positional argument, and every other parameter becomes an option. Pass --help after a tool’s name to list what it takes.

  • The result is GITHUB_TOKEN is not set. The identity has no secret by that name, or the tool doesn’t list it in secrets. Run bonanza identity check to see which secrets load.
  • The bag loads no tools. Run barry bag check --deep ~/repos/octo. An export with a malformed definition, or two tools with one qualified ID, stops the whole module.
  • A tool has no terminal command. A parameter that is an object, a union, a record, or a tuple has no command-line form, so the terminal skips that tool. Sessions still get it.
  • For anything else, see Troubleshooting.