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.
Before you begin
Section titled “Before you begin”- Create a bag with a
package.jsonthat depends on@barry-rocks/sdkand zod 3.25 or zod 4. - To try the
octoexample, have a GitHub token and add it to your identity’s secrets asGITHUB_TOKEN, the way Manage secrets describes.
Define the tools
Section titled “Define the tools”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.
Read the context
Section titled “Read the context”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:
secretsholds the values of the names in the tool’ssecretslist, and nothing else. Barry resolves them from the identity, so the value never reaches the model’s context.sessionIdnames the calling session. Prefer it to an environment variable, because one tool host serves many sessions.availableToolslists the tools the session has, for describing them. It doesn’t authorize anything; the host enforces access.bagNameslists the bags the session’s identity enabled. When it’s absent, Barry doesn’t know, which is different from an empty list.
Return a result
Section titled “Return a result”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.
Name and export the tools
Section titled “Name and export the tools”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.
Change a tool’s visibility in bag.yaml
Section titled “Change a tool’s visibility in bag.yaml”The manifest can override a tool’s visibility by name without changing its
code. Each entry is enabled, deferred, or disabled:
name: octodescription: Reads and stars GitHub repositories with your GitHub token.
tools: - star: deferredA 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.
Verify
Section titled “Verify”Install, pack, and grant the bag as Create a bag describes, then call a tool from the terminal:
barry install ~/repos/octobonanza pack octobonanza identity traits add octobonanza tool octo whoamiThe 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.
If it fails
Section titled “If it fails”- The result is
GITHUB_TOKEN is not set. The identity has no secret by that name, or the tool doesn’t list it insecrets. Runbonanza identity checkto 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.
What’s next
Section titled “What’s next”- Test a bag to call these handlers from vitest.
- Call Barry from a bag when a tool needs sessions, events, or schedules.
- Declare traits and bounds to grant the tools in narrower ways.
- The
/toolsSDK reference for every export.