Tool Use Architecture: Designing Extensible AI Agent Capabilities
The difference between an agent that works and one that breaks is rarely the model—it's how you structure the tools it can access.
I've built dozens of AI agents that operate in production. The ones that scale aren't the ones with the most tools. They're the ones with the most thoughtful tool architecture. When you're designing tool use systems, you're not just wiring up API calls—you're building the boundaries, discovery mechanisms, and execution safeguards that let your agent operate autonomously without catastrophic failures.
Here's what I've learned about building tool use architectures that actually work at scale.
Why Tool Architecture Matters
Each tool defines a contract: you specify what operations are available and what they return; Claude decides when and how to call them. Tool access is one of the highest-leverage primitives you can give an agent.
But here's the catch: as your tool library grows, agent performance degrades. The more tools an AI agent has access to, the harder it becomes to select the correct one. The increased number of tools likely contributes to performance drops.
This is where architecture becomes critical. You can't just dump 50 tools into Claude's context and expect it to pick the right one. You need a system that:
- Surfaces only relevant tools — based on the task at hand
- Enforces strict boundaries — what each tool can and cannot do
- Validates inputs and outputs — catching mistakes before they cascade
- Scales to hundreds of tools — without loading them all into context
This is the difference between a prototype and production. For a deeper look at production patterns, check out Building AI Agents That Actually Work.
The Three-Layer Tool Architecture
I've settled on a three-layer approach that works across most agent systems:
Layer 1: Tool Definition & Schema
JSON schemas are the structured descriptions that tell Claude exactly what each tool does and how to use it. These schemas are the only information Claude receives about your tools, making them crucial for successful function calling.
The schema is your contract. Make it explicit.
const tools = [
{
name: "database_query",
description: "Execute read-only queries against the customer database. Use for retrieving customer records, order history, or account information.",
input_schema: {
type: "object",
properties: {
query_type: {
type: "string",
enum: ["customer_lookup", "order_history", "account_status"],
description: "The type of query to execute"
},
customer_id: {
type: "string",
description: "The customer ID to query (required for all query types)"
}
},
required: ["query_type", "customer_id"]
}
}
]
Notice: specific enum values, clear descriptions, explicit required fields.
Rather than creating a separate tool for every action, group them into a single tool with an action parameter. Fewer, more capable tools reduce selection ambiguity and make your tool surface easier for Claude to navigate.
Layer 2: Tool Selection & Routing
This is where most architectures fail. You can't rely on Claude alone to pick the right tool from a massive library.
Effective tool selection uses both rule-based and prompt-based approaches, striking a balance between automation and flexibility. The choice of which one to employ depends on the task, its complexity, and the requirement for deterministic selection.
I use a hybrid approach:
Rule-based filtering — Pre-filter tools based on explicit conditions:
function selectToolsForTask(userQuery: string, availableTools: Tool[]): Tool[] {
// If query mentions "weather", only include weather tools
if (userQuery.toLowerCase().includes("weather")) {
return availableTools.filter(t => t.category === "weather");
}
// If query mentions "database" or "customer", include data access tools
if (/database|customer|order|account/.test(userQuery)) {
return availableTools.filter(t => t.category === "data");
}
// Default: return only safe, read-only tools
return availableTools.filter(t => t.riskLevel === "low");
}
Semantic routing — Use Claude to decide between tool categories:
const routingPrompt = `
Given this user request, which tool category is most relevant?
- data: Database queries, customer lookups, historical records
- external: API calls, third-party integrations, external services
- action: State-changing operations (create, update, delete)
User request: "${userQuery}"
Respond with only the category name.
`;
This two-step process—rules first, then semantic refinement—keeps your context window efficient while maintaining flexibility.
Layer 3: Execution & Validation
Others define strict function call schemas, requiring the model to select from a set of verifiable actions. Yet even with RAG and schema enforcement, LLM agents can go astray. That's why real-time validation is essential.
Before executing any tool, validate:
async function executeToolSafely(toolCall: ToolCall): Promise<ToolResult> {
// 1. Validate schema
const validation = validateAgainstSchema(toolCall.input, toolCall.tool.input_schema);
if (!validation.valid) {
return {
error: `Invalid input: ${validation.errors.join(", ")}`,
status: "validation_failed"
};
}
// 2. Check permissions
const hasPermission = await checkUserPermissions(toolCall.tool.name, toolCall.input);
if (!hasPermission) {
return {
error: "Permission denied for this operation",
status: "unauthorized"
};
}
// 3. Execute with timeout
try {
const result = await Promise.race([
executeToolLogic(toolCall),
timeout(30000) // 30 second timeout
]);
// 4. Validate output
return sanitizeToolOutput(result);
} catch (error) {
return {
error: `Tool execution failed: ${error.message}`,
status: "execution_failed"
};
}
}
This three-layer approach—definition, routing, execution—creates safety rails without blocking legitimate operations.
Plugin Architecture: Making Tools Extensible
The real power comes when you decouple tool definitions from your core agent logic. I use a plugin system where tools can be registered, versioned, and swapped without redeploying the agent.
Too many tools or overlapping tools can also distract agents from pursuing efficient strategies. Careful, selective planning of the tools you build (or don't build) can really pay off. Your AI agents will potentially gain access to dozens of MCP servers and hundreds of different tools–including those by other developers. When tools overlap in function or have a vague purpose, agents can get confused about which ones to use.
Here's how I structure it:
interface ToolPlugin {
name: string;
version: string;
category: string;
schema: JSONSchema;
execute: (input: any) => Promise<any>;
validate?: (input: any) => ValidationResult;
permissions?: string[];
timeout?: number;
}
class ToolRegistry {
private tools: Map<string, ToolPlugin> = new Map();
register(tool: ToolPlugin) {
const key = `${tool.name}@${tool.version}`;
this.tools.set(key, tool);
}
getToolsForAgent(agentRole: string): ToolPlugin[] {
return Array.from(this.tools.values())
.filter(tool => {
// Filter by agent permissions
return tool.permissions?.includes(agentRole) ?? true;
});
}
// Tool discovery with caching
getToolsByCategory(category: string, limit: number = 10): ToolPlugin[] {
return Array.from(this.tools.values())
.filter(t => t.category === category)
.slice(0, limit);
}
}
This lets you:
- Add new tools without touching agent code
- Version tools independently
- Grant different agents different tool access
- Disable problematic tools without redeploying
Handling Tool Complexity
By selectively implementing tools whose names reflect natural subdivisions of tasks, you simultaneously reduce the number of tools and tool descriptions loaded into the agent's context and offload agentic computation from the agent's context back into the tool calls themselves. This reduces an agent's overall risk of making mistakes.
I've found that tool complexity is inversely correlated with agent success. The simpler and more focused each tool, the better Claude uses it.
Bad tool:
{
name: "manage_data",
description: "Manages all data operations including retrieval, creation, updates, deletion, and transformation",
input_schema: { /* massive schema with dozens of optional fields */ }
}
Good tools:
{
name: "customer_get",
description: "Retrieve a single customer record by ID",
input_schema: {
type: "object",
properties: {
customer_id: { type: "string" }
},
required: ["customer_id"]
}
}
{
name: "customer_list",
description: "List customers matching filter criteria",
input_schema: {
type: "object",
properties: {
status: { type: "string", enum: ["active", "inactive"] },
limit: { type: "number", minimum: 1, maximum: 100 }
}
}
}
Multiple focused tools beat one bloated tool. Always.
Security Boundaries
Tool use is where agents touch your production systems. You need hard boundaries.
The best practice is to start with minimal permissions. If the agent determines that it requires a restricted tool, it must request access and possibly seek approval from a human.
I implement this with role-based tool access:
const toolPermissions = {
"read_agent": [
"database_query",
"api_fetch",
"file_read"
],
"write_agent": [
"database_query",
"database_update",
"api_fetch",
"file_read",
"email_send"
],
"admin_agent": [
// All tools available
]
};
function checkToolAccess(agentRole: string, toolName: string): boolean {
const allowedTools = toolPermissions[agentRole] || [];
return allowedTools.includes(toolName);
}
And for sensitive operations, I add human-in-the-loop:
async function executeRestrictedTool(
toolCall: ToolCall,
agent: Agent
): Promise<ToolResult> {
// If tool is marked sensitive, require approval
if (toolCall.tool.requiresApproval) {
const approval = await requestHumanApproval({
agent: agent.name,
tool: toolCall.tool.name,
input: toolCall.input,
reason: toolCall.tool.approvalReason
});
if (!approval.approved) {
return {
error: "Operation requires human approval",
status: "pending_approval"
};
}
}
return executeToolSafely(toolCall);
}
This is where you see the real value of standardized tool definitions. MCP tool definitions use a schema format similar to Claude's tool format. You just need to rename inputSchema to input_schema. The standardization makes it easier to enforce consistent security policies across all tools. For more on this, see Security Architecture for AI Agent Systems: Protecting Credentials and Limiting Access.
Performance Optimization
Large tool libraries kill performance. I use lazy loading and caching:
class OptimizedToolRegistry {
private cache: Map<string, ToolPlugin[]> = new Map();
private loadedTools: Set<string> = new Set();
// Only load tools when needed
async getToolsForTask(taskDescription: string): Promise<ToolPlugin[]> {
const cacheKey = hashTaskDescription(taskDescription);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
// Semantic search: find relevant tools
const relevantTools = await semanticSearch(taskDescription, this.toolIndex);
// Load only what we need
const tools = relevantTools
.filter(t => !this.loadedTools.has(t.name))
.slice(0, 5); // Limit to 5 tools per request
this.cache.set(cacheKey, tools);
return tools;
}
}
The goal: keep the active tool set under 10 tools per agent request. This dramatically improves selection accuracy.
Real-World Integration Patterns
When integrating with external systems, I use a wrapper pattern:
class ExternalServiceTool implements ToolPlugin {
name = "slack_send_message";
category = "communication";
async execute(input: { channel: string; message: string }) {
try {
// Validate before sending
if (!this.validateInput(input)) {
throw new Error("Invalid input");
}
// Call external service
const response = await this.slackClient.chat.postMessage({
channel: input.channel,
text: input.message,
metadata: {
agent: "customer_service_agent",
timestamp: new Date().toISOString()
}
});
// Return only relevant data
return {
success: true,
message_id: response.ts,
channel: input.channel
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
Notice the wrapper:
- Validates input before calling external service
- Adds metadata for observability
- Returns only relevant data (not raw API responses)
- Handles errors gracefully
This pattern scales across any external system—APIs, databases, webhooks, whatever.
Observability & Debugging
You can't optimize what you can't see. I instrument every tool call:
async function executeWithObservability(
toolCall: ToolCall,
agent: Agent
): Promise<ToolResult> {
const startTime = Date.now();
const span = tracer.startSpan("tool_execution", {
attributes: {
"tool.name": toolCall.tool.name,
"agent.name": agent.name,
"tool.input": JSON.stringify(toolCall.input)
}
});
try {
const result = await executeToolSafely(toolCall);
span.setAttributes({
"tool.status": result.status,
"tool.duration_ms": Date.now() - startTime,
"tool.output_size": JSON.stringify(result).length
});
return result;
} catch (error) {
span.recordException(error);
throw error;
} finally {
span.end();
}
}
This gives you visibility into:
- Which tools are being called
- How long they take
- What inputs are being sent
- What outputs are returned
- Which tools fail and why
Over time, this data shows you which tools need redesign and which agents are struggling with tool selection.
Bringing It Together
Tool use architecture is the bridge between your agent's reasoning and your production systems. Get it right, and your agent scales gracefully. Get it wrong, and you're debugging mysterious failures at 2 AM.
The key principles:
- Simple, focused tools — One job per tool
- Explicit routing — Don't rely on Claude alone for selection
- Hard security boundaries — Minimal permissions by default
- Real-time validation — Catch mistakes before they happen
- Plugin architecture — Make tools extensible without redeploying
- Observability — Instrument everything
If you're building production AI agents, this is where you'll spend 80% of your effort. And it's worth it.
For deeper patterns on building agents at scale, check out Building Production AI Agents: Lessons from the Trenches. For multi-tool orchestration at scale, Multi-Agent Systems: When One LLM Isn't Enough explores how to structure agents that coordinate tool use across specialized domains.
The architecture you build today determines whether your agent scales to 10 tools or 1,000. Choose wisely.
Want to discuss tool architecture for your specific use case? Get in touch — I'm always interested in hearing about production agent challenges.