Chapter 23

Citations API: Source Attribution for Document QA Systems and Handling Incompatibility with Structured Outputs

Chapter 23: Tool Search Tool: Dynamic Tool Discovery and Adaptive Capability Extension

23.1 The Need for Tool Discovery

In traditional Tool Use patterns, the tool list is passed statically with each API request. This is reasonable when there are few tools (5–10), but serious problems arise when a system needs to support hundreds of tools:

Problem 1: Context window limitations Large numbers of tool definitions consume many input tokens. 50 detailed tool definitions may consume 10,000+ tokens, taking up precious context space.

Problem 2: Attention diffusion Research shows that when the tool list is too long, Claude's tool selection accuracy decreases. The model must find the right tool among many, leading to confusion.

Problem 3: Dynamic tool ecosystem In enterprise environments, tools are continuously added, updated, and retired. A static tool list cannot adapt to this dynamic change.

Solution: Tool Search Tool

The Tool Search Tool is a meta-tool pattern that allows Claude to dynamically search for and discover available tools when needed, rather than receiving all tools at the start of a request.

23.2 Design Principles of the Tool Search Tool

The core idea of the Tool Search Tool is: make tool discovery itself a tool.

tool_search_tool = {
    "name": "search_tools",
    "description": """Search the list of available tools. Use this when you need a certain 
    capability but aren't sure if a corresponding tool exists, or when you want to browse 
    all available tools.
    
    Returns a list of matching tool definitions, each including the name, description, 
    and parameter schema. Once you find a suitable tool, call it by name using the 
    execute_dynamic_tool tool.
    
    NOTE: The system supports dynamic tool invocation: after discovering a tool through 
    search, you can execute it via execute_dynamic_tool even if it was not in the initial 
    tool list.""",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "Search keywords describing the capability you need, e.g. 'send email', 'query database', 'process image'"
            },
            "category": {
                "type": "string",
                "description": "Filter by tool category (optional)",
                "enum": [
                    "communication",
                    "data",
                    "integration",
                    "computation",
                    "media",
                    "utility"
                ]
            },
            "max_results": {
                "type": "integer",
                "default": 5,
                "minimum": 1,
                "maximum": 20,
                "description": "Maximum number of tools to return"
            }
        },
        "required": ["query"]
    }
}

execute_dynamic_tool = {
    "name": "execute_dynamic_tool",
    "description": """Execute a tool discovered through search_tools.
    You MUST use search_tools first to confirm the tool exists and understand 
    its parameter format before calling this tool.""",
    "input_schema": {
        "type": "object",
        "properties": {
            "tool_name": {
                "type": "string",
                "description": "Name of the tool to execute (must be from search_tools results)"
            },
            "tool_input": {
                "type": "object",
                "description": "Parameters to pass to the tool (must conform to the tool's input_schema)"
            }
        },
        "required": ["tool_name", "tool_input"]
    }
}

23.3 Implementing the Tool Registry

import json
import re
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class ToolRegistration:
    definition: dict
    func: callable
    category: str
    tags: List[str] = field(default_factory=list)
    usage_count: int = 0
    last_used: Optional[datetime] = None
    version: str = "1.0"


class DynamicToolRegistry:
    """Dynamic tool registry"""
    
    def __init__(self):
        self.tools: Dict[str, ToolRegistration] = {}
        self._search_index: Dict[str, List[str]] = {}
    
    def register(
        self,
        tool_definition: dict,
        func: callable,
        category: str = "utility",
        tags: List[str] = None
    ) -> "DynamicToolRegistry":
        name = tool_definition["name"]
        self.tools[name] = ToolRegistration(
            definition=tool_definition, func=func,
            category=category, tags=tags or []
        )
        self._index_tool(name, tool_definition, tags or [])
        return self
    
    def _index_tool(self, name: str, definition: dict, tags: List[str]):
        text = f"{name} {definition.get('description', '')} {' '.join(tags)}"
        keywords = set(re.findall(r'[\w]+', text.lower()))
        for keyword in keywords:
            self._search_index.setdefault(keyword, [])
            if name not in self._search_index[keyword]:
                self._search_index[keyword].append(name)
    
    def search(self, query: str, category: Optional[str] = None,
               max_results: int = 5) -> List[dict]:
        query_keywords = set(re.findall(r'[\w]+', query.lower()))
        tool_scores: Dict[str, float] = {}
        
        for keyword in query_keywords:
            if keyword in self._search_index:
                for tool_name in self._search_index[keyword]:
                    tool_scores[tool_name] = tool_scores.get(tool_name, 0) + 1.0
            for indexed_kw, tool_names in self._search_index.items():
                if indexed_kw.startswith(keyword) and indexed_kw != keyword:
                    for tool_name in tool_names:
                        tool_scores[tool_name] = tool_scores.get(tool_name, 0) + 0.5
        
        if category:
            tool_scores = {
                n: s for n, s in tool_scores.items()
                if self.tools.get(n) and self.tools[n].category == category
            }
        
        sorted_tools = sorted(tool_scores.items(), key=lambda x: x[1], reverse=True)
        results = []
        for tool_name, score in sorted_tools[:max_results]:
            if tool_name in self.tools:
                reg = self.tools[tool_name]
                results.append({
                    "name": tool_name,
                    "description": reg.definition.get("description", ""),
                    "category": reg.category,
                    "tags": reg.tags,
                    "input_schema": reg.definition.get("input_schema", {}),
                    "relevance_score": round(score, 2)
                })
        return results
    
    def execute(self, tool_name: str, tool_input: dict):
        if tool_name not in self.tools:
            raise ValueError(f"Tool not found: {tool_name}")
        reg = self.tools[tool_name]
        reg.usage_count += 1
        reg.last_used = datetime.now()
        return reg.func(**tool_input)
    
    def list_categories(self) -> Dict[str, int]:
        categories = {}
        for reg in self.tools.values():
            categories[reg.category] = categories.get(reg.category, 0) + 1
        return categories
    
    def get_popular_tools(self, top_n: int = 10) -> List[dict]:
        sorted_tools = sorted(
            self.tools.items(), key=lambda x: x[1].usage_count, reverse=True
        )
        return [
            {"name": n, "usage_count": r.usage_count, "category": r.category}
            for n, r in sorted_tools[:top_n]
        ]

23.4 Complete Dynamic Tool Discovery Agent

import anthropic

class DynamicToolAgent:
    """Agent with dynamic tool discovery support"""
    
    def __init__(self, registry: DynamicToolRegistry, model: str = "claude-opus-4-5"):
        self.client = anthropic.Anthropic()
        self.registry = registry
        self.model = model
        self.meta_tools = [tool_search_tool, execute_dynamic_tool]
    
    def _handle_search_tools(self, inputs: dict) -> str:
        results = self.registry.search(
            query=inputs["query"],
            category=inputs.get("category"),
            max_results=inputs.get("max_results", 5)
        )
        if not results:
            return json.dumps({
                "found": 0,
                "message": f"No tools found matching '{inputs['query']}'",
                "suggestion": "Try different keywords or search without a category filter"
            })
        return json.dumps({"found": len(results), "tools": results})
    
    def _handle_execute_dynamic_tool(self, inputs: dict) -> str:
        tool_name = inputs["tool_name"]
        tool_input = inputs["tool_input"]
        try:
            result = self.registry.execute(tool_name, tool_input)
            return json.dumps(result, default=str)
        except ValueError as e:
            return json.dumps({
                "error": str(e),
                "suggestion": "Use search_tools to verify the tool name is correct"
            })
        except Exception as e:
            return json.dumps({
                "error": f"Execution failed: {str(e)}",
                "tool_name": tool_name
            })
    
    def _process_tool_call(self, tool_name: str, tool_input: dict) -> str:
        if tool_name == "search_tools":
            return self._handle_search_tools(tool_input)
        elif tool_name == "execute_dynamic_tool":
            return self._handle_execute_dynamic_tool(tool_input)
        return json.dumps({"error": f"Unknown meta-tool: {tool_name}"})
    
    def run(self, user_message: str, system: str = "") -> str:
        default_system = """You are an assistant that can dynamically discover and use tools.

[Tool usage workflow]
1. Upon receiving a task, think about what types of capabilities are needed
2. Use search_tools to find relevant tools
3. Review the results to understand tool parameter formats
4. Use execute_dynamic_tool to execute the discovered tools
5. Analyze results; continue searching if more tools are needed

[Important]
- Never assume a tool exists; always confirm via search_tools first
- If the first search finds nothing, try different keywords
- When tool execution fails, verify that parameters match the tool's input_schema"""
        
        messages = [{"role": "user", "content": user_message}]
        
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=4096,
                system=system or default_system,
                tools=self.meta_tools,
                messages=messages
            )
            
            if response.stop_reason == "end_turn":
                return ' '.join(b.text for b in response.content if b.type == "text")
            
            if response.stop_reason == "tool_use":
                tool_results = []
                for block in response.content:
                    if block.type != "tool_use":
                        continue
                    print(f"[Meta-tool] {block.name}: {json.dumps(block.input)[:150]}")
                    result = self._process_tool_call(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })
                messages.append({"role": "assistant", "content": response.content})
                messages.append({"role": "user", "content": tool_results})
            else:
                break
        
        return "Task complete"

23.5 Building the Tool Ecosystem

Registering Many Tools in Practice

def build_enterprise_registry() -> DynamicToolRegistry:
    """Build an enterprise-scale tool registry (example)"""
    registry = DynamicToolRegistry()
    
    # Communication tools
    registry.register(
        tool_definition={
            "name": "send_email",
            "description": "Send an email to specified recipients",
            "input_schema": {
                "type": "object",
                "properties": {
                    "to": {"type": "string"},
                    "subject": {"type": "string"},
                    "body": {"type": "string"}
                },
                "required": ["to", "subject", "body"]
            }
        },
        func=lambda to, subject, body: {"status": "sent", "message_id": "msg_001"},
        category="communication",
        tags=["email", "notify", "message"]
    )
    
    registry.register(
        tool_definition={
            "name": "send_sms",
            "description": "Send an SMS to a specified phone number",
            "input_schema": {
                "type": "object",
                "properties": {
                    "phone": {"type": "string"},
                    "message": {"type": "string", "maxLength": 160}
                },
                "required": ["phone", "message"]
            }
        },
        func=lambda phone, message: {"status": "sent", "cost": 0.05},
        category="communication",
        tags=["sms", "text", "phone", "mobile"]
    )
    
    # Data tools
    registry.register(
        tool_definition={
            "name": "query_mysql",
            "description": "Execute a MySQL database query",
            "input_schema": {
                "type": "object",
                "properties": {
                    "sql": {"type": "string"},
                    "database": {"type": "string"}
                },
                "required": ["sql", "database"]
            }
        },
        func=lambda sql, database: {"rows": [], "affected": 0},
        category="data",
        tags=["database", "mysql", "sql", "query"]
    )
    
    # Integration tools
    registry.register(
        tool_definition={
            "name": "create_jira_ticket",
            "description": "Create a ticket in Jira",
            "input_schema": {
                "type": "object",
                "properties": {
                    "project": {"type": "string"},
                    "summary": {"type": "string"},
                    "description": {"type": "string"},
                    "issue_type": {"type": "string", "enum": ["Bug", "Story", "Task"]}
                },
                "required": ["project", "summary", "issue_type"]
            }
        },
        func=lambda **kw: {"ticket_id": "PROJ-001", "url": "https://jira.example.com/PROJ-001"},
        category="integration",
        tags=["jira", "ticket", "bug", "task", "project management"]
    )
    
    registry.register(
        tool_definition={
            "name": "get_salesforce_contact",
            "description": "Retrieve contact information from Salesforce CRM",
            "input_schema": {
                "type": "object",
                "properties": {
                    "email": {"type": "string"},
                    "name": {"type": "string"}
                }
            }
        },
        func=lambda **kw: {"contact_id": "003xx001", "name": "John Doe", "company": "Acme Corp"},
        category="integration",
        tags=["salesforce", "crm", "customer", "contact"]
    )
    
    print(f"Registry: {len(registry.tools)} tools registered")
    print(f"Category distribution: {registry.list_categories()}")
    return registry

23.6 Adaptive Tool Combination: Handling Unforeseen Tasks

The true power of the Tool Search Tool lies in handling tasks unforeseen during system design:

def demonstrate_adaptive_capability():
    registry = build_enterprise_registry()
    agent = DynamicToolAgent(registry)
    
    # Task 1: A predictable task
    result1 = agent.run("Send an email to [email protected] notifying them that tomorrow's meeting is cancelled")
    
    # Task 2: A complex task requiring multiple tools
    result2 = agent.run("""
    Read the customer list from /data/customers.xlsx,
    look up each customer's contact info in Salesforce,
    create a Jira follow-up ticket for each one,
    and send an email notifying the sales team.
    """)
    
    # Task 3: A completely unforeseen combination requirement
    result3 = agent.run("""
    I need to send SMS discount coupons to customers who have purchased more than $10,000 recently.
    First query the database to get the customer list, then send each one an SMS.
    """)
    
    return result1, result2, result3

23.7 Tool Versioning and Capability Evolution

As the business evolves, tool capabilities need to evolve too. The Tool Search pattern naturally supports seamless tool updates:

class VersionedToolRegistry(DynamicToolRegistry):
    """Registry with tool version management"""
    
    def register_versioned(
        self,
        tool_definition: dict,
        func: callable,
        version: str = "1.0",
        deprecated_versions: List[str] = None,
        **kwargs
    ):
        name = tool_definition["name"]
        
        if name in self.tools:
            old_version = self.tools[name].version
            print(f"Updating tool {name}: v{old_version} -> v{version}")
        
        description = tool_definition.get("description", "")
        if deprecated_versions:
            description += f"\n(Current version: {version}; deprecated: {', '.join(deprecated_versions)})"
        else:
            description += f"\n(Version: {version})"
        
        enhanced_definition = {**tool_definition, "description": description}
        self.register(enhanced_definition, func, **kwargs)
        self.tools[name].version = version
        return self

Summary

The Tool Search Tool pattern elevates tool usage from "static configuration" to "dynamic discovery," making it suitable for:

  1. Large tool counts (50+): Avoids passing the full tool list on every request
  2. Dynamic tool ecosystem: New tools go live without changing agent code
  3. Handling unforeseen tasks: Let Claude autonomously discover capabilities needed for a task
  4. Cross-team tool sharing: Tools from different teams registered centrally, discovered on demand

Core implementation elements:

The next chapter explores the Advisor Tool — an advanced pattern for having Claude perform metacognitive planning before executing tasks.

Rate this chapter
4.8  / 5  (9 ratings)

💬 Comments