Tools, MCP, and Action Surfaces
Ask most engineering teams what their agent's "tools" are and you'll get a shrug and a link to an API reference. That answer treats tool design as plumbing: wire the agent up to whatever endpoints already exist, point the model at the OpenAPI spec, ship it. It is the single most common way a harness engineering effort goes sideways after the first working demo, because the set of tools an agent can call is not an integration detail — it is the design artifact that defines what the agent actually is. A claims agent with a get_policy tool and a flag_for_review tool is a fundamentally different system from a claims agent with those two tools plus issue_payment, not because the model got smarter, but because the action surface changed.
Call this the action surface: the complete set of things an agent can do to the world, as opposed to the set of things it can say about the world. Everything upstream of the action surface — context, memory, prompting — shapes what the model decides. The action surface is the only place that decision becomes consequence. Get context wrong and the agent reasons badly. Get the action surface wrong and a badly reasoning agent does real damage, at machine speed, possibly before anyone notices.
Granularity is a design decision, not an afterthought
The first question in tool design is: how big should each tool be? There is no universal answer, but there is a reliable failure pattern on each side of the mistake.
Tools that are too fine-grained force the model to orchestrate minutiae it has no business managing. Imagine a utility field-service agent given get_crew_location, get_crew_skill_set, get_crew_availability, get_job_priority, get_job_location, and compute_distance as six separate tools, when the actual task is "assign the best available crew to this outage." The model now has to replicate dispatch logic — the exact sequence of calls, the comparison logic, the tie-breaking rules — inside its own reasoning, on every single run, with no guarantee it does so consistently. You have taken deterministic business logic that belongs in code and rebuilt it as a probabilistic prompt-following exercise.
Tools that are too coarse have the opposite problem: they hide decisions inside an opaque call the model can't reason about or explain. A single resolve_claim tool that takes a claim ID and returns "approved" or "denied" might look clean in a demo, but it has smuggled the entire adjudication policy — coverage checks, fraud screening, payout calculation, regulatory holds — into a black box the model invokes on faith. When it goes wrong, nobody can tell whether the model chose badly or the tool's internal logic had a bug, because the model never saw the intermediate reasoning it would need to catch the problem, and neither did your logs.
The right grain size is one tool per intent the model needs to express, where each intent corresponds to a decision worth exposing. "Assign this crew to this job" is one intent — one tool, with the comparison logic implemented deterministically inside it. "Approve this claim" is not one intent; it's several (verify coverage, check for duplicate submission, compute payout, decide on escalation), and collapsing them into one call removes the seams where oversight, logging, and guardrails need to attach.
A useful test: if you can't write one clear sentence describing when a tool should be called and when it shouldn't, it's probably the wrong grain. "Call schedule_crew when a validated outage ticket needs a crew assigned and at least one crew is available" is a sentence you can write, test, and put in a tool description. "Call resolve_claim" is not, because it doesn't tell you what happens inside.
Idempotency and safe retries
Agent loops retry things. Networks time out, models get interrupted mid-call, orchestration frameworks resend requests after an ambiguous failure. If your tools aren't built to tolerate this, retries turn small hiccups into real incidents — the classic case being a payment or ticket-creation tool that fires twice because the first response was slow and the harness assumed it failed.
Idempotency means a tool can be called twice with the same input and produce the effect once. The mechanism is not new: idempotency keys, the same technique payment processors have used for two decades. The agent (or the harness layer wrapping it) generates a unique key per logical action; the tool's backend checks whether it has already processed that key before executing anything. This is ordinary distributed-systems hygiene that becomes non-optional the moment a probabilistic caller is doing the calling instead of a deterministic client.
| Tool type | Retry risk without idempotency | Fix |
|---|---|---|
| Issue refund / payment | Duplicate payout on retry | Idempotency key tied to the claim/transaction ID |
| Create support ticket | Duplicate tickets, confused customer | Dedup on ticket key + short-window check before insert |
| Send customer email | Duplicate or repeated outbound message | Idempotency key + send-log check |
| Read policy record | None — reads are naturally safe | No special handling needed |
That last row matters as much as the others: idempotency is a tax you pay on writes, not reads. Which leads to the next design principle.
Read/write separation
Separate your tools cleanly into reads (safe, repeatable, no side effects) and writes (consequential, need guardrails, need idempotency, often need approval). This isn't just a naming convention — it should be structural, ideally enforced by different code paths, different permission scopes, and different logging verbosity. A model that's uncertain can always retry a read for free. A model that's uncertain and calls a write tool anyway has made a decision with consequences.
This separation also gives you a cheap, high-leverage safety lever: you can let an agent call every read tool it wants with a loose or no approval gate, while routing every write tool through a permissioning layer (lesson 10 covers this in depth). Most of an agent's useful work — gathering context, checking status, comparing records — is reads. Restricting the expensive oversight machinery to the small set of write tools keeps the agent fast where speed is safe and careful where carelessness is costly.
Error messages are prompts too
This is the principle teams miss most often, because error handling is usually written by whoever wrote the tool's backend, for a human debugging a stack trace — not for a model deciding what to do next. But the agent reads the error message as input, the same as any other tool output, and it will act on what that message says. A bad error message doesn't just fail to help; it actively misleads the model into a worse next action.
// Written for a human debugging in production
{ "error": "500", "message": "Internal Server Error" }
// Written for the model deciding what to do next
{
"error": "policy_lookup_failed",
"message": "Policy POL-88213 could not be retrieved because the policy
number format is invalid (expected format: POL-#####, five digits).
The ID you passed had six digits.",
"retry_recommended": false,
"suggested_action": "Re-extract the policy number from the source
document and verify it matches the POL-##### format before retrying."
}
The first error tells the model nothing. In practice, models faced with an opaque failure will often do one of two unhelpful things: retry blindly (burning tokens and time on a call that will fail identically every time), or — worse — quietly move on and report success anyway, because the harness gave it no clear signal that the action didn't happen. The second error is a course-correction prompt: it says what failed, why it failed, and what to try instead. Write every error message your tools can return as if you were briefing a competent new colleague who has to decide what to do next with no other information. Because that is exactly the situation the model is in.
The most dangerous tool failure mode is not the loud one — it's the silent one. A tool that catches an internal exception and returns {"status": "ok"} anyway, because someone wanted to avoid crashing the pipeline, teaches the model that the action succeeded when it didn't. The agent proceeds confidently on a false premise, and everything downstream — its summary to the customer, its next tool call, its final report — inherits the lie. A tool should fail loudly and specifically, every time, or not at all.
Tool descriptions are prompts
The description field on a tool definition is not documentation for other engineers. It is the primary instruction the model uses to decide whether to call the tool at all, and it gets re-read on every single turn the tool is in scope. Treat it exactly like you'd treat onboarding material for a new colleague on their first day: when should they reach for this, when should they explicitly not, and what should they expect back.
{
"name": "file_support_ticket",
"description": "Creates a new customer support ticket in the CRM queue.
Use this ONLY after you have confirmed the customer's account ID and
summarized their issue in under 200 words. Do NOT use this to escalate
billing disputes over $500 — use escalate_billing_dispute instead, which
routes to a human reviewer. Returns a ticket ID and estimated response
time. This action cannot be undone by the agent; tickets can only be
closed by support staff.",
"parameters": { "account_id": "string", "summary": "string", "priority": "enum[low,normal,high]" }
}
Notice what that description does beyond naming the parameters: it states a precondition (confirm the account ID first), a negative case (don't use this for billing disputes over $500 — use a different, more supervised tool), a return contract (ticket ID and response time), and a consequence warning (irreversible by the agent). Every one of those sentences prevents a specific failure a terser description would allow. A description that just says "Files a support ticket" leaves all four of those failure modes open.
A marketing content-ops team gave their agent a single publish_content tool with the description "Publishes content to the CMS." The agent, working from an ambiguous brief, published a draft blog post to the live site instead of the staging queue — technically correct per the tool's one-line description, disastrous per the team's actual workflow. The fix wasn't a smarter model; it was rewriting the description to state explicitly that this tool publishes directly to production, that a separate save_draft tool exists for anything not explicitly approved, and that approval means a named human signed off in the brief. Same model, same tool, zero repeat incidents.
MCP: standardizing the connection, not the judgment
Every enterprise that builds more than one agent runs into the same integration problem: each new agent needs its own bespoke code to talk to Salesforce, the claims database, the ticketing system, internal search. Multiply that by every model provider and internal system, and you get an N×M explosion of custom connectors nobody wants to maintain.
The Model Context Protocol (modelcontextprotocol.io) exists to collapse that explosion. MCP defines a standard way for a server to expose tools and resources — a claims system, a ticketing queue, a document store — so that any compliant agent harness can connect to it without custom glue code per pairing. You write the connector to your claims system once, as an MCP server, and every agent your organization builds afterward can use it the same way, regardless of which model or orchestration framework sits behind it.
It is worth being precise about what this solves and what it doesn't. MCP standardizes the wire format and discovery mechanism — how an agent finds out what tools exist and how it calls them. It does not standardize the design judgment covered earlier in this lesson: grain size, error message quality, whether a given action needs a write gate. An MCP server built around a single sprawling do_anything_to_the_claims_system tool is exactly as poorly designed as a hand-rolled one with the same flaw. Protocol compliance is not a substitute for tool design discipline — it just means the discipline, once applied, gets reused across every agent you build instead of rebuilt each time.
Least-capability principle
Give an agent the narrowest action surface that achieves its job, and nothing more. This sounds obvious stated plainly, and it is routinely violated in practice because the path of least implementation effort is to grant an agent broad API access "in case it needs it later." That instinct is backwards for a probabilistic caller in a way it isn't for a deterministic one — a script that only ever calls the three endpoints it was written to call is safe even with broad credentials, because it has no discretion. An agent with broad credentials has discretion on every call, and discretion plus a wide blast radius is precisely the combination that turns a reasoning mistake into an incident.
An agent that files support tickets does not need a tool that deletes customer records, even if the underlying API credential technically has that permission. An agent that drafts marketing copy does not need direct publish access to every regional site if only one region is in scope for its task. An agent that schedules field crews does not need the ability to modify payroll records just because the crew and payroll systems happen to share a database. In each case, the excess capability buys the agent nothing it needs for its actual job, and it converts every future reasoning error — misread instruction, hallucinated ID, adversarial prompt injection from a malicious document the agent ingested — into a much worse outcome than it needed to be.
Least-capability is often undone quietly, months after launch, by convenience requests. Someone on the team needs the agent to also update one adjacent field "just this once," and the fastest fix is widening an existing tool's scope rather than building a narrow new one. Six months later the ticket-filing agent has update access to a dozen unrelated tables nobody remembers granting. Treat every capability expansion as a design decision with the same rigor as the original tool build, not a config tweak.
Testing tools independently of the model
One of the most underused techniques in harness engineering is testing the tool layer as if the model didn't exist. Every tool your agent can call should have deterministic contract tests: given this input, does it return this shape of output; does it enforce idempotency on retry; does it reject malformed input with a model-readable error; does it correctly refuse an out-of-scope write. None of this requires a model in the loop, so it can run in CI on every commit, in seconds, for free, catching an entire category of production incident before an agent ever touches the tool.
This matters because it lets you separate two very different questions when something goes wrong in production: did the tool do the wrong thing, or did the model call the right tool badly? Without contract tests, every incident review starts by re-deriving the tool's correctness from scratch. With them, you rule the tool layer in or out in the time it takes to run the suite, and spend your investigation time where the actual ambiguity lives.
Common failure patterns
A few failure modes recur across every enterprise domain this course touches, precisely because they come from the shape of the tool layer rather than any particular business logic:
- Leaky abstractions — a tool description implies a clean, safe operation, but the underlying implementation has sharp edges (rate limits, partial failures, stale caches) the model has no way to know about until it hits them blind.
- Silent failures interpreted as success — covered above, and worth repeating: an ambiguous or swallowed error is worse than a loud one, because the agent builds everything downstream on a false premise.
- Over-broad "do anything" tools — a single tool that accepts a free-text command or arbitrary query string and executes it against a production system. This looks flexible in a demo and is nearly impossible to guardrail, audit, or contract-test in production.
- Tool sprawl — accumulating dozens of narrow, overlapping tools until their descriptions alone consume a meaningful share of the context budget on every call, crowding out the actual task context and confusing the model about which near-duplicate tool to pick.
The remedy for all four is the same discipline argued throughout this lesson: treat the action surface as a first-class design artifact, review it the way you'd review an API exposed to a junior engineer with good intentions and imperfect judgment, and keep it as small as the job allows.
This week, take one agent you have in flight — pilot or production — and list every tool it can call. For each one, write the one-sentence "when to call this, when not to" test from this lesson. If you can't write that sentence cleanly, the tool's grain is wrong. Then classify each tool as read or write, and check that every write tool has an idempotency mechanism and a model-readable error contract. If any write tool is missing either, that's your highest-priority fix before the next production incident finds it for you.
Key takeaways
- The action surface — everything an agent can do, not just say — is a design artifact, not an integration chore.
- Tool granularity is a real decision: too fine forces the model to orchestrate business logic it shouldn't own; too coarse hides decisions in an opaque, unauditable call.
- Writes need idempotency (retry-safe by design); reads don't. Separate them structurally and route only writes through heavier oversight.
- Error messages are prompts — the agent reads and acts on them. Say what failed, why, and what to try next; never fail silently.
- Tool descriptions are prompts too — write them like a briefing to a new colleague: when to use, when not to, what comes back.
- MCP standardizes the connection (write once, reuse across agents) but not the design judgment — grain size, error contracts, and write gating are still on you.
- Apply least-capability by default: an agent should have the narrowest surface that does its job, and every later capability expansion deserves the same scrutiny as the original design.
Check your understanding
1. A team gives its claims agent six separate fine-grained tools (get crew location, get crew skill set, get crew availability, etc.) instead of one assign_best_crew tool. What is the main risk of this design?
2. Why does the lesson insist that error messages should be written "for the model" rather than for a human debugging a stack trace?
3. What does the Model Context Protocol (MCP) actually standardize, according to this lesson?
4. Per the least-capability principle, why shouldn't a support-ticket-filing agent be given a credential that also allows customer-record deletion, even if it would "never need to use it"?