Earnings Call MCP
by dupletime-wq
README.md
# Earnings Call MCP
[MarketBeat](https://www.marketbeat.com/earnings/transcripts/)의 실적발표(어닝콜) 트랜스크립트를 검색·조회·본문 검색·번역할 수 있는 [MCP](https://modelcontextprotocol.io) 서버입니다. Cloudflare Workers 위에서 동작하며, 서버 상태를 따로 두지 않는(stateless) MCP HTTP 엔드포인트로 배포합니다.
## 제공하는 도구
| 도구 | 설명 |
| --- | --- |
| `search_earnings_calls` | 회사명 또는 티커로 트랜스크립트를 검색 (`query`, `limit?`) |
| `get_earnings_call_transcript` | URL 또는 티커(+분기)로 트랜스크립트 전문을 조회 (`url?`, `ticker?`, `quarter?`) |
| `find_in_transcript` | 특정 트랜스크립트 내에서 키워드/문구로 본문 검색 (`url?`/`ticker?`+`quarter?`, `query`, `maxMatches?`) |
| `translate_text` | Workers AI(`@cf/meta/m2m100-1.2b`)로 텍스트 번역 (`text`, `targetLanguage`, `sourceLanguage?`) |
`get_earnings_call_transcript`로 얻은 본문이나 `find_in_transcript`의 발췌문을 `translate_text`에 넘겨서 원하는 언어(예: 한국어)로 번역하는 흐름으로 사용하면 됩니다.
## 아키텍처
- **Worker 엔트리**: `src/index.ts` — `/mcp` 경로에서 `@modelcontextprotocol/server`의 `createMcpHandler`로 Streamable HTTP 트랜스포트를 처리합니다. (구 `agents` 패키지의 `McpAgent`는 feature-frozen 상태로 표시되어 있어, 최신 stateless `McpServer` + `createMcpHandler` 조합으로 구현했습니다.)
- **스크레이핑/파싱**: `src/marketbeat/parser.ts` — Workers 런타임 내장 `HTMLRewriter`로 스크립트/스타일/네비/푸터 등을 제거하고 본문 텍스트만 추출한 뒤, `발언자: 내용` 패턴으로 화자 세그먼트를 분리합니다. 목록 페이지(`/earnings/transcripts/`, `/stocks/{EXCHANGE}/{TICKER}/earnings/`)에서는 `/earnings/reports/{date}-{slug}-stock/` 형태의 링크를 파싱합니다.
- **클라이언트**: `src/marketbeat/client.ts` — 검색/티커 조회/캐싱을 담당. 대상 호스트가 설정된 MarketBeat 호스트와 다르면 요청을 거부합니다(SSRF 방지). 파싱된 결과는 Workers KV(`TRANSCRIPT_CACHE`)에 캐싱합니다 (트랜스크립트 14일, 최신 목록 30분).
- **번역**: `src/translate.ts` — `env.AI` 바인딩을 통해 Workers AI 번역 모델을 호출하는 얇은 래퍼.
## 로컬 개발 · 테스트
```bash
npm install
npm run typecheck # tsc --noEmit
npm test # vitest run (실제 workerd/HTMLRewriter 런타임에서 실행)
npm run dev # wrangler dev (로컬 개발 서버, http://localhost:8787)
```
`test/` 아래에 세 종류의 테스트가 있습니다.
- `parser.test.ts`, `client.test.ts`: MarketBeat HTML 파서와 클라이언트(캐싱, 티커 탐색, SSRF 가드, 에러 처리)를 fixture HTML(`test/fixtures/*.html`)로 검증하는 유닛 테스트.
- `mcp.test.ts`: `@modelcontextprotocol/client`의 `Client` + `InMemoryTransport`로 실제 MCP 프로토콜(`tools/list`, `tools/call`)을 통해 4개 도구를 종단 검증하는 테스트. `fetch`와 Workers AI 바인딩은 테스트에서 주입한 가짜 구현으로 대체합니다.
- `worker.test.ts`: Worker의 `/mcp` HTTP 엔드포인트가 실제로 `initialize`/`tools/list` JSON-RPC 요청에 응답하는지 확인하는 스모크 테스트.
37개 테스트가 모두 통과하며, `wrangler dev --local`로 실제 로컬 서버를 띄워 `initialize` → `tools/list` → `tools/call` 전 과정도 curl로 직접 확인했습니다.
### ⚠️ 알려진 제한 사항
이 코드는 개발 샌드박스에서 **marketbeat.com으로의 아웃바운드 네트워크가 차단된 상태**로 작성되었습니다. 즉, 파서의 CSS/텍스트 추출 로직은 검색 결과로 확인한 실제 URL 패턴(`/earnings/reports/{date}-{slug}-stock/`)과 일반적인 실적발표 트랜스크립트 페이지 구조(제목 `"{회사명} ({거래소}: {티커}) Q{n} {연도} Earnings Call Transcript"`, 본문의 `발언자: 문장` 반복 패턴)를 근거로 한 **방어적 구현**입니다. 실제 마켓비트 페이지의 마크업과 100% 동일하다고 보장할 수 없으므로, **배포 후 반드시 실제 사이트로 한 번 스모크 테스트**를 해보시고, 결과가 이상하면 아래 두 파일만 고치면 됩니다.
- `src/marketbeat/parser.ts`의 `parseTranscriptTitle`(제목 파싱 정규식), `segmentTranscript`(화자 분리 정규식) — 필요시 실제 페이지의 `<h1>` 텍스트나 문단 마크업에 맞춰 정규식/선택자만 조정하면 됩니다.
- `test/fixtures/*.html` — 실제 페이지 구조로 교체하면 회귀 테스트가 그대로 유지됩니다.
## 배포하기
이 세션에서는 Cloudflare API/계정 접근이 차단되어 있어 실제 배포는 사용자가 직접 진행해야 합니다.
```bash
npx wrangler login
# KV 네임스페이스 생성 후, 출력된 id를 wrangler.jsonc의
# kv_namespaces[0].id ("REPLACE_WITH_YOUR_KV_NAMESPACE_ID")에 붙여넣으세요.
npx wrangler kv namespace create TRANSCRIPT_CACHE
npm run deploy # wrangler deploy
```
배포되면 `https://<worker-name>.<subdomain>.workers.dev/mcp`가 MCP 엔드포인트입니다. Claude Desktop/Code 등에서 원격 MCP 서버로 연결하거나, [`mcp-remote`](https://www.npmjs.com/package/mcp-remote)를 통해 로컬 stdio 클라이언트와 연결할 수 있습니다.
### Workers AI 요금 관련
`translate_text` 도구는 Workers AI(`@cf/meta/m2m100-1.2b`)를 호출합니다. Workers AI는 로컬 개발 중에도 항상 원격 리소스를 사용하므로(`wrangler dev`가 경고를 출력합니다) 실제 호출 시 사용량 과금이 발생할 수 있습니다.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing