Figma → Storybook 컴포넌트 매칭 MCP 서버
목표
원격 MCP 서버를 만든다. Figma 디자인 노드를 입력받아 우리 팀의 React 컴포넌트(Storybook에 등록된)와 매칭시키고, 사용 코드 예시까지 생성하는 게 목적.
LLM(Claude)이 이 MCP를 통해 다음 같은 요청을 처리할 수 있어야 함:
- "이 Figma URL 분석해줘"
- "이 Figma 노드를 우리 컴포넌트로 어떻게 구현할지 알려줘"
- "후보 컴포넌트 3개 보여줘"
기술 스택
- 런타임: Cloudflare Workers
- 언어: TypeScript (strict mode)
- MCP:
@modelcontextprotocol/sdk+agents패키지 사용 - Transport: Streamable HTTP, 엔드포인트는
/mcp - 검증: zod
- 빌드/배포: wrangler
- 타깃 프레임워크: React (코드 생성 시 JSX 출력)
인증 (옵션 A: Bearer 토큰)
- 모든 MCP 요청은 헤더
Authorization: Bearer <token>필요 env.MCP_AUTH_TOKEN과 비교, 불일치 시 401 반환- 인증 실패는 명확한 에러 메시지 (
{"error": "invalid_token"})
환경변수 (wrangler에 정의)
FIGMA_TOKEN: Figma Personal Access Token (서버가 보관)STORYBOOK_URL: Storybook 베이스 URL (예:https://storybook.example.com)MCP_AUTH_TOKEN: 클라이언트 인증용 토큰COMPONENT_IMPORT_PREFIX: 코드 생성 시 import 경로 (기본@/components)
로컬 개발용은 .dev.vars, 프로덕션은 wrangler secret put으로 관리. wrangler.toml에는 더미 placeholder만 두기.
노출할 도구(Tools)
1. get_figma_node
설명: Figma URL을 받아서 노드의 핵심 정보를 정제된 형태로 반환
입력 (zod):
{
url: string // Figma 노드 URL (예: https://www.figma.com/file/XXX/...?node-id=1%3A2)
}
동작:
- URL에서
fileKey와nodeId파싱 (?node-id=1%3A2→1:2로 디코드) - Figma API 호출:
GET https://api.figma.com/v1/files/{fileKey}/nodes?ids={nodeId}- 헤더:
X-Figma-Token: {env.FIGMA_TOKEN}
- 헤더:
- 응답에서 다음만 추출 (Figma 응답은 너무 verbose하니 정제):
- 노드 이름 (
name) - 노드 타입 (
type: FRAME, INSTANCE, TEXT, ...) - 컴포넌트면 컴포넌트 이름 (
componentId→componentName) - 스타일: 배경색, 테두리, 보더 반경, 패딩, 레이아웃 모드(autolayout 방향), gap
- 텍스트면
characters와 폰트 정보 - 자식 구조: 자식 노드들의 이름/타입만 1단계 깊이로 (재귀 X, 너무 길어짐)
- 컴포넌트 변수/variants 정보(있으면)
- 노드 이름 (
출력: 위 정보를 담은 정제된 JSON 객체
에러: URL 파싱 실패, Figma API 4xx/5xx, 토큰 만료 등 구분해서 에러 메시지
2. get_figma_subtree
설명: 노드의 전체 트리를 재귀적으로 가져옴 (전체 페이지/프레임 분석용)
입력:
{
url: string,
maxDepth?: number // 기본 3, 너무 깊으면 토큰 폭발
}
동작: get_figma_node와 비슷하지만 자식을 maxDepth까지 재귀. 각 자식도 정제된 형식.
3. list_stories
설명: 우리 Storybook의 컴포넌트 목록을 반환
입력: 없음 (또는 { filter?: string } 검색용)
동작:
${env.STORYBOOK_URL}/index.jsonfetch- (실패 시 폴백)
${env.STORYBOOK_URL}/stories.json시도 entries객체에서type: "story"인 것만 추출 (docs 페이지 제외)- 다음 형식으로 변환:
{
id: string,
componentName: string, // title에서 마지막 "/" 뒤 부분 (예: "Forms/Button" → "Button")
storyName: string, // name 필드
fullTitle: string, // 원본 title
tags: string[]
}[]
캐싱: 응답을 5분간 in-memory 캐싱 (KV 안 써도 됨, 단순 변수로). Workers는 인스턴스가 짧게 살아있으니 너무 길게 잡지 말 것.
4. get_story_details
설명: 특정 스토리의 상세 정보 (props, args)
입력:
{ storyId: string }
동작:
index.json에서 해당 ID 찾음- 가능하면
${STORYBOOK_URL}/stories.json또는 ID 기반 메타에서 argTypes 추출 - props 시그니처 정리:
{
id: string,
componentName: string,
description?: string,
props: {
name: string,
type: string,
required: boolean,
description?: string,
defaultValue?: any
}[]
}
argTypes 못 가져오면 props는 빈 배열로, 대신 note: "argTypes unavailable" 추가.
5. match_figma_to_components
설명: Figma 노드 데이터와 매칭되는 컴포넌트 후보를 점수와 함께 반환 (핵심 도구)
입력:
{
figmaNode: <get_figma_node 출력 형식>,
topK?: number // 기본 3
}
동작:
list_stories로 전체 컴포넌트 가져옴- 각 컴포넌트에 대해 매칭 점수 계산:
- 이름 유사도 (가중치 0.5): Figma 노드 이름 vs
componentName- 정확 일치: 1.0
- 대소문자 무시 일치: 0.9
- 포함 관계: 0.6
- Levenshtein 거리 기반: 0~0.5
- 구조 매칭 (가중치 0.3): 자식 패턴 추론
- Figma 자식이 텍스트만 → "Button", "Label" 후보 +
- 아이콘 + 텍스트 → "Button", "Tag", "Chip" 후보 +
- 여러 카드 형태 자식 → "List", "Grid" 후보 +
- 태그 일치 (가중치 0.2): Storybook 스토리 태그에 Figma 노드 이름의 키워드 포함
- 이름 유사도 (가중치 0.5): Figma 노드 이름 vs
- 상위 K개 반환 (기본 3):
{
storyId: string,
componentName: string,
score: number, // 0~1
reasons: string[] // 왜 매칭됐는지 사람이 읽을 수 있게
}[]
매칭 점수 0.3 미만은 제외 (의미 없는 매칭 거름).
6. generate_component_usage
설명: 매칭된 컴포넌트 + Figma 노드 정보로 React JSX 코드 예시 생성
입력:
{
storyId: string,
figmaNode: <get_figma_node 출력 형식>
}
동작:
get_story_details로 props 시그니처 가져옴- Figma 노드의 텍스트, 스타일, 변형 정보를 props에 매핑 시도
- Figma 텍스트 →
children또는labelprop - Figma variant 이름 → matching prop value
- Figma 텍스트 →
- JSX 코드 문자열 생성
출력:
{
code: string, // <Button variant="primary">Click me</Button>
importStatement: string, // import { Button } from "@/components/Button"
notes: string[] // 매핑 추측이나 빠진 정보 안내
}
import path는 환경변수 env.COMPONENT_IMPORT_PREFIX(기본값 "@/components") 기준.
프로젝트 구조
figma-storybook-mcp/
├── src/
│ ├── index.ts # Worker 진입점, 인증 미들웨어, MCP 라우팅
│ ├── mcp.ts # MyMCP 클래스 (도구 등록)
│ ├── auth.ts # Bearer 토큰 검증
│ ├── figma/
│ │ ├── client.ts # Figma REST API 호출
│ │ ├── url-parser.ts # URL → fileKey + nodeId
│ │ └── normalizer.ts # Figma 응답 → 정제된 형식
│ ├── storybook/
│ │ ├── client.ts # index.json fetch + 캐싱
│ │ └── types.ts
│ ├── matching/
│ │ ├── scorer.ts # 매칭 점수 계산
│ │ └── name-similarity.ts # Levenshtein 등
│ ├── codegen/
│ │ └── react.ts # JSX 코드 생성
│ └── types.ts # 공통 타입
├── tests/
│ ├── url-parser.test.ts
│ ├── normalizer.test.ts
│ └── scorer.test.ts
├── wrangler.toml
├── .dev.vars.example # 실제 .dev.vars는 gitignore
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── README.md
구현 요구사항
- 타입 안전: 모든 도구 입력 zod 스키마, 출력 타입은 명시적으로 정의
- 에러 핸들링:
- Figma 401 → "Figma 토큰 만료/잘못됨"
- Figma 404 → "노드를 찾을 수 없음"
- Storybook fetch 실패 → 명확한 메시지
- 모든 에러는 MCP가 이해할 수 있는 형식으로 반환
- 로깅:
console.log로 도구 호출 시작/끝, 에러는console.error. Workers 대시보드에서 보임 - 테스트: vitest로 핵심 로직 단위 테스트 (URL 파싱, 매칭 점수, 정제 로직)
- README 갱신:
- 무엇을 하는 도구인지
- 환경변수 설명
- 로컬 실행 (
npm run dev) - 배포 (
npm run deploy) - Claude Desktop / Claude.ai에 연결하는 방법
- 각 도구의 입출력 예시
작업 순서 (단계별로 보고하면서 진행)
Phase 1: 셋업
- 프로젝트 초기화, 의존성 설치
wrangler.toml,tsconfig.json작성- 빈 MCP 서버가
/mcp에서 응답하는지 확인 (도구 0개여도 OK)
Phase 2: 인증
- Bearer 토큰 검증 미들웨어
- 잘못된 토큰으로 호출 시 401 확인
Phase 3: Figma 도구
figma/url-parser.ts+ 단위 테스트figma/client.ts(실제 API 호출)figma/normalizer.ts(응답 정제)get_figma_node도구 등록- 실제 Figma URL로 동작 확인
Phase 4: Storybook 도구
storybook/client.ts(index.json fetch + 캐싱)list_stories,get_story_details등록
Phase 5: 매칭
matching/scorer.ts+ 단위 테스트match_figma_to_components등록
Phase 6: 코드 생성
codegen/react.tsgenerate_component_usage등록
Phase 7: 마무리
get_figma_subtree추가- README 작성
.dev.vars.example제공
각 Phase 끝나면 짧게 "이거 했고 다음 이거 할게" 보고하고 진행.
주의사항
- Cloudflare Workers는 Node.js API 일부만 지원. fs, child_process 등 안 됨. fetch 기반으로 작성
@modelcontextprotocol/sdk최신 안정 버전 사용- MCP 표준은 빠르게 변하니
agents패키지의 최신 패턴 따를 것 - 한 번에 다 만들지 말고 Phase별로 검증하면서 진행
- 코드는 명확하게, 주석은 비즈니스 로직(매칭 점수 같은 거)에만
시작
Phase 1부터 시작해줘.