Agentic Patterns & MCP for Real Apps
Reach for an agent only when a task is genuinely open-ended, and use MCP to connect Claude to real tools and data through one standard interface.
"Agent" is an overused word. In practice most AI features are a single call or a short tool-use loop — and you should keep them that way. Reach for a full agent only when the task is genuinely open-ended and hard to script.
The ladder of complexity
- Single call — classify, summarise, extract. One request, one response. Most features live here.
- Tool-use loop — Claude asks to call your functions, you run them, you feed results back, it answers. This covers "look something up, then respond".
- Agent — a longer, model-driven loop that decides its own steps. Powerful, but higher cost and latency, and errors need to be catchable. Justify it before you build it.
MCP: one interface to many tools
The Model Context Protocol lets Claude connect to external tools and data sources — your database, GitHub, a search index — through a single standard interface, instead of you hand-wiring each integration. You point Claude at an MCP server and its tools become available. It is the clean way to give an assistant real capabilities in a real product.
Keep the human in control
Whatever the tier, you execute the tools. Gate anything destructive or irreversible behind a confirmation, and log what the model chose to call. Autonomy is a spectrum you dial in, not an on-switch.
Example
// A tool-use LOOP β the right tier for 'look it up, then answer'.
// Not a full agent: your code stays in control of every step.
const tools = [{
name: "get_order_status",
description: "Look up an order's status. Call when the user asks about an order.",
input_schema: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
},
}];
let messages = [{ role: "user", content: userMessage }];
let res = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, tools, messages });
while (res.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: res.content });
const results = [];
for (const block of res.content) {
if (block.type === "tool_use") {
// YOU run the tool β gate anything destructive behind your own checks.
const output = await lookupOrder(block.input.order_id);
results.push({ type: "tool_result", tool_use_id: block.id, content: output });
}
}
messages.push({ role: "user", content: results });
res = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, tools, messages });
}When to use it
- Build a multi-step agent that calls a searchProducts tool, then a checkInventory tool, before answering a user query.
- Connect Claude to your internal CRM via an MCP server so it can look up account data without custom API glue code.
- Use a tool-use loop with a human-approval step for any tool that modifies data, such as deleteRecord or sendEmail.
More examples
Two-tool agent loop
Drives the full agent loop until Claude reaches end_turn, executing any number of sequential tool calls.
const tools = [searchTool, inventoryTool];
let messages = [{ role: "user", content: userQuery }];
while (true) {
const res = await client.messages.create({ model: "claude-sonnet-4-5", max_tokens: 512, tools, messages });
if (res.stop_reason === "end_turn") { console.log(res.content[0].text); break; }
const calls = res.content.filter(b => b.type === "tool_use");
const results = await Promise.all(calls.map(c => runTool(c.name, c.input).then(r => ({
type: "tool_result", tool_use_id: c.id, content: JSON.stringify(r)
}))));
messages = [...messages, { role: "assistant", content: res.content }, { role: "user", content: results }];
}MCP server config for a CRM
Registers a local MCP server for the CRM so Claude Code can call CRM tools without any custom handler code.
{
"mcpServers": {
"crm": {
"command": "node",
"args": ["./mcp/crm-server.js"],
"env": {
"CRM_API_KEY": "${CRM_API_KEY}",
"CRM_BASE_URL": "https://crm.internal.example.com"
}
}
}
}Human-in-the-loop before destructive tools
Intercepts calls to destructive tools and requires explicit user confirmation before executing them.
async function runTool(name, input) {
const DESTRUCTIVE = ["delete_record", "send_email", "charge_card"];
if (DESTRUCTIVE.includes(name)) {
const confirmed = await promptUser(`Claude wants to call ${name} with ${JSON.stringify(input)}. Allow? (y/n)`);
if (confirmed !== "y") return { error: "Action cancelled by user" };
}
return toolHandlers[name](input);
}
Discussion