Giving Claude Hands: Building Tools an Agent Can Use
An agent is only as capable as the tools you hand it. Everything clever the model does, it does through those tools, which is why, in my experience, tools end up being where a lot of the real engineering (and a lot of the real risk) actually lives.
In Anatomy of an Agent Harness I made the case that the model is just one input into a running system, and that the harness around it is where most of the engineering discipline goes. One claim from that post is worth its own treatment, because it is where I have found most of the actual work lives: an agent is only as capable as its tools, and tool design is largely prompt design.
That second half is not really a metaphor. Every tool’s name, description, and schema gets stamped into the model’s context on every single request. The tool menu is part of the prompt. So when you build a tool, you are not just writing a function off to the side; you are writing prompt that the model reads every turn and reasons against. In my experience, get the tools right and a competent model does real work. Get them wrong and even a very strong model tends to flounder.
At Hero Plus, where I work on payment infrastructure across Southeast Asia, “real work” means payment operations: looking up a stuck transaction, checking whether a settlement batch cleared, tracing a ledger entry, summarizing a chargeback. This post is about how I have been thinking about exposing that kind of tooling to Claude safely. This space is young and moving fast, so treat it as one practitioner’s current take rather than settled doctrine.
What MCP is, and why it matters
The Model Context Protocol (MCP) is a standard interface between agents and tools. It is now stewarded under the Linux Foundation and adopted fairly broadly across the ecosystem, which matters because a standard is only useful when everyone speaks it. The core idea is simple: MCP draws a clean line between tool implementation and agent logic. Your agent does not need to know how a tool is built or where it runs. It speaks one protocol; any compliant tool answers.
That separation is what, in my experience, turns tools into a first-class, swappable layer of the harness instead of bespoke glue welded into the loop. You can build a payment-ops MCP server once and let any compliant agent (Claude in the terminal, a headless automation, a teammate’s setup) use it without rewiring anything. The tool becomes the contract, not the integration.
flowchart TB
AGENT[Agent, reasoning loop] -->|speaks MCP| SERVER
subgraph SERVER[Payment Ops MCP Server]
AUTHZ[Authorization, least privilege]
ALLOW[Write Allowlist]
AUDIT[Audit Log, every call]
TOOLS[Tool Handlers]
end
AUTHZ --> TOOLS
ALLOW --> TOOLS
TOOLS --> AUDIT
TOOLS -->|read only by default| LEDGER[(Ledger DB)]
TOOLS -->|read only by default| SETTLE[(Settlement Service)]
TOOLS -->|read only by default| PSP[(PSP / Acquirer APIs)]
The thing I would point to in that diagram: authorization and audit live in the server, between the agent and the internal systems. The model never talks to the ledger directly. It asks the server, and the server decides.
A worked example: payment-ops tools for Claude
Here is the starter set I tend to reach for. Notice that every one of them is read-heavy and safe by default.
lookup_transaction: fetch the status and key fields of a single transaction by id.get_settlement_status: check whether a settlement batch has cleared, and when.search_ledger_entries: find ledger entries matching a tight filter (account, date range, amount).summarize_chargeback: pull a chargeback case and return a compact summary with the relevant evidence pointers.
None of these move money. None of them mutate state. The worst a confused agent can do with this set is read something it did not need to read, which is the property I want in place before letting an agent near a payments backend.
Here is what one tool definition looks like in TypeScript, with a Zod schema so invalid calls are unrepresentable:
import { z } from "zod";
export const lookupTransaction = {
name: "lookup_transaction",
description:
"Fetch status and key fields for ONE transaction by id. " +
"Use when you have a specific transaction id. " +
"Do NOT use to browse or search; use search_ledger_entries for that.",
inputSchema: z.object({
transactionId: z.string().regex(/^txn_[a-zA-Z0-9]{16,}$/),
fields: z
.array(z.enum(["status", "amount", "currency", "createdAt", "psp"]))
.min(1)
.default(["status", "amount", "currency"]),
}),
// handler enforces authz, then returns a COMPACT object, not a raw row dump
};
The schema does real work here. The id has to match the transaction id shape, the field selector is a closed enum, and there is a sensible default. A malformed call fails at the boundary, before any handler logic runs, before any tokens are spent reasoning about an error.
Tool design principles, concretely
A few habits I have landed on, learned mostly by getting them wrong first. Your mileage may vary, but the short version fits in a table:
| Dimension | Good tool | Bad tool |
|---|---|---|
| Scope | Does one verb well | Fetches, decides, and submits in one call |
| Schema | Closed enums, regex-constrained ids, required filters | Free-form strings that can scan a whole table |
| Return shape | Compact summary, a count, a few representative rows | Raw 4,000-row dump straight into context |
| Side effects | Read-only and idempotent by default | Mutates state on a call the model can retry blindly |
| Naming | get_settlement_status, obvious to a new engineer | proc_settle_v2, a riddle with no tribal knowledge |
The longer version, principle by principle:
A handful of focused tools tends to beat a sprawl of overlapping ones. With a small, clean set the model can hold the whole menu in its head and, in my experience, pick correctly more often. Add find_payment, query_transactions, and get_txn alongside lookup_transaction and you tend to get tool confusion: the model coin-flips between near-synonyms. The menu is prompt, so a bloated menu is a bloated prompt.
I try to name tools the way I would name them for a new engineer. get_settlement_status is obvious. proc_settle_v2 is a riddle. The model is reading these names cold on every request, with no tribal knowledge.
I have found it helps when descriptions say when NOT to use a tool. Half the value of a good description, for me, is negative space. The lookup_transaction description above tells the model to reach for search_ledger_entries instead of brute-forcing ids. That single sentence has prevented a whole class of wrong calls in my own usage.
Tight schemas make invalid calls unrepresentable. Enums over free strings, regex-constrained ids, required filters on anything that could otherwise scan a whole table. Every constraint you push into the schema is a mistake the model cannot make.
Return compact, structured, summarized results. This is the one I see skipped most often, and one I skipped myself early on. A tool that dumps a 4,000-row ledger query into the transcript is poisoning its own context. I covered context rot at length in the harness post; raw tool dumps are, in my experience, one of its biggest causes. Return the summary, the count, and a handful of representative rows, with a way to drill in if needed. The agent perceives the world through its context window, so I try not to fog it up.
I make tools idempotent and side-effect-free wherever I can. A read you can run twice with no consequence is a read the agent can retry safely after a timeout. Idempotence turns flaky calls into non-events.
The safety model for tools that touch a real system
Read-only tools are the easy case. The moment a tool can change state, I tighten the rules hard. This is also where the headless pattern from my workflow post raises the stakes: when no human is watching each step, the guardrails have to live entirely in the tooling.
flowchart TB
REQ[Agent requests a write action] --> AZ{Authorized for this action?}
AZ -->|No| DENY[Reject, log attempt]
AZ -->|Yes| AL{On the write allowlist?}
AL -->|No| DENY
AL -->|Yes| IRR{Irreversible, moves money?}
IRR -->|Yes| HUMAN[Require human approval]
IRR -->|No| IDEM[Apply with idempotency key]
HUMAN --> IDEM
IDEM --> RATE[Rate limit check]
RATE --> AUDIT[Write audit record]
AUDIT --> DONE[Execute]
The principles I lean on behind that flow:
Least-privilege scoping, read-only by default. I give a tool the narrowest access that lets it do its job, and nothing it does not need is reachable. Most payment-ops tools never need write access at all.
An explicit allowlist of write actions. Writes are not implicit. I keep a named, reviewed list of state-changing actions, and anything not on it is denied. Adding to that list is a deliberate decision, not a side effect of shipping a new tool.
I treat authorization as the server’s job, never the model’s. The model can ask for anything. The server decides what actually happens, checking the caller’s real permissions against the requested action. I assume the model can be confused or adversarially steered, and I make that assumption harmless by putting the gate where the model cannot reach it.
Authorization is the server’s job, never the model’s. That is the rule I hold to: the model can ask for anything; the server is what decides.
Idempotency keys for any state change. Every write carries a key so a retry applies once, not twice. In payments, a double-applied write is a duplicate charge or a duplicate refund, which is the kind of bug that ends up in a regulator’s inbox.
Rate limits and audit logging of every tool call. Rate limits bound the blast radius of a runaway loop. The audit log records who called what, with which arguments, and what came back, so every action an agent took is reconstructable after the fact. For a payments shop I do not treat this as optional.
Anything irreversible gets a human approval step. I do not make money movement an autonomous tool. The agent can prepare a refund, summarize the case, and stage the action, but a human approves the actual movement. Automate the analysis, gate the irreversible step.
How badly-built tools fail
A short, honest list of the ways I have watched tools sabotage an otherwise capable agent, often ones I built myself:
Chatty tools that flood context. A tool that returns everything drowns the working context in noise, and quality drops turn over turn even though nothing errored.
Overlapping tools that confuse the model. Three tools that almost do the same thing force the model to guess, and in my experience it guesses wrong often enough to matter.
Tools that leak PII or secrets into the transcript. A ledger tool that returns full card numbers or customer details writes that data into the context window and anywhere the transcript is later stored. I redact at the tool boundary, not after.
Tools that do too much in one call. A single process_chargeback that fetches, decides, and submits hides three decisions the agent (and the human reviewing it) should see separately. I try to split the verbs.
The throughline, at least the one I keep coming back to, is the same as in the harness post: the model was rarely the constraint. The tools were. I try to build them like they are part of the prompt, because they are, and scope them like they can touch money, because at Hero Plus they can. When I get both right the agent does real work safely. When I skip either, I tend to find out the hard way.