pubdata-mcp
# pubdata-mcp
한국 공공데이터를 수집·정규화해 DuckDB 데이터마트에 적재하고, 이를 MCP 서버로 노출한 뒤
Ollama 로컬 LLM 에이전트가 자연어 질문에 답하는 end-to-end 파이프라인입니다.
데이터 엔지니어링(수집·정규화·안전한 쿼리 서빙)과 AI 오케스트레이션(MCP + LLM 툴콜)을
하나의 프로젝트로 보여주는 백엔드 포트폴리오입니다.
## 아키텍처
```
data.go.kr ──ingest──> DuckDB 데이터마트 ──> MCP Server ──stdio──> Ollama Agent Host
(실거래가/기상/대기질) (정규화 테이블) (툴/리소스) (자연어 → 툴콜 → 답변)
```
- ingest: 커넥터 인터페이스 + 3개 커넥터 (아파트 실거래가, 일별 기상, 일별 대기질). API 키 없으면 mock 폴백
- store: DuckDB 적재 및 read-only 쿼리 (지역 partition 기반 idempotent 적재)
- server: MCP 서버 (`list_datasets`, `describe_schema`, `query_sql`, `price_stats`, `correlate` + `pubdata://catalog` 리소스)
- host: MCP 서버를 subprocess로 띄우고 Ollama 툴콜 루프로 질의에 답하는 에이전트
### 데이터셋
- `apt_trade` — 아파트 매매 실거래가 (지역/월)
- `weather` — 일별 기온·강수·풍속 (지역/일)
- `air_quality` — 일별 PM10·PM2.5·오존 (지역/일)
`weather`와 `air_quality`는 `(obs_date, region_code)`를 공유해 `correlate` 툴로 크로스 도메인
상관분석이 가능합니다 (예: 풍속 vs 미세먼지 → corr ≈ -0.95, 바람이 강한 날 미세먼지가 낮음).
핵심 백엔드 포인트는 `server/safe_sql.py`의 **read-only SQL 가드**(단일문 강제, DDL/DML 차단,
LIMIT 강제)와 DuckDB read-only 커넥션 이중 방어입니다.
## 빠른 시작
```bash
# 1. 의존성 설치 (uv)
uv sync
# 2. 데이터 적재 (키 없으면 mock 데이터로 동작)
for r in 11680 11650 11440; do
uv run pubdata-ingest apt_trade --region $r --ym 202406
uv run pubdata-ingest weather --region $r --ym 202406
uv run pubdata-ingest air_quality --region $r --ym 202406
done
# 3. MCP 서버 단독 실행 (Claude Desktop / MCP Inspector에 연결 가능)
uv run pubdata-server
# 4. 에이전트로 자연어 질의 (CLI)
uv run pubdata-agent "강남구와 서초구 아파트 평균 거래가를 비교해줘"
# 5. 웹 UI (질문 + 답변 + 툴 호출 트레이스 표시) — http://localhost:8000
uv run pubdata-web
```
## 웹 UI / API
`pubdata-web`은 FastAPI 앱을 띄웁니다.
- `GET /` — 질문 입력 → 답변과 함께 에이전트가 호출한 MCP 툴 트레이스(인자·결과·소요시간)를 표시
- `POST /api/ask` `{"question": "..."}` → `{answer, model, steps, elapsed_ms, trace[]}`
- `GET /api/health` — 상태 및 설정된 모델
## Docker
Ollama는 호스트에서 실행 중이라고 가정합니다. compose는 host networking으로 호스트의
`localhost:11434` Ollama에 바로 접근합니다 (Linux 기준).
```bash
docker compose up --build
# http://localhost:8000
```
첫 기동 시 마트가 비어 있으면 mock 데이터로 3개 지역을 자동 적재합니다.
호스트에 tool-calling 모델(`ollama pull qwen2.5:7b`)이 있어야 합니다.
Docker Desktop(mac/Windows)에서는 `docker-compose.yml` 주석의 대안 설정을 참고하세요.
## 실제 API 사용
`.env.example`를 `.env`로 복사하고 `PUBDATA_DATA_GO_KR_KEY`에 data.go.kr 서비스 키를 넣으면
mock 대신 실데이터를 수집합니다.
## Claude Desktop 연결 예시
```json
{
"mcpServers": {
"pubdata": {
"command": "uv",
"args": ["run", "pubdata-server"],
"cwd": "/home/jcyeom/dlwlr/pubdata-mcp"
}
}
}
```
## 관측성 (Observability)
모든 에이전트 실행은 `data/traces.jsonl`에 append됩니다 (질문·모델·툴 궤적·소요시간·에러·답변).
- `GET /api/traces?limit=20` — 최근 실행 조회 (최신순)
- best-effort 로깅이라 실패해도 요청에 영향 없음
## 평가 (Eval)
골든 케이스로 에이전트 품질을 회귀 검증합니다.
```bash
uv run pubdata-eval
```
각 케이스는 3가지를 채점합니다:
- answer-correctness: 답변에 기대값(실데이터 숫자)이 포함되는가 (콤마 무시 비교)
- tool-trajectory: 기대한 MCP 툴이 호출됐는가
- error-free: 툴 에러가 없었는가
결과는 표로 출력되고 `data/eval_report.json`에 저장됩니다. 하나라도 실패하면 exit code 1 (CI 연동 가능).
로컬 LLM 재현성을 위해 에이전트는 `temperature=0`으로 호출합니다.
## 테스트
```bash
uv run pytest # 23 tests: SQL 가드, 파이프라인, 관측성, eval 스코어링
```
## 로드맵
- [x] end-to-end 파이프라인 (ingest → DuckDB → MCP → Ollama 에이전트)
- [x] read-only SQL 가드 + idempotent 적재
- [x] Docker 배포 + 웹 UI (툴 트레이스 표시)
- [x] 툴콜 정확도 eval 셋 + 요청 트레이싱/관측성
- [x] 기상/대기질 커넥터 추가 → 크로스 도메인 상관분석 (correlate 툴)
- [ ] 기상/대기질 실 API 연동 (현재 mock, apt_trade는 실 API 지원)
- [ ] 수집 스케줄러(APScheduler) + 증분 적재
TDQS
Scored across 5 tools
Each tool has a clear, distinct purpose: listing datasets, describing schemas, running SQL queries, computing price statistics, and calculating correlations. No overlap or ambiguity.
All tools use consistent snake_case naming with verb-first patterns (list_datasets, describe_schema, query_sql, price_stats, correlate). The single verb 'correlate' fits the pattern.
Five tools cover the core functionality of data discovery, schema exploration, custom queries, and common analytics. This is a well-scoped set for a data mart server.
The tool set covers essential workflows: discover (list_datasets), understand (describe_schema), query (query_sql), and two analytics helpers. Minor gaps like missing aggregation functions, but overall complete for the intended domain.