Tools, MCP, Skills, 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 13 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.
What changed in the 2026-07-28 revision
MCP moves fast enough that a harness built against last year's spec is already carrying assumptions the protocol has dropped. The 2026-07-28 specification (2026) is the largest revision since the protocol launched, and most of its changes push MCP toward something that behaves like an ordinary stateless web API. The ones that matter for harness design:
- Stateless core. The
initializehandshake and theMcp-Session-Idheader are gone. Every request carries its protocol version and client capabilities in_meta, and list endpoints no longer vary per connection. A server that needs state across calls mints an explicit handle and passes it as an ordinary tool argument. For you, this means MCP servers can sit behind a plain load balancer and be scaled, restarted, and retried like any other stateless service, and a dropped connection no longer strands a session. server/discover. Servers must implement this RPC to advertise their supported versions, capabilities, and identity. Clients may call it up front to pick a version, which replaces what the handshake used to do.- Tasks become an extension. Long-running work (the experimental tasks feature) moved out of the core into an official extension, with polling via
tasks/getand client input viatasks/update. If your harness depends on long-running MCP calls, check that both sides negotiate the extension rather than assuming it. - Multi round-trip requests replace server-initiated calls. Previously a server could turn around mid-request and ask the client for things: a model completion (sampling), user input (elicitation), or the workspace roots. Now the server returns a result with
resultType: "input_required"listing what it needs, and the client retries the original request with the answers attached. Control stays with the harness: nothing reaches into your model or your user without going back through your own loop, where permissioning and logging already live. - Deprecations. Sampling, Roots, and Logging are deprecated. The suggested migrations are telling: pass directories as tool parameters instead of Roots, call your LLM provider directly instead of borrowing the client's model through Sampling, and log to
stderror OpenTelemetry instead of Logging. Under the new feature lifecycle policy, deprecated features keep working for a minimum twelve-month window before removal, so you have time, but new servers should not adopt them. - Deterministic
tools/listordering. Servers should return tools in a deterministic order, which the changelog says is to enable client-side caching and "improve LLM prompt cache hit rates." Tool definitions usually sit near the front of the prompt, so a server that shuffles its tool list on every call silently invalidates your cached prefix on every turn (lesson 6 covers why prefix stability is worth protecting). List results now also carryttlMsandcacheScopefreshness hints. - OpenTelemetry propagation. The spec documents conventions for carrying
traceparent,tracestate, andbaggagein_meta, so a trace can follow a request from the agent loop into the MCP server and back. That makes "which tool call was slow, and why" a trace query instead of a log-correlation project.
The through-line is that the protocol is getting out of the harness's way. Session state, model access, and user interaction all move back to the harness, where they can be governed. If you maintain MCP servers, the cheap wins this quarter are deterministic tool ordering and trace propagation; the migration off Sampling and Roots can follow on the deprecation clock.
Installing a third-party MCP server means letting a stranger write part of your prompt. Tool descriptions go into the model's context verbatim, and users typically see only a short summary in the UI. Invariant Labs (2025) named this a tool poisoning attack: hidden instructions in a description that tell the model to read sensitive files or send data somewhere, plus a "rug pull" variant where a server changes its descriptions after you approved them. Treat tool descriptions from outside your organization as untrusted input: pin versions, diff descriptions on update, and review them as you would a dependency. Lesson 14 covers prompt injection and the defenses in depth.
Skills: packaging procedure, not connectivity
MCP answers "how does the agent reach this system?" It does not answer "how does the agent know the right way to do this job here?" A tool can expose create_invoice; it cannot teach the model your finance team's twelve-step month-end close, which reports to cross-check, and which spreadsheet template legal insists on. Teams used to stuff that knowledge into the system prompt, where it costs tokens on every turn whether or not the task needs it.
Agent Skills are the packaging format that grew up to fill that gap. As the Agent Skills specification site (2026) describes it, a skill is a folder containing a SKILL.md file: YAML frontmatter with at least a name and description, followed by instructions. The folder can also bundle scripts, reference documents, and templates. The format was originally developed by Anthropic and released as an open standard; the site's client showcase lists dozens of adopting products, including Claude Code, Codex, Gemini CLI, Cursor, GitHub Copilot, OpenCode, and OpenHands.
month-end-close/
├── SKILL.md
├── scripts/reconcile.py
└── references/close-checklist.md
--- SKILL.md ---
---
name: month-end-close
description: Run the finance month-end close for a business unit.
Use when asked to close a month, reconcile ledgers, or prepare
close reports. Not for ad-hoc invoice questions.
---
1. Run scripts/reconcile.py for the unit and period.
2. Any variance over 0.5% goes in the exceptions table; do not
adjust entries yourself.
3. Fill references/close-checklist.md and attach it to the report.
The mechanism that makes skills cheap is progressive disclosure, which the spec describes in three stages. At startup the agent loads only each skill's name and description. When a task matches a description, it reads the full SKILL.md into context. Only then, while executing, does it run the bundled scripts or open the referenced files. An agent can have a hundred skills installed and pay for a hundred short descriptions, not a hundred manuals. Notice that the description is doing the same job a tool description does: it is the routing prompt, and it deserves the same "when to use, when not to" discipline from earlier in this lesson.
The three mechanisms overlap enough that teams get confused about which to reach for. A rough division:
| Reach for | When the problem is | Example | Watch out for |
|---|---|---|---|
| A plain tool | One well-defined action with a contract you control, used by one harness | schedule_crew inside your dispatch agent | Re-implementing the same integration in every agent |
| An MCP server | Access to a system that several agents or harnesses need, with its own auth and lifecycle | The claims database, exposed once and reused by every agent | Third-party descriptions you haven't reviewed; tool sprawl |
| A Skill | Procedural know-how: how to combine existing tools and files to do a job the way your organization does it | Month-end close, release checklist, brand-review procedure | Bundled scripts are executable code; a skill with scripts needs the same review and sandboxing as any other code |
They compose rather than compete. A skill often tells the agent which MCP tools to call and in what order, and a skill's bundled script might itself call an internal API. The useful question is where each piece of knowledge belongs: connectivity in a server, a single consequential action in a tool with a tight contract, and "how we do this here" in a skill that loads only when needed.
Code mode: letting the model write the glue
Classic tool calling puts the model in the middle of every step. To find the top five customers by revenue, the model calls query_orders, receives a few thousand rows into its context, reads them, calls the next tool, and so on. Every intermediate result passes through the context window and costs a model round trip, even when the model's only job is to copy a value from one call's output into the next call's input.
Code mode, also called programmatic tool calling, turns that around. The harness exposes tools as functions in a sandboxed runtime, the model writes a short program that calls them, and only the program's output comes back into context. Cloudflare's Code Mode (2025) converts MCP tools into a TypeScript API and runs the model's code in V8 isolates, arguing that models have seen far more real code than synthetic tool-call formats. Anthropic's programmatic tool calling (2026) does the same inside its code execution container: tools opt in with an allowed_callers field, intermediate results are not loaded into the model's context, and on agentic search benchmarks Anthropic reports an average 11% improvement with 24% fewer input tokens. DeepSeek Harness's ptc-runtime (2026) is an open implementation of the same idea: the model writes one program that calls host-provided functions as ordinary async calls, it runs in a fresh Node process under the configured sandbox policy, and only printed output and the return value come back.
// What the model writes (one turn, one round trip)
const orders = await query_orders({ since: "2026-07-01" });
const byCustomer = {};
for (const o of orders) {
byCustomer[o.customer_id] = (byCustomer[o.customer_id] ?? 0) + o.total;
}
const top = Object.entries(byCustomer)
.sort((a, b) => b[1] - a[1]).slice(0, 5);
console.log(JSON.stringify(top)); // only this enters context
The wins are real when a task chains several calls, filters large results, or loops over many items. The model uses its fluency in code for the glue logic, and the context window holds five rows instead of five thousand. It also changes your design calculus from the start of this lesson: fine-grained read tools hurt less in code mode, because the orchestration happens in deterministic code the model wrote rather than in token-by-token reasoning. Consequential writes should still be coarse, gated tools. A loop that can call issue_refund a thousand times in one program is exactly the blast radius the least-capability principle warns about.
Code mode means executing model-written code. That makes the sandbox part of your action surface. The runtime needs filesystem, network, and time limits, and the only functions the program can reach should be the ones you bound in. Anthropic's docs are explicit that allowed_callers "is not a hard API-level block" and should not be relied on as a security boundary; the boundary has to be the sandbox and your permission layer. Lesson 12 covers how to build that environment.
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. Finally, list every third-party MCP server the agent loads, read its full tool descriptions (not the UI summary), and check whether it returns tools/list in a stable order; if any procedure is pasted into the system prompt "just in case," move it into a Skill.
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 and tool descriptions are both prompts. Errors should say what failed, why, and what to try next (never fail silently); descriptions should brief a new colleague on when to use the tool, when not to, and what comes back.
- MCP standardizes the connection (write once, reuse across agents) but not the design judgment. Its 2026-07-28 spec is stateless (no handshake or session ID,
server/discoverinstead), moves Tasks to an extension, replaces server-initiated sampling and elicitation with multi round-trip requests, deprecates Sampling, Roots, and Logging on a twelve-month clock, and asks for deterministictools/listordering so your prompt cache survives. - Skills package procedure: a
SKILL.mdplus scripts and references, loaded by progressive disclosure. Use a tool for one action, an MCP server for shared system access, and a Skill for "how we do this job here." - Code mode lets the model write a program that calls tools in a sandbox, cutting round trips and keeping intermediate data out of context. It makes the sandbox part of the action surface, and third-party tool descriptions are untrusted input that can be poisoned.
- 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. A finance team wants its agents to follow the company's twelve-step month-end close, which uses the ledger system (already exposed as an MCP server) plus a reconciliation script and a checklist template. The procedure is needed a few times a month. What is the best way to give agents this capability?
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"?