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)
}

동작:

  1. URL에서 fileKeynodeId 파싱 (?node-id=1%3A21: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, ...)
    • 컴포넌트면 컴포넌트 이름 (componentIdcomponentName)
    • 스타일: 배경색, 테두리, 보더 반경, 패딩, 레이아웃 모드(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 } 검색용)

동작:

  1. ${env.STORYBOOK_URL}/index.json fetch
  2. (실패 시 폴백) ${env.STORYBOOK_URL}/stories.json 시도
  3. entries 객체에서 type: "story"인 것만 추출 (docs 페이지 제외)
  4. 다음 형식으로 변환:
{
  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 }

동작:

  1. index.json에서 해당 ID 찾음
  2. 가능하면 ${STORYBOOK_URL}/stories.json 또는 ID 기반 메타에서 argTypes 추출
  3. 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
}

동작:

  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):
{
  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 출력 형식>
}

동작:

  1. get_story_details로 props 시그니처 가져옴
  2. Figma 노드의 텍스트, 스타일, 변형 정보를 props에 매핑 시도
    • Figma 텍스트 → children 또는 label prop
    • Figma variant 이름 → matching prop value
  3. 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

구현 요구사항

  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부터 시작해줘.

MCP Server · Populars

MCP Server · New