우리 API를 Claude 도구로 — Rainbow Prism MCP 빌드 회고
왜 우리 API를 MCP로 만드나
핵심 요점
- API는 있어도 사용자가 직접 호출하지 않는다 — MCP는 그 API를 생성형 AI가 대화 중 도구로 부르게 하는 표준 인터페이스다.
- 한 번 MCP로 노출하면 Claude Code·Desktop·Cursor 등 모든 클라이언트가 같은 도구를 재사용한다.
우리에겐 이미 유튜브 떡상(구독 대비 조회수 폭발)을 발굴·통계 내는 REST API가 있었다. 문제는 사용자가 그걸 직접 호출하지 않는다는 것. "게임 쇼츠에서 구독 대비 터진 영상 찾아줘"라고 말하면 Claude가 알아서 yt_finder를 부르는 경험 — 그게 MCP(Model Context Protocol)다.
MCP는 도구(tool)의 표준 인터페이스다. 한 번 노출해두면 Claude Code·Claude Desktop·Cursor 등 모든 MCP 클라이언트가 같은 도구를 재사용한다. API를 '사람이 읽는 문서'에서 'AI가 부르는 도구'로 승격시키는 얇은 층이다.
전제: OpenAPI + skill.md로 API를 AI-ready로
핵심 요점
- openapi.json(기계용 스펙)과 skill.md(AI용 사용설명)를 API가 서빙하면 웹 커넥터는 URL 등록만으로 동작하고, MCP 래핑도 스키마를 그대로 옮기면 된다.
MCP를 얹기 전에 우리 API는 이미 두 가지를 서빙하고 있었다. openapi.json(기계가 읽는 스펙)과 skill.md(생성형 AI용 사용설명 — 엔드포인트·파라미터·인증을 자연어로 정리). ChatGPT GPTs나 Claude 웹 커넥터는 OpenAPI URL만 등록하면 바로 도구로 부른다.
이 토대가 있으면 MCP는 그 위의 얇은 래퍼가 된다. 도구 목록·스키마를 openapi에서 거의 그대로 옮기면 되기 때문이다. API를 만들 때부터 openapi/skill.md를 같이 내면, 나중에 어떤 AI 인터페이스(커넥터·MCP)로도 빠르게 확장된다.
1단계: 로컬 stdio MCP 서버
핵심 요점
- 로컬 stdio MCP는 SDK로 도구를 등록하고 API를 호출하는 얇은 프로세스 — claude mcp add 한 줄로 붙고, 키는 .env.local에서 로드한다.
- SDK Client로 tools/list·tools/call을 실제 호출해 서버를 검증할 수 있다.
첫 구현은 @modelcontextprotocol/sdk로 만든 stdio 서버였다(node server.mjs). finder(떡상 발굴)·trends·cadence(업로드 케이던스)·썸네일/제목/댓글 코호트 등 9개 읽기 도구를 등록하고, 각 도구는 우리 API 엔드포인트를 호출한다. 인증 키는 같은 폴더 .env.local에서 자동 로드(gitignore)했다.
claude mcp add rainbowprism -- node …/server.mjs 한 줄로 Claude Code에 붙고, 대화에서 도구로 노출된다. 검증은 SDK의 Client로 stdio 서버를 띄워 tools/list·tools/call을 실제로 호출해 확인했다.
2단계: 쓰기 도구는 어드민만
핵심 요점
- 부수효과(쓰기) 도구는 환경 플래그가 있을 때만 조건부 등록 — 배포본은 읽기 전용이 되어 외부의 임의 쓰기를 원천 차단한다.
읽기 도구는 누구에게 열어도 안전하다(자기 키·레이트리밋). 하지만 '리포트 발행'처럼 부수효과가 있는 도구는 다르다. 외부 사용자나 플러그인 설치자가 함부로 우리 사이트에 글을 올리면 안 된다.
그래서 발행 도구는 RAINBOW_PRISM_ADMIN=1 환경 플래그가 있을 때만 도구 목록에 조건부 등록했다. 어드민의 로컬에만 그 플래그가 있고, 배포본(Expert 키·플러그인·원격 MCP)엔 없다 → 외부는 읽기 9종만 보이고 발행 도구는 아예 존재하지 않는다. "배포되는 것엔 권한을 싣지 않는다"는 원칙을 코드 한 줄(if (process.env.RAINBOW_PRISM_ADMIN==='1') tools.push(publish))로 구현한 셈이다.
3단계: 원격 MCP — 워커가 URL로 서빙
핵심 요점
- 원격 MCP(Streamable HTTP)를 워커에 추가하면 사용자는 URL 한 줄로 등록한다 — 로컬 설치가 필요 없다.
- tools/call을 기존 REST 라우트의 내부 디스패치(app.request)로 구현하면 인증·레이트리밋·파싱 로직이 한 곳에만 살아 중복이 0이 된다.
외부 Expert 사용자에게 로컬 node 설치를 요구하는 건 마찰이 크다. 그래서 백엔드 워커에 원격 MCP 엔드포인트(POST /api/v1/mcp, Streamable HTTP JSON-RPC)를 추가했다. 그러면 OpenAPI 커넥터처럼 URL 한 줄로 등록된다: claude mcp add --transport http rainbowprism <URL> --header "Authorization: Bearer <키>".
구현의 핵심은 로직 중복을 만들지 않은 것이다. tools/call은 해당 도구의 기존 v1 REST 경로를 워커 내부에서 다시 호출(app.request)한다 — finder 도구는 내부적으로 /api/v1/youtube/finder를 부른다. 인증·레이트리밋·쿼리 파싱이 전부 한 곳(REST 핸들러)에만 산다. initialize/tools/list는 키+Expert를 확인하고, 나머지는 내부 디스패치가 처리한다.
4단계: 깔끔한 URL — 리버스 프록시
핵심 요점
- 기존 /api/* 리버스 프록시에 MCP를 올려 브랜드 도메인 URL로 노출 — 단일 오리진 고정이라 SSRF/오픈프록시 위험이 없고 인증은 워커가 강제한다.
원격 MCP의 정본 URL이 개인 *.workers.dev면 프로답지 않다. 다행히 프론트(Cloudflare Pages)에는 /api/*를 백엔드 워커로 포워딩하는 리버스 프록시 함수가 이미 OAuth 콜백을 깔끔히 보이려고 존재했다. MCP 엔드포인트도 별도 설정 없이 그 위에 올라타 prism.ai-teammate.net/api/v1/mcp로 노출된다.
이 프록시는 보안적으로도 무해하다 — 단일 오리진 고정이라 임의 URL을 프록시하지 않고(SSRF·오픈프록시 없음), 메서드·헤더·바디를 그대로 전달할 뿐 인증은 워커가 끝까지 강제한다. 설정 페이지에는 📄 skill.md·🔌 OpenAPI·🔌 MCP 세 커넥터를 나란히 뒀다.
비용과 보안
핵심 요점
- 원격 MCP는 기존 인프라 위 라우트라 마진 비용 ≈0이고 LLM 토큰도 안 든다 — 게이팅은 REST와 동일한 Expert·레이트리밋 함수를 재사용한다.
- MCP 도구 정의는 매 턴 컨텍스트를 먹으므로 도구 수를 절제하고 tool search를 권한다.
원격 MCP는 기존 워커·D1 위에서 도는 라우트 하나라 마진 비용이 거의 0이다(Cloudflare 무료티어 안). 생성형이 아니라 데이터 API라 LLM 토큰 비용도 없다. 인증은 REST v1과 똑같은 함수(키 검증 + Expert 구독 + 키별 레이트리밋 120/분·5,000/일)를 재사용해, prism 도메인이든 workers.dev든 게이팅이 일관된다.
주의할 단 하나는 Context cost — MCP 도구 정의는 매 턴 컨텍스트에 로드돼 토큰을 먹는다. 그래서 도구를 9개로 절제하고, 클라이언트엔 tool search(지연 로딩)를 권한다.
스킬 vs MCP vs 플러그인 — 언제 무엇을
핵심 요점
- 스킬=절차 텍스트, MCP=외부 연결 도구, 플러그인=둘을 한 번에 배포하는 컨테이너 — 먼저 스킬로 되는지 보고, 외부 연결이 필요할 때 MCP를 더한다.
이 빌드에서 셋의 역할이 또렷해졌다. 스킬은 '절차를 가르치는 텍스트'(리포트를 어떻게 만들고 검수할지). MCP는 '외부 시스템에 연결하는 도구'(떡상 데이터 조회). 플러그인은 그 둘(+에이전트·훅)을 한 묶음으로 배포하는 컨테이너 — 설치 한 번에 스킬과 MCP가 같이 온다.
실무 순서는 "먼저 스킬로 표현 가능한가(가장 저렴) → 외부 연결이 꼭 필요하면 MCP → 컨텍스트 격리가 필요하면 서브에이전트"다. 우리는 떡상 분석 절차를 스킬로, 데이터 접근을 MCP로 두고, 배포 시 플러그인으로 묶는다. 쓰기(발행)는 어디서나 어드민 게이팅으로 분리한 채.