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:
- Large tool counts (50+): Avoids passing the full tool list on every request
- Dynamic tool ecosystem: New tools go live without changing agent code
- Handling unforeseen tasks: Let Claude autonomously discover capabilities needed for a task
- Cross-team tool sharing: Tools from different teams registered centrally, discovered on demand
Core implementation elements:
- A tool registry (supporting semantic search)
- Meta-tools (
search_tools+execute_dynamic_tool) - Clear usage workflow instructions in the system prompt to guide Claude in using this pattern
The next chapter explores the Advisor Tool — an advanced pattern for having Claude perform metacognitive planning before executing tasks.