피싱멈춰 — 스미싱 판별기 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에서 검증).
# 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
동작 확인
# 헬스체크
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>) - 활용신청: 파일데이터는 로그인 없이 즉시 다운로드, 무료, 별도 승인 불필요.
- 사용법:
- 위 data.go.kr 페이지에서 CSV(2컬럼: 날짜, URL)를 다운로드합니다.
- 파일을
KISA_PHISHING_CSV_PATH(기본./data/kisa_phishing_urls.csv)에 둡니다. - 서버 재시작 → 차단목록 로드(
/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>) - 활용신청:
- <https://www.data.go.kr> 회원가입/로그인.
- "인터넷주소(도메인, IP) 정보검색 서비스"(데이터셋 15094277) 검색 → 활용신청(개발/운영계정, 자동승인).
- 마이페이지 > 오픈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(동작 유지) — 향후 v5hashes:search또는 Web Risk로 이전 필요. - 발급:
- Google Cloud Console 프로젝트 생성/선택.
- Safe Browsing API 사용 설정.
- 사용자 인증 정보 > 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 존재).
⚠️ PlayMCP in KC 발급은 예선 기간(2026-06-15 ~ 07-14)에만 가능합니다. 심사 시간을 고려해 미리 발급하세요. 계정당 최대 2대.
경로 A — Git 소스 빌드 (권장)
- 이 프로젝트를 GitHub 등 Git 저장소에 push (저장소 루트에
Dockerfile포함 — 이미 있음). - <https://playmcp.kakaocloud.io> 접속 → 카카오 계정(=PlayMCP 가입 계정) 로그인.
+ 새 MCP 서버 등록→Git 소스 빌드선택 후 입력:
- MCP 서버 이름 / 설명: 자유(PlayMCP 표시와 무관).
- Git URL: 저장소 주소. (private이면 PAT 입력, public이면 비움)
- 브랜치/ref: 보통
main. - Dockerfile 경로: 보통
Dockerfile.
등록하기→ StatusStarting→ 수십 초~수 분 후Active.- 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 등록 & 예선 접수 절차 (공식)
- 로컬에서 개발·테스트 완료 (
npm run selftest, MCP Inspector 권장 — 아래 8장). - PlayMCP in KC에서 배포 → Endpoint URL 획득(6장).
- PlayMCP 등록: <https://playmcp.kakao.com/console> 로그인 →
새로운 MCP 서버 등록→ MCP Endpoint에 위 Endpoint URL 입력 →정보 불러오기클릭.
- ⚠️ "정보 불러오기"가 성공해야 합니다. 실패하면 MCP 서버에 문제가 있는 것입니다(스펙 미준수 등).
- 인증이 필요하면 OAuth 또는 커스텀 헤더(이 서버는
INBOUND_API_KEY설정 시X-API-Key검사)를 사용합니다.
- 정보 입력 후
임시 등록클릭. (이 단계에서 "등록 및 심사요청"을 누르지 말 것) MCP 상세 미리보기→도구함에 추가→ PlayMCP AI 채팅으로 충분히 테스트.- 테스트 완료 후 임시 등록 상태에서
심사 요청. (영업일 1~2일, 최대 7일)
- 반려 시 카카오계정 대표이메일로 사유 발송 → 수정 후 재요청.
- 승인되면 공개 상태가 "나에게만 공개" → "전체 공개"로 전환.
- 전체 공개된 MCP 상세페이지 주소 복사 (예:
https://playmcp.kakao.com/mcp/12345...). - 공모전 페이지(<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초 이내가 보장됩니다.
남은 ⚠️ 확인 필요 항목 (비공개/미검증)
- 카카오툴즈 위젯 카드(widget json)의 정확한 스키마: 개발가이드는 "tool 결과가 widget json이 아니면 마크다운 텍스트 권장"이라고만 명시하고, widget json의 구체 규격은 공개돼 있지 않습니다. 본 서버는 호스트가 카드 규격을 몰라도
risk_level/reasons/action_guide로 렌더되도록 설계하고card구조화 필드(배지/버튼)를 함께 제공합니다. 결선 진출 시 카카오 제공 규격에 맞춰 buildCard()를 조정하세요. - KISA WHOIS
regDate날짜 포맷(14자리 추정) 미검증 → parseWhoisDate()가 다중 포맷 방어 파싱. 실제 응답으로 확인 권장. - 사기 전화번호/계좌 조회 API: 안정적 공개 무료 오픈API 가용성 불확실 → 제외, 발신번호 형식 휴리스틱으로 대체(추후 추가 여지).
- KISA 피싱 CSV 자동 다운로드 직링크: data.go.kr 파일 다운로드는 세션 의존 → 수동 다운로드 기본 +
KISA_CSV_DOWNLOAD_URL자동화 옵션. - 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






