# D5 · Tools and MCPs

Tool-use lifecycle, tool descriptions and schemas, tool_choice, parallel tools, client- vs server-side tools, tool search, MCP fundamentals, authoring MCP servers, and configuring MCP in Claude Code and the Messages API.

import { Accordions, AccordionItem, Tabs, TabItem } from '@prosefly/astro-components';

This domain is roughly **5 of 53 items**. It tests whether you can define tools well (the description is the single most important lever), run the tool-use lifecycle correctly, choose between client- and server-side tools, and understand and author MCP servers. The theme: **tools are how Claude acts on the world – describe them precisely and connect them safely.**

## Learning objectives

By the end of this page you should be able to:

1. Run the **tool-use / function-calling lifecycle** with correct JSON.
2. Write effective **tool descriptions** and design **`input_schema`**.
3. Use **`tool_choice`** (`auto`/`any`/`tool`/`none`) and know the **Fable 5.1** restriction.
4. Use **parallel tool calls** and distinguish **client-side vs server-side** tools.
5. Use **tool search + `defer_loading`**, **programmatic tool calling**, and **approval patterns**.
6. Explain **MCP fundamentals** and author an MCP server in **Python (FastMCP)** and **TypeScript**.
7. Configure MCP in **Claude Code / Desktop** and via the **MCP connector** in the Messages API.

---

## 5.1 The tool-use lifecycle

<Tabs>
  <TabItem label="1. Define tools">
```json
{
  "tools": [{
    "name": "get_weather",
    "description": "Get the current weather for a city. Use when the user asks about weather, temperature, or conditions in a named location.",
    "input_schema": {
      "type": "object",
      "properties": {
        "city": {"type": "string", "description": "City name, e.g. 'Paris'"},
        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"}
      },
      "required": ["city"]
    }
  }]
}
```
  </TabItem>
  <TabItem label="2. Claude requests">
```json
{
  "stop_reason": "tool_use",
  "content": [
    {"type": "text", "text": "Let me check the weather."},
    {"type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": {"city": "Paris"}}
  ]
}
```
  </TabItem>
  <TabItem label="3. Return result">
```json
{
  "role": "user",
  "content": [
    {"type": "tool_result", "tool_use_id": "toolu_01", "content": "18C, partly cloudy"}
  ]
}
```
Then call the API again; Claude produces the final `end_turn` answer.
  </TabItem>
</Tabs>

Errors are returned with `"is_error": true` in the `tool_result` so Claude can recover.

### A complete round-trip on the wire

The four Tabs above are the four wire messages of one round-trip. Read them as a single conversation so the `tool_use_id` threading is unmistakable. The request declares tools; Claude answers with `stop_reason: 'tool_use'`; you run the function and send a `tool_result` referencing the same id; Claude produces the `end_turn` answer.

```python
import anthropic
client = anthropic.Anthropic()

TOOLS = [{
    "name": "get_weather",
    "description": "Get current weather for a city. Use when the user asks about "
                   "weather, temperature, or conditions in a named location. "
                   "Returns a short text summary. Read-only, no side effects.",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "City name, e.g. 'Paris'"},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"},
        },
        "required": ["city"],
    },
}]

messages = [{"role": "user", "content": "What's the weather in Paris?"}]

# 1. Model turn -> Claude asks for the tool
resp = client.messages.create(
    model="claude-sonnet-5", max_tokens=1024, tools=TOOLS, messages=messages,
)
assert resp.stop_reason == "tool_use"          # NEVER parse text to decide this
messages.append({"role": "assistant", "content": resp.content})

# 2. You run the tool for each tool_use block
for block in resp.content:
    if block.type == "tool_use":
        result = fetch_weather(**block.input)   # your real function
        messages.append({"role": "user", "content": [{
            "type": "tool_result",
            "tool_use_id": block.id,            # MUST match the originating id
            "content": result,
        }]})

# 3. Model turn again -> Claude gives the final answer
final = client.messages.create(
    model="claude-sonnet-5", max_tokens=1024, tools=TOOLS, messages=messages,
)
assert final.stop_reason == "end_turn"
print(final.content[0].text)
```

:::caution[Loop on `stop_reason`, not on text]
The canonical agent loop is `while stop_reason == 'tool_use': run tools; call again`. Parsing the assistant's prose to decide whether to continue is anti-pattern #1. When a tool fails, return `{"type": "tool_result", "tool_use_id": id, "content": "Error: timeout", "is_error": true}` so Claude can retry or apologise – do not silently drop the result (anti-pattern #7).
:::

---

## 5.2 Tool descriptions – the most important lever

The **description** is the primary determinant of whether Claude uses a tool correctly. Invest here more than anywhere else. A good description reads like the docstring you would write for a colleague who has never seen the function.

### Good vs bad tool descriptions – four pairs

| Bad | Good |
| --- | --- |
| `get_data` — 'Gets data' | `get_order` — 'Retrieve one order by its ID. Use when the user references a specific order number. Returns status, line items and totals. Read-only.' |
| `search` — 'Search' | `search_kb` — 'Full-text search of the help-centre knowledge base. Use for how-to and policy questions, NOT for live account data. Returns up to 5 article snippets with URLs.' |
| `update` — 'Updates a record' | `update_shipping_address` — 'Change the shipping address on an unshipped order. Fails if the order has shipped. Side effect: writes to the orders DB. Confirm with the user first.' |
| `run` — 'Runs a query' | `run_sql_readonly` — 'Execute a read-only SELECT against the analytics warehouse. Rejects INSERT/UPDATE/DELETE. Use for reporting questions. Returns at most 1000 rows as JSON.' |

Each good description answers four questions: *what* it does, *when* to use it (and when not), *what it returns*, and *what side effects / irreversibility* it carries.

| Good description | Poor description |
| --- | --- |
| States *what* the tool does, *when* to use it, *when not to*, and what it returns | 'Gets data' |
| Documents each parameter with type, meaning and example | Undocumented params |
| Notes side effects and irreversibility | Silent on side effects |

:::tip[Exam signal]
"Claude picks the wrong tool / calls it with bad arguments" → improve the tool **description** and `input_schema` (types, enums, `required`, examples). Reach for the description before touching temperature or the model.
:::

---

## 5.3 `input_schema` design checklist

The schema is JSON Schema. It constrains what Claude may pass and, with `strict: true`, guarantees the shape. Design it deliberately.

- **Types** — give every property a `type`. Use the narrowest type (`integer` over `number` when whole).
- **Enums** — constrain closed sets with `enum` so Claude cannot invent a value: `{"type": "string", "enum": ["celsius", "fahrenheit"]}`.
- **`required`** — list every argument the tool genuinely needs. Omitted-but-required arguments are a top cause of malformed calls.
- **Descriptions per property** — describe each field, with an example. Claude reads these.
- **Nullable / optional** — optional params simply omit from `required`; to allow an explicit null, use `{"type": ["string", "null"]}`.
- **Bounds and formats** — `minimum`, `maximum`, `pattern`, `format: 'date'` reduce garbage input.
- **`strict: true`** — enable strict schema adherence so outputs conform exactly (also a way to get structured behaviour on Fable 5.1 where forced tools 400).

```json
{
  "name": "create_ticket",
  "description": "Open a support ticket. Use only after the user confirms they want one.",
  "input_schema": {
    "type": "object",
    "properties": {
      "priority": {"type": "string", "enum": ["low", "normal", "high", "urgent"],
                   "description": "Urgency; default to 'normal' unless the user signals otherwise"},
      "summary":  {"type": "string", "description": "One-line summary, e.g. 'Cannot log in'"},
      "due_date": {"type": ["string", "null"], "format": "date",
                   "description": "ISO date, or null if none"}
    },
    "required": ["priority", "summary"]
  },
  "strict": true
}
```

---

## 5.5 `tool_choice`

| Value | Behaviour | Notes |
| --- | --- | --- |
| `auto` | Claude decides whether to use a tool (default) | Best general default; combine with a clear instruction |
| `any` | Claude must use one of the tools | Forces *some* tool; **400 on Fable 5.1** |
| `{type: 'tool', name: '…'}` | Force one specific tool | Deterministic single-tool extraction; **400 on Fable 5.1** |
| `none` | Disable tools for this turn | Model answers from context only |

:::danger[Fable 5.1 restriction]
On Fable 5.1, **`tool_choice: 'any'` and forced `{type: 'tool'}` return `400`.** Use `auto` with a clear instruction, `strict: true` schemas, or structured outputs (`output_config.format`). This is a client error — do not retry it with backoff.
:::

---

## 5.6 Parallel tool calls

When subtasks are independent, Claude can emit **multiple `tool_use` blocks in one response**. Execute them concurrently and return all `tool_result` blocks in the next `user` message (matching each `tool_use_id`). If the calls are *dependent* (result of one feeds the next), Claude will serialise them across turns instead.

```python
tool_uses = [b for b in resp.content if b.type == "tool_use"]
results = run_concurrently(tool_uses)     # independent tools in parallel
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": [
    {"type": "tool_result", "tool_use_id": u.id, "content": r}
    for u, r in zip(tool_uses, results)]})   # ALL results in ONE user turn
```

:::caution[Return every result, once]
You must return exactly one `tool_result` per `tool_use` block, in the same user turn, each keyed to its `tool_use_id`. Missing or duplicated results cause a 400. To discourage parallelism when tools are stateful, set `disable_parallel_tool_use: true` in `tool_choice`.
:::

---

## 5.7 Client-side vs server-side tools

| Type | Runs where | Examples |
| --- | --- | --- |
| **Client-side** (custom) | Your code | Your APIs, DB queries, business functions |
| **Server-side** (Anthropic-hosted) | Anthropic | Web search, code execution, computer use, text editor, bash, memory |

Server-side tools are enabled by declaring them; Anthropic executes them and may pause the turn (`pause_turn`) while running.

### Server-side built-in tools

| Tool | What it does | Typical use |
| --- | --- | --- |
| **Web search** | Live web queries with citations | Current events, facts past the knowledge cutoff |
| **Code execution** | Runs code in a sandbox | Data analysis, calculation, plotting |
| **Text editor** | Views and edits files via a str-replace interface | Multi-file code edits |
| **Bash** | Runs shell commands in a sandbox | Build/test/install steps in agentic coding |
| **Memory** | Cross-session persistent store | Remembering user preferences across sessions |
| **Computer use** | Screenshots + mouse/keyboard control | Operating a GUI when no API exists |

:::tip[Exam signal]
"Search the live web / run code in a sandbox / edit files / operate a computer / remember across sessions" → server-side built-in tools. "Call our internal API / database / business function" → client-side custom tool.
:::

---

## 5.8 Tool search, defer_loading and programmatic calling

- Beyond **~10 tools**, use the **tool search tool** with **`defer_loading: true`** so the full tool definitions are loaded on demand rather than all upfront — reduces context bloat and wrong-tool selection (anti-pattern #8).
- **Programmatic tool calling** lets code invoke tools directly in a controlled loop.
- **Approval patterns**: require human/hook approval before irreversible tool actions (see hooks, Domains 3 and 6).

```python
# Large catalogue: defer full definitions, expose the search tool.
tools = [
    {"type": "tool_search_tool_20250000", "name": "tool_search"},  # lets Claude find tools on demand
    {"name": "get_order",     "description": "...", "input_schema": {...}, "defer_loading": True},
    {"name": "issue_refund",  "description": "...", "input_schema": {...}, "defer_loading": True},
    # ...50 more deferred tools; only the ones Claude searches for are hydrated into context
]
resp = client.messages.create(model="claude-sonnet-5", max_tokens=2048,
                              tools=tools, messages=messages)
```

The pattern keeps the active tool set small (4–5 in context at a time) even when the catalogue is large, which is exactly the fix for anti-pattern #8.

---

## 5.9 MCP fundamentals

The **Model Context Protocol** is an open standard (JSON-RPC 2.0) for connecting Claude to external tools and data.

| Primitive | Controlled by | Example |
| --- | --- | --- |
| **Tools** | Model | Callable functions |
| **Resources** | Application | Files, records the app exposes |
| **Prompts** | User | Reusable prompt templates |

- **Transports**: `stdio` (local subprocess) and **Streamable HTTP** (remote; SSE is legacy).
- **Capability negotiation** happens on `initialize`.
- **OAuth 2.1** secures remote servers.

```text
Host (Claude Desktop / Code)
   │  JSON-RPC 2.0
   ├── stdio ──► local MCP server (subprocess)
   └── Streamable HTTP ──► remote MCP server (OAuth 2.1)
```

---

## 5.10 Authoring an MCP server

A minimal but *complete* server: it names itself, declares one tool with a typed schema and a docstring description, handles its own errors, and runs over stdio. Both SDKs below are runnable as-is.

<Tabs>
  <TabItem label="Python (FastMCP)">
```python
# weather_server.py — run: python weather_server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather")

@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> str:
    """Get current weather for a city.

    Use when the user asks about weather, temperature or conditions in a
    named location. Returns a short text summary. Read-only, no side effects.
    """
    if not city:
        raise ValueError("city is required")
    return fetch_weather(city, unit)

@mcp.resource("config://units")
def default_units() -> str:
    """Application-controlled resource: the default unit system."""
    return "celsius"

if __name__ == "__main__":
    mcp.run()   # stdio transport by default; use mcp.run(transport='streamable-http') for remote
```
  </TabItem>
  <TabItem label="TypeScript">
```typescript
// weather_server.ts — run: node weather_server.js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({ name: 'weather', version: '1.0.0' });

server.tool(
  'get_weather',
  'Get current weather for a city. Use when asked about weather in a named location. Read-only.',
  { city: z.string().describe('City name, e.g. Paris'),
    unit: z.enum(['celsius', 'fahrenheit']).default('celsius') },
  async ({ city, unit }) => {
    if (!city) throw new Error('city is required');
    return { content: [{ type: 'text', text: await fetchWeather(city, unit) }] };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);   // for remote: StreamableHTTPServerTransport
```
  </TabItem>
</Tabs>

---

## 5.11 Configuring MCP

<Tabs>
  <TabItem label="Claude Code (CLI)">
```bash
# stdio (local subprocess)
claude mcp add weather --scope project -- python weather_server.py

# HTTP (remote server)
claude mcp add --transport http docs https://mcp.example.com/mcp --scope user

# list / remove
claude mcp list
claude mcp remove weather
```
Scopes: `local` (this machine, private), `project` (shared via `.mcp.json`, committed), `user` (all your projects).
  </TabItem>
  <TabItem label=".mcp.json (project)">
```json
{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["weather_server.py"],
      "env": { "WEATHER_API_KEY": "${WEATHER_API_KEY}" }
    },
    "docs": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${DOCS_TOKEN}" }
    }
  }
}
```
The `stdio` server uses `command`/`args`; the remote server uses `type: 'http'` + `url`. Secrets come from env vars — never hard-code them here.
  </TabItem>
  <TabItem label="Messages API connector">
The **MCP connector** lets the Messages API call a remote MCP server directly, so a hosted server's tools are available to the model without you re-declaring each tool.

```python
resp = client.beta.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Search the docs for retry policy."}],
    mcp_servers=[{
        "type": "url",
        "url": "https://mcp.example.com/mcp",
        "name": "docs",
        "authorization_token": "Bearer …",   # OAuth 2.1 token for the remote server
    }],
    betas=["mcp-client-2025-04-04"],
)
```
  </TabItem>
</Tabs>

---

## 5.12 MCP vs custom tool vs Skill – decision table

These three ways of extending Claude are frequently confused on the exam.

| Choose | When | Runs / lives | Controlled by |
| --- | --- | --- | --- |
| **Custom tool** (Messages API) | You need one or a few functions for a single app; full control of execution | Your code, per request | Your API integration |
| **MCP server** | You want reusable tools/resources shared across apps, hosts (Code, Desktop, API) and teams | A separate process (stdio/HTTP), reused everywhere | An open protocol |
| **Skill** | You want to package *instructions + files + scripts* that load progressively on demand in Claude Code | `.claude/skills/<name>/SKILL.md`, loaded when relevant | A markdown file with frontmatter |

Rule of thumb: **one app, few functions → custom tools; reuse across hosts/teams → MCP; packaged know-how and helper scripts for Claude Code → Skill.**

:::tip[Exam signal]
"Share the same tools across Claude Code, Desktop and our API" → MCP. "Bundle a workflow with reference files and helper scripts that Claude loads only when needed" → Skill. "Just call our one internal endpoint from this service" → custom tool.
:::

---

## 5.13 MCP transports, capability negotiation and OAuth 2.1

The wire details of MCP recur in exam items. MCP is **JSON-RPC 2.0**; a host and server negotiate capabilities on `initialize`, then exchange tool/resource/prompt calls over a transport.

| Aspect | stdio | Streamable HTTP |
| --- | --- | --- |
| Where the server runs | Local subprocess | Remote endpoint |
| Auth | Local process trust / env | **OAuth 2.1** |
| Best for | Local dev tools, Desktop/Code plugins | Shared/team servers, the Messages API connector |
| Legacy note | — | SSE-only transport is legacy; prefer Streamable HTTP |

```text
Host ──initialize──► Server         (capability negotiation: tools? resources? prompts?)
Host ◄─capabilities─ Server
Host ──tools/list───► Server         (discover model-controlled tools)
Host ──tools/call───► Server         (Claude invokes a tool)
Host ──resources/read► Server        (application-controlled data)
Host ──prompts/get──► Server         (user-controlled templates)
```

:::tip[Exam signal]
'Local subprocess tool' → **stdio**. 'Remote/shared server, secure it' → **Streamable HTTP + OAuth 2.1**. 'Which primitive does the model decide to call?' → **tools** (resources = application, prompts = user). Capability negotiation happens on `initialize`.
:::

---

## 5.14 Approval and human-in-the-loop for irreversible tools

Some tools are irreversible (refunds, deletes, sends, deploys). The correct design gates them deterministically, not by trusting the model.

| Control | Mechanism | Notes |
| --- | --- | --- |
| **Allowlist** | Only expose the tools the task needs | Least privilege; keep the irreversible ones out of routine agents |
| **PreToolUse hook** | Block (exit 2) and route to approval | The model cannot argue past it (anti-pattern #3) |
| **Approval workflow** | Human sign-off before execution | For payments/deletes/deploys |
| **`is_error` on results** | Report tool failures back so Claude recovers | Never drop a result silently (anti-pattern #7) |

```python
# Gate an irreversible tool behind approval; return is_error so Claude can adapt.
if block.name == "issue_refund" and block.input["amount_cents"] > 50000:
    result = {"type": "tool_result", "tool_use_id": block.id,
              "content": "Refund over $500 requires human approval; request queued.",
              "is_error": True}
```

:::caution[Approval is enforcement, not a prompt]
'Ask the user before refunding' in the tool description is guidance the model may skip. The guarantee comes from a hook/approval gate plus keeping the tool out of the allowlist where it is not needed.
:::

---

## 5.15 Common misconceptions

| Misconception | Reality | Why it matters on the exam |
| --- | --- | --- |
| A vague description is fine if the schema is tight | The description is the primary lever for correct tool use | Wrong-tool/bad-args questions |
| `tool_choice: 'any'` works everywhere | Fable 5.1 rejects `any`/forced tools (400) | A recurring breaking-change trap |
| A 400 on forced tool choice is transient | It is deterministic; change the request, do not backoff | Distinguishes layers |
| Web search is a tool you implement | Web search and code execution are server-side built-ins | Client vs server-side sorting |
| Resources are model-controlled like tools | Resources are application-controlled; prompts are user-controlled | MCP primitive mapping |
| Parallel tool results can span multiple turns | All results go in one user turn, each keyed to its `tool_use_id` | Protocol-shape 400s |
| More tools make selection better | Beyond ~10, selection degrades; use tool search + `defer_loading` | Anti-pattern #8 |
| A Skill can share tools across hosts | Skills are Claude-Code packaged know-how; cross-host reuse is MCP | Skill/MCP/tool decision |

---

## 5.16 Scenario walkthrough: a payments tool that must be safe and portable

**Scenario.** A fintech wants Claude to look up orders, search a help centre, and — carefully — issue refunds, and they want the *same* tools available in Claude Code, Claude Desktop and a production Messages API service, maintained in one place. They are on Fable 5.1 for its reasoning quality. Early attempts force the refund tool with `tool_choice: {type: 'tool', name: 'issue_refund'}` (getting 400s), sometimes drop a tool result when two tools run in parallel (getting 400s), and issue refunds with no approval step.

**Expert reasoning trace.**

1. **Choose the reuse mechanism.** 'Same tools across Code, Desktop and the API, one place' → build an **MCP server** and connect each host (stdio locally, Streamable HTTP for the shared/remote server secured with **OAuth 2.1**; the Messages API uses the MCP connector). Copying custom-tool JSON into each app is the maintenance trap.
2. **Fix the Fable 5.1 400s on tool choice.** Fable 5.1 **rejects forced/`any` tool choice**. Use `tool_choice: 'auto'` with a clear instruction, or structured outputs / `strict: true` for the extraction parts. Retrying the 400 with backoff is wrong — it is deterministic.
3. **Fix the parallel-result 400s.** Return **exactly one `tool_result` per `tool_use` block, all in the same following user turn**, each keyed to its `tool_use_id`; on failure return `is_error: true` so Claude can recover. Dropping one result causes the 400.
4. **Make refunds safe.** Refunds are irreversible → gate them with a **PreToolUse hook / approval workflow** and keep the refund tool out of any routine read-only agent's allowlist. 'Confirm before refunding' in the description is not enforcement.
5. **Right-size the catalogue.** Keep ~4–5 active tools; if the catalogue grows, use **tool search + `defer_loading`**.
6. **Reject the tempting alternatives.** 'Force the refund tool for determinism' — 400 on Fable 5.1. 'Retry the 400 with backoff' — deterministic error. 'Put the tools in `CLAUDE.md` to share them' — `CLAUDE.md` holds instructions, not executable tools. 'Use a Skill to share across hosts' — Skills are Claude-Code-only packaged know-how.

**Correct decision.** An MCP server (OAuth 2.1 for the remote transport) connected to all hosts; `auto` + instruction or structured outputs on Fable 5.1 instead of forced tool choice; correct parallel `tool_result` handling with `is_error`; refunds behind an approval hook and out of routine allowlists; tool search + `defer_loading` if the catalogue grows.

---

## Exam traps in this domain

| Trap | Why it is wrong |
| --- | --- |
| Blaming the model when it mis-uses a tool | Fix the description and `input_schema` first |
| Forcing a tool on Fable 5.1 | `any`/forced return 400; use structured outputs / `strict` / `auto` |
| Loading 18 tools upfront | Overloads selection; use tool search + `defer_loading` (anti-pattern #8) |
| Returning tool errors as plain success | Mark `is_error` so Claude can recover |
| Mismatching `tool_use_id` on results | Results must reference the originating `tool_use_id` |
| Using a client tool for web search | Web search is a server-side built-in |
| Treating MCP resources as model-controlled | Resources are application-controlled; tools are model-controlled |
| Ignoring OAuth for remote MCP | Remote servers use OAuth 2.1 |
| Parsing assistant text to decide whether to keep calling tools | Loop on `stop_reason == 'tool_use'` (anti-pattern #1) |
| Using a Skill when tools must be shared across hosts | Cross-host/team reuse is MCP; Skills are Claude-Code packaged know-how |
| Hard-coding secrets in `.mcp.json` | Reference env vars (`${VAR}`); never commit credentials |
| Securing a remote MCP server with a static header instead of OAuth 2.1 | Remote/Streamable HTTP servers use OAuth 2.1 |
| Returning parallel tool results across multiple user turns | All results go in one user turn, each keyed to its `tool_use_id` |
| Relying on 'confirm before refunding' in the tool description | Guidance, not enforcement; gate irreversible tools with a hook/approval and least privilege |
| Retrying a forced-tool 400 on Fable 5.1 with backoff | Deterministic client error; use `auto`/structured outputs, do not retry |
| Using stdio for a shared, remote team server | stdio is for local subprocesses; remote/shared → Streamable HTTP + OAuth 2.1 |

---

## Practice questions

<Accordions>
  <AccordionItem title="Q1 · Claude frequently calls the wrong tool and passes malformed arguments. What is the FIRST thing to improve? (Select one)">
    A. Lower temperature.
    B. Rewrite the tool descriptions and tighten `input_schema` (types, enums, required, examples, when-to-use).
    C. Switch to Opus 5.
    D. Increase `max_tokens`.

    **Answer: B.** The description and schema are the primary levers for correct tool use. Temperature (A), model (C) and `max_tokens` (D) are secondary.
  </AccordionItem>

  <AccordionItem title="Q2 · A developer sets `tool_choice: 'any'` on Fable 5.1 and receives a 400. What is the correct approach? (Select one)">
    A. Retry with backoff.
    B. Use `tool_choice: 'auto'` with an instruction, or structured outputs / `strict: true`, since Fable 5.1 rejects forced tool choice.
    C. Switch to `none`.
    D. Increase `max_tokens`.

    **Answer: B.** Fable 5.1 rejects `any`/forced tool choice; use `auto` + instruction or structured outputs. It is a 400 client error, not transient (A); `none` (C) disables tools.
  </AccordionItem>

  <AccordionItem title="Q3 · An agent has grown to 18 tools and selection quality has dropped. What is the recommended fix? (Select one)">
    A. Add more descriptive tools.
    B. Reduce to a focused set and use the tool search tool with `defer_loading` for the larger catalogue.
    C. Force a tool every turn.
    D. Raise temperature.

    **Answer: B.** Too many tools is anti-pattern #8; the fix is fewer tools plus tool search + `defer_loading`. More tools (A) worsens it.
  </AccordionItem>

  <AccordionItem title="Q4 · Which of the following are server-side (Anthropic-hosted) tools? (Select two)">
    A. Web search.
    B. Your company's internal orders API.
    C. Code execution.
    D. A custom database query function you wrote.
    E. A local shell script you maintain.

    **Answer: A and C.** Web search and code execution are Anthropic-hosted server-side tools. Your API (B), custom DB function (D) and local script (E) are client-side.
  </AccordionItem>

  <AccordionItem title="Q5 · A team wants a remote MCP server's tools available directly from the Messages API without re-declaring each tool. What enables this? (Select one)">
    A. The Files API.
    B. The MCP connector in the Messages API.
    C. Prompt caching.
    D. Batch API.

    **Answer: B.** The MCP connector lets the Messages API call remote MCP servers directly. The Files API (A), caching (C) and batching (D) are unrelated.
  </AccordionItem>

  <AccordionItem title="Q6 · In the tool-use loop, how should code decide whether to run another tool round-trip? (Select one)">
    A. Parse the assistant's text for phrases like 'let me check'.
    B. Check whether `resp.stop_reason == 'tool_use'` and, if so, run the tools and call the API again.
    C. Loop a fixed 5 times regardless of output.
    D. Continue until the response is empty.

    **Answer: B.** The API tells you deterministically via `stop_reason`. Parsing prose (A) is anti-pattern #1; a fixed cap (C) is anti-pattern #2; empty-response detection (D) is unreliable.
  </AccordionItem>

  <AccordionItem title="Q7 · A `get_order` tool returns a timeout. How should the result be sent back to Claude? (Select one)">
    A. Omit the `tool_result` so Claude ignores it.
    B. Return an empty string as the content.
    C. Return a `tool_result` with the same `tool_use_id`, an error message, and `is_error: true`.
    D. Raise an exception and stop the conversation.

    **Answer: C.** Errors must be reported back with the matching id and `is_error: true` so Claude can retry or apologise. Omitting (A) causes a 400; empty content (B) is a silent failure (anti-pattern #7); crashing (D) hides diagnostics.
  </AccordionItem>

  <AccordionItem title="Q8 · Which properties in an `input_schema` best prevent Claude from inventing an invalid argument value for a closed set like a priority level? (Select one)">
    A. A high `max_tokens`.
    B. An `enum` constraining the allowed values, plus listing the field in `required`.
    C. Setting `tool_choice: 'any'`.
    D. Lowering temperature.

    **Answer: B.** `enum` restricts the value space and `required` guarantees the field is provided. `max_tokens` (A) and temperature (D) are unrelated to schema validity; `tool_choice` (C) governs whether a tool is used, not its arguments.
  </AccordionItem>

  <AccordionItem title="Q9 · A support agent needs to reliably extract structured order data on Fable 5.1, but forcing a specific tool returns 400. What are the TWO valid approaches? (Select two)">
    A. Use `tool_choice: 'auto'` with a clear instruction to call the extraction tool.
    B. Retry the forced call with exponential backoff.
    C. Use structured outputs via `output_config.format` with a JSON schema.
    D. Switch `tool_choice` to `none`.
    E. Downgrade to Haiku 4.5 for all traffic.

    **Answer: A and C.** Fable 5.1 rejects forced/`any` tool choice, so use `auto` + instruction or structured outputs. Backoff (B) will not fix a 400 client error; `none` (D) disables tools; changing model for everything (E) is an over-broad, unwarranted change.
  </AccordionItem>

  <AccordionItem title="Q10 · A developer wants the SAME set of tools available in Claude Code, Claude Desktop, and their production Messages API service, maintained in one place. What is the best mechanism? (Select one)">
    A. Copy the custom tool JSON into each app.
    B. Build an MCP server and connect each host to it (stdio/HTTP; connector for the API).
    C. Write a Skill in `.claude/skills/`.
    D. Put the tools in `CLAUDE.md`.

    **Answer: B.** MCP is the reuse-across-hosts mechanism. Copying JSON (A) duplicates maintenance; a Skill (C) is Claude-Code-only packaged know-how, not a cross-host tool server; `CLAUDE.md` (D) is instructions, not executable tools.
  </AccordionItem>

  <AccordionItem title="Q11 · Which MCP primitive is model-controlled, meaning Claude decides when to invoke it? (Select one)">
    A. Resources.
    B. Prompts.
    C. Tools.
    D. Transports.

    **Answer: C.** Tools are model-controlled. Resources (A) are application-controlled, Prompts (B) are user-controlled, and transports (D) are the connection mechanism (stdio / Streamable HTTP), not a primitive Claude invokes.
  </AccordionItem>

  <AccordionItem title="Q12 · Claude asks for two independent tools (`get_weather` for Paris and `get_weather` for Tokyo) in a single response. How must the results be returned? (Select one)">
    A. Two separate user turns, one result each.
    B. One user turn containing both `tool_result` blocks, each keyed to its own `tool_use_id`.
    C. A single `tool_result` combining both cities.
    D. Ignore one and answer the other.

    **Answer: B.** Parallel `tool_use` blocks are answered with all `tool_result` blocks in one user turn, each matching its originating `tool_use_id`. Splitting turns (A), merging ids (C), or dropping a result (D) all break the protocol / cause a 400.
  </AccordionItem>

  <AccordionItem title="Q13 · You need to bundle a multi-step invoice-processing workflow with reference templates and a helper Python script so Claude Code loads it only when relevant. Which mechanism fits best? (Select one)">
    A. A custom Messages API tool.
    B. A remote MCP server.
    C. A Skill (`SKILL.md` with frontmatter and bundled files, loaded progressively).
    D. A PreToolUse hook.

    **Answer: C.** Skills package instructions plus files and scripts that load on demand in Claude Code — exactly this scenario. A custom tool (A) is a single function; an MCP server (B) is for cross-host tool sharing; a hook (D) is a deterministic guardrail, not a packaged workflow.
  </AccordionItem>

  <AccordionItem title="Q14 · A team must expose a shared, remote MCP server to Claude Desktop, Claude Code and the Messages API. Which transport and auth are appropriate? (Select one)">
    A. stdio with local process trust.
    B. Streamable HTTP secured with OAuth 2.1.
    C. A static bearer header hard-coded in `.mcp.json`.
    D. SSE-only transport, which is the current recommended default.

    **Answer: B.** Remote/shared servers use Streamable HTTP secured with OAuth 2.1. stdio (A) is for local subprocesses; a hard-coded header (C) is a secrets/hygiene problem and not the standard auth; SSE-only (D) is legacy, not the recommended default.
  </AccordionItem>

  <AccordionItem title="Q15 · Claude asks for two independent tools in one response, but the code returns each result in its own separate user turn, and requests start failing with 400. What is the fix? (Select one)">
    A. Merge both outputs into a single combined `tool_result`.
    B. Return both `tool_result` blocks in one user turn, each keyed to its own `tool_use_id`.
    C. Drop one result to simplify the turn.
    D. Retry the 400 with exponential backoff.

    **Answer: B.** Parallel `tool_use` blocks must be answered with all `tool_result` blocks in one user turn, each matching its originating id. Merging ids (A) or dropping a result (C) breaks the protocol; the 400 is deterministic, so backoff (D) will not help.
  </AccordionItem>

  <AccordionItem title="Q16 · An agent may issue refunds, but any refund over $500 needs human approval. Where must this be enforced? (Select one)">
    A. In the `issue_refund` tool description ('confirm before refunding over $500').
    B. In a PreToolUse hook / approval workflow that blocks the over-threshold refund and routes to a human, with the tool kept out of routine agents' allowlists.
    C. By lowering the model's temperature.
    D. By asking the model to double-check the amount.

    **Answer: B.** Irreversible actions are gated deterministically by a hook/approval and least privilege, not by the description or self-checks. Description text (A) and self-checks (D) are guidance the model can skip (anti-pattern #3); temperature (C) is irrelevant.
  </AccordionItem>

  <AccordionItem title="Q17 · Which MCP interaction happens on `initialize`, and how are the three primitives controlled? (Select one)">
    A. Capability negotiation occurs on `initialize`; tools are model-controlled, resources application-controlled, prompts user-controlled.
    B. Authentication occurs on `initialize`; all three primitives are model-controlled.
    C. Nothing happens on `initialize`; primitives are negotiated per call.
    D. `initialize` returns the OAuth token; tools are user-controlled.

    **Answer: A.** Hosts and servers negotiate capabilities on `initialize`, and the control mapping is tools (model), resources (application), prompts (user). B mislabels all primitives; C is wrong (negotiation is at init); D scrambles the mapping and misstates OAuth.
  </AccordionItem>

  <AccordionItem title="Q18 · A support agent on Fable 5.1 must reliably extract structured order data, but forcing the extraction tool returns 400. Which TWO approaches are valid? (Select two)">
    A. Use `tool_choice: 'auto'` with a clear instruction to call the extraction tool.
    B. Use structured outputs via `output_config.format` with a JSON schema.
    C. Retry the forced-tool call with backoff.
    D. Switch `tool_choice` to `none`.
    E. Downgrade all traffic to Haiku 4.5.

    **Answer: A and B.** Fable 5.1 rejects forced/`any` tool choice, so `auto` + instruction or structured outputs are valid. Backoff (C) will not fix a deterministic 400; `none` (D) disables tools; changing every request's model (E) is an over-broad, unwarranted change.
  </AccordionItem>

  <AccordionItem title="Q19 · A remote MCP server tool call fails with a timeout during a parallel call alongside a successful tool. How should the failed call be represented? (Select one)">
    A. Omit its `tool_result` so Claude moves on.
    B. Return a `tool_result` with the same `tool_use_id`, an error message, and `is_error: true`, in the same user turn as the successful result.
    C. Retry silently and return empty content.
    D. Crash the conversation to force a restart.

    **Answer: B.** Every `tool_use` needs a matching `tool_result` (with `is_error: true` on failure) in the same turn so Claude can recover. Omitting it (A) causes a 400 and hides the failure; empty content (C) is silent suppression (anti-pattern #7); crashing (D) loses diagnostics.
  </AccordionItem>
</Accordions>

## Key takeaways

- The tool-use lifecycle: define tools → `tool_use` → run and return `tool_result` (matching `tool_use_id`) → final answer.
- The tool **description** and `input_schema` are the single most important lever for correct tool use.
- `tool_choice` is `auto`/`any`/`tool`/`none`; Fable 5.1 rejects `any` and forced tools – use structured outputs / `strict` / `auto`.
- Independent tools can run in parallel; return all results in one `user` turn.
- Client-side tools run in your code; server-side built-ins (web search, code execution, computer use, text editor, bash, memory) run at Anthropic.
- Beyond ~10 tools, use tool search + `defer_loading`; require approval for irreversible actions.
- MCP is JSON-RPC 2.0 with tools (model), resources (app), prompts (user); stdio for local, Streamable HTTP + OAuth 2.1 for remote; author with FastMCP (Python) or `@modelcontextprotocol/sdk` (TS).
- Capability negotiation happens on `initialize`; choose stdio for local subprocess tools and Streamable HTTP + OAuth 2.1 for shared/remote servers (SSE-only is legacy).
- Gate irreversible tools (refunds/deletes/sends/deploys) with a PreToolUse hook / approval workflow and least privilege — the tool description is guidance, not enforcement.
- Return every parallel `tool_result` in one user turn keyed to its `tool_use_id`, and mark failures with `is_error: true` so Claude can recover.
- A forced-tool 400 on Fable 5.1 is deterministic — switch to `auto`/structured outputs rather than retrying with backoff.
