๐ŸŽ“ Claude Architect KB CCAR-F ยท CCAR-P study guide

Domain 4 โ€“ Facts

Tools & MCP ยท the things to remember, in plain words

Each item is one rule that decides exam questions. Read the short headline, say the rule to yourself, then read the line to check. Open why if the reason is not clear, and follow the question link to see the rule in action. Collected from the lesson, the practice questions and two full third-party mock exams. They help you remember; they do not guarantee a score.

Back to Domain 4 lesson ยท Domain 4 โ€“ Questions ยท Domain Q&A

Built-in tools: search and edit

๐Ÿ“– The story: find and refactor in 3 scenarios

  1. Edit, replace_all, or Read + Write? Ask: how many places should change? Every copy of the same text in one file โ†’ one Edit with replace_all: true. One place with unique text โ†’ a normal Edit. Edit fails as not unique and only one copy should change โ†’ Read the whole file, change that spot, Write it back (the exam's fallback every time). Concept drill ยท Q297 ยท Concept drill ยท Q299 ยท Concept drill ยท Q311 ยท โŒ wrong choice to avoid: replace_all when only one copy should change; 12 or 31 separate Edit calls; Bash sed
    why

    Edit replaces exact text, and its old_string must appear exactly once, or Edit refuses. Scenario 1: rename userData โ†’ customerData, 12 times in one file, all should change โ†’ replace_all: true (one call, not 12 Edits). Scenario 2: a host name appears 31 times in values.yaml and every one must change โ†’ same, replace_all: true. Scenario 3: the same 5-line retry block is pasted in several tests, and only the one in test_refund_partial should change. replace_all would change all of them, and longer old_string still matches several copies โ†’ Read the file, change that one block, Write the full file back. Write always replaces the whole file; it never merges.

  2. Edit says "not unique"? If every copy should change โ†’ repeat the Edit with replace_all: true. If only one copy should change โ†’ Read + Write the whole file. The exam marks "retry with a longer old_string" wrong, even though the docs list it as one option. Concept drill ยท Q299 ยท Concept drill ยท Q311 ยท โŒ wrong choice to avoid: widening old_string again and again on identical blocks
    why

    The official exam guide (Task 2.5) names it: "When Edit fails due to non-unique text matches, using Read + Write as a fallback for reliable file modifications." The error is a safety stop: Edit won't guess which match you meant. With identical or near-identical blocks (pasted tests, generated config, an interface in three places), more context still doesn't single one out, so the exam's fallback is the full rewrite: read the file, change the one block in memory, write it back. Splitting files, editing by line number with sed, or reporting failure to a human are distractors.

  3. Explore a codebase: which tool first? Go from cheap to expensive: Glob finds files by name or path (**/*.test.ts), Grep finds which files and lines contain text (charge(), then Read only the files that matter. Build understanding step by step instead of reading everything. โŒ wrong choice to avoid: reading every file into the main session, or pasting a list of 30 paths
    why

    Every file you Read costs context. Glob and Grep return short lists, so you can narrow down first. Scenario: "how does authorization work across 30 files?" โ†’ Grep for the policy class, Read the 3โ€“4 central files, then follow the imports. For a very large discovery phase, run it in a subagent (like Explore) so only the summary comes back to the main conversation.

  4. Grep missed some callers? Text search finds names, not meaning. When callers must change (you remove a function or change its parameters) and it is re-exported under another name (export { charge as pay }), Grep for charge( misses the pay( callers. Grep the export too, then each alias. A pure rename doesn't need this: update the export line and alias callers keep working. โŒ wrong choice to avoid: one Grep for the original name, reported as complete
    why

    Scenario: you must change charge() and find all callers. One file re-exports it as pay, another imports pay and calls it. A single Grep for the old name looks complete but isn't. Search for the export statement, note every alias, and search for each alias.

Tool definitions & selection

  1. What does the model see? Here's the thing: the model never sees your code. A tool is just name + description + input_schema (JSON Schema), plus optional strict: true. The model reads those; your dispatcher runs the real function. You resend tools[] on every request, because the API remembers nothing. โŒ wrong choice to avoid: thinking the model reads or runs your function code
    why

    Claude reads those three strings and decides from them whether to propose a call and which arguments to send. Your dispatcher (the code that routes tool calls) gets the proposed tool_use block, checks it, runs the function and sends back the result. The Messages API keeps no session, so the model only knows the tools that were in the last request.

  2. What goes in a description? Think of the description as the model's only menu text. Say what the tool does, when to use it, what the input points to, what comes back, and when NOT to use it. "Get data" can't tell an order lookup from a refund. โŒ wrong choice to avoid: a description that only repeats the tool name
    why

    The model picks tools by reading descriptions, nothing else. So put the deciding details there: the purpose, when to call it, what the input identifies (an order ID, not a customer name), and what it returns. A description that only repeats the name gives the model nothing, so two such tools get picked almost at random. Add the "not for refunds" line too, so close cases land on the right tool.

  3. Model picks the wrong tool? If the model mixes up search_orders and get_order, the first fix is to rewrite the descriptions: when to use each, when not to, and merge tools that truly overlap. Wrong answers: a stronger prompt, fine-tuning, few-shot examples, a third chooser tool, or forcing tool_choice. Practice Q&A ยท Q21 ยท Practice Q&A ยท Q63 ยท โŒ wrong choice to avoid: a stronger prompt, fine-tuning, or forcing tool_choice
    why

    The confusion lives in the tool definitions, so fix it there: search finds candidate order IDs, get_order reads one ID you already know. A prompt or few-shot examples put the rule in a second place that drifts out of sync. Fine-tuning is too early while the interface is unclear, a third tool adds one more decision, and forcing tool_choice only works when your code already knows the route. Fix the contract first, then test the real selection errors.

  4. Your tools ignored for built-ins? If the agent picks Grep and Bash over your relevant MCP tools 87% of the time, your one-line descriptions are losing to well-documented built-ins. Expand them. Don't cut to 5โ€“6 tools; there is no magic tool count. โŒ wrong choice to avoid: reducing the tool set to 5โ€“6 tools per session
    why

    Built-ins ship with full parameter docs and examples; your MCP tools say only "Analyzes dependencies". The model picks mainly by description, so write what each tool does, its inputs and outputs, and when it beats the generic tool. Too many tools can hurt selection, but fourteen tools is not proof of overload and there is no universal 5โ€“6 limit. Removing tools is the fix for out-of-role tools, not for relevant ones that are under-described.

  5. Tool doesn't fit the role? Simple rule: relevant but ignored โ†’ describe it better; out of role โ†’ remove it. A search agent calling summarise or sentiment_analysis should not have those tools. A prompt telling it to ignore them, or PreToolUse hooks blocking them, are the wrong answers. โŒ wrong choice to avoid: a system-prompt instruction to ignore the analysis tools, or PreToolUse hooks that block them
    why

    A raw-data search worker should only collect evidence; analysis belongs to a specialist agent with its own tools. Giving it analysis tools and hoping it ignores them is a gamble. Hooks that block the calls are extra safety, but the useless tools are still on offer. Scoping which tools each agent gets is deterministic (it works every time), so move the analysis tools to the agent whose job they serve.

  6. Contract or invoice? A contract that mentions invoice dates is still a contract. Fix the descriptions: extract_contract_fields covers agreements with payment terms, and extract_invoice_fields is for standalone invoices only. Renaming, toggling allowedTools, or a prompt rule are the wrong fixes. โŒ wrong choice to avoid: removing the invoice tool from allowedTools per document type, or a system-prompt routing rule
    why

    A supplier agreement with a payment schedule is a contract; its invoice clauses don't turn it into a bill. Today both descriptions only repeat their names, so the model has nothing to decide with when both words appear. Put the boundary where the model chooses, in the descriptions. Renaming explains no boundary, toggling allowedTools assumes a reliable upstream classifier the scenario never has, and a prompt rule is weaker than the description the model reads while building the call.

  7. Right tool, wrong SQL? If the model picks the right tool but writes the wrong SQL dialect (string_agg instead of LISTAGG), say Snowflake SQL in the tool description. Not a validation layer, not auto-translation, not a function list in the system prompt. โŒ wrong choice to avoid: a validation layer or auto-translation instead of naming the dialect
    why

    Picking the tool and writing a good argument are two different steps. Here the pick was right, but "Accepts SQL" never said which dialect. Put the rule next to the tool it governs: say it expects Snowflake SQL and give an example like LISTAGG. A validator or translator is fragile extra machinery that hides missing information, and a system-prompt list costs tokens on every turn even when the tool isn't used.

  8. Where does the description come from? With @mcp.tool(), your docstring becomes the description the model reads (unless you pass description=), and your type hints become the inputSchema. What the function returns is the result of one call, never the description. โŒ wrong choice to avoid: thinking the return value becomes the description
    why

    The decorator registers a normal Python function with the MCP server object and builds the advertised metadata from it. You still write the function body yourself. A lazy one-line docstring gives you exactly the under-described tool that loses to built-ins.

  9. How to make a clean schema? Use required, enum and additionalProperties: false so bad arguments fail early. Name tools verb + domain (get_invoice, issue_credit). Keep results short, because every tool result uses up context window.
    why

    required stops an empty {}, enum limits a field to legal values, and additionalProperties: false rejects extra fields instead of silently ignoring them. So bad input fails at validation, not during execution. Verb-plus-domain names show at a glance which tools read and which change things. Every result is added to the conversation and paid for on every later turn, so return what the model needs and keep the rest in logs.

tool_choice, strict & the tool_use โ†” tool_result contract

  1. Need one exact tool? To guarantee one specific tool, name it: {"type":"tool","name":"generate_report"}. The tool_choice options are auto (model decides), any (some tool, model picks which), the named form (that tool), and none (no tools). any may pick a different tool. โŒ wrong choice to avoid: setting tool_choice to any on every turn
    why

    tool_choice is the request field that limits what the model may pick. auto allows a text answer or a tool call. any forces some call, but the model might choose web_search instead of generate_report. Only the named form fixes which tool runs. A sentence in the system prompt is only advice and enforces nothing.

  2. Force once, then release? tool_choice goes with each request, so force generate_report on turn one and send auto after that. Forcing never runs anything. And while forcing is on, the model can't reply in text, so it can't ask the user a question. โŒ wrong choice to avoid: leaving the tool forced on later turns
    why

    The API is stateless, so every request carries its own tool_choice. Your app forces the tool on request one, runs the approved call, appends the result, and sends the next request with auto. Forcing still only makes the model propose a tool_use block; your harness (the code running the loop) executes it. Leave a forced or any setting on, and the model can never answer in plain text, not even with a clarifying question.

  3. What does strict check? "strict": true sits in the tool definition next to name and input_schema. The Claude API then won't let the model produce {}, wrong types, or extra fields. But O-999 still passes: strict checks the shape, not whether the order exists or who owns it. โŒ wrong choice to avoid: believing strict checks that the record exists or belongs to the user
    why

    Strict mode makes the API keep the generated arguments inside your declared schema, on supported models. It checks structure only: a perfect-looking order_id like O-999 is accepted even if no such order exists or it belongs to another customer. Only your backend can check those business facts. Strict is a Claude API setting; an MCP server connected through .mcp.json never supplies it.

  4. Strict on, still validate? Yes. strict adds no validator code to your app. Run jsonschema on tool_use.input before dispatch. If the tool name is unknown or the arguments are invalid, send back an error tool_result; never execute it. โŒ wrong choice to avoid: skipping validation because strict is on
    why

    Your dispatcher checks the input against the schema, looks the name up in a list of approved functions, and checks permission from the trusted session. Anything that fails becomes a tool_result with is_error: true and a helpful message tied to the call ID. The model can then recover, and nothing unexpected runs. This is the harness's job on every request, with or without strict.

  5. What's the tool-call order? The order follows who has what: tools[] sent โ†’ model returns stop_reason: "tool_use" โ†’ harness validates and authorizes โ†’ executes โ†’ tool_result in a user-role message. You can't validate arguments that don't exist yet. Practice Q&A ยท Q65 ยท โŒ wrong choice to avoid: validating the input before the model has returned arguments, and sending the result in an assistant-role message
    why

    The model can only propose a call after it sees the definitions, and your harness can only check arguments after the model writes them. When the response comes back with stop_reason: "tool_use", the harness checks and authorizes the input, runs the backend, and returns the result in the next user-role message. Validating before the proposal, or sending tools[] after the call, is an impossible order. A tool_result in an assistant-role message is the wrong role.

  6. Where does the result go? First append the assistant's full content, then send {"type":"tool_result","tool_use_id":"toolu_1","content":"โ€ฆ","is_error":false} in a user message. Your app wrote that block, even though the role says user. Practice Q&A ยท Q65 ยท Practice Q&A ยท Q26 ยท โŒ wrong choice to avoid: dropping the assistant's tool_use content, or sending the result with another role
    why

    The history must keep the assistant's complete response, including its tool_use block. Then comes a user-role message holding the tool_result, matched by tool_use_id. The user role means its place in the conversation, not who wrote it; your app builds the block from what the backend returned. Drop the assistant content or use another role, and the pairing the API expects breaks.

  7. Several calls at once? If one response has several tool_use blocks, run each allowed call and return one tool_result per tool_use_id, all in one user message. A blocked or failed call still gets an error result, never silence. Practice Q&A ยท Q26 ยท โŒ wrong choice to avoid: returning one combined result for both calls
    why

    One response can ask for an order lookup and a ticket lookup with two different IDs. Check and authorize each, run the allowed ones (in parallel when safe), and put one result block per ID in the next user message. Running only the first leaves a request unanswered; merging both into one result loses which answer belongs to which call. A refused or failed call still gets an is_error result, because a missing pair breaks the conversation.

  8. Why did the whole run die? If one timeout kills the whole run, your harness threw an exception instead of returning is_error: true with a message. is_error: true does not end the loop; it gives the model a chance to recover. Practice Q&A ยท Q70 ยท โŒ wrong choice to avoid: believing that a tool_result with is_error true terminates the loop
    why

    An error result is just information: the model reads it and may retry with new input, explain, or stop, while your code limits the retries. An uncaught exception escaping the dispatcher kills the loop before the model ever sees a result. So a run that dies on a timeout means missing error handling in the harness, not a problem with the error flag. Catch expected failures, return a clear error tied to the call ID, and keep sensitive details in server logs.

  9. What does stop_reason say? stop_reason tells your harness what to do next. tool_use โ†’ run the tools and continue. end_turn โ†’ this turn is done (maybe just a question back, not task success). max_tokens โ†’ cut off, so never run partial arguments. There is no stop_turn. โŒ wrong choice to avoid: treating end_turn as task success, or a made-up stop_turn
    why

    tool_use means the model is waiting for tool results. end_turn means this assistant turn finished, which may be a question to the user rather than a finished task. max_tokens means generation stopped early, so any tool arguments may be incomplete and must not be run. stop_turn is not a value; seeing it in an answer means someone misremembered the field.

  10. Who grants permission? Never the model. Approval is server-side state, not an argument like approved: true. Who owns an order comes from the session, not model input. Retries reuse an idempotency key from trusted state. Valid JSON never creates permission. โŒ wrong choice to avoid: trusting an approved: true argument from the model
    why

    The model can write any well-formed argument, so approved: true proves nothing. A customer's refund confirmation is a record your backend saved when they clicked the confirm button. Ownership comes from the logged-in session, not from a customer_id the model typed. When a change is retried, the backend reuses its own idempotency key (an ID that makes a repeated request do nothing new), so a network hiccup can't issue two refunds.

MCP: roles, primitives, discovery, execution

  1. Host, client, or server? The host is the app talking to the model (Claude Code, Desktop, your backend). The client is the connector inside the host, one per server. The server is the process that offers tools, resources and prompts. Your own backend can be the MCP client. Practice Q&A ยท Q23 ยท โŒ wrong choice to avoid: thinking only Claude Code can be an MCP host
    why

    The host owns the conversation with the model. Inside it, an MCP client keeps one protocol connection per server, sends discovery and call requests, and gets results. The server is a separate process that actually implements the tools. If you write your own agent loop, your backend is the host and embeds the client; nothing about the roles is specific to Claude Code.

  2. Who starts each primitive? Tools are started by the model, resources by the application, prompts by the user. "Controlled" means who starts it, not who has authority. Sampling is the wrong answer for "application-controlled". Practice Q&A ยท Q23 ยท Practice Q&A ยท Q68 ยท โŒ wrong choice to avoid: swapping the initiators (tools user-chosen, prompts model-invoked)
    why

    Remember one example: the model asks for an order lookup (tool), the host attaches a refund-policy document (resource), the user picks an incident-summary template (prompt). Host permissions still govern all three. Sampling is the server asking for model output, so it isn't application-attached context; reading a document is a resource, not sampling.

  3. What does model-controlled mean? It means the model has the initiative: it decides when to ask for a tool. Running the tool stays with the server, behind your harness's checks. The model never holds the JSON-RPC connection or registers tools. Practice Q&A ยท Q67 ยท โŒ wrong choice to avoid: the model holding the JSON-RPC connection and dispatching tools/call itself
    why

    Claude decides when to propose get_order. The host's client carries the request over the connection it owns, and the server reads the order only after host and server checks pass. The model doesn't run server code, manage the transport, or register and clean up tools; those are host, client and server jobs.

  4. Who runs an MCP tool? The connected MCP server runs it. A tool_use naming mcp__github__create_issue travels as JSON-RPC through the host's client to that server. Not the model, not the client, not Anthropic (only web_search_โ€ฆ server tools run there). Practice Q&A ยท Q61 ยท โŒ wrong choice to avoid: the host's MCP client executing the tool
    why

    The model proposes a call; the host turns it into a tools/call JSON-RPC request; the client carries it; the server runs its create_issue code (maybe calling GitHub's API) and sends the result back through the client. The client is the messenger, not the worker. Anthropic-hosted server tools like web search are a separate route; an ordinary MCP server your host connects to never moves into Anthropic's infrastructure.

  5. Who discovers the tools? The client does: it calls tools/list, then the host filters what the model may see. The server returns inputSchema; the host maps it to Claude's input_schema. tools/call runs a tool; it never lists. Practice Q&A ยท Q62 ยท โŒ wrong choice to avoid: the model discovering each server's tools by calling tools/list
    why

    When it connects, the host's MCP client sends tools/list and gets the server's catalog, each entry with an inputSchema. The host decides which tools the model may see and converts them into tools[]. The model never does discovery and can't pick a tool it wasn't shown. The server advertises its tools; it doesn't discover the host's.

  6. What's the startup order? For stdio, each step needs the one before: host spawns the child process โ†’ initialize with protocolVersion and capability negotiation โ†’ notifications/initialized โ†’ tools/list โ†’ approved tools show up as mcp__tickets__โ€ฆ. Practice Q&A ยท Q66 ยท โŒ wrong choice to avoid: calling tools/list right after spawning, before the initialization handshake
    why

    No process, nothing to talk to. No initialize handshake, no agreed protocolVersion or capabilities. The client sends notifications/initialized before it asks for the catalog. Only then does tools/list return definitions, which the host exposes with prefixes like mcp__tickets__create so two servers can't clash.

  7. Two IDs, two conversations? Yes. tools/call sends params.name and params.arguments and gets back content[] + isError; your adapter maps isError โ†’ is_error. The JSON-RPC id: 3 and toolu_1 are separate matchings that never replace each other. Practice Q&A ยท Q61 ยท โŒ wrong choice to avoid: using the JSON-RPC id as the tool_use_id
    why

    Two exchanges run at once: Claude's, where the tool_use block carries toolu_1, and the MCP server's, where the JSON-RPC request carries id: 3. Your adapter holds both. It sends tools/call, gets the result, and builds a tool_result with tool_use_id: toolu_1 and is_error copied from isError. Only the adapter knows which pairs belong together.

  8. Planner keeps listing catalogs? Then publish the slow-changing catalogs as MCP resources, read by URI with resources/read. A cached tool result still costs a call. The host must read and attach resources itself; they are never injected automatically. โŒ wrong choice to avoid: caching the listing tool results so repeated calls return faster
    why

    Resources are made for this: the server publishes catalogs by URI, and the host reads them and attaches a summary up front. The planner sees what data exists without spending exploration calls. Caching makes the listing tool faster, but the model still has to call it. The host chooses which resources to read and checks access and freshness; large catalogs need an index or a summary.

  9. Sampling vs elicitation? Sampling (sampling/createMessage) is the server asking the client's model for text in the middle of a tool call; it is deprecated in the 2026-07-28 revision. Elicitation is the server asking the user something through the client. Neither one is reading a document. Practice Q&A ยท Q23 ยท โŒ wrong choice to avoid: calling document reading sampling
    why

    With sampling a server could, for example, summarize notes before returning its result. It's a separate request with MCP field names like maxTokens, not access to the outer conversation. Protocol revision 2026-07-28 deprecates it in favor of direct model-provider integration; the tools/resources/prompts mapping stays the same. Reading a document into context is a resource.

  10. Why use MCP at all? MCP turns Nร—M custom integrations into N+M: one server per system, one client per host. It does not make tokens cheaper, inference faster, or replace the Claude API. One app with its own tools โ†’ plain tools[]. Two or more hosts sharing a tool โ†’ MCP. Check community servers before building. Practice Q&A ยท Q22 ยท โŒ wrong choice to avoid: building a custom MCP server with exactly the endpoints the team needs
    why

    Wiring three systems into three apps by hand is nine integrations that drift apart. With a standard protocol, each system exposes one server and each app embeds one client. That's a picture of integration effort, not a promise of anything else. Before building a server for Jira or GitHub, look at maintained community servers and build custom only for team-specific workflows they can't cover.

  11. Forty agents, one tool? 40 agents across 3 hosts need ticketing โ†’ one MCP server, owned by the ticketing team, with a per-agent allowlist. Not copied registries, not full access for everyone, not 40 stdio servers each holding credentials. Practice Q&A ยท Q64 ยท โŒ wrong choice to avoid: a central catalog service with every agent granted the full tool set
    why

    You need reuse and least privilege (each agent gets only what it needs) at the same time, and only a central server with per-agent limits gives both. Forty copied modules are forty places to patch. A catalog that gives everyone every tool ignores access limits. Forty stdio servers multiply credential exposure. Hiding a tool from a list is not backend authorization, so the server still checks permissions when it runs.

Transports, config, scopes & secrets

  1. Local helper for one developer? Use stdio: the host starts the server as a child process and talks JSON over stdin/stdout, with logs on stderr. Not Streamable HTTP, WebSockets, or gRPC. Practice Q&A ยท Q24 ยท โŒ wrong choice to avoid: WebSockets or gRPC (not MCP transports)
    why

    The host pipes JSON-RPC into the server's standard input and reads replies from its standard output. Normal logs must go to stderr, or they corrupt the message stream. The helper can still call a backend over the network. A localhost HTTP server would work but adds a listening endpoint and a separate service nothing here needs; WebSockets and gRPC are not standard MCP transports.

  2. When Streamable HTTP? Use it for remote, shared servers with many clients and OAuth. HTTP doesn't mean remote: local/remote is where the server runs, stdio/HTTP is how messages travel. The old HTTP+SSE transport is a different thing. โŒ wrong choice to avoid: treating HTTP as meaning remote
    why

    Streamable HTTP carries MCP messages over HTTP, with JSON replies or an SSE stream. It fits when many hosts share one server over the network with authentication. Keep the two axes apart: a local server can still be reached over HTTP, and a stdio child can call remote backends. The older HTTP+SSE transport is a separate legacy option, not another name for it.

  3. What does .mcp.json look like? .mcp.json โ†’ mcpServers.<name> with either type: "stdio", command, args or type: "http", url. CLI: claude mcp add --transport stdio --scope project orders -- python3 server.py. npx/uvx launch a package, not an endpoint. โŒ wrong choice to avoid: calling an npx entry a remote endpoint
    why

    A stdio entry says what to start (command plus args); an HTTP entry says where to connect (url). They are two options for one server name, not two registrations. In the CLI, -- separates Claude Code's own options from the command that starts the server. With npx or uvx, the host runs a package as a local child process, so the entry is still stdio even if the package talks to a remote service.

  4. Which scope for a team server? Project scope: .mcp.json, in git, for the whole team. Local is just you in this repo; user (~/.claude.json) is you across all your projects. A team Jira server goes in project .mcp.json, not in each developer's ~/.claude.json. โŒ wrong choice to avoid: adding the team integration to each developer's ~/.claude.json
    why

    Scope decides who gets the server. Local is private to you in one checkout. Project is the version-controlled .mcp.json every clone picks up. User is your personal ~/.claude.json for all your projects. A team integration should be shared and reviewed like code, so it goes in project scope; user scope is for personal or experimental servers and would make every developer set up the team tool by hand.

  5. How to share without the secret? Commit the server in .mcp.json with ${GITHUB_TOKEN} and document the variable in the README. Each developer's own shell supplies the value. Wrong answers: a shared token in .env, a placeholder value, or per-user ~/.claude.json. โŒ wrong choice to avoid: a shared service-account token in a gitignored .env file
    why

    Variable expansion lets you share the tool without sharing the credential: the config is identical for everyone and no secret goes into git. A shared service-account token in .env is a shared secret protected only by a fragile .gitignore. A placeholder value makes each developer override it by hand. Per-user config means clones of the repo don't get the tool at all.

  6. Why does Claude Code ask first? An MCP server is third-party code, so project-scoped servers ask for approval on first use. Their tools show up as mcp__server__tool for allow/deny rules. A .mcp.json connection never sets tools[i].strict. โŒ wrong choice to avoid: thinking .mcp.json can turn on strict
    why

    A .mcp.json checked into a repo can launch any code on a developer's machine, so Claude Code asks before using a project-scoped server the first time. After that, its tools appear as mcp__<server>__<tool>, which is what permission allow and deny rules match. The connection touches no Claude API request fields: strict is set per tool by the app that builds tools[], never by MCP config.

Errors, retries & security

  1. Which error kind? Four kinds, four fixes: transient (timeout, 503, 429 โ†’ retry with backoff), validation (bad input โ†’ fix it, then retry), business (policy โ†’ don't retry, explain), permission (escalate). A generic "Operation failed" is the anti-pattern. โŒ wrong choice to avoid: one generic "Operation failed" message
    why

    Transient clears by itself, so wait and resend the same call. Validation needs corrected input before a new attempt. Business is a rule the system will enforce again, so explain instead of retrying. Permission needs a different person or scope. A generic message hides all four, and the model has to guess, often retrying something that can never work.

  2. Would a retry work? Ask: could the exact same call succeed later? HTTP 503 โ†’ transient, isRetryable: true. Restricted journal โ†’ business, false. Malformed DOI โ†’ validation, false. Don't call everything transient just because each one stopped the tool. โŒ wrong choice to avoid: marking all three failures transient and retryable because each stopped the tool
    why

    A 503 at peak hours can pass later, so it is retryable. A journal you have no licence for will refuse forever; that's a policy limit, so business, not retryable. A DOI that fails the format check fails it every time, so validation, not retryable; a corrected DOI is a new attempt with new input.

  3. Hit a 429? Return errorCategory: "transient", isRetryable: true, retryAfterMs. Not hidden backoff inside the server, not queuing all 50, not business/false. The harness schedules the wait; the error fields only inform it. โŒ wrong choice to avoid: automatic retry with exponential backoff hidden inside the MCP server
    why

    HTTP 429 means the upstream limits your request rate, and that clears after a delay. A generic failure hides this, so the agent gives up on twenty queries that would have worked. With the category, the retry flag and a delay, the agent can space out the rest, prioritize, or report partial progress. Hidden server backoff or queuing the whole batch takes those choices away and blocks the agent. Your application code enforces the bounded wait.

  4. Nothing found, or failed? Keep them apart. No matches โ†’ {"results": [], "isError": false}. Timeout โ†’ isError: true, errorCategory: "transient", isRetryable: true. Never retry a successful empty result. A plain status string is the wrong answer. โŒ wrong choice to avoid: a status string of "empty" versus "failed" instead of the structured error fields
    why

    A lookup that finished and found nothing is a success with zero matches. A timeout is a failure that might work later. If both return an empty array, the agent can't tell them apart: it retries a valid empty result, or reports "nothing found" when it actually hit an outage. Use the protocol's isError flag plus structured category and retry fields, not a homemade status string.

  5. What should a failure report? A subagent's failed query sends back three things: the failure type, the query it tried, and any partial results. Not silent retries, and not "no results found" in place of the error. โŒ wrong choice to avoid: reporting only the attempted query and stopping there
    why

    The coordinator handles errors, so it needs enough to decide. The type tells it whether a retry makes sense. The query lets it retry smartly or rephrase. The partial results stop it from redoing work that already worked. Silent retries hide the failure, and calling a failure an empty result mixes up two different situations. Sending only the query leaves out the type it needs to choose retry or reroute.

  6. Protocol or your convention? Only isError is the MCP field (is_error in Claude). errorCategory, isRetryable, retryAfterMs are your app's conventions. Subagents retry transient errors themselves and pass up only what they can't fix. โŒ wrong choice to avoid: thinking errorCategory is an MCP protocol field
    why

    MCP gives you one error mechanism: isError on the tool result, which your adapter maps to Claude's is_error. The richer fields are names your team picks and puts inside the result content, so their spelling is your choice, not an MCP rule. Split the work: the subagent does limited local retries of transient failures and hands the coordinator only what it can't resolve, with the structured context.

  7. Where did the attack enter? Ask where the bad text came in. "Include API keys" inside a tool description = tool poisoning; inside a tool_result = indirect prompt injection. Defend: vet and pin servers, least-privilege credentials per server, treat descriptions and results as data. Practice Q&A ยท Q25 ยท Practice Q&A ยท Q69 ยท โŒ wrong choice to avoid: calling it a confused-deputy flaw, or indirect injection via a tool_result, when the instruction sat in the description
    why

    A discovered tool isn't automatically trustworthy: its description enters the model's context like any other text, so a hidden instruction there is an attack through tool metadata. The same text in a result is the other channel, indirect prompt injection. Review and pin server versions, give each server only the credentials and actions it needs, keep secrets out of model context, and keep authorization outside the model. Confused deputy and over-broad allowlists describe how much harm follows, not how the text got in.

  8. Tool too broad? Narrow the tool itself. A document worker using fetch_url for searches โ†’ replace it with load_document, which rejects search/query URLs and validates in the backend. Not a prompt sentence, a domain denylist, or routing via the coordinator. Reuse the HTTP client inside. โŒ wrong choice to avoid: a prompt sentence limiting fetch_url to document links, or routing every fetch through the coordinator
    why

    A general fetch tool gives too much power, so the worker will eventually run web searches that belong to the search agent. Least privilege puts the limit at the interface: accept only document IDs or document URLs, and let the backend check the destination, follow redirects safely and limit content size. A prompt sentence is a gamble, a hand-kept denylist has gaps, and coordinator routing adds a round trip while still relying on a model's URL judgment. The shared HTTP library can still sit behind the narrow tool.

  9. Too many round trips? If synthesis goes back to the coordinator for simple checks 85% of the time, give it a scoped verify_fact tool and escalate the complex 15%. Not all web-search tools, not batching, not more coordinator parallelism. โŒ wrong choice to avoid: giving the synthesis agent access to all web-search tools
    why

    Each simple check now costs two or three coordinator round trips; the delay is routing overhead, not coordinator speed. A small read-only verify_fact lets synthesis settle the common lookups itself, while the harder 15% still go through the coordinator to the search agent. All web-search tools would turn it into a search agent; batching delays finishing and holds unchecked claims; more parallelism speeds up trips that shouldn't exist. A narrow tool name isn't authorization, so the backend still checks permissions.

Review sources

Domain 4 lesson and worked explanations ยท Official reference↗

Fact wording rewritten as concrete numbered facts on 25 September 2026. The mock providers' original keys are preserved on their review pages; these recall lines retain the engineering caveats.