Chapter 6 of 8
Domain 4: tool design and MCP integration
Domain 4 is the most underestimated on the exam, and its central sentence is that the model selects which tool to call based on the description, not the name and not the input schema. A minimal description like "analyzes a document" produces unpredictable behavior when two tools overlap, while a rich description that states what the tool returns, what formats it accepts, when to use it versus alternatives, and what it does not do produces precise selection. When Claude Code has a built-in tool that overlaps with a custom MCP tool, the model may prefer the built-in one, and the fix is to strengthen the MCP description by naming the concrete advantage it offers. The tool_choice parameter controls selection: auto lets the model decide, any forces some tool call when you need guaranteed structured output, and a specific tool name forces a guaranteed first step. Transport selection is simple: stdio for a local subprocess on the same machine, SSE for remote or cloud servers across a network. Team-wide MCP servers go in the committed project .mcp.json, personal ones in the untracked user config. And error handling is where candidates lose points: an MCP tool signals failure with isError true, and a structured error with errorCategory, isRetryable, partial_results, and alternatives lets the coordinator recover, while a generic "operation failed" leaves it stuck, and returning an empty result is worse because it reads as no matches found. In this lab you score a rich description against a minimal one, select the transport, map tool_choice, and prove the structured error enables recovery.
The lab: read it, then run it
#!/usr/bin/env python3
"""
LAB CF6: Tool design and MCP integration (Domain 4, 18%).
The model selects a tool by its DESCRIPTION, not its name or schema. This lab
scores a rich description against a minimal one and shows only the rich one
lets the model disambiguate two similar tools. It then picks the right MCP
transport (stdio local, SSE remote), maps tool_choice strategies, and proves a
structured isError response lets a coordinator recover where a generic
"operation failed" leaves it stuck.
Deterministic, offline, standard library only.
Run: python3 modules/academy-content/labs/cca-f/cf6-tool-design.py
"""
import sys
# STEP 1: description-driven selection. Two tools with similar names. A rich
# description names what it returns, what formats it takes, and when to use it,
# so a keyword-scoring selector can tell them apart. A minimal one cannot.
TASK = "analyze a pdf document for financial figures and tables"
RICH = {
"name": "analyze_document",
"description": ("analyze a pdf or docx file for structure entities and "
"financial figures returns json tables and numeric values"),
}
MINIMAL = {"name": "analyze_document", "description": "analyzes a document"}
OTHER = {"name": "extract_line_items", "description": "extract line items from a parsed invoice"}
def score(desc: str, task: str) -> int:
task_words = set(task.split())
return len(task_words & set(desc.split()))
def select(candidates, task):
return max(candidates, key=lambda c: score(c["description"], task))["name"]
rich_pick = select([RICH, OTHER], TASK)
rich_score = score(RICH["description"], TASK)
minimal_score = score(MINIMAL["description"], TASK)
other_score = score(OTHER["description"], TASK)
print("STEP 1: the model selects by description")
print(f" rich desc score : {rich_score} -> selects {rich_pick}")
print(f" minimal desc score : {minimal_score} (weak, near the sibling tool)")
print(f" sibling tool score : {other_score}")
# rich description wins decisively: it scores far above both the minimal version
# of itself and the competing sibling tool, so selection is unambiguous.
desc_ok = (rich_pick == "analyze_document"
and rich_score > other_score
and rich_score > minimal_score)
# STEP 2: transport selection. stdio for local subprocess, SSE for remote.
def transport_for(location: str) -> str:
return "stdio" if location == "local" else "sse"
transport_ok = (transport_for("local") == "stdio" and transport_for("cloud") == "sse")
print("")
print("STEP 2: MCP transport")
print(f" local subprocess -> {transport_for('local')}")
print(f" cloud/remote -> {transport_for('cloud')}")
# STEP 3: tool_choice strategy.
def tool_choice_for(need: str) -> str:
return {
"model decides": "auto",
"guarantee structured output": "any",
"force a specific first step": "tool",
}[need]
choice_ok = (tool_choice_for("model decides") == "auto"
and tool_choice_for("guarantee structured output") == "any"
and tool_choice_for("force a specific first step") == "tool")
print("")
print("STEP 3: tool_choice")
for need in ("model decides", "guarantee structured output", "force a specific first step"):
print(f" {need:<30} -> {tool_choice_for(need)}")
# STEP 4: structured isError vs generic error. Only the structured one lets the
# coordinator make an autonomous recovery decision.
generic = {"isError": True, "content": "operation failed"}
structured = {
"isError": True,
"content": {
"errorCategory": "transient", "isRetryable": True,
"message": "search timed out after 30s",
"partial_results": [{"title": "AI Music 2025", "relevance": 0.8}],
"alternative_approaches": ["try a narrower query"],
},
}
def can_recover(err) -> bool:
c = err["content"]
return isinstance(c, dict) and "isRetryable" in c and "errorCategory" in c
print("")
print("STEP 4: structured error enables recovery")
print(f" generic error -> coordinator can recover? {can_recover(generic)}")
print(f" structured error -> coordinator can recover? {can_recover(structured)}")
error_ok = (not can_recover(generic)) and can_recover(structured)
# Silent suppression check: returning [] on failure is read as 'no matches', a
# different and misleading condition. isError must be set.
silent = []
suppression_is_wrong = (silent == [] and not structured.get("content") == silent)
ok = desc_ok and transport_ok and choice_ok and error_ok and suppression_is_wrong
print("")
print(f" rich description drives correct selection : {desc_ok}")
print(f" transport chosen by locality : {transport_ok}")
print(f" tool_choice mapped correctly : {choice_ok}")
print(f" structured isError enables recovery : {error_ok}")
print("")
print(f"TOOL DESIGN RULES HOLD (rich desc + transport + tool_choice + isError): {'YES' if ok else 'NO'}")
if not ok:
sys.exit(1)
print("Write descriptions that select, stdio local SSE remote, structured errors that recover. Next: context and reliability.")
Runnable lab
cf6-tool-design.pyScore rich versus minimal tool descriptions, select the transport, map tool_choice, and prove a structured isError enables recovery.
Proves: TOOL DESIGN RULES HOLD (rich desc + transport + tool_choice + isError): YES
Runs in your browser via Pyodide. First run loads the runtime once; no install, no server.
The rich description scored far above both the minimal version and the competing sibling tool, so selection was decisive rather than a coin flip, and that is the whole point: descriptions are the selection mechanism. The transport router sent the local subprocess to stdio and the cloud server to SSE. And only the structured error let the coordinator decide to retry, because it carried the category, the retryable flag, and partial results, where the generic string carried nothing. Remember the tools-versus-resources distinction too: tools perform actions with side effects, resources provide read-only context like a schema or a task list so the agent does not have to explore with tool calls. Next you close the domains with context management and reliability.
Check your understanding
1. What does the model use to decide which tool to call?
2. An MCP server runs as a subprocess on the same machine as the host. Which transport?
3. How should an MCP tool report a failure so the coordinator can recover?
4. Which tool_choice value guarantees the model returns a tool call rather than text?