Harness engineeringLesson 04

Tools, MCP and Error Design

A tool is a contract, not a function. Its schema, error messages and side effects determine whether the agent can use it reliably.

Working24 min readUpdated November 2026Toggle tools in a mission →

Tools, MCP and Error Design

The model cannot see your codebase, your database, or your filesystem. It can only see the tool descriptions you give it and the results you return. That interface is the agent’s entire world.

The anatomy of a tool

A tool definition has:

  • Name — short, specific, and stable.
  • Description — what it does, when to use it, when not to use it.
  • Parameters — typed fields, required/optional, constraints, examples.
  • Return value — structured data, not a prose blob.
  • Errors — specific, actionable messages.
  • Side effects — whether it reads, writes, deletes, spends or sends.

A typical schema:

{
  "name": "search_repository",
  "description": "Search tracked source files for a literal string or regex. Returns file paths, line numbers and a short matching excerpt. Use this to locate code before reading it.",
  "parameters": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "description": "Literal text or regex" },
      "glob": { "type": "string", "description": "Optional path filter, e.g. src/**/*.ts" },
      "max_results": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }
    },
    "required": ["query"]
  }
}

The description is not decoration. It is the model’s only guide to when the tool is appropriate. “Search files” is worse than “Search tracked source files for a literal string or regex; use this before reading a file.”

Tool granularity

There is a tradeoff between few broad tools and many narrow tools:

  • Broad tools (a generic shell) are flexible but dangerous and hard to describe.
  • Narrow tools (read_file, edit_file, run_tests) are safer and easier to use, but multiply the schema surface.
  • Composed tools (apply_patch) encode a workflow and reduce the chance of malformed edits.

A good default: narrow, typed tools for common operations, plus one escape hatch (shell) behind stricter policy. The model should not need a different tool for every file type.

For coding agents, a useful set is:

  • read_file(path, offset, limit)
  • search(query, glob, max_results)
  • edit_file(path, old_string, new_string)
  • write_file(path, content)
  • run_command(command, cwd, timeout)
  • list_directory(path)
  • get_diagnostics(path)

Each has a clear contract and a bounded output.

Error messages are part of the interface

When a tool fails, the error goes back into the model’s context. A vague error invites a loop:

Error: invalid input

A useful error tells the model what was wrong and what to do:

Error: expected `path` to be a relative path inside the workspace.
Received: /etc/passwd
Allowed: paths under the repository root. Try `src/config.ts`.

Or:

Error: test command exited with code 1.
The assertion failed: expected 42, received 41.
Relevant file: src/cart.ts:13.

The second error gives the model a next step. The first gives it a reason to try random variations.

Error design principles:

  • Be specific about the invalid field and the expected shape.
  • Include a small, relevant excerpt, not the entire log.
  • Distinguish retryable from fatal errors.
  • Do not leak secrets in error messages.
  • Keep the error format stable so the model can learn it.

Output design

Tool output is also context. A tool that returns 50 KB of logs can crowd out the task. Options:

  • Truncate with a pointer: return the first and last N lines plus a path to the full log.
  • Summarize deterministically: counts, status codes, failed test names.
  • Paginate: return a cursor the model can request.
  • Filter by relevance: return only lines matching the query.
  • Structure: JSON with named fields beats a wall of text.

The model should be able to ask for more detail without re-running the whole operation. A good result includes enough metadata to decide what to do next.

Side effects and idempotency

Tools fall on a spectrum:

Tool typeExampleRetry safety
Read-onlyread_file, searchsafe
Local writeedit_filesafe if the edit is deterministic
External writesend_email, charge_carddangerous without idempotency key
Destructivedelete_file, drop_tabledangerous, require approval
Expensiverun_cluster_jobdangerous, require budget

A retry is only safe if the operation is idempotent or keyed. An agent that retries “send email” can send the same email five times. An agent that retries “create payment” can charge a customer twice. The harness must know which tools are safe to retry and which require a different recovery path.

MCP: a standard interface

The Model Context Protocol (MCP) standardizes how applications expose tools, resources and prompts to AI clients. Instead of writing a custom integration for every model and every harness, a server implements MCP and any compatible client can use it.

MCP is useful because it:

  • decouples tool providers from harnesses,
  • gives tools a discoverable schema,
  • supports resources and prompts as well as functions,
  • encourages a common vocabulary for capabilities.

MCP is not a security boundary. A tool exposed over MCP is still a tool with whatever permissions the server has. Risks include:

  • Tool poisoning — malicious descriptions or metadata that steer the model.
  • Over-broad credentials — the server has more access than the task needs.
  • Untrusted servers — a third-party server can return manipulated content.
  • Confused deputy — the agent uses its authority on behalf of untrusted content.
  • Supply chain — a dependency or server update changes behavior silently.

Treat MCP servers like any other dependency: pin versions, review permissions, run in a sandbox, and log calls.

Tool selection and context

More tools can make a model worse. A long list of similar tools increases ambiguity and context size. Strategies:

  • Group tools by task and expose only the relevant group.
  • Use clear, distinct names — read_file vs. read_url vs. read_pdf.
  • Write descriptions that say when not to use the tool.
  • Provide examples for tricky parameters.
  • Let the model ask for help if no tool fits.

A tool that is rarely used but always in the schema is a tax on every request. Consider loading it dynamically or moving it to a subagent.

Testing tools

Tools need tests like any other interface:

  • Schema validation: malformed args produce useful errors.
  • Permission tests: forbidden paths and commands are rejected.
  • Output size tests: large outputs are truncated safely.
  • Error tests: failures return actionable messages.
  • Idempotency tests: retries do not duplicate side effects.
  • Prompt-injection tests: untrusted content cannot redirect the tool.

A tool that works in a demo but fails on a path with spaces, a symlink, or a 10 MB log is not ready.

Next: permissions and sandboxing — why a prompt is not a security boundary, and how to design authority that matches the task.

Sources and further reading

One question before you go

retrieval check
What is the best error message for a failed tool call?
Why: Tool results are model input. Specific, actionable errors let the agent recover; vague errors invite loops.
Type at least two characters. Search is indexed after the production build.
Lessons · labs · glossary↑↓ to browse · esc to close