Introduction to Tool Use Patterns with Claude Code
Tool use is one of the most powerful capabilities of Claude Code, Anthropic's command-line AI coding assistant. By allowing Claude to interact with external tools, APIs, and system resources, you can transform it from a conversational AI into an autonomous agent capable of executing complex development workflows. This guide covers everything you need to know about designing, implementing, and optimizing tool use patterns with Claude Code.
What Is Tool Use in Claude Code?
Tool use (also called function calling) is the mechanism by which Claude can request to execute external functions, scripts, or commands during a conversation. Instead of merely suggesting code, Claude can actually run tests, query databases, manipulate files, and interact with your development environment directly.
In Claude Code, tool use is built into the core experience. Claude already has access to a set of default tools like file reading, file editing, and bash command execution. However, the real power comes from understanding the patterns that make tool use effective and from extending Claude with custom tools tailored to your workflow.
Why Tool Use Matters
- Autonomous execution: Claude can perform multi-step tasks without manual intervention.
- Grounded responses: By querying real data through tools, Claude's answers are based on actual state rather than assumptions.
- Iterative refinement: Claude can run a tool, inspect the output, and adjust its approach based on results.
- Workflow integration: Tools bridge the gap between AI reasoning and your existing development infrastructure.
- Reduced context switching: Developers stay in the terminal while Claude handles cross-system operations.
Understanding the Tool Use Lifecycle
Every tool use interaction follows a predictable lifecycle. Understanding this cycle is essential for designing effective tools and debugging issues.
The Four-Phase Cycle
Phase 1 — Decision: Claude analyzes the user's request and determines whether a tool is needed. This is driven by the tool descriptions provided in the system prompt and the conversation context.
Phase 2 — Invocation: Claude generates a structured tool use request containing the tool name and arguments. The runtime executes the tool and captures the output.
Phase 3 — Observation: The tool result is fed back into the conversation as a tool result message. Claude reads this output to inform its next step.
Phase 4 — Action: Claude either calls another tool, provides a final answer, or asks the user for clarification. This cycle repeats until the task is complete.
Core Tool Use Patterns
Different tasks call for different tool use strategies. Below are the most common and effective patterns you will encounter when working with Claude Code.
Pattern 1: Read-Then-Act
This is the most fundamental pattern. Claude first reads the current state of the world (files, database, API), then takes an action based on what it found. This prevents Claude from making assumptions about code or configuration that might be outdated.
# Example: Claude reads a file before editing it
User: "Fix the failing test in auth.test.js"
# Claude's internal process:
# 1. Tool call: read_file("tests/auth.test.js")
# 2. Tool call: read_file("src/auth.js")
# 3. Tool call: bash("npm test -- --grep auth")
# 4. Observes failure output
# 5. Tool call: edit_file("src/auth.js", changes)
# 6. Tool call: bash("npm test -- --grep auth")
# 7. Confirms test passes
The key insight is that Claude never guesses at file contents. It always reads first, which dramatically reduces errors.
Pattern 2: Explore-Plan-Execute
For complex tasks, Claude uses a multi-phase approach where it first explores the codebase to build understanding, formulates a plan, and then executes changes step by step.
# Example workflow for adding a new API endpoint
# Phase 1: Explore
# - list_files("src/routes/")
# - read_file("src/routes/users.js") # learn the pattern
# - read_file("src/middleware/auth.js")
# - bash("cat package.json") # check dependencies
# Phase 2: Plan (Claude communicates the plan to the user)
"""
I'll create a new endpoint at /api/products following the
existing pattern in users.js. Steps:
1. Create src/routes/products.js
2. Register the route in src/app.js
3. Add validation middleware
4. Write tests in tests/products.test.js
"""
# Phase 3: Execute (one tool call at a time, verifying each)
# - write_file("src/routes/products.js", ...)
# - edit_file("src/app.js", add import and route registration)
# - bash("npm test")
Pattern 3: Iterative Debugging Loop
When fixing bugs or getting tests to pass, Claude enters a tight feedback loop: run, observe failure, adjust, run again. This is one of the most valuable patterns for developers.
# Claude's iterative debugging process
# Iteration 1
bash("npm test")
# Result: TypeError: Cannot read property 'id' of undefined
# Claude reads the relevant code
read_file("src/handlers/order.js")
# Identifies: req.body.user is not being validated
# Iteration 2
edit_file("src/handlers/order.js", "add null check")
bash("npm test")
# Result: 1 test still failing — assertion error on total
# Iteration 3
read_file("src/handlers/order.js", lines 45-60)
edit_file("src/handlers/order.js", "fix calculation")
bash("npm test")
# Result: All tests passing
Pattern 4: Parallel Tool Calls
Claude can make multiple independent tool calls in a single turn. This is useful when gathering information from multiple sources that don't depend on each other.
# When asked to review a pull request, Claude can simultaneously:
[
{ "tool": "bash", "input": { "command": "git diff main...feature-branch" } },
{ "tool": "bash", "input": { "command": "git log --oneline main..feature-branch" } },
{ "tool": "read_file", "input": { "path": "package.json" } },
{ "tool": "bash", "input": { "command": "npm run lint 2>&1 | tail -20" } }
]
# All four results come back together, and Claude synthesizes
# a comprehensive review in one pass.
Pattern 5: Conditional Chaining
Claude uses the output of one tool call to decide which tool to call next. This creates branching logic that adapts to runtime conditions.
# Example: Deploy only if tests pass
bash("npm test")
# If exit code 0:
# bash("npm run build")
# If build succeeds:
# bash("npm run deploy")
# read deployment logs
# If build fails:
# read error output, fix code, retry
# If exit code non-zero:
# read test output, fix failing tests
Building Custom Tools for Claude Code
While Claude Code ships with built-in tools, you can extend it with custom tools using MCP (Model Context Protocol) servers or by configuring custom commands. This lets you integrate Claude with your specific infrastructure.
Creating an MCP Server Tool
MCP servers are the primary way to add custom tools to Claude Code. Here is a complete example of a custom tool that queries a project's database schema:
// tools/schema-server/index.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
import { Client } from "pg";
const server = new Server(
{ name: "schema-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
const dbClient = new Client({
connectionString: process.env.DATABASE_URL,
});
await dbClient.connect();
server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "query_schema",
description:
"Query the database schema for a specific table. " +
"Returns column names, types, and constraints. " +
"Use this before writing SQL queries to understand " +
"the available columns.",
inputSchema: {
type: "object",
properties: {
table_name: {
type: "string",
description: "The name of the table to inspect",
},
},
required: ["table_name"],
},
},
{
name: "run_readonly_query",
description:
"Execute a read-only SQL query against the database. " +
"Only SELECT statements are allowed. Use this to " +
"verify data assumptions before making code changes.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "A SELECT SQL query",
},
},
required: ["query"],
},
},
],
}));
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
if (name === "query_schema") {
const result = await dbClient.query(
`SELECT column_name, data_type, is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position`,
[args.table_name]
);
return {
content: [
{
type: "text",
text: JSON.stringify(result.rows, null, 2),
},
],
};
}
if (name === "run_readonly_query") {
if (!args.query.trim().toUpperCase().startsWith("SELECT")) {
return {
content: [
{
type: "text",
text: "Error: Only SELECT queries are allowed.",
},
],
isError: true,
};
}
const result = await dbClient.query(args.query);
return {
content: [
{
type: "text",
text: JSON.stringify(result.rows, null, 2),
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
Registering the MCP Server with Claude Code
Once you have built your MCP server, register it in your project's Claude Code configuration:
// .claude/settings.json
{
"mcpServers": {
"schema-server": {
"command": "node",
"args": ["./tools/schema-server/dist/index.js"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost:5432/myapp"
}
}
}
}
After registration, Claude Code automatically discovers the tools and can use them when relevant. You do not need to explicitly tell Claude to use them — the tool descriptions guide Claude's decision-making.
Writing Effective Tool Descriptions
The tool description is the single most important factor in whether Claude uses a tool correctly. Claude decides which tool to call based entirely on the description and input schema. Poor descriptions lead to missed tool calls or incorrect arguments.
Anatomy of a Great Tool Description
A strong tool description includes four elements: purpose, when to use it, when not to use it, and expected behavior.
// Poor description — too vague
{
name: "search_code",
description: "Search the codebase"
}
// Good description — specific and actionable
{
name: "search_code",
description:
"Search for code patterns across the entire repository using " +
"ripgrep. Returns matching file paths, line numbers, and " +
"surrounding context. Use this when you need to find where " +
"a function is defined, where a variable is used, or locate " +
"all occurrences of a specific pattern. Do NOT use this for " +
"reading a specific known file — use read_file instead. " +
"Supports regex patterns in the query parameter.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "A regex pattern to search for, e.g. 'useEffect\\(' or 'async function handle'"
},
file_pattern: {
type: "string",
description: "Optional glob pattern to limit search scope, e.g. '*.ts' or 'src/**/*'"
},
max_results: {
type: "number",
description: "Maximum number of matches to return. Default 50."
}
},
required: ["query"]
}
}
Description Best Practices
- Be explicit about when to use the tool: "Use this when..." is more effective than a generic summary.
- Include negative guidance: Tell Claude when NOT to use the tool to prevent confusion with similar tools.
- Describe the output format: Claude needs to know what to expect in the result to process it correctly.
- Document edge cases: Mention error conditions and what they mean.
- Keep descriptions under 200 words: Longer descriptions consume context window space and can dilute focus.
Advanced Patterns
Pattern 6: Tool Composition
Complex workflows often require composing multiple tools into a pipeline. Claude naturally chains tools when each step's output informs the next, but you can encourage this behavior with clear descriptions.
# Example: Claude composing tools to investigate a production issue
# Step 1: Fetch recent error logs
bash("tail -100 /var/log/app/error.log | grep 'ERROR'")
# Step 2: Based on the error, query the database for affected records
run_readonly_query("SELECT * FROM orders WHERE status = 'failed' AND created_at > NOW() - INTERVAL '1 hour'")
# Step 3: Read the relevant handler code
read_file("src/handlers/order_processor.js")
# Step 4: Check the git history for recent changes to that file
bash("git log --oneline -10 -- src/handlers/order_processor.js")
# Step 5: Identify the problematic commit and show the diff
bash("git show abc1234 -- src/handlers/order_processor.js")
# Step 6: Propose and apply a fix
edit_file("src/handlers/order_processor.js", fix_description)
Pattern 7: Guard-Railed Execution
For tools that have side effects (deployments, database writes, infrastructure changes), implement guard rails inside the tool itself. Claude should not be the only line of defense.
// tools/deploy-server/index.ts — guarded deployment tool
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
if (name === "deploy") {
// Guard rail 1: Environment check
const allowedEnvs = ["staging", "production"];
if (!allowedEnvs.includes(args.environment)) {
return {
content: [{ type: "text", text:
`Invalid environment: ${args.environment}. ` +
`Allowed: ${allowedEnvs.join(", ")}`
}],
isError: true,
};
}
// Guard rail 2: Require confirmation token
if (!args.confirm_token) {
const token = generateConfirmToken();
pendingDeploys.set(token, args);
return {
content: [{ type: "text", text:
`Deployment to ${args.environment} requires confirmation. ` +
`Reply with confirm_token: "${token}" to proceed.`
}],
};
}
// Guard rail 3: Verify token
const pending = pendingDeploys.get(args.confirm_token);
if (!pending) {
return {
content: [{ type: "text", text: "Invalid or expired confirmation token." }],
isError: true,
};
}
// Guard rail 4: Run pre-deployment checks
const checks = await runPreDeployChecks(args.environment);
if (!checks.passed) {
return {
content: [{ type: "text", text:
`Pre-deployment checks failed:\n${checks.errors.join("\n")}`
}],
isError: true,
};
}
// All guards passed — execute deployment
const result = await executeDeployment(args.environment);
return {
content: [{ type: "text", text:
`Deployment to ${args.environment} completed.\n` +
`Version: ${result.version}\n` +
`URL: ${result.url}`
}],
};
}
});
Pattern 8: Context-Aware Tools
Tools can receive context about the current session, allowing them to adapt their behavior. This is particularly useful for tools that need to know about the project structure or user preferences.
// A tool that adapts based on project type
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
if (name === "run_tests") {
// Detect project type from files in the working directory
const hasPackageJson = await fileExists("./package.json");
const hasGoMod = await fileExists("./go.mod");
const hasCargoToml = await fileExists("./Cargo.toml");
const hasPyproject = await fileExists("./pyproject.toml");
let command: string;
if (hasPackageJson) {
const pkg = JSON.parse(await readFile("./package.json", "utf-8"));
command = pkg.scripts?.test ? "npm test" : "npx jest";
} else if (hasGoMod) {
command = "go test ./...";
} else if (hasCargoToml) {
command = "cargo test";
} else if (hasPyproject) {
command = "pytest";
} else {
return {
content: [{ type: "text", text:
"Could not detect project type. No package.json, " +
"go.mod, Cargo.toml, or pyproject.toml found."
}],
isError: true,
};
}
// Optionally filter by test name
if (args.test_name) {
command += ` --grep "${args.test_name}"`;
}
const result = await execCommand(command);
return {
content: [{ type: "text", text: result.stdout + result.stderr }],
};
}
});
Best Practices for Tool Use with Claude Code
Design Principles
- Minimize tool count: Fewer, more capable tools are better than many narrow ones. Claude performs better with a smaller decision space.
- Make tools composable: Design tools so their output can naturally feed into other tools. Return structured data when possible.
- Prefer read-only by default: Tools that modify state should require explicit confirmation or tokens. Read-only tools can be called freely.
- Return actionable output: Tool results should contain enough information for Claude to make decisions without calling the tool again.
- Include error context: When a tool fails, return a helpful error message that guides Claude toward a fix, not just a stack trace.
Performance Optimization
Tool calls consume tokens and time. Optimizing how tools return data keeps conversations efficient and within context limits.
// Inefficient: returns entire file contents
if (name === "get_log") {
const log = await readFile("/var/log/app.log", "utf-8");
return { content: [{ type: "text", text: log }] };
// Could be 100,000+ tokens
}
// Efficient: returns relevant, filtered results
if (name === "get_log") {
const lines = await readFile("/var/log/app.log", "utf-8");
const filtered = lines
.split("\n")
.filter(l => l.includes("ERROR") || l.includes("WARN"))
.slice(-50) // last 50 relevant lines
.join("\n");
return { content: [{ type: "text", text: filtered }] };
// Maybe 500-2000 tokens
}
Security Considerations
- Never expose raw credentials in tool outputs. Mask API keys, passwords, and tokens in results.
- Validate all inputs server-side. Do not trust Claude to always provide valid arguments.
- Use least-privilege database connections. Tools should connect with read-only users when possible.
- Log all tool invocations. Maintain an audit trail of what Claude executed, especially for write operations.
- Sandbox file access. Restrict file tools to the project directory to prevent accidental access to system files.
Debugging Tool Use Issues
When Claude is not using tools correctly, the problem is almost always in the tool description or schema. Here is a debugging checklist:
# Debugging checklist for tool use issues
# 1. Is the tool being discovered?
# Check Claude Code's tool list output to confirm registration.
# 2. Is the description clear enough?
# Ask yourself: "If I only had this description, would I know
# when to use this tool?" If not, rewrite it.
# 3. Is there a conflicting tool?
# If two tools have overlapping descriptions, Claude may
# choose the wrong one. Differentiate them clearly.
# 4. Is the schema too restrictive?
# Overly strict input schemas can cause Claude to fail
# validation. Allow flexibility where reasonable.
# 5. Is the output too large?
# If tool results exceed a few thousand tokens, Claude may
# lose track of the conversation. Truncate and summarize.
# 6. Are errors informative?
# Replace generic errors like "Operation failed" with
# specific messages like "File not found: /src/config.js.
# Did you mean /src/config.ts?"
Real-World Example: A Complete CI/CD Assistant Tool
Let's put everything together with a practical MCP server that gives Claude Code the ability to manage CI/CD pipelines. This tool demonstrates multiple patterns: read-then-act, guard rails, and context-aware behavior.
// tools/cicd-server/index.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
import { execSync } from "child_process";
import { readFileSync, existsSync } from "fs";
const server = new Server(
{ name: "cicd-assistant", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
const PIPELINE_HISTORY: Map<string, any[]> = new Map();
server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "check_pipeline_status",
description:
"Check the status of CI/CD pipelines for the current branch. " +
"Returns the status of the most recent pipeline run including " +
"stage results, duration, and any failure messages. " +
"Use this before deploying or when investigating build failures.",
inputSchema: {
type: "object",
properties: {
branch: {
type: "string",
description: "Branch name. Defaults to current branch if omitted.",
},
},
},
},
{
name: "get_failed_jobs",
description:
"Get details of failed jobs in the most recent pipeline run. " +
"Returns job name, failure stage, and the last 50 lines of " +
"job output logs. Use this to diagnose why a pipeline failed.",
inputSchema: {
type: "object",
properties: {
pipeline_id: {
type: "string",
description: "Pipeline ID. If omitted, uses the most recent.",
},
},
},
},
{
name: "retry_pipeline",
description:
"Retry a failed pipeline or specific job. Requires a " +
"confirmation token for safety. Use this after fixing " +
"the issue that caused a pipeline failure.",
inputSchema: {
type: "object",
properties: {
pipeline_id: { type: "string" },
job_name: {
type: "string",
description: "Specific job to retry. If omitted, retries entire pipeline.",
},
confirm_token: {
type: "string",
description: "Confirmation token from a previous call.",
},
},
required: ["pipeline_id"],
},
},
{
name: "trigger_deployment",
description:
"Trigger a deployment to a specified environment. " +
"Requires confirmation token. Only allows deployment " +
"if the latest pipeline on the branch passed. " +
"Use this after verifying tests pass and pipeline is green.",
inputSchema: {
type: "object",
properties: {
environment: {
type: "string",
enum: ["staging", "production"],
description: "Target deployment environment.",
},
branch: {
type: "string",
description: "Branch to deploy. Defaults to current branch.",
},
confirm_token: { type: "string" },
},
required: ["environment"],
},
},
],
}));
function getCurrentBranch(): string {
return execSync("git rev-parse --abbrev-ref HEAD").toString().trim();
}
function maskSecrets(text: string): string {
return text
.replace(/sk-[a-zA-Z0-9]{20,}/g, "sk-***REDACTED***")
.replace(/password["\s:=]+[^\s"]+/gi, "password=***REDACTED***")
.replace(/token["\s:=]+[^\s"]+/gi, "token=***REDACTED***");
}
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "check_pipeline_status": {
const branch = args.branch || getCurrentBranch();
const output = execSync(
`gh run list --branch ${branch} --limit 1 --json status,conclusion,name,createdAt,databaseId`,
{ encoding: "utf-8" }
);
const runs = JSON.parse(output);
if (runs.length === 0) {
return {
content: [{ type: "text", text:
`No pipeline runs found for branch: ${branch}`
}],
};
}
const run = runs[0];
return {
content: [{ type: "text", text:
`Pipeline #${run.databaseId}\n` +
`Name: ${run.name}\n` +
`Status: ${run.status}\n` +
`Conclusion: ${run.conclusion || "in progress"}\n` +
`Started: ${run.createdAt}\n` +
`Branch: ${branch}`
}],
};
}
case "get_failed_jobs": {
const pipelineId = args.pipeline_id;
const output = execSync(
`gh run view ${pipelineId} --log-failed 2>&1 | tail -50`,
{ encoding: "utf-8" }
);
return {
content: [{ type: "text", text: maskSecrets(output) }],
};
}
case "retry_pipeline": {
if (!args.confirm_token) {
const token = `retry-${Date.now()}`;
return {
content: [{ type: "text", text:
`Retry requires confirmation.\n` +
`Pipeline: ${args.pipeline_id}\n` +
`Job: ${args.job_name || "all jobs"}\n` +
`Confirm with token: ${token}`
}],
};
}
const cmd = args.job_name
? `gh run rerun ${args.pipeline_id} --failed`
: `gh run rerun ${args.pipeline_id}`;
const result = execSync(cmd, { encoding: "utf-8" });
return {
content: [{ type: "text", text: `Pipeline retry initiated.\n${result}` }],
};
}
case "trigger_deployment": {
const env = args.environment;
const branch = args.branch || getCurrentBranch();
// Verify pipeline is green before deploying
const statusOutput = execSync(
`gh run list --branch ${branch} --limit 1 --json conclusion`,
{ encoding: "utf-8" }
);
const runs = JSON.parse(statusOutput);
if (runs.length === 0 || runs[0].conclusion !== "success") {
return {
content: [{ type: "text", text:
`Cannot deploy: latest pipeline on ${branch} ` +
`did not pass. Fix issues and ensure pipeline ` +
`is green before deploying.`
}],
isError: true,
};
}
if (!args.confirm_token) {
const token = `deploy-${env}-${Date.now()}`;
return {
content: [{ type: "text", text:
`Deployment confirmation required.\n` +
`Environment: ${env}\n` +
`Branch: ${branch}\n` +
`Pipeline: PASSED\n` +
`Confirm with token: ${token}`
}],
};
}
// Execute deployment
const deployResult = execSync(
`gh workflow run deploy.yml --ref ${branch} -f environment=${env}`,
{ encoding: "utf-8" }
);
return {
content: [{ type: "text", text:
`Deployment triggered.\n` +
`Environment: ${env}\n` +
`Branch: ${branch}\n` +
`Output: ${deployResult}`
}],
};
}
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
}
} catch (error: any) {
return {
content: [{ type: "text", text:
`Tool execution failed: ${error.message}\n` +
`Stderr: ${error.stderr || "none"}`
}],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
With this MCP server registered, Claude Code can naturally manage your CI/CD workflow. You can ask it to "check if the latest pipeline passed and deploy to staging if it did," and Claude will chain the appropriate tools together, respecting the confirmation guard rails.
Common Pitfalls and How to Avoid Them
Pitfall 1: Overloading Claude with Too Many Tools
When you register 20+ tools, Claude's decision-making degrades. It may call the wrong tool or hesitate to call any. Consolidate related tools and use parameters to handle variations. For example, instead of separate read_json, read_yaml, and read_text tools, use a single read_file tool that handles all formats.
Pitfall 2: Returning Unstructured Output
When tools return free-form text, Claude has to parse it, which can lead to misinterpretation. Where possible, return structured data.
// Avoid: unstructured output
return { content: [{ type: "text", text:
"Found 3 users: Alice (admin), Bob (user), Carol (user)"
}] };
// Prefer: structured output
return { content: [{ type: "text", text: JSON.stringify({
count: 3,
users: [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Carol", role: "user" }
]
}, null, 2) }] };
Pitfall 3: Not Handling Edge Cases in Tool Logic
Claude will eventually call your tool with unexpected inputs. Empty strings, null values, extremely long strings, and special characters are all possible. Always validate and handle gracefully.
// Robust input handling
if (name === "search_code") {
const query = (args.query || "").trim();
if (query.length === 0) {
return {
content: [{ type: "text", text:
"Search query cannot be empty. Provide a pattern to search for."
}],
isError: true,
};
}
if (query.length > 500) {
return {
content: [{ type: "text", text:
"Search query too long (max 500 characters). " +
"Use a more specific pattern."
}],
isError: true,
};
}
// Escape shell metacharacters to prevent injection
const safeQuery = query.replace(/[;&|`$()]/g, "\\$&");
// ... execute search
}
Pitfall 4: Forgetting to Handle Async Errors
Uncaught promise rejections in your MCP server will crash the server, causing all subsequent tool calls to fail. Always wrap async operations in try-catch blocks and return error information as tool results.
Conclusion
Tool use is the mechanism that transforms Claude Code from a code suggestion engine into a capable development agent. By understanding the core patterns — read-then-act, explore-plan-execute, iterative debugging, parallel calls, conditional chaining, tool composition, guard-railed execution, and context-aware tools — you can design workflows that let Claude handle complex, multi-step tasks autonomously and safely. The key to success lies in writing clear tool descriptions, implementing robust guard rails, optimizing output size, and iterating on your tools based on observed behavior. Start with a small set of well-designed tools, observe how Claude uses them, and expand gradually. With thoughtful tool design, Claude Code becomes a deeply integrated member of your development team, capable of understanding your codebase, running your tests, managing your deployments, and solving problems end to end.