smishing-stop-mcp
README.md
# 피싱멈춰 — 스미싱 판별기 MCP 서버
카카오 **PlayMCP** 등록용 원격 MCP 서버입니다. 의심스러운 문자/메시지를 받아 **스미싱·메신저피싱·보이스피싱 유도** 여부를 판정하고, 근거와 상황별 대응을 한국어로 돌려줍니다. (카카오 AGENTIC PLAYER 10 출품작)
> 보안 도구의 본질상 **오판이 치명적**이므로, 이 서버는 **정확성과 보수성**을 최우선으로 설계했습니다. 절대 "100% 안전"이라고 단정하지 않으며, 불확실하면 최소 "의심" 또는 "판단보류"로 내립니다.
---
## 1. 핵심 설계: 2층 구조
스미싱 문구는 끝없이 변형됩니다. 그래서 문구를 일일이 나열하지 않고 **두 개의 층**으로 나눠 판단합니다.
```
입력 메시지
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 1층 — 확정 신호 (닫힌 데이터 조회, 결정론적) │
│ · URL 추출 → 단축URL 펼치기 → KISA 피싱 차단목록 / Google │
│ Safe Browsing / KISA WHOIS 도메인 나이 조회 │
│ · 발신번호: 국제발신(+), 070, 변작 의심 │
│ ★ KISA/Safe Browsing 매칭 = '악성 확정' → verdict '위험' 고정 │
└──────────────────────────────────────────────────────────────┘
│ (확정 신호는 아래 2층이 절대 덮어쓰지 못함)
▼
┌──────────────────────────────────────────────────────────────┐
│ 2층 — 사회공학 추론 (점수에만 기여) │
│ · 휴리스틱(키 불필요): 긴급성·결제/환급·기관 사칭·링크 유도· │
│ APK 설치·인증번호/계좌 요구·지인 사칭의 '본질'을 카테고리로 판별 │
│ · LLM(선택): 새로운 표현·우회 문구·맥락 사칭을 보완(0~1 위험도) │
└──────────────────────────────────────────────────────────────┘
│
▼
점수 합산(0~100) → 3단계 판정(위험 / 의심 / 판단보류)
+ 근거 · 탐지 URL · 상황별 대응 · 위젯 카드 · 면책
```
**불변 규칙**: 2층 추론은 점수에만 기여하며, **1층의 확정 악성 신호를 절대 덮어쓰지 못합니다.** 1층에서 악성 URL이 확정되면 텍스트가 정상으로 보여도 "위험"으로 고정됩니다.
### 보수성(안정성) 규칙
- "안전" 단정 출력 금지. 가장 낮은 등급도 **"판단보류"**입니다.
- 거짓 음성(놓침)을 거짓 양성보다 **더 위험**하게 취급합니다.
- 링크 평판을 검증할 수 없는 상태에서 사회공학 정황이 함께 발견되면 보수적으로 **"의심"으로 상향**합니다.
- 분석 중 예외가 나도 절대 "안전"으로 비치지 않게 **"판단보류"**를 반환합니다.
### Graceful degradation (키 없이도 동작)
모든 외부 API 키는 **선택**입니다. 키가 없으면 해당 데이터 소스만 비활성화되고, 사용 가능한 신호만으로 판정하되 `data_sources`에 상태를 투명하게 표기합니다. 즉 **키 0개로도 서버가 정상 작동**합니다(2층 휴리스틱 + 발신번호/단축URL 분석).
---
## 2. 도구 스펙: `check_smishing`
### 입력
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| `message_text` | string | ✅ | 분석할 의심 메시지 원문 |
| `sender` | string | | 발신번호 또는 발신자명 (예: `+1 213-...`, `070-...`, `국민은행`) |
| `clicked` | boolean | | 링크 클릭 여부 (알 때만) |
| `installed` | boolean | | 앱/APK/파일 설치 여부 (알 때만). `true`면 즉시 대처 가이드로 전환 |
### 출력 (구조화 JSON)
| 필드 | 타입 | 설명 |
|---|---|---|
| `risk_level` | `"위험" \| "의심" \| "판단보류"` | 3단계 판정 |
| `score` | number (0~100) | 위험 점수 |
| `reasons` | string[] | 판정 근거(점수 기여 순) |
| `detected_urls` | `{url, expanded_url?, verdict, source, detail?}[]` | URL별 평판. `verdict`: `악성확정/의심/확인불가/특이사항없음` |
| `action_guide` | string[] | 상황별 대응 |
| `follow_up_questions` | string[] | 호스트가 조건부로 되물을 질문(비면 되묻지 않음) |
| `card` | object | 카카오툴즈 위젯 카드용 구조(배지/근거/대응 버튼) |
| `data_sources` | object | 각 소스의 사용/비활성 상태(투명성) |
| `disclaimer` | string | 면책 및 신고 안내 |
### 인터뷰 분기(편의성 보호)
- 호스트(LLM)는 먼저 `message_text`만으로 호출합니다. **미리 캐묻지 않습니다.**
- 결과의 `follow_up_questions`가 **비어 있지 않을 때만** 사용자에게 되묻습니다. 이 배열은 **판정이 위험/의심이고 clicked/installed가 미상이라 출력이 바뀔 수 있을 때만** 채워집니다. 비어 있으면 되묻지 않습니다.
- `installed=true`로 다시 호출되면 `action_guide`가 **즉시 대처 모드**(모바일 백신 검사, APK 삭제, 통신사 소액결제 차단, 118 신고, 금전 피해 시 112)로 전환됩니다.
---
## 3. 빠른 시작 (로컬 실행)
요구사항: Node.js ≥ 18 (개발 환경은 Node 22/25에서 검증).
```bash
# 1) 의존성 설치
npm install
# 2) 환경변수(선택) — 키 없이도 동작합니다
cp .env.example .env
# 필요하면 .env 에 KISA_WHOIS_SERVICE_KEY / GOOGLE_SAFE_BROWSING_API_KEY /
# ANTHROPIC_API_KEY / KISA_PHISHING_CSV_PATH 를 채웁니다.
# 3) 빌드 + 실행
npm run build
npm start # 기본 http://localhost:3000 , MCP 엔드포인트 POST /mcp
# 개발 모드(핫리로드)
npm run dev
# 오프라인 자체 검증(네트워크 없이 판정 로직 점검)
npm run selftest
```
### 동작 확인
```bash
# 헬스체크
curl http://localhost:3000/healthz
# MCP 도구 호출 (Streamable HTTP는 Accept 헤더 두 개를 요구)
curl -s -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"check_smishing","arguments":{"message_text":"[CJ대한통운] 주소불일치로 배송보류. 앱 설치 후 확인 http://bit.ly/abcd","sender":"+1 213-555-0199"}}}'
```
---
## 4. 환경변수
| 변수 | 기본값 | 설명 |
|---|---|---|
| `PORT` | `3000` | 서버 포트 |
| `INBOUND_API_KEY` | (없음) | 설정 시 인바운드 `X-API-Key`(또는 Bearer) 검사. PlayMCP 커스텀 헤더 인증 대응 |
| `CORS_ORIGIN` | `*` | CORS 허용 오리진(`*` 또는 콤마구분) |
| `OFFLINE_MODE` | `false` | `true`면 모든 외부 호출 차단(테스트용) |
| `FETCH_TIMEOUT_MS` | `4000` | 외부 호출 타임아웃 |
| `MAX_REDIRECT_HOPS` | `5` | 단축 URL 펼치기 최대 hop |
| `KISA_PHISHING_CSV_PATH` | `./data/kisa_phishing_urls.csv` | KISA 피싱 URL CSV 로컬 경로 |
| `EXTRA_BLOCKLIST_DOMAINS` | (없음) | 보강/테스트용 악성 도메인(콤마구분) |
| `KISA_WHOIS_SERVICE_KEY` | (없음) | 공공데이터포털 KISA WHOIS serviceKey |
| `GOOGLE_SAFE_BROWSING_API_KEY` | (없음) | Google Safe Browsing v4 키 |
| `ANTHROPIC_API_KEY` | (없음) | 2층 LLM 추론용(선택) |
| `LLM_MODEL` | `claude-haiku-4-5-20251001` | 2층 LLM 모델(경량 권장) |
| `ANTHROPIC_BASE_URL` | `https://api.anthropic.com` | Anthropic API 베이스 URL |
---
## 5. 외부 API 활용신청 방법
각 API의 스펙은 **공식 문서로 검증**했습니다. 검증 결과와 출처는 아래에 명시합니다.
### 5-1. KISA 피싱사이트 URL 차단목록 (공공데이터포털, 데이터셋 15109780 / 최신 15143094)
- **검증 결과**: 이 데이터셋은 **REST 오픈API가 아니라 CSV "파일데이터" 다운로드**입니다. `serviceKey`나 실시간 조회 엔드포인트가 **존재하지 않습니다.** (출처: <https://www.data.go.kr/data/15109780/fileData.do>, 최신본 <https://www.data.go.kr/data/15143094/fileData.do>)
- **활용신청**: 파일데이터는 **로그인 없이 즉시 다운로드**, 무료, 별도 승인 불필요.
- **사용법**:
1. 위 data.go.kr 페이지에서 CSV(2컬럼: 날짜, URL)를 다운로드합니다.
2. 파일을 `KISA_PHISHING_CSV_PATH`(기본 `./data/kisa_phishing_urls.csv`)에 둡니다.
3. 서버 재시작 → 차단목록 로드(`/healthz`의 `blocklist.size`로 확인).
- 도우미: `node scripts/update-kisa-db.mjs` (수동 다운로드 안내. `KISA_CSV_DOWNLOAD_URL` 환경변수로 직링크 자동화 가능)
- 키가 없어도 `EXTRA_BLOCKLIST_DOMAINS`로 보강 가능합니다.
### 5-2. KISA WHOIS 오픈API (도메인 등록일/나이, 공공데이터포털 15094277)
- **검증 결과**: REST API. `GET http://apis.data.go.kr/B551505/whois/domain_name?serviceKey=...&query=<도메인>&answer=json`, 응답에 `regDate`(등록일)·`endDate`(만료일) 포함. **⚠️ `.kr`/`.한국` 국가도메인만 조회 가능**(국제 TLD 불가). (출처: <https://www.data.go.kr/data/15094277/openapi.do>, <https://whois.kr/kor/openkey/keyCre.do>)
- **활용신청**:
1. <https://www.data.go.kr> 회원가입/로그인.
2. "인터넷주소(도메인, IP) 정보검색 서비스"(데이터셋 15094277) 검색 → **활용신청**(개발/운영계정, **자동승인**).
3. 마이페이지 > 오픈API > 개발계정 상세에서 **serviceKey 발급**(Encoding 키 권장) → `.env`의 `KISA_WHOIS_SERVICE_KEY`에 입력.
- 한도: 개발계정 일 10,000건, 운영계정 활용사례 등록 시 최대 일 100,000건.
### 5-3. Google Safe Browsing API (v4 Lookup)
- **검증 결과**: `POST https://safebrowsing.googleapis.com/v4/threatMatches:find?key=API_KEY`. 요청 `threatInfo.threatEntries[{url}]`, 응답 `matches[]`(비면 미탐). `threatType` 중 `SOCIAL_ENGINEERING`이 피싱 신호. 무료. (출처: <https://developers.google.com/safe-browsing/v4/lookup-api>) **참고: v4는 deprecated(동작 유지)** — 향후 v5 `hashes:search` 또는 Web Risk로 이전 필요.
- **발급**:
1. [Google Cloud Console](https://console.cloud.google.com) 프로젝트 생성/선택.
2. **Safe Browsing API** 사용 설정.
3. 사용자 인증 정보 > **API 키** 생성 → `.env`의 `GOOGLE_SAFE_BROWSING_API_KEY`에 입력.
### 5-4. 단축 URL 펼치기 (키 불필요)
- 리다이렉트 자동추적을 끄고 `Location` 헤더를 hop마다 직접 검증하며 최대 N회 따라갑니다. **SSRF 방어**: 각 hop의 호스트가 사설/내부/클라우드 메타데이터 대역(`10/8`,`127/8`,`169.254/16`,`172.16-31`,`192.168/16`, IPv6 ULA 등)이면 차단, `http/https`만 허용, 타임아웃·hop 상한 적용, 최종 페이지 본문/JS는 실행하지 않습니다.
### 5-5. 2층 LLM (선택, Anthropic)
- 없으면 2층은 휴리스틱만으로 동작합니다. 발급: <https://console.anthropic.com> → API 키 → `.env`의 `ANTHROPIC_API_KEY`. 분류 작업이므로 경량 모델(`claude-haiku-4-5-...`) 권장.
- 호스트 LLM 샘플링(`sampling/createMessage`)은 광범위하게 지원되지 않으므로 **의존하지 않습니다.**
---
## 6. 배포 — PlayMCP in KC (공식 경로)
> 출처: PlayMCP 공식 공모전 가이드(Notion). **Agentic Player 10 예선은 반드시 카카오가 제공하는 `PlayMCP in KC`(MCP 서버 배포 서비스, 무상)로 배포**해야 합니다. 이 서버의 [Dockerfile](Dockerfile)이 그대로 사용됩니다(저장소 루트에 Dockerfile 존재).
⚠️ **PlayMCP in KC 발급은 예선 기간(2026-06-15 ~ 07-14)에만 가능**합니다. 심사 시간을 고려해 미리 발급하세요. 계정당 최대 2대.
### 경로 A — Git 소스 빌드 (권장)
1. 이 프로젝트를 GitHub 등 Git 저장소에 push (저장소 루트에 `Dockerfile` 포함 — 이미 있음).
2. <https://playmcp.kakaocloud.io> 접속 → 카카오 계정(=PlayMCP 가입 계정) 로그인.
3. **`+ 새 MCP 서버 등록` → `Git 소스 빌드`** 선택 후 입력:
- **MCP 서버 이름 / 설명**: 자유(PlayMCP 표시와 무관).
- **Git URL**: 저장소 주소. (private이면 **PAT** 입력, public이면 비움)
- **브랜치/ref**: 보통 `main`.
- **Dockerfile 경로**: 보통 `Dockerfile`.
4. **`등록하기`** → Status `Starting` → 수십 초~수 분 후 **`Active`**.
5. Active 서버 상세에서 **Endpoint URL** 복사 → 7장에서 PlayMCP 등록 시 사용.
### 경로 B — 컨테이너 이미지
로컬에서 `docker build` 후 이미지를 등록하는 방식도 지원합니다(PlayMCP in KC의 "컨테이너 이미지로 MCP 서버 등록하기" 가이드 참고).
### (참고) 자체 호스팅 대체 경로
원리상 **공개 HTTPS Streamable HTTP URL**이면 동작하므로, 개발/테스트 단계에서는 Railway/Render/Fly.io나 임의 Docker 호스트로도 띄울 수 있습니다. 단, **공모전 예선 제출은 PlayMCP in KC Endpoint를 써야 합니다.** 컨테이너에는 `/healthz` HEALTHCHECK가 포함돼 있습니다.
---
## 7. PlayMCP 등록 & 예선 접수 절차 (공식)
1. **로컬에서 개발·테스트 완료** (`npm run selftest`, MCP Inspector 권장 — 아래 8장).
2. **PlayMCP in KC에서 배포** → Endpoint URL 획득(6장).
3. **PlayMCP 등록**: <https://playmcp.kakao.com/console> 로그인 → **`새로운 MCP 서버 등록`** → **MCP Endpoint**에 위 Endpoint URL 입력 → **`정보 불러오기`** 클릭.
- ⚠️ "정보 불러오기"가 **성공해야** 합니다. 실패하면 MCP 서버에 문제가 있는 것입니다(스펙 미준수 등).
- 인증이 필요하면 **OAuth** 또는 **커스텀 헤더**(이 서버는 `INBOUND_API_KEY` 설정 시 `X-API-Key` 검사)를 사용합니다.
4. 정보 입력 후 **`임시 등록`** 클릭. (이 단계에서 "등록 및 심사요청"을 누르지 말 것)
5. **`MCP 상세 미리보기` → `도구함에 추가`** → PlayMCP **AI 채팅으로 충분히 테스트**.
6. 테스트 완료 후 임시 등록 상태에서 **`심사 요청`**. (영업일 1~2일, 최대 7일)
- 반려 시 카카오계정 대표이메일로 사유 발송 → 수정 후 재요청.
7. 승인되면 공개 상태가 **"나에게만 공개"** → **"전체 공개"로 전환**.
8. 전체 공개된 MCP 상세페이지 주소 복사 (예: `https://playmcp.kakao.com/mcp/12345...`).
9. **공모전 페이지**(<https://b.kakao.com/views/PlayMCP/AGENTIC_PlAYER_10>) → **`Player 예선 참여`** → 비즈폼 접수(최대 2개 MCP 등록 가능).
> **일정**: 예선 접수 **2026-06-15 ~ 07-14**, 본선 카카오톡 공개투표 **2026-08-31~09-28**, 시상 **2026-10-23**. 평가 **창의성·편의성·기술 안정성**.
---
## 8. ✅ PlayMCP 개발가이드 준수 체크리스트
공식 개발가이드(2026-06-12 기준)의 필수 요건과 본 서버의 충족 상태입니다. (가이드: <https://kko.to/PlayMCPdevguide>)
| 요건 | 상태 |
|---|---|
| MCP 지원버전 2025-03-26 ~ 2025-11-25 | ✅ SDK 1.29가 범위 내 협상(테스트 시 2025-06-18) |
| **Streamable HTTP만** 지원 | ✅ `StreamableHTTPServerTransport` |
| **Remote, 공개 URL** | ✅ PlayMCP in KC Endpoint |
| **Stateless 권장** | ✅ `sessionIdGenerator: undefined` |
| 인증: OAuth 또는 커스텀 헤더 | ✅ 커스텀 헤더(`X-API-Key`, 선택) |
| 서버/툴 이름에 **"kakao" 미포함** | ✅ `smishing-stop-mcp` / `check_smishing` |
| 툴 이름: 1~128자, `[A-Za-z0-9_-]` | ✅ `check_smishing` |
| 툴 개수 ≤ 20 (권장 3~10) | ⚠️ 현재 1개(집중형). 필요 시 보조 툴 추가 가능 |
| 필수 property: name·description·inputSchema·**annotations** | ✅ annotations 5개 힌트 모두 지정 |
| annotations: title·readOnlyHint·destructiveHint·openWorldHint·idempotentHint | ✅ `true/false/true/true` |
| description ≤ 1,024자 + 서비스명 영/국문 병기 | ✅ 991자, `Smishing-Stop(피싱멈춰)` 포함 |
| result 크기 최소화 / error·비위젯은 마크다운 텍스트 | ✅ 간결한 text 폴백 + 구조화 content |
| **툴 응답 p99 ≤ 3,000ms 필수** | ✅ 전체 데드라인 `OVERALL_DEADLINE_MS`(기본 2800ms)로 강제 |
| 툴 평균 응답 ≤ 100ms | ⚠️ 외부 키 미설정 시 로컬(KISA CSV+휴리스틱)으로 <10ms. 외부 API 사용 시 지연↑ — 아래 참고 |
| 광고 유도 금지 | ✅ 해당 없음 |
| **MCP Inspector 사전 점검** | ☐ 제출 전 직접 수행 권장: `npx @modelcontextprotocol/inspector` |
**평균 100ms vs 정확도 트레이드오프**: 외부 평판 API(Safe Browsing/WHOIS) 및 단축 URL 펼치기는 네트워크 지연이 있습니다. 엄격한 평균 100ms가 필요하면 외부 키를 비우거나 `OFFLINE_MODE=true`로 두어 **로컬 결정론 모드**(KISA CSV + 휴리스틱, <10ms)로 운용하세요. 정확도를 우선하면 키를 채워 전체 2층을 가동하되, p99는 데드라인으로 3초 이내가 보장됩니다.
### 남은 ⚠️ 확인 필요 항목 (비공개/미검증)
1. **카카오툴즈 위젯 카드(widget json)의 정확한 스키마**: 개발가이드는 "tool 결과가 widget json이 아니면 마크다운 텍스트 권장"이라고만 명시하고, widget json의 구체 규격은 공개돼 있지 않습니다. 본 서버는 호스트가 카드 규격을 몰라도 `risk_level`/`reasons`/`action_guide`로 렌더되도록 설계하고 `card` 구조화 필드(배지/버튼)를 함께 제공합니다. 결선 진출 시 카카오 제공 규격에 맞춰 [buildCard()](src/pipeline/actionGuide.ts)를 조정하세요.
2. **KISA WHOIS `regDate` 날짜 포맷**(14자리 추정) 미검증 → [parseWhoisDate()](src/layer1/whois.ts)가 다중 포맷 방어 파싱. 실제 응답으로 확인 권장.
3. **사기 전화번호/계좌 조회 API**: 안정적 공개 무료 오픈API 가용성 불확실 → 제외, 발신번호 형식 휴리스틱으로 대체(추후 추가 여지).
4. **KISA 피싱 CSV 자동 다운로드 직링크**: data.go.kr 파일 다운로드는 세션 의존 → 수동 다운로드 기본 + `KISA_CSV_DOWNLOAD_URL` 자동화 옵션.
5. **Safe Browsing 정확한 쿼터 수치**: 공식 미게시 → Cloud Console에서 확인.
---
## 9. 프로젝트 구조
```
src/
├── index.ts 진입점: Express + Streamable HTTP(stateless), 인증/CORS/헬스체크
├── server.ts McpServer 구성 + check_smishing 등록
├── config.ts 환경변수 + 점수 가중치(WEIGHTS) 중앙 집중
├── types.ts 공용 타입(출력 스키마와 1:1)
├── util.ts 타임아웃 fetch, 호스트/IP 파싱, SSRF 차단
├── tool/
│ ├── schema.ts 입력/출력 zod shape + 도구 설명(호스트 행동 규칙)
│ └── checkSmishing.ts 핸들러: structuredContent + 텍스트 폴백, 예외 안전
├── pipeline/
│ ├── index.ts 6단계 오케스트레이션
│ ├── parse.ts URL/발신번호/키워드 추출(NFC 정규화, 한글 안전)
│ ├── scoring.ts 점수 합산 + 3단계 판정 + 보수성 규칙
│ └── actionGuide.ts 상황별 대응 + 되묻기 + 카드 빌더
├── layer1/ 확정 신호(결정론적)
│ ├── urlReputation.ts URL별 KISA+SafeBrowsing+WHOIS 종합
│ ├── unshorten.ts 단축 URL 펼치기 + SSRF 방어
│ ├── kisa.ts 피싱 CSV 로더 + 호스트 매칭
│ ├── safeBrowsing.ts Google Safe Browsing v4 클라이언트
│ ├── whois.ts KISA WHOIS 도메인 나이
│ └── sender.ts 발신번호 분석
└── layer2/ 사회공학 추론(점수 기여)
├── heuristics.ts 카테고리 기반 결정론적 판별
└── llm.ts 선택적 Anthropic 추론(graceful)
```
---
## 10. 한국어 인코딩
- 입력을 **NFC 정규화**하여 자모 분해로 인한 키워드 매칭 누락을 방지합니다.
- 토크나이저에 의존하지 않고 **유니코드 인지 정규식**으로 직접 스캔합니다(한글이 빈 토큰이 되지 않음).
- Express `express.json()` + LLM 호출은 모두 UTF-8로 처리합니다.
## 11. 면책
본 도구의 결과는 **참고용이며 최종 판단과 책임은 본인에게 있습니다.** 스미싱 의심 신고 **118**(불법스팸대응센터), 금전 피해 발생 시 **112**(경찰), 개인정보 노출 시 KISA **보호나라**(privacy.kisa.or.kr) 명의도용 차단 서비스를 이용하세요.
라이선스: MIT
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues