Hands-on Lab1,540 words

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.

StageWhat you practiseMinutes
1Defining a tool and answering its calls12
2Mixed turns, domains and a large tool response14
3Wiring MCP in three places12
4Choosing extensions and building a custom tool12

Stage 1 — Define the tool

Skills: CCDVF-U8.T1.LO1.S1, CCDVF-U8.T1.LO1.S2 Minutes: 12

ReturnsDesk's first tool definition:

python
TOOLS = [{ "name": "order status", "description": "Order stuff", "input_schema": ORDER_SCHEMA}]
  1. Name two problems with this definition, and rewrite it. Your description should be three or four sentences.
  2. The handler cannot find the order ID it was given. Write the tool_result block it should return, so that Claude knows what went wrong and what to try next.
  3. ReturnsDesk will add a refund tool whose input is a nested list of items, each with a reason code. Would you add input_examples to 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):

json
{"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"}]}
  1. There is no flag that says the web fetch call is unfinished. How does your code tell?
  2. 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?
  3. 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.
  4. list_orders can 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

  1. Claude Code. The returns team's MCP server, named returns, exposes a prompt refund_note that takes an order ID and a reason. Write the command that runs it for order A-1042 with the reason "damaged box".
  2. MCP connector. From the Messages API, ReturnsDesk reaches a public payments MCP server at https://pay.example/mcp. Write the mcp_servers entry and the toolset so that every tool is enabled except issue_refund.
  3. Agent SDK. A back-office agent runs a local returns-db MCP server started with returns-db --stdio. Its key is in the environment variable RDB_KEY. Write the options so that the server gets the key and only its lookup tool runs without a prompt.
  4. (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

  1. 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.
  2. Write the SKILL.md frontmatter for the refund-policy skill, and say where the 40-page policy text goes.
  3. Write an in-process custom tool return_window for 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_error with 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_refund in the toolset, and passes the key through env with the tool listed in allowed_tools by 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_error from 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:

python
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:

python
{"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:

python
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:

python
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:

python
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:

yaml
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:

python
@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

Ready to study Claude Certified Developer - Foundations (CCDV-F)?

Practice tests, flashcards, and all study notes — free, no sign-up needed.

Start Studying — Free