2.2 — Structured Error Responses · Core
Return structured metadata so the coordinator can recover intelligently — a generic "Operation failed" hides the context needed to decide anything:
isError, errorCategory (transient = retry; validation = fix input, no retry; business = policy violation, explain to user; permission = not authorised, escalate), isRetryable, attemptedQuery, partialResults, and — for business-rule failures — a customerMessage the agent can relay directly.
isRetryable is guidance, not a directive. The tool only signals a retry is possible; the agent loop owns the retry budget — max attempt count, backoff between attempts, escalation once the budget is exhausted. Marking a permanent failure isRetryable: true causes infinite retry loops. A retry-after hint on the error, when present, should drive the backoff delay; when absent, the loop falls back to exponential backoff on its own.
- Distinguish access failures from valid empty results. A timeout (needs a retry decision) is not the same as a successful query that legitimately found nothing (
isError: false, results: []) — conflating the two hides information the agent needs, and can make a broken connection look, from the agent's perspective, exactly like a search that simply had no matches.
- Use a
verdict field, not a boolean, for verification tools. A fact-checker needs three distinct outcomes: "supported" (sources confirm it), "contradicted" (sources refute it), "not_found" (simply absent from any source) — "not_found" and "contradicted" demand different downstream handling (report "unverifiable" vs. flag as actively wrong), and a boolean collapses that distinction into an ambiguous single false.
- Subagents recover locally from transient errors first, and only propagate what they can't resolve, along with what was attempted and any partial results.
- A business-rule error still needs a technical
errorCategory and a plain-language customerMessage — the agent uses the category to decide what to do next (don't retry, don't escalate as a bug), and relays the message so the customer isn't shown an internal string like "REFUND_LIMIT_EXCEEDED". Neither field alone is enough: the category without the message leaves the agent unable to explain anything; the message without the category leaves it unable to decide what to do next.
- 4–5 tools per agent is optimal; ~18 tools degrades selection reliability and causes agents to misuse tools outside their specialisation (e.g. a synthesis agent attempting web searches).
- Give each agent a scoped role with limited cross-role tools for high-frequency needs — e.g. a narrow
verify_fact tool for the synthesis agent's simple 85%-case lookups, while complex verifications still route through the coordinator to the web-search agent. Over-provisioning with the full toolset violates least privilege.
- Replace generic tools with constrained alternatives — swap a free-form
fetch_url for a load_document that validates document URLs at the API level.
tool_choice values: {"type": "auto"} (model decides, default), {"type": "any"} (must call some tool — guarantees structured output, no plain text), {"type": "tool", "name": "..."} (must call this specific tool — good for forcing a required first step), {"type": "none"} (no tool calls).
- Force a specific tool only for the turn that needs it (e.g. extract metadata before enrichment), then switch back to
"any" or "auto" for subsequent steps — don't re-apply forced selection on every turn.
- Principle of least privilege drives distribution decisions. When a synthesis agent burns 2–3 extra round-trips on simple fact-checks, the fix is a scoped
verify_fact tool for that 85% case, not handing it the entire web-research toolset "to be safe" — over-provisioning re-creates the exact selection-reliability problem tool scoping exists to prevent.
2.4 — MCP Configuration · Core
.mcp.json (project root, committed) → team / version-controlled tooling, discovered by everyone who clones the repo. ~/.claude.json → personal / experimental servers, not shared via version control. Both are discovered at connection time and available simultaneously.
- Environment-variable expansion for credentials (e.g.
${GITHUB_TOKEN}) — never hardcode a token in .mcp.json; git history is permanent.
- MCP resources vs tools: resources expose readable content catalogs (issue summaries, doc hierarchies, database schemas) at connection time, giving visibility without exploratory tool calls; tools perform actions. Confusing the two means an agent burns several exploratory
list_issues/get_schema-style tool calls just to learn what's available — content a well-designed resource would have handed it up front, for free, at connection time.
- Write detailed MCP tool descriptions — a thin description makes Claude fall back to a built-in (e.g.
Grep) instead of a more capable MCP tool. The fix when this happens is always to enrich the MCP tool's description — never to remove or disable the built-in, which would just eliminate a useful fallback for cases the MCP server doesn't cover.
- Prefer an existing community MCP server for standard integrations (GitHub, Jira); reserve custom servers for team-specific, proprietary workflows.
- Scope is the question, not capability. A
/review command every developer should get on clone goes in project-scoped .claude/commands/; a personal, still-experimental MCP integration goes in user-scoped ~/.claude.json — "shared via version control" vs. "just me" is the entire decision, independent of how polished either one is.
- Grep — search file contents for a pattern (function names, error strings, imports).
- Glob — find files by path/name pattern (e.g.
**/*.test.tsx), regardless of location.
- Read — load a full file's contents. Write — create or fully replace a file. Edit — targeted change via a unique text anchor; the most efficient option when it applies.
- Edit → Read+Write fallback: when the anchor text isn't unique, Edit can't safely locate the spot — fall back to Read the full file, then Write it back with the change, rather than a blind global replace that would touch every occurrence.
- Build understanding incrementally — Grep first to find entry points, Read only what's relevant to follow imports and trace flows. Don't load every file up front.
- Tracing a function across wrapper modules — identify all exported/re-exported names first, then Grep for each alias; a single-name search misses callers using a wrapper name.
- Bash is the fallback — reach for it when no other built-in fits (running a script, a package-manager command), not as a default first move ahead of the more targeted tools above.
If an agent misuses a tool, the fix is to remove the tool and create a dedicated subagent for that job. NEVER patch it with instructions, few-shot examples, or tool_choice — the model will keep reaching for a tool that's simply outside its role.
⚠ Often-missed — Edit Anchor Mismatches (Whitespace) · Gap
An Edit that reports "not found" immediately after a successful Read almost always means a whitespace mismatch — spaces vs tabs, or LF vs CRLF line endings. Copy old_string verbatim from the Read output rather than retyping it.
When several tools are needed together, prompt Claude to emit multiple tool_use blocks in one turn. Don't build a composite "do-everything" tool — that just trades one round-trip problem for a brittle, hard-to-route mega-tool that's harder for the model to select correctly and harder for you to give clean, structured errors from.
⚠ Often-missed — .mcp.json Credentials Are Never Hardcoded · Gap
A hardcoded token in .mcp.json — even one later removed — stays recoverable in git history forever. The only correct pattern is environment-variable expansion (${GITHUB_TOKEN}) with the actual secret injected at runtime from the environment, never committed in any form.
Exam reflexes for Domain 2
- "Tool misrouting" → fix tool descriptions first (not few-shot, not
tool_choice, not a keyword router).
- "Agent misusing a tool" → remove it + dedicated subagent, never patch with instructions.
- "18+ tools per agent" → split into scoped agents (4–5 each).
- "Timeout returns empty success" → silent suppression anti-pattern → structured error with
isError.
- "Boolean verification result loses nuance" →
verdict: supported / contradicted / not_found.
- "
isRetryable: true causing infinite loop" → the agent loop owns the retry budget, not the tool.
- "Multiple tools, different formats" →
PostToolUse hook to normalise (Domain 1).
- "Team-shared MCP server" →
.mcp.json · "personal/experimental" → ~/.claude.json.
- "Edit fails right after a successful Read" → whitespace/line-ending mismatch — copy
old_string verbatim.
- "Find all callers of X" → Grep; "find all
*.test.tsx" → Glob.
- "Agent making exploratory calls just to see what's available" → expose an MCP resource (content catalog), not more tools.
- "Synthesis agent doing 2–3 extra fact-check round-trips" → scoped
verify_fact tool for the common case, not the full toolset.
- "MCP tool ignored in favor of a built-in" → enrich the MCP description; don't remove the built-in fallback.