본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 30. 11:33

B2B 리드 인리치먼트 (Lead Enrichment)를 위해 Hunter.io와 Apollo를 결합한 MCP 서버를 구축한 방법

요약

Hunter.io와 Apollo.io API를 결합하여 B2B 리드 인리치먼트를 수행하는 오픈 소스 MCP 서버 구축 방법을 소개합니다. FastMCP와 Python을 사용하여 AI 에이전트가 외부 데이터를 효율적으로 활용할 수 있는 도구 계층을 구현하는 과정을 다룹니다.

핵심 포인트

  • Hunter.io와 Apollo.io의 장점을 결합한 단일 도구 계층 구축
  • Model Context Protocol(MCP)을 통한 LLM과 외부 API의 표준화된 연결
  • FastMCP SDK를 활용한 비동기 Python 기반의 간결한 서버 구현
  • AI 에이전트의 데이터 환각 문제를 도구(Tools) 제공으로 해결

AI 에이전트 (AI agents)는 강력하지만, 실제 데이터가 없다면 그저 자신감 넘치는 헛소리를 하는 존재일 뿐입니다. Claude를 CRM에 연결하고 "직원 수 50~200명 사이의 핀테크 분야 리드를 찾아줘"라고 하루 종일 요청하더라도, Claude는 내용을 지어낼 것입니다. 실제로 데이터를 가져올 수 있는 도구 (tools)를 제공하기 전까지는 말이죠.

그것이 바로 MCP 서버 (MCP servers)가 존재하는 이유입니다. 그리고 그것이 제가 만든 것입니다.

이 포스트에서는 제가 최근에 출시한 오픈 소스 (open-source) MCP 서버인 b2b-enrichment-mcp에 대해 설명합니다. 이 서버는 두 개의 B2B 데이터 API (이메일용 Hunter.io, 기업 정보용 Apollo.io)를 Claude가 직접 사용할 수 있는 단일 도구 계층 (tool layer)으로 결합합니다. 전체 시스템은 비동기 Python (async Python), FastMCP로 구성되어 있으며, MIT 라이선스를 따르고 무료 티어 (free tier) API 키로 실행됩니다.

저는 아키텍처 (architecture), 문제가 발생했던 부분들, 그리고 이를 어떻게 해결했는지 보여드릴 것입니다.

단일 API 기반 리드 생성 (Lead Gen)의 문제점
리드 인리치먼트 (lead enrichment) 워크플로우 (workflow)를 설정해 본 적이 있다면 그 고통을 잘 알 것입니다. Hunter는 도메인에서 이메일을 찾는 데는 훌륭합니다. 하지만 기업 수준의 데이터에는 그리 뛰어나지 않습니다. Apollo는 기업 그래프 (company graph)를 보유하고 있지만, 그들의 이메일 찾기 기능은 무료 티어에서 속도 제한 (rate-limited)이 매우 심합니다.

그래서 대부분의 사람들은 다음과 같은 설정 중 하나를 선택하게 됩니다:

  1. 두 벤더 (vendors) 모두에게 비용을 지불하고 그들 사이를 연결하는 글루 코드 (glue code)를 작성한다.
  2. 하나만 선택하고 다른 하나는 없는 셈 친다.
  3. 페이로드 (payload)가 이상해 보일 때마다 깨져버리는 Zapier 플로우 (flow)를 구축한다.

당신이 실제로 원하는 것은 하나의 인터페이스입니다. AI 에이전트(AI agent)는 어떤 API가 어떤 데이터를 보유하고 있는지 신경 쓸 필요가 없어야 합니다. 그저 "이 도메인을 인리치먼트(enrich)해줘"라고 말하기만 하면 모든 정보를 돌려받아야 합니다. 이것이 바로 도구(tools)를 LLM에 정규화된 방식으로 노출하는 MCP의 핵심 가치입니다.

왜 MCP인가, 그리고 왜 FastMCP인가

MCP가 생소한 분들을 위해 짧은 배경 설명을 드리겠습니다. Model Context Protocol (MCP)은 LLM이 외부 도구와 통신할 수 있도록 하는 Anthropic의 오픈 표준입니다. 한쪽에는 AI 에이전트가 있고, 다른 한쪽에는 도구 서버(tool server)가 있으며, 그 사이에는 구조화된 호출(structured calls)이 존재합니다. Claude Desktop, Cursor 및 기타 여러 클라이언트들이 이를 네이티브로 지원합니다.

FastMCP는 서버 구축을 믿을 수 없을 정도로 단순하게 만들어주는 Python SDK입니다. 비동기 함수(async functions)를 작성하고, @mcp.tool() 데코레이터를 붙이기만 하면 기본적으로 끝납니다. SDK가 JSON-RPC, 전송(transport), 스키마 생성(schema generation) 등 모든 지루한 작업들을 처리합니다.

최소한의 도구는 다음과 같이 생겼습니다:

from fastmcp import FastMCP
import httpx

mcp = FastMCP("b2b-enrichment")

@mcp.tool()
async def find_emails(domain: str, limit: int = 10) -> dict:
"""Hunter.io를 통해 도메인과 관련된 이메일을 찾습니다."""
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.hunter.io/v2/domain-search",
params={"domain": domain, "limit": limit, "api_key": HUNTER_KEY}
)
return response.json()

이것이 작동하는 도구입니다. 이제 Claude는 find_emails("stripe.com")를 호출하여 구조화된 데이터를 돌려받을 수 있습니다. Flask도, FastAPI도, 보일러플레이트(boilerplate)도 필요 없습니다.

아키텍처 (The Architecture)

이 서버는 두 개의 제공업체(providers)로 나뉜 9개의 도구를 노출합니다:

Hunter 도구 (5개):

  • find_emails_by_domain — 도메인에 대한 이메일 목록 조회
  • find_email_by_name — 특정 인물의 이메일 찾기
  • verify_email — 전달 가능성 (deliverability) 확인
  • count_emails — API 크레딧을 소모하기 전 빠른 수량 확인
  • get_account_info — 남은 할당량(quota) 확인, 에이전트가 스스로 속도를 조절(self-throttle)하는 데 유용함

Apollo 도구 (4개):

enrich_company — 회사 규모 (company size), 산업군 (industry), 기술 스택 (tech stack), 자금 조달 현황 (funding)
search_companies — 기준에 부합하는 기업 검색
get_company_employees — 특정 기업의 직원 목록 조회
bulk_enrich — 한 번의 호출로 최대 10개의 도메인을 일괄 처리 (batch)

모든 도구는 비동기(async)로 작동하며, 단일 httpx.AsyncClient 인스턴스를 공유하고, 동일한 에러 핸들링 (error handling) 패턴으로 래핑(wrapped)되어 있습니다. 에이전트는 의도(intent)에 따라 도구를 선택합니다. 이메일이 필요하면 Hunter를 사용하고, 기업 정보 (firmographics)가 필요하면 Apollo를 사용합니다. 두 가지 모두가 필요하다면, 두 도구를 모두 호출한 뒤 스스로 결과를 병합(merge)합니다.

정신을 잃지 않고 속도 제한 (Rate Limiting) 관리하기
이 지점이야말로 대부분의 튜토리얼이 대충 얼버무리고 지나가는 부분이며, 여러분이 서버 호출마다 429 에러(Too Many Requests)를 맞닥뜨리며 고생을 통해 깨닫게 되는 지점입니다.

Hunter와 Apollo 모두 관대한 무료 티어 (free tiers)를 제공하지만, 무제한은 아닙니다. Hunter는 무료 버전에서 월 25회의 검색을 제공하고, Apollo는 50 크레딧을 제공합니다. 에이전트는 이 사실을 알지 못합니다. 만약 에이전트가 호출을 마구 쏟아내도록(spam) 방치한다면, 5분 만에 크레딧을 모두 소진하게 될 것입니다.

제가 사용하는 패턴은 클라이언트 래퍼 (client wrapper) 내부에서 강제되는 제공자별 토큰 버킷 (token bucket) 방식입니다:

import asyncio  
from time import monotonic

class RateLimiter:  
    def __init__(self, rate: float, burst: int):
        self.rate = rate  
        self.burst = burst  
        self.tokens = burst  
        self.last = monotonic()  
        self.lock = asyncio.Lock()

async def acquire(self):
    async with self.lock:
        now = monotonic()
...```

각 도구(tool)는 API를 호출하기 전에 토큰을 획득합니다. 버킷(bucket)이 비어 있다면, 잠시 대기(sleep)합니다. 재시도(retry)나 예외 루프(exception loop) 없이, 에이전트(agent)는 그저 기다립니다. Claude는 인내심이 강하므로, 2초 정도는 기다리게 해도 괜찮습니다.

Apollo 무료 티어의 함정
이 부분은 제가 직접 겪은 문제였고, 이를 바탕으로 설계를 다시 해야 했던 부분입니다.
Apollo의 무료 티어(free tier) 설명에는 공식적으로 기업 정보 인리치먼트(company enrichment)와 인물 검색(people search)이 포함되어 있다고 되어 있습니다. 하지만 구축 중간 단계에서, 무료 계정의 경우 `people_search`와 `email_finder`가 빈 페이로드(empty payload)를 반환하기 시작한다는 것을 발견했습니다. 에러도, 403 에러도 발생하지 않고, 그저 조용히 아무것도 반환하지 않았습니다. 알고 보니 Apollo가 어느 시점에 이 엔드포인트(endpoint)들을 유료 플랜으로 조용히 제한해 버렸고, 문서(docs)에는 아직 반영되지 않은 상태였습니다.

제 선택지는 다음과 같았습니다:

1. 사용자들에게 Apollo 유료 계정이 필요하다고 알린다 (프로젝트의 무료 티어 약속을 저버리는 일입니다)
2. Apollo를 완전히 제외한다 (기업 수준의 데이터를 모두 잃게 됩니다)
3. Hunter가 주요 작업을 수행하고, Apollo는 여전히 작동하는 기능(기업 인리치먼트)에만 사용하도록 구조를 재편한다

저는 3번 옵션을 선택했습니다. 현재의 흐름은 다음과 같습니다:

- 이메일 찾기 작업은 Hunter로 전달됩니다 (무료 버전에서도 여전히 작동합니다)
- 기업 정보(firmographics)는 Apollo의 `enrich_company`로 전달됩니다 (무료 버전에서도 여전히 작동합니다)
- 공개 버전에서 제외된 인물 검색(people search)은 README에 명확한 노트를 남긴 채 유료 티어용 코드 경로(code path)로 유지합니다

이 결정이 프로젝트를 살렸습니다. 만약 제가 1번 옵션을 고집했다면, 아무도 이 프로젝트를 시도하지 않았을 것입니다.
교훈은 간단합니다. 제3자 API(third-party API), 특히 그들의 무료 티어에 의존할 때는 언제든 기반이 흔들릴 수 있다고 가정하고 구축해야 합니다. 전체 서버를 다시 작성하지 않고도 제공업체(provider)를 교체할 수 있도록 도구의 표면(tool surface)을 충분히 모듈화(modular)해 두십시오.

도구가 도구를 호출할 때의 결함 격리 (Fault Isolation)
에이전트는 종종 도메인에 대해 이메일, 기업 데이터, 검증까지 엔드 투 엔드(end-to-end)로 인리치먼트하기를 원합니다. 이는 최소 두 개의 제공업체에 걸쳐 세 번의 API 호출이 필요함을 의미합니다.
단순한 접근 방식은 이들을 순차적으로 기다리는(await) 것이지만, 그렇게 하지 마십시오. 한 제공업체가 다운되면 전체 도구가 멈춰버립니다.
제가 최종적으로 채택한 패턴은 `asyncio.gather`에 `return_exceptions=True`를 사용하는 방식입니다. 이렇게 하면 Hunter에 장애가 발생하더라도 Apollo의 결과가 중단되지 않습니다.

[@mcp](https://dev.to/mcp).tool()  
async def full_domain_enrichment(domain: str) -> dict:  
"""단일 도메인에 대해 Hunter와 Apollo 데이터를 결합합니다."""  
hunter_task = find_emails_by_domain(domain, limit=20)  
apollo_task = enrich_company(domain)

hunter_result, apollo_result = await asyncio.gather(
hunter_task, apollo_task, return_exceptions=True
)
...


에이전트(Agent)는 명확한 에러 필드가 포함된 부분적인 데이터를 받게 됩니다. 에이전트는 그 데이터를 어떻게 처리할지, 사용자에게 물어볼지, 재시도할지, 혹은 무시할지를 결정할 수 있습니다. 그것은 에이전트의 역할이지, 서버의 역할이 아닙니다.

실제 사용 사례 (Real Usage)
제가 함께 일하는 작은 아웃바운드(Outbound) 팀은 이 서버를 Claude Desktop을 통해 실행합니다. 워크플로우는 다음과 같습니다:

1. 영업 담당자가 타겟 도메인 목록을 채팅창에 넣습니다.
2. Claude가 각 도메인에 대해 `full_domain_enrichment`를 호출합니다.
3. 검증된 이메일은 별도의 MCP 도구를 통해 Google Sheet로 전송됩니다.
4. 담당자는 이미 회사 컨텍스트가 로드된 상태에서 Claude를 통해 검토하고 아웃리치(Outreach) 초안을 작성합니다.

물량은 주당 약 300개의 리드(Lead) 수준입니다. 무료 티어(Free tier)를 사용하면 비용은 0원입니다. 무료 범위를 넘어서서 소규모 유료 티어를 사용할 경우, 두 서비스를 합쳐 월 약 30달러 정도가 듭니다. Clay의 시트(Seat) 비용이나 유료 Apollo 계정 비용과 비교하면 계산이 딱 맞습니다.

다르게 한다면 (What I'd Do Differently)
만약 처음부터 다시 시작한다면, 프로바이더 추상화(Provider abstraction)를 더 일찍 구축했을 것입니다. 현재는 Hunter와 Apollo가 각각 자신만의 클라이언트 클래스를 가지고 있습니다. 프로바이더가 두 개일 때는 작동했지만, 만약 Clearbit이나 PDL을 추가한다면 많은 코드를 중복해서 작성하게 될 것입니다. 다음 버전에서는 단일 `EnrichmentProvider` 베이스 클래스를 만들고 특정 API들을 끼워 넣는 방식을 사용할 것입니다.

또한 첫날부터 지속성 캐시(Persistent cache)를 추가했을 것입니다. 이러한 조회 작업의 상당수는 결정론적(Deterministic)입니다. 오늘 인리치먼트(Enrichment)한 도메인을 내일 다시 조회해도 거의 동일한 데이터가 반환됩니다. 7일의 TTL(Time To Live)을 가진 간단한 SQLite 캐시를 사용한다면, 반복되는 워크플로우에서 API 사용량을 60~70%까지 줄일 수 있을 것입니다.

직접 해보기 (Try It)
저장소(Repo)는 여기 있습니다: github.com/Aleksey-Panf/b2b-enrichment-mcp
MIT 라이선스입니다. 테스트용으로는 무료 티어 API 키로도 충분히 잘 작동합니다. 설정 방법은 `git clone` 후, `.env` 파일에 키를 넣고, Claude Desktop이나 Cursor가 이를 가리키도록 하면 됩니다.

만약 여러분이 에이전트 (Agents)를 구축 중이며, 여러분만의 데이터 소스나 API를 래핑 (Wrapping)하는 커스텀 MCP 서버를 원하신다면, 제가 이러한 작업을 수행합니다. Telegram @AlexAi14로 연락하시거나 리포지토리 (Repo)에 이슈 (Issue)를 생성해 주세요.
[![ ](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikcdg6oy01r4qnhou7id.png)](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikcdg6oy01r4qnhou7id.png)

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0