Tools and MCPs — practice exercise
Tools and MCPs — practice exercise
Difficulty: intermediate · Estimated duration: 45–60 minutes
You are the engineer responsible for ReturnsDesk, an invented assistant that helps an online shop's staff handle product returns. It looks up orders, reads carriers' tracking pages, drafts refund notes and checks return windows. You will review its tools and its extensions and write down the change you would make at each step. Every stage can be completed without an API key and without spending anything: you read the material given here, write code, configuration or a decision, and check it against the reference solution. Stage 3 has an optional step: if you have your own API key you may run your Agent SDK configuration, which uses your own credit.
What you need: a text editor and a notes file. Python is used for the code; any language is fine for the reasoning.
| Stage | What you practise | Minutes |
|---|---|---|
| 1 | Defining a tool and answering its calls | 12 |
| 2 | Mixed turns, domains and a large tool response | 14 |
| 3 | Wiring MCP in three places | 12 |
| 4 | Choosing extensions and building a custom tool | 12 |
Stage 1 — Define the tool
Skills: CCDVF-U8.T1.LO1.S1, CCDVF-U8.T1.LO1.S2 Minutes: 12
ReturnsDesk's first tool definition:
TOOLS = [{
"name": "order status",
"description": "Order stuff",
"input_schema": ORDER_SCHEMA}]- Name two problems with this definition, and rewrite it. Your description should be three or four sentences.
- The handler cannot find the order ID it was given. Write the
tool_resultblock it should return, so that Claude knows what went wrong and what to try next. - ReturnsDesk will add a refund tool whose input is a nested list of items, each with a reason code. Would you add
input_examplesto that tool, to the order-status tool, or to both? Why?
Stage 2 — Handle a mixed turn and a big result
Skills: CCDVF-U8.T1.LO1.S3, CCDVF-U8.T1.LO1.S4 Minutes: 14
ReturnsDesk also uses web fetch, a server tool, to read carriers' tracking pages. One response came back like this (trimmed):
{"stop_reason": "tool_use",
"content": [
{"type": "server_tool_use",
"id": "srvtoolu_01",
"name": "web_fetch"},
{"type": "tool_use",
"id": "toolu_02",
"name": "get_order_status"}]}- There is no flag that says the web fetch call is unfinished. How does your code tell?
- Your conversation store assumes that each server call and its result arrive in the same response. What will break after you continue this turn, and how should the store pair them instead?
- The team restricts web fetch with the allowed-domain entry
https://track.carrier.example/parcels, and it does not work. Write the entry that does. list_orderscan return thousands of rows and floods the context. Add parameters to it and write the message it should return when it cuts a result short.
Stage 3 — Wire MCP three ways
Skills: CCDVF-U8.T2.LO2.S1, CCDVF-U8.T2.LO2.S2, CCDVF-U8.T2.LO2.S3 Minutes: 12
- Claude Code. The returns team's MCP server, named
returns, exposes a promptrefund_notethat takes an order ID and a reason. Write the command that runs it for orderA-1042with the reason "damaged box". - MCP connector. From the Messages API, ReturnsDesk reaches a public payments MCP server at
https://pay.example/mcp. Write themcp_serversentry and the toolset so that every tool is enabled exceptissue_refund. - Agent SDK. A back-office agent runs a local
returns-dbMCP server started withreturns-db --stdio. Its key is in the environment variableRDB_KEY. Write the options so that the server gets the key and only itslookuptool runs without a prompt. - (Optional, uses your own API key and credit.) Run your Agent SDK options once against a local stand-in server and check each server's status in the init message.
Stage 4 — Choose and build extensions
Skills: CCDVF-U8.T2.LO3.S1, CCDVF-U8.T2.LO3.S2, CCDVF-U8.T2.LO3.S3 Minutes: 12
- Pick an extension for each need, with one reason each:
- (a) Every session must use British spelling in customer notes.
- (b) A long refund-policy playbook is needed only for disputed returns.
- (c) The same setup must be installed in the partner shop's repository.
- (d) Checking a year of return logs for patterns would flood the conversation.
- Write the SKILL.md frontmatter for the refund-policy skill, and say where the 40-page policy text goes.
- Write an in-process custom tool
return_windowfor the Agent SDK. It takes an order ID and returns the days left to return it. For an unknown order it returns an error Claude can act on.
Acceptance checks
- Stage 1 fixes the name's characters, writes a description that says what, when, when not and what it does not return, returns
is_errorwith a next step, and adds examples to the nested tool only. - Stage 2 detects the unfinished call by its unmatched id, pairs by
tool_use_id, writes the domain without a scheme or a path, and pages the tool with a truncation message that says how to get more. - Stage 3 splits the prompt's arguments into single tokens, denylists
issue_refundin the toolset, and passes the key throughenvwith the tool listed inallowed_toolsby exact name. - Stage 4 maps needs to CLAUDE.md, a skill, a plugin and a subagent, writes a description that says what and when, and returns
is_errorfrom the handler.
Reference solution
Stage 1. (1) The name has a space, and tool names may contain only letters, digits, underscores and hyphens. The description is far too thin: aim for at least three or four sentences. A good rewrite:
TOOLS = [{
"name": "get_order_status",
"description": DESC,
"input_schema": ORDER_SCHEMA}]
DESC = (
"Returns one order's status:"
" shipped, delivered or"
" returned. Use it when staff"
" ask where an order is. Not"
" for refunds. order_id is"
" the shop's ID, e.g. A-1042."
" It does not return the"
" customer's address.")(2) Return the message with is_error set, and say what to try next:
{"type": "tool_result",
"tool_use_id": block.id,
"content": "No order A-1402."
" Check the ID or search"
" by customer email.",
"is_error": True}(3) Only the refund tool. Input examples are most useful for complex tools with nested objects, optional parameters or format-sensitive inputs; a single string ID does not need them.
Stage 2. (1) Look for a server_tool_use block whose id has no matching result block in the same response. (2) After you send the client results, the web fetch result arrives at the start of the next response, and the server_tool_use block is not repeated there. Pair a server call and its result by tool_use_id, not by position. (3) track.carrier.example. Domains are written without the scheme, and web fetch matches on the domain only, so an entry with a path never matches a fetch. (4) One good version:
def list_orders(customer=None,
status=None,
limit=25, page=1):
...
# when cut short, add:
# "Showing 25 of 3,410. Filter
# by customer or status, or ask
# for page 2."Filtering and pagination with small defaults keep the response small, and the truncation message steers the agent towards a narrower call.
Stage 3. (1) /mcp__returns__refund_note A-1042 damaged-box. Claude Code splits arguments on whitespace, so each argument must be a single token. (2) One good version:
servers = [{
"type": "url",
"url": "https://pay.example/mcp",
"name": "pay"}]
tools = [{
"type": "mcp_toolset",
"mcp_server_name": "pay",
"configs": {"issue_refund":
{"enabled": False}}}]Tools are enabled by default, and the per-tool config switches off the one that moves money. (3) One good version:
KEY = os.environ["RDB_KEY"]
opts = ClaudeAgentOptions(
mcp_servers={"rdb": {
"command": "returns-db",
"args": ["--stdio"],
"env": {"RDB_KEY": KEY}}},
allowed_tools=[
"mcp__rdb__lookup"])MCP tools need explicit permission, and listing the exact name permits only that tool. (4) Check that the server's status is connected; do not treat pending on its own as a failure.
Stage 4. (1) (a) CLAUDE.md, because it is loaded in every session. (b) A skill, because its body loads only when it is used. (c) A plugin, because a second repository needs the same setup. (d) A subagent, because it works in its own context and returns only a summary. (2) One good frontmatter:
name: refund-policy
description: Applies the shop's
refund rules to a disputed
return. Use when staff ask if
a refund is allowed.The long policy text goes in a separate file in the skill's folder, which Claude reads only when a task needs it. (3) One good version:
@tool("return_window",
"Days left to return an"
" order. Input: order_id.",
{"order_id": str})
async def return_window(a):
days = WINDOWS.get(
a["order_id"])
if days is None:
return {"content": [{
"type": "text",
"text": "Unknown order."
" Check the ID."}],
"is_error": True}
return {"content": [{
"type": "text",
"text": f"{days} days"}]}Wrap it with create_sdk_mcp_server and list mcp__<server>__return_window in allowed_tools.
Sources
- Define tools — https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools
- Handle tool calls — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls
- Server tools — https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools
- Writing effective tools for agents — https://www.anthropic.com/engineering/writing-tools-for-agents
- Connect Claude Code to tools via MCP — https://code.claude.com/docs/en/mcp
- MCP connector — https://platform.claude.com/docs/en/agents-and-tools/mcp-connector
- Connect to external tools with MCP (Agent SDK) — https://code.claude.com/docs/en/agent-sdk/mcp
- Extend Claude Code — https://code.claude.com/docs/en/features-overview
- Agent Skills — https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview
- Extend Claude with skills — https://code.claude.com/docs/en/skills
- Give Claude custom tools — https://code.claude.com/docs/en/agent-sdk/custom-tools