Chapter 06 — MCP Integration and Built-in Tools
Domain 2: Tool Design & MCP Integration (18%) — task statements 2.2, 2.3, 2.5
Two halves: how MCP servers are configured and how MCP errors are structured, then the built-in tool selection table. Both halves are heavily fact-based — this is the chapter where exact strings matter most.
⚠️ Scope note: deploying or hosting MCP servers is explicitly out of scope. You will not be asked about containers, networking, or process supervision. You will be asked which config file a server belongs in.
1. MCP server configuration
📎 The two config locations
| File | Scope | Version-controlled? | Use for |
|---|---|---|---|
.mcp.json (project root) |
This project, shared with the team | Yes — commit it | Servers the whole team needs: the company Jira, the internal API, the shared database reader |
~/.claude.json (user home) |
All projects, this user only | No | Personal servers: your own scratch tools, personal accounts |
🎯 The item shape: "the team wants every developer to get the same MCP servers automatically when they clone the repo." → .mcp.json, committed. The distractor is the user-level file, which is per-machine and never shared.
📎 Environment variable expansion
MCP config supports ${VAR} expansion, which is how secrets stay out of committed files:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
},
"jira": {
"command": "npx",
"args": ["-y", "mcp-server-jira"],
"env": {
"JIRA_HOST": "${JIRA_HOST}",
"JIRA_API_TOKEN": "${JIRA_API_TOKEN}"
}
}
}
}
🎯 This is the answer to "how do we commit a shared MCP config without committing credentials?" — commit .mcp.json with ${GITHUB_TOKEN} references; each developer supplies the value in their own environment.
⚠️ Don't overthink this into an out-of-scope answer. Options mentioning secret managers, key rotation, or OAuth flows are outside the blueprint (§4 of the README). The examinable answer is env var expansion.
🎯 Tool discovery timing
Tools from all configured servers are discovered at connection time and are simultaneously available to the agent. Consequences:
- You don't "load" a server mid-conversation. If it's configured, its tools are in the tool list from the start.
- Every configured server adds to the tool count — which ties directly back to Chapter 05 §2. Configuring six MCP servers "just in case" can push an agent from 5 tools to 25 and degrade selection. Server configuration is tool distribution.
🎯 Prefer community servers for standard integrations
For a standard third-party system — the guide's example is Jira — the answer is to use an existing community MCP server rather than writing your own. Building a custom server for a well-supported integration is effort spent re-solving a solved problem.
The inverse also holds: for your proprietary internal systems, a custom server is the only option. The discriminator in the stem is whether the target system is standard/public or internal/proprietary.
📎 MCP resources — for content catalogs
MCP servers expose resources as well as tools. Resources are addressable content the agent can list and read.
🎯 The examinable use: when an agent is making many exploratory tool calls just to find out what content exists, expose a resource catalog instead. Rather than calling search_docs five times to discover what documentation is available, the agent lists resources once and reads the relevant ones.
The pattern: resources for "what exists," tools for "do something."
⚠️ When the agent prefers a built-in over your MCP tool
A recurring situation: you've added an MCP tool for searching your internal knowledge base, but the agent keeps using the built-in Grep instead.
The fix is to enhance the MCP tool's description so it's clear when that tool is the right choice and what it offers that Grep doesn't. This is Chapter 05's Reflex 3 applied inside Domain 2: the agent is choosing by description, and yours isn't winning.
Distractors here: remove Grep from allowedTools (over-restrictive — Grep is genuinely useful for local code), or instruct the model in the system prompt (probabilistic, and fights the descriptions rather than fixing them).
2. MCP error handling
📎 The isError flag
MCP tool results carry an isError flag. A tool result with isError: true tells the agent the call failed, as opposed to succeeding with unexpected content.
🎯 The critical distinction the exam draws: an access failure is not a valid empty result.
| Situation | Correct signal | If you get it wrong |
|---|---|---|
| Query ran, genuinely zero matches | isError: false, empty result set |
— |
| Permission denied / server unreachable / query malformed | isError: true with category |
Agent concludes "there is no data" and reports a confident wrong answer |
⚠️ This is one of the most consequential bugs in agentic systems and it appears on the exam: returning [] for a permission failure makes the agent believe the search space is empty. It will then tell the user there are no matching orders, no relevant documents, no such customer — with no indication anything went wrong. Never encode a failure as an empty success.
🎯 The four-category error taxonomy
Memorize these four and their retryability:
| Category | Meaning | Retryable? |
|---|---|---|
| Transient | Timeout, temporary unavailability, rate limit from the backend | Yes — retry with backoff |
| Validation | Malformed input: bad ID format, missing required field | Yes, after correction — the agent can fix the input and retry |
| Business | The operation is legitimately disallowed by policy: refund window expired, account closed | No — retriable: false. Retrying will never succeed. |
| Permission | The caller lacks authorization for this resource | No — needs escalation or different credentials, not a retry |
📎 Structured error metadata
An error crossing a boundary should be structured, not a string:
{
"isError": true,
"errorCategory": "business",
"isRetryable": false,
"message": "Refund window expired. This order shipped 94 days ago; the refund policy covers 90 days from shipment.",
"attempted": "process_refund(order_id=ORD-10023841, amount=249.00)",
"alternatives": ["escalate_to_human for policy-exception review"]
}
Each field earns its place:
errorCategory— lets the agent choose a strategy class rather than guessing from prose.isRetryable/retriable: false— stops the agent from retrying a business rule forever. 🎯 Business-rule violations must be marked non-retryable; a retry loop against a policy is a named failure.- human-readable description — so the agent can explain the situation to the user accurately, including why.
- what was attempted and alternatives — so the agent's next move is informed.
⚠️ The distractor: a generic "search unavailable" or "operation failed" string. It hides the category, the retryability, and the cause, and forces the agent to guess. Generic errors are a design defect.
🎯 Local recovery, then propagate with partial results
The rule for errors inside a multi-agent system:
- Recover locally where possible. A subagent that hits a transient failure retries within itself. It does not bother the coordinator with problems it can solve.
- Propagate only what it cannot resolve — and propagate it with partial results, structured context, and suggested alternatives.
Two symmetric anti-patterns, both of which appear as options:
| 🚫 Anti-pattern | Why it's wrong |
|---|---|
| Silently suppressing the error and returning success | Downstream agents build on a false premise; the final output is confidently incomplete with no indication |
| Terminating the entire workflow because one subagent failed | Discards all the successful work; three of four research streams completed fine |
The correct behaviour is the middle path: complete what you can, report precisely what you couldn't, and annotate the gap in the output. In Scenario 3, that means the synthesis agent's report carries coverage annotations — "regulatory filings could not be retrieved (permission denied); the following sections draw only on press coverage."
3. Built-in tools — the selection table
Task statement 2.5. Scenario 4 (Developer Productivity) is built on this. Pure recall; know it exactly.
| 📎 Tool | Purpose | Use when |
|---|---|---|
| Grep | Search file contents | You know roughly what the code says: a function name, a string literal, an import |
| Glob | Match file paths by pattern | You know the naming shape: **/*.test.tsx, src/**/*.py |
| Read | Read a full file | You need the whole file's content and structure |
| Write | Write a full file | Creating a new file, or replacing one wholesale |
| Edit | Targeted change via unique text match | Changing a specific known passage inside a larger file |
| Bash | Run shell commands | Builds, tests, git, anything with a CLI |
🎯 Grep vs Glob
The single most likely built-in-tools item. Grep searches inside files; Glob matches filenames.
- "Find every file that calls
processPayment" → Grep - "Find every test file in the project" → Glob with
**/*.test.tsx
⚠️ If the stem gives you a path pattern — an extension, a directory shape, a filename convention — the answer is Glob, even though grepping for the same string might also work. The tool that matches the intent wins.
🎯 Edit vs Read + Write
Edit requires a unique text anchor. It locates the passage by exact match and replaces it. If the string you want to change appears more than once, Edit cannot disambiguate and will fail.
The fallback is Read + Write: read the whole file, construct the modified content, write it back. This is the documented answer for when Edit's anchor isn't unique — for example, changing the third occurrence of a repeated boilerplate block, or reformatting a file where the target text is inherently repetitive.
| Situation | Tool |
|---|---|
| Change one clearly unique passage | Edit |
| Target text appears multiple times, no unique surrounding context | Read + Write |
| Create a new file | Write |
| Restructure most of a file | Read + Write |
⚠️ Don't reflexively answer Edit because it's the more precise tool. The exam tests whether you know its precondition.
🎯 Incremental codebase understanding
The pattern for exploring an unfamiliar codebase (Scenario 4's core activity):
1. Grep for entry points → find main(), route definitions, exported handlers
2. Read those files → understand structure and follow the imports
3. Grep / Glob the imports → locate the next layer
4. Repeat, narrowing → build understanding incrementally
Grep to locate, Read to follow. The point is that you never load the whole codebase — you traverse it. 🧠 This connects to Domain 5: uncontrolled exploration floods context, which is why the Explore subagent exists (Chapter 08 §2) to isolate verbose discovery from the main session.
4. Worked examples
Example 1 — error signaling
A research subagent queries an internal document repository. When its credentials lack access to a collection, the MCP server returns an empty result array with
isError: false. The final report states that no relevant internal documents exist. What is the primary problem?A. The subagent should retry the query with different parameters. B. The server is reporting an access failure as a valid empty result. C. The coordinator should have assigned a different subagent. D. The report generator should note when sections have no sources.
Answer and reasoning
B.
The permission failure is being encoded as a successful search that found nothing. Every downstream conclusion is built on a false premise, and nothing in the system can detect the difference. The server must set isError: true with errorCategory: "permission" and isRetryable: false.
- A — Retrying won't help: the failure is authorization, not query formulation. And the subagent doesn't know there was a failure to retry.
- C — A different subagent with the same credentials hits the same wall.
- D — A reasonable additional safeguard (coverage annotation, Chapter 12 §4), but it treats the symptom. With
isError: false, "no sources" and "no access" are indistinguishable, so the annotation would be wrong too.
Reflex applied: #6. Distinguish access failures from valid empty results; never encode failure as success.
Example 2 — built-in selection
An agent must locate all React test files across a large monorepo where tests live alongside their components in many directories. Which built-in tool is most appropriate?
A. Glob with the pattern
**/*.test.tsxB. Grep for the stringdescribe(C. Read on each directory's index file D. Bash with afindcommand
Answer and reasoning
A.
The task is defined by a filename pattern spread across directories — exactly Glob's purpose. **/*.test.tsx matches at any depth in one call.
- B — Grep searches contents. It would find test files, but also any file mentioning
describe(in a comment or utility, and it would miss test files using a different framework idiom. Wrong tool for a path-shaped question. - C — Doesn't scale and depends on index files enumerating tests, which they don't.
- D — Works, but reaches for the shell when a purpose-built tool exists. On this exam, the dedicated tool beats shelling out.
Reflex applied: path pattern → Glob; content → Grep.
5. Decision reflexes (reproduce from memory)
.mcp.json(project, committed, team-shared) vs~/.claude.json(user, personal, not shared).- Secrets in shared config →
${ENV_VAR}expansion. Not secret managers, not OAuth — those are out of scope. - All configured servers' tools are discovered at connection time and are simultaneously available → every server you add is tool-count pressure.
- Standard integration (Jira) → community MCP server. Proprietary internal system → custom server.
- Many exploratory calls just to discover what exists → MCP resources as a content catalog.
- Agent prefers built-in
Grepover your MCP tool → improve the MCP tool's description, don't removeGrep. isError: truefor failures. An access failure is not an empty result — that bug makes the agent confidently report "nothing exists."- Error categories: transient (retry) · validation (correct, then retry) · business (
retriable: false) · permission (escalate, don't retry). - Structured error = category + retryability + human description + what was attempted + alternatives. Generic "operation failed" is a defect.
- Recover locally; propagate only the unresolvable, with partial results. Never suppress silently; never kill the whole workflow.
- Grep = contents · Glob = paths · Read/Write = whole file · Edit = unique text anchor · Bash = commands.
- Edit needs a unique anchor; when it isn't unique, fall back to Read + Write.
- Explore a codebase with Grep for entry points → Read to follow imports, incrementally.