Figma Storybook Component Matching MCP Server
README.md
# 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)**:
```typescript
{
url: string // Figma 노드 URL (예: https://www.figma.com/file/XXX/...?node-id=1%3A2)
}
```
**동작**:
1. URL에서 `fileKey`와 `nodeId` 파싱 (`?node-id=1%3A2` → `1:2`로 디코드)
2. Figma API 호출: `GET https://api.figma.com/v1/files/{fileKey}/nodes?ids={nodeId}`
- 헤더: `X-Figma-Token: {env.FIGMA_TOKEN}`
3. 응답에서 다음만 추출 (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`
**설명**: 노드의 전체 트리를 재귀적으로 가져옴 (전체 페이지/프레임 분석용)
**입력**:
```typescript
{
url: string,
maxDepth?: number // 기본 3, 너무 깊으면 토큰 폭발
}
```
**동작**: `get_figma_node`와 비슷하지만 자식을 maxDepth까지 재귀. 각 자식도 정제된 형식.
### 3. `list_stories`
**설명**: 우리 Storybook의 컴포넌트 목록을 반환
**입력**: 없음 (또는 `{ filter?: string }` 검색용)
**동작**:
1. `${env.STORYBOOK_URL}/index.json` fetch
2. (실패 시 폴백) `${env.STORYBOOK_URL}/stories.json` 시도
3. `entries` 객체에서 `type: "story"`인 것만 추출 (docs 페이지 제외)
4. 다음 형식으로 변환:
```typescript
{
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)
**입력**:
```typescript
{ storyId: string }
```
**동작**:
1. `index.json`에서 해당 ID 찾음
2. 가능하면 `${STORYBOOK_URL}/stories.json` 또는 ID 기반 메타에서 argTypes 추출
3. props 시그니처 정리:
```typescript
{
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 노드 데이터와 매칭되는 컴포넌트 후보를 점수와 함께 반환 (핵심 도구)
**입력**:
```typescript
{
figmaNode: <get_figma_node 출력 형식>,
topK?: number // 기본 3
}
```
**동작**:
1. `list_stories`로 전체 컴포넌트 가져옴
2. 각 컴포넌트에 대해 매칭 점수 계산:
- **이름 유사도** (가중치 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 노드 이름의 키워드 포함
3. 상위 K개 반환 (기본 3):
```typescript
{
storyId: string,
componentName: string,
score: number, // 0~1
reasons: string[] // 왜 매칭됐는지 사람이 읽을 수 있게
}[]
```
매칭 점수 0.3 미만은 제외 (의미 없는 매칭 거름).
### 6. `generate_component_usage`
**설명**: 매칭된 컴포넌트 + Figma 노드 정보로 React JSX 코드 예시 생성
**입력**:
```typescript
{
storyId: string,
figmaNode: <get_figma_node 출력 형식>
}
```
**동작**:
1. `get_story_details`로 props 시그니처 가져옴
2. Figma 노드의 텍스트, 스타일, 변형 정보를 props에 매핑 시도
- Figma 텍스트 → `children` 또는 `label` prop
- Figma variant 이름 → matching prop value
3. JSX 코드 문자열 생성
**출력**:
```typescript
{
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
```
## 구현 요구사항
1. **타입 안전**: 모든 도구 입력 zod 스키마, 출력 타입은 명시적으로 정의
2. **에러 핸들링**:
- Figma 401 → "Figma 토큰 만료/잘못됨"
- Figma 404 → "노드를 찾을 수 없음"
- Storybook fetch 실패 → 명확한 메시지
- 모든 에러는 MCP가 이해할 수 있는 형식으로 반환
3. **로깅**: `console.log`로 도구 호출 시작/끝, 에러는 `console.error`. Workers 대시보드에서 보임
4. **테스트**: vitest로 핵심 로직 단위 테스트 (URL 파싱, 매칭 점수, 정제 로직)
5. **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.ts`
- `generate_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부터 시작해줘.