TL;DR:
- A lead research agent can reduce the manual time per prospect from 20-30 minutes to under 2 minutes
- The stack: a web search MCP server, a company data tool (Clearbit or Apollo free tier), and an LLM synthesizing findings into structured output
- Output format matters as much as the data — structured JSON that maps directly to CRM fields makes the agent actually useful
Manually researching leads is one of the highest-value, highest-time-cost activities in sales. Before a meaningful outreach, a rep typically needs: what the company actually does, recent news that creates a relevant hook, the prospect’s role and recent activity, and signals that suggest buying intent. Most reps either skip steps or spend 20-30 minutes per prospect.
A well-built lead research agent handles the entire information-gathering phase in under two minutes, producing output structured to drop directly into your CRM. Here’s how to build one.
Architecture Overview
The agent needs three capabilities:
- Web search — for company news, product launches, hiring signals, press mentions
- Company data lookup — for firmographic details (headcount, industry, funding, tech stack)
- LinkedIn context — for decision-maker background (available through Proxycurl API or Apollo)
These map cleanly to MCP tools. The agent has a system prompt describing its research workflow, receives a company name and contact name as input, and produces structured JSON.
Setting Up the MCP Tools
Use a web search MCP server (Brave Search MCP or Exa MCP both work) alongside a company enrichment tool. For a lightweight stack, Exa’s search API handles both news search and company background:
from anthropic import Anthropic
import json
client = Anthropic()
tools = [
{
"name": "search_web",
"description": "Search the web for recent news, company information, or any topic",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"num_results": {"type": "integer", "default": 5}
},
"required": ["query"]
}
},
{
"name": "search_company_news",
"description": "Search for recent news about a specific company",
"input_schema": {
"type": "object",
"properties": {
"company_name": {"type": "string"},
"days_back": {"type": "integer", "default": 90}
},
"required": ["company_name"]
}
}
]
The Research Prompt
The system prompt defines the research workflow and output schema. Being specific about structure prevents the LLM from getting creative with output formats that break your CRM integration:
SYSTEM_PROMPT = """You are a sales research agent. Given a company name and contact name,
research the prospect thoroughly and return structured JSON.
Research workflow:
1. Search for recent company news (last 90 days)
2. Look up the company's core business and products
3. Find the contact's role and any public professional context
4. Identify a relevant hook (recent funding, product launch, hiring surge, news)
Return your findings as JSON with these fields:
{
"company_summary": "2-3 sentence description of what the company does",
"company_size": "estimated headcount range",
"industry": "primary industry",
"recent_news": ["up to 3 recent news items as strings"],
"buying_signals": ["signals that suggest potential need for our product"],
"contact_context": "what this person likely cares about based on their role",
"suggested_hook": "one specific, relevant opening for outreach",
"research_confidence": "high | medium | low"
}
If you cannot find reliable information for a field, use null rather than guessing."""
Implementing the Tool Execution Loop
import anthropic
from exa_py import Exa
exa = Exa(api_key="your-exa-key")
def execute_tool(tool_name: str, tool_input: dict) -> str:
if tool_name == "search_web":
results = exa.search(
tool_input["query"],
num_results=tool_input.get("num_results", 5),
use_autoprompt=True
)
return json.dumps([{"title": r.title, "url": r.url, "snippet": r.text[:500]}
for r in results.results])
elif tool_name == "search_company_news":
results = exa.search(
f"{tool_input['company_name']} news funding product launch",
num_results=5,
use_autoprompt=True,
start_published_date=get_date_days_ago(tool_input.get("days_back", 90))
)
return json.dumps([{"title": r.title, "url": r.url, "date": r.published_date}
for r in results.results])
def research_lead(company: str, contact_name: str, contact_title: str) -> dict:
messages = [
{
"role": "user",
"content": f"Research this prospect: Company: {company}, Contact: {contact_name}, Title: {contact_title}"
}
]
while True:
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=2000,
system=SYSTEM_PROMPT,
tools=tools,
messages=messages
)
if response.stop_reason == "end_turn":
# Extract JSON from final response
for block in response.content:
if hasattr(block, 'text'):
return json.loads(extract_json(block.text))
# Process tool calls
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(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})
Handling Scale: Batch Processing
For a list of prospects (a CSV export from your CRM or LinkedIn Sales Navigator), run the research agent concurrently across rows. Rate limits on both the LLM API and the search API are the main constraint. A practical batch size is 5-10 concurrent requests, with exponential backoff on failures:
import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
async def research_lead_with_retry(company, contact, title):
return await asyncio.to_thread(research_lead, company, contact, title)
async def process_leads_batch(leads: list, concurrency: int = 5) -> list:
semaphore = asyncio.Semaphore(concurrency)
async def bounded_research(lead):
async with semaphore:
return await research_lead_with_retry(
lead["company"], lead["contact"], lead["title"]
)
return await asyncio.gather(*[bounded_research(l) for l in leads])
CRM Integration
The structured output maps directly to CRM fields. For HubSpot:
import hubspot
from hubspot.crm.contacts import SimplePublicObjectInput
def update_contact_with_research(contact_id: str, research: dict):
client = hubspot.Client.create(access_token="your-token")
properties = {
"lead_research_summary": research["company_summary"],
"buying_signals": "\n".join(research.get("buying_signals", [])),
"suggested_hook": research["suggested_hook"],
"research_confidence": research["research_confidence"],
"research_date": datetime.now().isoformat()
}
client.crm.contacts.basic_api.update(
contact_id,
simple_public_object_input=SimplePublicObjectInput(properties=properties)
)
Cost and Performance
At roughly 3-5 tool calls per lead, with an average of 800 tokens input and 400 tokens output per LLM call, expect costs around $0.008-0.015 per lead using Claude Opus 4. Exa search API costs roughly $0.003-0.01 per search call. Total cost per researched lead: $0.02-0.05.
For 500 leads per month, that’s $10-25 in API costs to replace several hours of manual research. The ROI is straightforward.
What to Monitor
Track research_confidence over time. If the agent regularly returns "low" confidence for certain types of companies (niche B2B, private companies with minimal web presence), tune the search queries for those cases or add a fallback data source. The hook suggestions are the hardest to evaluate automatically — periodic human review of a sample helps calibrate whether the agent is finding genuinely relevant angles or defaulting to generic observations.