raw
Development

디자인시스템에 MCP 서버를 달았다 — 조회에서 검증, 피드백 루프까지

2026.07.11·25분

MCP가 뭔데?

본론 전에 MCP를 짧게 정리하자. MCP(Model Context Protocol) 는 Anthropic이 공개한 표준 프로토콜로, LLM 기반 도구(Claude Code, Cursor 등)가 외부 시스템과 대화하는 방법을 규격화한 것이다. 흔히 "AI계의 USB-C"라고 부른다 — 도구(호스트)마다 제각각이던 연동 방식을 하나의 포트로 통일한다는 의미다.

구조는 단순하다. 호스트(Claude Code 같은 AI 도구)가 MCP 서버(내가 만드는 것)에 연결하면, 서버는 자신이 할 수 있는 일을 선언하고 모델이 그것을 호출한다. 서버가 노출할 수 있는 채널은 크게 네 가지고, 이 글 전체가 사실 "네 채널을 각각 언제 써야 하는가"를 몸으로 배운 이야기다.

채널발동 주체성격
Tools모델이 자율 호출함수. "컴포넌트 목록 줘", "이 코드 검사해줘"
Resources사용자가 명시적으로 첨부문서. @로 붙이는 레퍼런스
Prompts사용자가 슬래시 메뉴에서 선택시작 템플릿. 워크플로 진입점
Instructions연결 시 자동 주입항상 컨텍스트에 실리는 규칙

전송은 로컬이면 stdio(표준 입출력), 원격이면 HTTP를 쓴다. shak-mcp는 npm으로 설치해 로컬에서 stdio로 붙는 서버고, 사용자 입장에서 "서버를 단다"는 것은 설정 파일에 이 네 줄을 넣는 일이 전부다:

json
1{
2 "mcpServers": {
3 "shak": { "command": "shak-mcp" }
4 }
5}

이 순간부터 그 프로젝트의 모든 AI 세션은 shak의 사실(컴포넌트 API·토큰·워크플로)에 접근할 수 있다.

왜 만들었나 — 마이그레이션의 진짜 병목

우리 팀은 Panda CSS 기반 디자인시스템 shak을 만들었고, 사내 서비스들이 여기로 갈아타야 했다. 그런데 마이그레이션에는 구조적인 문제가 있다. 어느 서비스팀에게도 마이그레이션은 기능 개발보다 우선순위가 낮다. 급하지 않은 일은 영원히 시작되지 않는다.

"그럼 AI에게 시키자"가 자연스러운 다음 수였는데, 여기서 두 번째 문제를 만났다. 에이전트가 컴포넌트 API를 지어낸다. 실제로 겪은 사례를 나중에 메타데이터 소스 주석에 그대로 박아뒀다:

text
1Loop 1에서 에이전트가
2(1) `Tab`이 Panel인 걸 모르고 루트로 착각하거나,
3(2) Modal.Bottom 안 버튼 배치(Bottom.Group)·RadioGroup 조합을 추측했다.

원인은 명확하다. AI 도구는 Storybook을 열어볼 수 없고, 학습 데이터에 우리 사내 패키지가 있을 리 없다. 참조할 사실이 없으면 모델은 그럴듯한 것을 생성한다 — 존재하지 않는 prop, 존재하지 않는 조립 구조를.

정리하면 요구사항은 이렇다. 에이전트가 (1) 정확한 사실을 조회하고 (2) 생성한 결과를 스스로 검증할 수 있는 채널. 이게 MCP 서버를 직접 만든 이유다.

철학 — 손으로 쓰는 스펙 문서는 반드시 어긋난다

서버를 만들기 전에 정한 원칙이 하나 있다. MCP가 제공하는 데이터는 사람이 쓰지 않는다. 전부 소스에서 빌드 타임에 추출한다.

디자인시스템 문서의 고질병은 드리프트다. 컴포넌트에 prop이 추가됐는데 문서는 그대로인 상황 — 사람이 쓰는 문서는 소스와 별개의 사본이라 반드시 어긋난다. 그 어긋난 문서를 에이전트에게 먹이면 할루시네이션을 막으려던 도구가 할루시네이션의 출처가 된다.

그래서 추출 파이프라인을 세웠다. react-docgen-typescript로 Props를 전개하고, 배럴 파일(src/index.ts)로 공개 표면을 확정하고, docgen이 못 읽는 부분은 TS Compiler API로 AST를 직접 파싱해 갭을 메운다. 컴포넌트 설명은 스토리 파일의 description을 단일 출처로 끌어온다. 이렇게 만든 components.json·tokens.json이 MCP 도구의 응답이 된다.

text
1react 소스 (@pinkfong-tech/shak/src)
2
3 ┌──────────┬───┼────────┬──────────────┐
4 ▼ ▼ ▼ ▼ ▼
5 docgen barrel compound types-ast stories
6 Props 전개 공개표면 이름정규화 AST 갭필 설명 추출
7 │ │ │ │ │
8 └──────────┴───────┴────┬───┴───────────┘
9
10 components.json / tokens.json
11
12 MCP 도구 응답

주의할 점도 배웠다. 이 파이프라인은 정답이 고정된 영역이 아니라 회색지대다. "어떤 prop을 노출할지, 설명을 어디서 끌어올지"는 react 패키지의 컨벤션 약속에 기대는 정규화 작업이라, 컨벤션이 바뀌면 추출 규칙도 함께 흔들린다. 그래서 추출이 잘 됐는지의 진실 판정은 스펙이 아니라 출력물이다 — 생성된 JSON을 직접 열어 컴포넌트 수와 prop 전개를 확인하는 것이 1차 방어선이다.

첫 버전 — 조회 도구 6개면 충분할 줄 알았다

첫 릴리즈의 모양은 소박했다. MCP 패키지 하나에 전부 들어 있었다:

  • 조회 도구 6개get_design_system_overview(시작점), list_components, search_components, get_component, get_usage_examples, list_tokens
  • 추출 스크립트scripts/extract.ts(802줄짜리 단일 파일)가 빌드 때 JSON을 생성해 번들에 인라인

"에이전트가 추측하는 게 문제니까, 조회할 수 있게 해주면 끝"이라는 가설이었다. 도구 이름도 의도적으로 흐름을 짰다 — 개요 → 목록/검색 → 상세 → 사용법 → 토큰. 에이전트가 이 순서로 파고들 것을 기대했다.

절반은 맞았다. 도구를 호출하기만 하면 정확한 답이 나갔다. 문제는 나머지 절반이었다.

첫 번째 벽 — 도구는 있는데 아무도 안 쓴다

만들고 보니 도구가 발견되지 않았다. 에이전트는 shak MCP가 연결돼 있어도 UI 작업을 할 때 도구를 부를 생각을 안 하고 하던 대로 추측했다. 사용자도 마찬가지 — "이 서버로 뭘 할 수 있는데?"에 대한 답이 도구 목록뿐이면, 어떤 순서로 뭘 해야 하는지 아무도 모른다.

그래서 프롬프트 4종과 공통 규칙 문서(common.md)를 추가했다. build_with_shak(신규 제작), migrate_to_shak(마이그레이션) 같은 워크플로별 진입점을 슬래시 메뉴에 노출해서, 사용자가 하나를 고르면 에이전트가 따라야 할 단계 전체가 컨텍스트에 실리게 했다.

이때 배운 것: MCP 서버의 UX는 도구 시그니처가 아니라 "언제 무엇을 하게 되는가"의 설계다. 사람 대상 라이브러리로 치면 API 레퍼런스만 있고 Getting Started가 없는 상태였던 셈이다. 다만 이 해법에는 남은 구멍이 있었고, 한참 뒤에 더 큰 개편으로 이어진다. (뒤에서 계속.)

두 번째 벽 — 데이터가 MCP 안에 갇혀 있다

다음 압력은 예상 못 한 방향에서 왔다. CLI가 필요해졌다.

MCP는 에이전트용 채널이다. 그런데 같은 정보 — "이 컴포넌트의 정확한 props", "이 코드가 shak 규약을 지키는가" — 는 사람도, CI도, 에디터 훅도 필요로 한다. 그때마다 MCP를 띄울 수는 없으니 shak CLI를 만들기 시작했는데, 여기서 구조 문제가 드러났다. 추출 파이프라인이 MCP 패키지 안에 있었다. CLI가 같은 데이터를 쓰려면 (a) MCP에 의존하거나 (b) 추출 로직을 복사해야 한다. (a)는 배보다 배꼽이 크고, (b)는 시한폭탄이다 — 두 사본은 반드시 어긋난다.

그래서 패키지를 셋으로 쪼갰다:

text
1@pinkfong-tech/shak-metadata ← 추출 파이프라인 + 데이터 + 조회/검증 엔진 (정본)
2@pinkfong-tech/shak-mcp ← metadata를 에이전트 채널(MCP)로 노출
3@pinkfong-tech/shak-cli ← metadata를 사람/CI 채널(터미널)로 노출

지금 MCP 패키지의 데이터 모듈은 통째로 재노출 한 파일이 전부다. 실제 코드가 이렇게 생겼다:

typescript
1/*
2 * 데이터 로드·조회 헬퍼는 @pinkfong-tech/shak-metadata가 단일 소스다.
3 * MCP 핸들러가 기존 경로를 그대로 쓰도록 여기서는 재노출만 한다.
4 */
5export {
6 components,
7 tokens,
8 icons,
9 patterns,
10 findComponent,
11 recipeFor,
12 examplesFor, // ...
13} from '@pinkfong-tech/shak-metadata';

이 분리의 효과는 유지보수 편의를 넘어선다. 데이터 정합성 검증이 추출 파이프라인 테스트 하나로 수렴한다. MCP가 주는 답과 CLI가 주는 답이 다를 수 있는 구조 자체가 사라졌기 때문이다. 나중에 eslint 플러그인이 추가됐을 때도 소비자가 하나 늘었을 뿐, 정본은 그대로였다.

802줄 단일 파일이던 추출 스크립트도 이때 전후로 패스별 분리(1파일 1패스, 파일당 ~150줄)를 거쳤다. 어느 패스가 깨졌는지 추적하는 비용이 파이프라인 유지의 병목이었기 때문이다.

세 번째 벽 — 조회했는데도 틀린다: 검증의 3단계

조회 도구가 자리 잡자 다음 층의 문제가 보였다. 정확한 사실을 조회하고도 결과물이 틀린다. 조회는 입력의 품질을 보장할 뿐, 출력의 품질은 별개다. 그래서 검증 채널을 층층이 쌓았다.

1단 — 정적 검증: check_usage. 에이전트가 작성한 코드를 받아 shak 규약 위반(존재하지 않는 prop, 허용값 밖 variant, raw 토큰 직접 사용 등)을 잡는다. 핵심 결정은 검증 엔진을 MCP에 두지 않고 metadata의 checkSource로 두고 CLI shak check와 공유한 것이다. 에이전트가 통과한 코드는 CI에서도 통과한다 — 검증 기준의 정본도 한 곳이다.

2단 — 렌더 검증: preview_page. 정적 검증의 한계는 실제 핸들러 주석이 잘 말해준다:

text
1check_usage(정적)는 "컴파일·prop·구조"만 보장하고 "실제 렌더"는 못 본다 —
2매 에포크 반복된 렌더 버그(Tab 패널 게이팅·Radio 라벨 위치·ToggleButton 노브)는
3전부 정적 게이트를 통과했다.

컴파일도 되고 prop도 맞는데 화면이 이상한 부류가 끈질기게 남았다. preview_page는 실행 중인 로컬 앱을 헤드리스 브라우저로 띄워 렌더된 ARIA 트리를 돌려준다. 에이전트가 "작성 → 미리보기 → 자기 수정" 루프를 혼자 돌 수 있게 됐다. 설계 디테일 두 가지: playwright는 동적 import + graceful degrade로 하드 의존을 피했고(미설치면 에러 대신 안내 텍스트), 에이전트가 자율 호출하는 도구라 로컬 URL만 허용한다 — localhost.evil.com류 우회까지 정규식으로 막는다. 범용 web-fetch로 오용(SSRF)되는 것을 원천 차단하기 위해서다.

3단 — 시각 동등성: visual_diff. 마이그레이션의 최종 판정은 "픽셀이 같은가"다. 원본 사이트와 교체본을 같은 경로·여러 뷰포트로 풀페이지 캡처해 픽셀 diff 리포트를 만드는 프롬프트+캡처 스크립트 리소스를 추가했다. 코드는 수정하지 않고 차이만 보고한다 — 판정과 수정을 분리해야 판정을 신뢰할 수 있다.

네 번째 벽 — 사람은 프롬프트를 고르지 않는다

첫 번째 벽에서 남겨뒀던 구멍이 여기서 터진다. 프롬프트 방식의 전제는 "사용자가 작업 전에 슬래시 메뉴에서 워크플로를 고른다"인데, 현실의 사용자는 그냥 말한다. "이 페이지 shak으로 바꿔줘." 프롬프트는 선택되지 않았고, 에이전트는 조회 없이 코드부터 만들었다.

답은 채널을 옮기는 것이었다. 프롬프트(수동 발동)에 있던 핵심 규칙을 server instructions(자동 주입)로 승격했다. instructions는 MCP 연결 시 항상 모델 컨텍스트에 실리므로, 사용자가 아무것도 고르지 않아도 에이전트는 "조회 우선 → 토큰 사용 → 검증" 순서를 안다. 실제 instructions 소스의 주석이 이 결정을 요약한다:

text
1Prompt(슬래시 메뉴)는 사용자가 명시적으로 골라야 발동되지만 instructions는 항상 실린다.
2그래서 "UI 작업이면 사용자가 아무것도 안 골라도 따라야 하는" 핵심 순서를 여기 압축한다.
3매 세션 컨텍스트를 점유하므로 짧게 유지하고, 상세는 리소스·프롬프트로 위임한다.

트레이드오프에 주목할 것 — instructions는 매 세션 토큰을 점유한다. 그래서 전문을 넣는 대신 순서 요약만 넣고, 상세 워크플로는 get_workflow라는 도구로 뺐다. 에이전트가 마이그레이션 요청을 감지하면 get_workflow(kind: migrate)를 호출해 그때 상세 절차를 받아간다. 같은 본문이 프롬프트로도 남아 있어 수동 진입도 가능하다. 자동 채널(instructions→도구)과 수동 채널(슬래시 메뉴→프롬프트)이 같은 정본을 읽는 구조다.

이 시기에 채널 정리를 한 번 더 했다. 초기에 만들었던 컴포넌트별 resource 노출은 들어냈다 — 조회는 전부 도구가 하므로 중복이었다. 지금 서버 조립 코드의 주석이 최종 원칙이다:

text
1컴포넌트 조회·검색은 전부 도구가 한다(모델 자율 호출).
2Resource는 "사용자가 @로 첨부하는 레퍼런스 문서"에만 쓴다.
3Instructions는 사용자 선택 없이 항상 컨텍스트에 실린다 —
4"말만 해도 shak 규약대로" 동작하게 하는 자동 트리거 지점.

MCP의 네 채널을 "무엇이 가능한가"가 아니라 "발동 주체가 누구인가" 로 나눠 쓰게 된 것 — 모델이 스스로 판단할 일은 tools, 사람이 의도를 밝힐 일은 prompts, 사람이 자료를 줄 일은 resources, 누구도 챙기지 않아도 지켜져야 할 일은 instructions. 이게 반 년 운영이 준 채널 사용법이다.

기계가 못 주는 것 — 사람이 큐레이션한 조립 레시피

자동 추출 원칙에는 한 가지 의도적 예외가 있다. docgen은 개별 컴포넌트의 props는 완벽하게 주지만, "어느 part가 컨테이너고 어떤 순서로 중첩되는가"는 타입 어디에도 없다. 서두의 실패 사례(Tab을 루트로 착각, Modal.Bottom 배치 추측)가 정확히 이 빈칸에서 나왔다.

그래서 compound 컴포넌트의 조립 구조만은 사람이 쓴 레시피(structure 한 줄 힌트 + example 실제 tsx + notes 함정 목록)로 채우고, 이것도 metadata에 둬서 MCP와 CLI가 같이 읽는다. 원칙은 "자동 추출"이 아니라 "정본 한 곳" 이었다는 걸 여기서 다시 확인했다 — 자동이냐 수동이냐는 수단이고, 사본이 생기지 않는 것이 목적이다.

마지막 층 — 피드백 루프: 만들고 끝이 아니라 쓰이게

마지막 층은 도구가 아니라 운영이다. 디자인시스템 팀이 가장 놓치기 쉬운 신호는 소비자 레포 안에 있다. 에이전트가 마이그레이션 중 "shak에 이 기능이 없어서 우회했다"고 남긴 흔적, 사용자가 겪은 컴포넌트 버그 — 전부 소비자 코드베이스에 흩어진 채 디자인시스템 팀에게 도달하지 않는다.

그래서 갭 신고를 시스템으로 만들었다. 에이전트가 마이그레이션 중 막히면 TODO(shak) 주석을 남기게 하고(갭 정책 — instructions와 워크플로에 명시), shak report-gaps가 그 신호를 수집·중복 제거해 GitHub 이슈로 승격한다. 사용자 불만은 report-issue가 받는다. instructions에는 "사용자가 shak 컴포넌트에 불만을 표현하면 시도한 것·기대·실제·재현 코드를 정리해 이슈를 만들라"는 규칙까지 실려 있다.

운영해 보니 여기도 함정이 있었다. 에이전트는 여러 세션이 동시에 돈다. 같은 갭을 두 세션이 발견하면 이슈가 중복 생성된다 — 파일 락과 "같은 제목이 열려 있으면 스킵, 비슷한 맥락 이슈는 후보로 반환해 에이전트가 직접 판단" 로직이 가장 최근 패치로 들어갔다. 사람 도구였다면 없었을 요구사항이다. 에이전트를 사용자로 둔 도구는 동시성·멱등성을 기본값으로 설계해야 한다.

정리 — 다섯 개의 철학

흐름을 되짚으면, 각 벽이 하나씩의 원칙을 남겼다.

  1. 문서는 소스에서 파생시킨다. 사람이 쓰는 스펙 문서는 반드시 드리프트한다. 추출 파이프라인이 곧 문서다.
  2. 에이전트 UX도 UX다. 도구는 존재만으로 쓰이지 않는다. 발견 가능성을 설계하고, 그마저 사용자 행동(프롬프트를 안 고름)에 맞춰 자동 채널로 옮긴다.
  3. 정본은 한 곳, 채널은 여럿. MCP·CLI·eslint가 같은 metadata를 읽는다. 자동 추출과 사람 큐레이션 모두 이 원칙 아래에 있다.
  4. 생성보다 검증이 어렵고, 검증은 층으로 쌓인다. 정적 검사 → 렌더 확인 → 픽셀 diff. 각 층은 아래층이 통과시킨 버그를 잡는다.
  5. 도구는 피드백이 돌아야 산다. 소비자 레포의 신호를 수집하는 경로까지가 도구의 일부다. 에이전트가 사용자라면 동시성까지 설계 범위다.

첫 버전의 가설 — "조회만 시켜주면 끝" — 은 틀렸다. 하지만 틀린 방식이 유익했다. 매 단계의 실패가 다음 층의 요구사항을 정확히 알려줬고, 그 순서(조회 → 발견 → 분리 → 검증 → 자동화 → 피드백)는 지나고 보니 도구가 아니라 제품이 성숙하는 순서였다. 디자인시스템의 소비자에 사람만 있던 시대는 끝났다. 에이전트라는 새 소비자는 문서를 읽지 않고 조회하며, 지치지 않고 틀리고, 검증 루프만 쥐여주면 스스로 고친다. 그 소비자를 위한 인터페이스를 설계하는 일은, 해보니 API 설계의 가장 순수한 형태였다.