Tools & Agentic Tasks in Claude Code
Claude Code can do more than answer questions — it can take actions. It reads and writes files, runs terminal commands, uses MCP-connected tools, and chains multiple steps together autonomously. This module covers every type of tool Claude can use, how to define custom tools, how multi-tool orchestration works, and how to handle errors in tool calls.
File System Actions
Claude can read any file you share via @, create new files, and apply edits across multiple files in one request. Ask: "Add error handling to every function in @utils.ts" — Claude does it all at once. It can also create directories, rename files, and manage project structure.
Terminal & Commands
Claude opens the VS Code integrated terminal and runs commands — installs packages, runs tests, starts servers, executes scripts. It reads the output and continues working based on the result. If a command fails, Claude diagnoses the error and retries.
Web Search
Claude Code can search the web mid-conversation. Ask: "What is the current Stripe webhook signature format?" Claude searches, reads the relevant docs, and answers — without you leaving VS Code. This is essential for staying current on rapidly-changing APIs.
MCP Tools
Model Context Protocol (MCP) connects Claude to external systems — your database, CRM, order management system, or any API. Once configured, you can ask Claude to query live data directly from chat. MCP is an open protocol, so you can build custom servers for any data source.
The Agentic Loop — How Claude Chains Steps
The most powerful aspect of Claude Code is its ability to work through multi-step tasks autonomously. This is called the "agentic loop" — Claude plans, acts, observes results, and iterates.
You give a high-level task
"Add a discount code field to the checkout form, hook it up to our promotions table, and write a test for it." This is a task with at least 5 sub-steps — Claude figures out the steps.
Claude plans the steps
Claude reads @checkout.tsx, @promotions.ts, and the database schema — then outlines what it intends to do before acting. You can approve or redirect at this point. This planning step is where Claude's reasoning ability matters most.
Claude executes and checks
It edits the form component, updates the API route, runs the tests, reads the output. If a test fails, it fixes the error and re-runs without being asked. This observe-act-iterate cycle can run for 10+ steps on complex tasks.
Claude reports back
Once all steps complete, Claude summarises what it changed and flags anything it wasn't certain about — giving you a clear handoff for review.
Claude Code asks for permission before taking potentially destructive actions (deleting files, running unfamiliar commands). You can configure the permission level: always ask, ask for new tools, or auto-approve for trusted actions. For production work, keep the default "ask" mode — a 2-second approval click prevents minutes of cleanup.
Defining Tools for Claude (API)
When building applications with the Anthropic API, you can define custom tools that Claude can call. A tool is simply a function described with a JSON schema that tells Claude what the function does, what parameters it expects, and what it returns.
import anthropic, json client = anthropic.Anthropic() # Define tools as JSON schemas — Claude will call these when needed tools = [ { "name": "lookup_order", "description": "Look up a ThreadCo order by order ID. Returns order status, tracking number, and delivery estimate.", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "The order ID (e.g., 'ORD-4821')" } }, "required": ["order_id"] } }, { "name": "check_stock", "description": "Check current stock level for a product by SKU.", "input_schema": { "type": "object", "properties": { "sku": {"type": "string", "description": "Product SKU (e.g., 'SGT-M-BLU')"} }, "required": ["sku"] } }, { "name": "send_email", "description": "Send an email to a customer. Requires human approval before sending.", "input_schema": { "type": "object", "properties": { "to": {"type": "string", "description": "Recipient email address"}, "subject": {"type": "string"}, "body": {"type": "string"} }, "required": ["to", "subject", "body"] } } ]
The description field is the most important part of a tool definition. Claude decides which tool to call based primarily on the description. A vague description like "look up stuff" will cause Claude to call the wrong tool. A specific description like "Look up a ThreadCo order by order ID. Returns order status, tracking number, and delivery estimate" tells Claude exactly when and how to use it.
The Function Calling Loop
When Claude decides it needs to use a tool, the API returns a special tool_use response. Your code must execute the tool and send the result back. This loop continues until Claude has all the information it needs.
def execute_tool(name: str, input: dict) -> str: """Execute a tool call and return the result as a string.""" if name == "lookup_order": # In production, this queries your actual database return json.dumps({ "order_id": input["order_id"], "status": "shipped", "tracking": "GB12345678", "carrier": "Royal Mail", "estimated_delivery": "2026-04-17" }) elif name == "check_stock": return json.dumps({"sku": input["sku"], "in_stock": 23, "warehouse": "London"}) else: return json.dumps({"error": f"Unknown tool: {name}"}) def chat_with_tools(user_message: str) -> str: """Run a conversation with tool use, handling the full loop.""" messages = [{"role": "user", "content": user_message}] while True: resp = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, tools=tools, messages=messages ) # If Claude wants to use tools, execute them and continue if resp.stop_reason == "tool_use": # Add Claude's response (contains tool_use blocks) messages.append({"role": "assistant", "content": resp.content}) # Execute each tool call and add results tool_results = [] for block in resp.content: if block.type == "tool_use": result = execute_tool(block.name, block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": result }) messages.append({"role": "user", "content": tool_results}) else: # Claude is done — return the final text response return resp.content[0].text # Usage: answer = chat_with_tools("Customer #4821 is asking where their order is. Look it up and draft a reply.") print(answer)
Multi-Tool Orchestration
Claude can call multiple tools in a single turn and use the results together. This enables complex workflows:
You: Customer Maya Johnson (order #4821) is asking if she can exchange her Sunset Gradient Tee (size M) for a size L. Check: 1. Her order status (is it still exchangeable?) 2. Stock for SKU SGT-L-AMB (Sunset Gradient, Large, Amber) 3. If both are good, draft the exchange confirmation email. Claude's internal process: Step 1: Call lookup_order("ORD-4821") → Order delivered 3 days ago, within return window Step 2: Call check_stock("SGT-L-AMB") → 23 in stock Step 3: Both conditions met → Draft email using send_email tool (but waits for human approval before sending)
| Orchestration Pattern | Description | Example |
|---|---|---|
| Sequential | Tool B depends on Tool A's result | Look up order → Use tracking number to check carrier status |
| Parallel | Multiple tools called independently in one turn | Check stock for 3 different SKUs simultaneously |
| Conditional | Tool B is only called if Tool A's result meets a condition | Check stock → Only draft restock email if stock < 10 |
| Iterative | Same tool called multiple times with different inputs | Check stock for each item in a customer's cart |
| Fallback | If Tool A fails, try Tool B as an alternative | If order lookup fails, search by customer email instead |
Error Handling in Tool Calls
Tool calls can fail — the database might be down, the API might return an error, or the input might be invalid. How you return errors to Claude determines whether it recovers gracefully or spirals into confusion.
def execute_tool_safely(name: str, input: dict) -> dict: """Execute a tool with proper error handling for Claude.""" try: result = execute_tool(name, input) return {"type": "tool_result", "content": result} except ValueError as e: # Input validation error — tell Claude what was wrong return { "type": "tool_result", "content": json.dumps({ "error": "invalid_input", "message": str(e), "hint": "Check the parameter format and try again" }), "is_error": True # Tells Claude this is an error, not a result } except ConnectionError: # Service unavailable — Claude should tell the user, not retry endlessly return { "type": "tool_result", "content": json.dumps({ "error": "service_unavailable", "message": "The order database is temporarily unavailable", "suggestion": "Inform the customer and offer to follow up" }), "is_error": True } except Exception as e: # Unexpected error — log it and give Claude a generic message print(f"Tool error: {name} — {e}") # Log for debugging return { "type": "tool_result", "content": json.dumps({"error": "internal_error", "message": "An unexpected error occurred"}), "is_error": True }
If a tool call fails and you return a vague error, Claude will often retry the same call with the same input — creating an infinite loop. Always include specific information about why the call failed and what Claude should do instead (try different parameters, inform the user, use a fallback tool). The is_error: true flag tells Claude this is an error, not a valid result.
Setting Up MCP Tools for ThreadCo
// .vscode/settings.json — add MCP servers for Claude Code { "claude.mcpServers": { "threadco-orders": { "command": "node", "args": ["./mcp-servers/orders-server.js"], "description": "Look up ThreadCo order status and tracking info" }, "threadco-stock": { "command": "node", "args": ["./mcp-servers/stock-server.js"], "description": "Check live stock levels by SKU" } } }
You (in Claude Code chat): Customer #4821 is asking where their order is. Look up the order and draft a reply email. Claude (uses threadco-orders MCP tool automatically): Order #4821 — Maya Johnson Status: Shipped Carrier: Royal Mail, tracking GB12345678 Estimated delivery: Thursday 17 April Draft reply: "Hi Maya, your order shipped yesterday via Royal Mail. Tracking number: GB12345678 — expected Thursday. Let us know if it doesn't arrive by Friday and we'll investigate."
Safety Guardrails for Tool Use
Read vs Write Tools
Separate your tools into read-only (lookup, search, check) and write (send email, update database, delete). Allow Claude to use read tools freely but require human confirmation for write tools. This prevents Claude from taking irreversible actions.
Input Validation
Validate all tool inputs before execution. Check types, ranges, and formats. A malformed order ID or SQL injection attempt should be caught by your tool code, not by Claude's judgment. Never trust Claude's input — validate it the same way you'd validate user input.
Rate Limiting
Limit how many tool calls Claude can make per conversation or per minute. Without limits, a confused Claude might make hundreds of API calls trying to recover from an error. Set a maximum of 10-20 tool calls per conversation for most use cases.
Audit Logging
Log every tool call: what was called, with what parameters, what was returned, and whether it was successful. This is essential for debugging, security auditing, and understanding how Claude uses your tools in practice.
Module 16 (MCP In Depth) walks through configuring MCP servers, available server types, and how to build a custom MCP server for your own systems.
Hands-On Exercises
Give Claude Code a multi-step task in your project: "Add input validation to @api/orders.ts, write a test for the validation, and run the tests." Watch the agentic loop in action. Count the steps Claude takes. Did it plan before acting? Did it handle any errors that arose?
Design a tool definition (JSON schema) for a function your team uses regularly. Write the name, description, and input_schema. Test whether Claude calls it correctly by describing a scenario where the tool should be used. Refine the description until Claude uses it reliably.
Using the tool use loop code from this module, simulate three error scenarios: (a) invalid input, (b) service unavailable, (c) unexpected error. Return appropriate error messages to Claude. Does Claude handle each error gracefully? Does it inform the user appropriately? Adjust your error messages until Claude's recovery behaviour is satisfactory.
If you have not already, set up one MCP server in your VS Code settings. Use a pre-built server (filesystem, SQLite, or GitHub) from the MCP server registry. Test it by asking Claude a question that requires the tool. Verify that Claude calls it automatically and uses the result correctly.
Design a customer service workflow that requires at least 3 tool calls: (1) look up order, (2) check stock for replacement, (3) draft response email. Implement the tools (even with mock data) and run the full workflow through Claude. Track whether Claude calls the tools in the correct order and combines the results coherently.