Skip to content

Latest commit

 

History

History
431 lines (302 loc) · 50 KB

File metadata and controls

431 lines (302 loc) · 50 KB

devup-mcp

Rust-native MCP server that reads Figma designs and generates DevupUI artifacts. One binary acts as a local stdio MCP server and a read-only client of Figma Remote MCP.

저장소는 Cargo workspace이며 devup-mcp 실행 crate, OAuth·upstream·snapshot을 담당하는 devup-mcp-figma, TSX·theme projection을 담당하는 devup-mcp-devup-ui, PNG 비교 library/CLI인 devup-mcp-visual로 구성됩니다. 별도 IR/auth/server crate 없이 MCP 제품 설치 단위는 devup-mcp 하나입니다.

도구

Figma 쪽 4개, 프로젝트 쪽 3개, 모두 7개입니다.

  • devup_figma_export: Figma를 한 번 수집해 요청한 outputs만 투영합니다. TSX가 산출물이고, componentTsx·responsiveTsx·devup.json·source map·raw snapshot·asset manifest·reference PNG를 같은 수집에서 함께 얻거나, cache.artifactId로 재수집 없이 추가 투영할 수 있습니다.
  • devup_figma_search: 파일 전체의 page, section, frame, component를 이름으로 탐색
  • devup_figma_explore: 링크된 요구사항/라벨 주변의 실제 화면 후보를 공간 순서로 탐색
  • devup_figma_auth: 연결 상태 확인, 브라우저 OAuth 로그인, 로그아웃, 사전 등록 자격증명 주입(configure), 연결 실패 원인을 실측해 보고하는 doctor
  • devup_project_context: 프로젝트의 실제 devup.json 토큰, openapi.json 엔드포인트, Vespertide 모델을 읽음
  • devup_ui_validate: 생성한 TSX를 프로젝트의 실제 devup.json에 대조해 검증
  • devup_stack_diff: DB 모델부터 생성된 API 클라이언트까지의 층간 드리프트 탐지

devup_figma_to_uidevup_figma_to_json은 각각 devup_figma_exportoutputs: ["tsx"], outputs: ["devupJson"]을 넘긴 것과 같아서 제거했습니다. 도구가 셋이면 모든 클라이언트가 세 개의 스키마를 컨텍스트에 싣고도 어느 것을 부를지 매번 판단해야 했습니다.

devup-mcp는 Figma Plugin API의 readable data property를 raw JSON으로 보존하고, 알려지지 않은 runtime field는 extra, 실패한 getter는 fieldErrors로 유지합니다.

응답에 무엇이 들어오는가

devup_figma_export요청한 outputs가 만드는 키만 추가합니다.

output 추가되는 키
tsx tsx
componentTsx componentTsx
responsiveTsx responsiveTsx, responsiveSlots, (표현 불가한 값이 있으면) responsiveUnrepresented
devupJson devupJson, themeCounts, themeCompleteness, conflicts, unresolvedVariables
sourceMap / assetManifest / referencePng 같은 이름의 키
rawSnapshot / rawPayload 같은 이름의 키 — debug: true 필요

그 밖에 항상 붙는 것은 status, quality, completeness, cache, collection, source, targetKind, failures, outputPaths뿐입니다. fidelitycompletenessReport는 결과가 exact/complete가 아닐 때, 또는 includeDiagnostics: true일 때만 나옵니다 — 깨끗한 결과에서는 quality가 이미 한 말을 되풀이할 뿐이라 빼두었고, 그만큼(측정값 797 B) 매 응답이 가벼워집니다.

rawSnapshotrawPayload는 수집한 디자인을 raw로 담은 것이라 debug: true 없이는 거절됩니다. 화면을 구현하는 데는 필요 없습니다 — 실제 캡처 10개 화면에서 tsx가 node·text·typography·asset·layout 기대치를 100% 담고 있습니다. 쓰는 자리는 하나입니다: 화면이 이상해 보일 때 생성기 탓인지 디자인이 원래 그런지 판정하는 것. 그때는 디자인을 코드 옆에 놓고 읽어야 하고, 그게 이 플래그입니다.

에러는 호출 자체가 잘못된 경우(DEVUP_INVALID_INPUT, 없는 node/파일, 만료·부적합한 artifactId 등) JSON-RPC -32602 INVALID_PARAMS로, 그 밖의 실패는 -32603 INTERNAL_ERROR로 옵니다. 인자를 고쳐 다시 부를 일인지 멈추고 보고할 일인지를 메시지를 파싱하지 않고 구분할 수 있습니다. 정확한 coderetryable은 예전처럼 data에 그대로 실립니다.

devup-mcp는 Figma Remote MCP에 직접 붙습니다 — OAuth discovery, Dynamic Client Registration, PKCE S256, 일시적인 127.0.0.1 callback을 구현합니다. Figma는 MCP Catalog에 승인된 client의 registration만 허용하므로 등록은 allowlist에 있는 client_name으로 이루어집니다(기본값 Codex). Figma PAT나 사용자가 만든 OAuth app은 필요하지 않습니다.

빌드와 설치

MCP Bundle (.mcpb) — 툴체인 없이 한 번에 설치

릴리스마다 devup-mcp-<version>.mcpb 파일 하나가 함께 올라갑니다. 이 하나에 Linux x86_64, Windows x86_64, macOS universal 바이너리가 모두 들어 있고, manifest.jsonserver.mcp_config.platform_overrides가 실행 시점에 호스트의 운영체제에 맞는 바이너리를 고릅니다. 운영체제별로 어떤 파일을 받아야 하는지 고를 필요가 없고, Rust 툴체인도 Node 런타임도 cargo install도 필요하지 않습니다.

  1. Releases에서 devup-mcp-<version>.mcpb를 받습니다.
  2. .mcpb를 지원하는 호스트(예: Claude for macOS/Windows)에서 파일을 엽니다.
  3. 설치 대화상자의 Workspace directory에 코드를 생성할 프로젝트 디렉터리를 지정합니다. 이 디렉터리가 devup-mcp가 파일을 쓸 수 있는 유일한 위치이며, ..·다른 drive·symlink로 그 밖을 가리키는 경로는 기록 전에 거절됩니다. 여러 root가 필요하면 아래 stdio 설정으로 --allow-write-root를 반복해 등록하세요.

.mcpb는 그냥 zip이므로 unzip -l로 내용을 확인할 수 있고, 안의 바이너리는 같은 릴리스에 따로 올라가는 것과 같은 파일입니다. CI는 pack 직후 아카이브를 다시 읽어 Unix 바이너리에 실행 비트가 남아 있는지 확인하고, 없으면 릴리스를 게시하지 않습니다. 그럼에도 macOS에서 설치 직후 서버가 EACCES로 실패한다면 호스트가 압축을 풀면서 권한을 지운 경우이며(mcpb#294), 그때는 같은 릴리스의 devup-mcp-macos-universal 바이너리를 직접 받아 아래 stdio 설정으로 등록하면 됩니다.

소스에서 빌드

Rust 1.98 이상이 필요합니다. compile-in Figma 탐색 행동 fixture를 직접 실행하려면 CI와 동일한 Node.js 24가 필요하며 제품 binary에는 Node가 필요하지 않습니다.

cargo install --git https://github.com/dev-five-git/devup-mcp.git --branch owjs3901/figma-remote-mcp devup-mcp

설치 또는 binary 교체 후에는 먼저 로컬 진단을 실행합니다.

devup-mcp --version
devup-mcp --self-check

--version은 package version과 build ID를, --self-check는 network/OAuth 없이 binary, credential backend 초기화와 server 구성을 안전한 JSON으로 확인합니다. 둘 다 성공하지만 등록된 connector가 Transport closed를 반환하면 MCP host가 교체 전 process의 종료된 stdio pipe를 보유한 상태이므로 host의 MCP 연결을 재시작하거나 다시 등록해야 합니다. 새로 실행된 server가 host가 보유한 이전 pipe를 스스로 복구할 수는 없습니다.

소스에서 검증하려면 다음을 실행합니다.

cargo fmt --all -- --check
node --test crates/devup-mcp-figma/tests/explore_script_behavior.mjs
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo test -p devup-mcp --test stdio_smoke
cargo insta test --workspace --all-features --check
cargo build --workspace --release

MCP 설정

stdio MCP를 지원하는 클라이언트에 다음과 같이 등록합니다.

{
  "mcpServers": {
    "devup-mcp": {
      "command": "devup-mcp",
      "args": ["--allow-write-root", "/absolute/path/to/workspace"]
    }
  }
}

시스템 브라우저는 devup_figma_authlogin을 명시적으로 호출할 때만 열립니다. 변환 도구는 자격증명이 없으면 브라우저를 열지 않고 로그인이 필요하다는 오류를 반환합니다. 인증 정보는 운영체제 credential store에만 저장되며 logout은 해당 정보만 삭제합니다.

인증

{ "action": "status" }

actionstatus, login, logout, configure, doctor 중 하나이며, 스키마가 이 목록을 그대로 게시합니다. status/login/logout의 응답 형태는 항상 { "status": "connected" | "disconnected" }입니다. Figma에 붙지 못하는 이유를 알고 싶으면 doctor를 호출하세요.

{ "action": "doctor" }
{
  "status": "disconnected",
  "paths": {
    "direct": {
      "available": false,
      "credentialSource": "none",
      "tokenState": "absent",
      "callbackPort": { "port": null, "free": null },
      "reason": "저장된 자격증명 없음. ..."
    },
  },
  "clientSetup": { "constraints": { ... }, "opencode": { ... }, "claudeCode": "...", "codex": "..." }
}

doctor는 네트워크 호출을 전혀 하지 않습니다. paths.direct.credentialSourcecli-arg, env, credential-store, none 중 하나이고, tokenStatevalid, expired, absent 중 하나이며, callbackPort--figma-callback-port를 지정했을 때만 실측한 port/free를 담습니다. 자세한 제약은 아래 "Figma 연결 설정" 절을 참고하세요.

direct 경로에 사전 등록된 client 자격증명 주입하기

Figma MCP Catalog에 승인된 client(예: 직접 waitlist로 등록해 발급받은 client)의 client_id/client_secret을 이미 가지고 있다면, devup-mcp에 다음 세 가지 방법 중 하나로 주입해 Dynamic Client Registration을 완전히 건너뛸 수 있습니다. 우선순위는 시작 인자 > 환경변수 > configure로 저장한 값입니다.

  • 시작 인자: devup-mcp --figma-client-id <id> --figma-client-secret <secret>
  • 환경변수: DEVUP_FIGMA_CLIENT_ID, DEVUP_FIGMA_CLIENT_SECRET
  • 도구: devup_figma_auth { "action": "configure", "clientId": "...", "clientSecret": "..." } — OS credential store(시작 인자/환경변수와는 별도 항목)에 저장되어 프로세스를 재시작해도 유지됩니다.

자격증명이 해석되면 devup_figma_auth { "action": "login" }은 registration 엔드포인트를 전혀 호출하지 않고 바로 authorization_code + PKCE 흐름으로 진입합니다. 자격증명이 없으면 DCR을 시도하고, 403이면 그대로 보고합니다. DCR 요청의 client_name 기본값은 "Codex"입니다(DEFAULT_CLIENT_NAME). allowlist는 이름을 정확히 일치시켜 판정하고 "devup-mcp"는 거기에 없으므로, 그 이름으로 보내면 등록이 403으로 거절되어 direct 경로 자체가 성립하지 않습니다. 이 등록은 Figma에게 devup-mcp가 아니라 Codex로 기록됩니다. 본인 client가 카탈로그에 승인되면 --figma-client-name 또는 DEVUP_FIGMA_CLIENT_NAME으로 그 이름을 넘기세요. client_secret은 로그, 에러, MCP 응답, doctor 출력 어디에도 노출되지 않으며 doctorcredentialSource로 존재 여부만 보고합니다.

Figma 연결 설정

devup-mcp가 Figma에 붙는 경로는 하나입니다 — 원격 OAuth (direct). devup_figma_auth { action: "login" }으로 브라우저 인증. Figma MCP Catalog에 승인된 client만 등록할 수 있습니다. 현재 사용 가능한지는 devup_figma_auth { action: "doctor" }로 확인하세요.

Figma 데스크톱 앱의 로컬 Dev Mode MCP(http://127.0.0.1:3845/mcp)는 세 번째 경로로 안내했으나 제거했습니다. 읽기 도구 6개(get_design_context, get_variable_defs, get_screenshot, get_motion_context, get_metadata, get_figjam)만 제공하고 그중에 use_figma가 없습니다. devup-mcp의 수집은 snapshot·explore·section index·theme 모두 use_figma로 스크립트를 실행하므로 로컬에서는 실행할 도구 자체가 없습니다. 도구들이 fileKey를 받지 않고 데스크톱 앱에 열려 있는 파일만 가리키는 것도 같은 이유로 맞지 않습니다. "OAuth 없이 바로 쓸 수 있다"는 안내는 확신에 차서 틀린 안내였고, 믿은 쪽이 한 턴을 버린 뒤에야 알게 됩니다.

원격 OAuth 등록 제약 (실측)

Figma Remote MCP 등록 엔드포인트는 POST https://api.figma.com/v1/oauth/mcp/register입니다. 요청 본문의 client_name은 정확히 일치하는 allowlist로만 승인됩니다.

client_name 결과
Codex 200 (client_id + client_secret 발급)
Claude Code 403 (2026-09-06 실측; 이전 표에는 200으로 적혀 있었음)
OpenCode 403
opencode 403
Cursor 403
VS Code 403

403 응답 본문은 JSON이 아니라 평문 Forbidden입니다. 그래서 많은 클라이언트가 Invalid OAuth error response ... Raw body: Forbidden으로 파싱까지 깨집니다. X-Figma-Plugin-Bundle 헤더나 User-Agent를 바꿔도 결과는 바뀌지 않습니다. 신규 client 등록은 waitlist를 통해서만 가능합니다: https://www.figma.com/mcp-catalog/.

redirect_uri도 형태가 고정되어 있습니다.

redirect_uri 결과
http://127.0.0.1:<port>/callback 200
http://127.0.0.1:<port>/mcp/oauth/callback 400
http://localhost:<port>/mcp/oauth/callback 400

경로는 정확히 /callback이어야 하고 호스트는 127.0.0.1이어야 합니다 (localhost 불가). Figma PAT(figd_...)는 Authorization: Bearer, X-Figma-Token 어느 방식으로도 원격 MCP에서 지원되지 않습니다.

숨은 함정 — 콜백 포트 점유

로컬 OAuth 콜백이 쓰는 포트를 OS나 보안 소프트웨어(예: 사내 보안 에이전트)가 이미 점유하고 있으면, 브라우저는 리다이렉트에 "성공"한 것처럼 보이지만 그 요청은 다른 프로세스로 전달됩니다. 클라이언트는 아무 에러 없이 Waiting for authorization... 상태로 영원히 남습니다. 로그인이 멈춘 것처럼 보이면 가장 먼저 콜백 포트를 다른 프로세스가 쓰고 있지 않은지 확인하세요.

기본값은 OS가 매번 빈 임시 포트를 골라주므로(0) 이 충돌을 피합니다. 사전 등록한 client의 redirect_uri가 고정 포트로 등록되어 있어 특정 포트를 고정해야 한다면 devup-mcp --figma-callback-port <port>를 지정하세요. 이 경우 devup-mcp는 그 포트가 이미 사용 중이면 연결을 기다리지 않고 DEVUP_FIGMA_CALLBACK_PORT_IN_USE 오류를 즉시 반환합니다. devup_figma_auth { "action": "doctor" }paths.direct.callbackPort.free에서도 지정한 포트가 실제로 비어 있는지 실측한 값을 확인할 수 있습니다.

opencode에서 direct 경로 미리 설정하기

Dynamic Client Registration을 건너뛰려면 mcp.<name>.oauth에 이미 발급받은 clientId/clientSecret을 직접 지정합니다.

{
  "mcp": {
    "figma": {
      "type": "remote",
      "url": "https://mcp.figma.com/mcp",
      "oauth": {
        "clientId": "<allowlist된 client_name으로 등록해 발급받은 client_id>",
        "clientSecret": "<allowlist된 client_name으로 등록해 발급받은 client_secret>",
        "scope": "mcp:connect",
        "callbackPort": 19876,
        "redirectUri": "http://127.0.0.1:19876/callback"
      }
    }
  }
}

Codex는 allowlist에 있어 별도 설정 없이 등록할 수 있습니다. Claude Code는 한때 200이었으나 2026-09-06 실측에서 403으로 거절됐습니다 — allowlist는 Figma가 바꿀 수 있으며, 위 표는 측정 시점의 기록입니다.

devup-mcp는 직접 경로만 씁니다. 호스트(Codex)에 등록된 공식 Figma MCP를 빌리는 우회 경로는 만들지 않습니다 — devup-mcp가 스스로 Codex로 등록해 Figma 원격 MCP에 붙고, 수집에 필요한 use_figma를 그 연결로 직접 부릅니다. 호스트에 Figma MCP를 따로 설정할 필요가 없고, 설정돼 있어도 devup-mcp는 그것을 쓰지 않습니다.

claude mcp add --transport http figma https://mcp.figma.com/mcp
codex mcp add figma --url https://mcp.figma.com/mcp

Figma → DevupUI

{
  "url": "https://www.figma.com/design/<file-key>/<name>?node-id=1-2",
  "outputs": ["tsx"],
  "componentName": "OptionalComponentName",
  "rootLayout": "standalone",
  "scope": "node",
  "outputPaths": { "tsx": "optional/path/Component.tsx" }
}

결과에는 tsx와 함께 status, quality, cache, collection, source가 포함됩니다. import 목록과 사용 token은 별도 키로 보내지 않습니다 — 각각 TSX의 첫 줄과 본문의 $token이 이미 같은 내용을 담고 있어, 응답에 두 번 싣는 만큼이 그대로 낭비였습니다. Auto Layout은 Flex, 일반 container는 Box, text는 Text로 변환하고 theme binding이 있으면 JSX prop에서 $token을 우선 사용합니다. 변수 token은 비어 있지 않은 Figma codeSyntax.WEB을 우선하고, 없으면 변수 경로의 마지막 이름을 정규화합니다. 따라서 TSX의 $token, devup.json key와 source map이 같은 이름을 사용합니다. rootLayout 기본값인 standalone은 선택한 root의 크기·위치 제약까지 포함하고 Figma instance의 실제 자식 상태를 펼쳐 정의되지 않은 component 참조를 만들지 않습니다. 이미 레이아웃을 소유한 React 부모 안에 삽입할 때는 rootLayout: "embedded"로 root의 외부 크기·위치 제약만 생략합니다.

Figma → devup.json

{
  "url": "https://www.figma.com/design/<file-key>/<name>?node-id=1-2",
  "outputs": ["devupJson"],
  "scope": "file",
  "outputPaths": { "devupJson": "optional/path/devup.json" }
}

결과는 theme.colors, theme.typography, theme.length, theme.shadow를 포함하는 결정적 JSON 문자열과 counts, completeness를 반환합니다.

outputPath를 생략하면 결과를 메모리와 MCP 응답에만 유지합니다. 명시하면 생성된 TSX 또는 devup.json만 해당 경로에 기록하고 실제 절대 경로를 응답합니다. 기본 허용 write root는 devup-mcp process 시작 당시의 current directory 하나이며, 그 밖의 workspace는 반복 가능한 --allow-write-root <directory> 시작 인자로만 추가할 수 있습니다. Tool 입력으로 root 자체를 넓힐 수 없고 .., 다른 drive/UNC, alternate data stream, symlink/junction을 통한 root 탈출은 기록 전에 거절됩니다.

여러 text/asset output은 모두 검증한 뒤 같은 directory의 exclusive 임시 파일에 staging하고 한 transaction으로 교체합니다. 정상 runtime 오류에서는 이미 교체한 파일을 역순으로 되돌리고 기존 파일을 복구합니다. 개별 rename은 atomic하지만 여러 directory와 process crash를 가로지르는 완전한 원자성은 일반 filesystem 특성상 보장하지 않습니다.

통합 수집과 다중 출력

{
  "url": "https://www.figma.com/design/<file-key>/<name>?node-id=1-2",
  "outputs": ["tsx", "responsiveTsx", "devupJson", "rawSnapshot", "rawPayload", "sourceMap", "assetManifest", "referencePng"],
  "scope": "node",
  "strict": true,
  "refresh": false,
  "delivery": "auto"
}

devup_figma_export는 동일한 node/resource acquisition에서 여러 projection을 생성합니다. 응답의 cache.artifactId를 다음 요청의 artifactId로 넘기면 Figma를 다시 호출하지 않고 다른 output을 만들 수 있습니다. URL 요청은 같은 process 안에서 10분 TTL, 최대 8개/항목당 32 MiB/전체 128 MiB인 memory-only LRU cache를 재사용하며, refresh: true는 완료 cache뿐 아니라 진행 중 요청 공유도 우회해 URL을 새로 수집합니다. 동일 acquisition의 선행 작업이 취소되더라도 닫힌 in-flight 표식을 다음 요청이 원자적으로 제거하고 다시 수집하므로 같은 key가 process 수명 동안 오염되지 않습니다. cache에는 reuseKind, ageSeconds, remainingTtlSeconds, avoidedFigmaToolCalls, 원 수집의 originCollection이 포함되고, 응답 최상위 collection은 현재 요청이 실제로 실행한 호출만 집계합니다. cache.capabilities는 artifact의 kind(design, theme-only, search, explore), collectionScope, resourceScope, referencePng 보유 여부와 redacted assetCaptureCount만 공개합니다. 내부 artifact는 asset ID·format·scale 전체를 보존하고 세 값이 정확히 같은 capture만 추가 Figma 호출 없이 재사용합니다. 재사용 요청이 이 범위를 넘으면 DEVUP_FIGMA_HANDOFF_INVALID로 투영과 파일 기록 전에 거절합니다. 예를 들어 node/used-resource artifact로 file 전체 devupJson을 만들거나 screenshot을 수집하지 않은 artifact로 referencePng를 만들 수 없습니다. credential, screenshot과 asset binary는 cache key나 통계에 포함하지 않고, process가 끝나면 cache도 사라집니다.

deliveryauto | inline | resource입니다. auto는 JSON escape, base64와 structured/text 이중 표현을 포함한 실제 MCP wire 크기를 계산해 개별 256 KiB·합계 1 MiB 이하만 inline으로 반환하고, 그보다 큰 결과는 native MCP ResourceLinkdevup://artifact/... URI로 바꿉니다. 링크 URI는 JSON manifest를 가리키므로 link MIME은 application/json이고 payload MIME·길이·SHA-256은 payload* metadata로 분리합니다. resource는 크기와 무관하게 TSX/JSON/PNG를 bounded chunk resource로 제공하며, binary chunk는 base64 MCP blob입니다. asset manifest는 binary를 내장하지 않고 각 asset의 독립 resource URI·MIME·길이·SHA-256을 참조하므로 resources/read로 원본 bytes를 정확히 재구성할 수 있습니다. 같은 artifact와 정규화한 projection은 content hash가 같은 resource를 재사용합니다. 파일 출력과 새 resource publication을 함께 요청하면 resource 조회를 reservation 동안 차단한 하나의 transaction으로 다루며, 파일 commit이 전부 성공한 뒤에만 resource와 LRU 변경을 공개합니다. 실패하면 원래 파일을 fingerprint로 검증해 복원하고 복원 불능 backup 경로를 구조화해 보고합니다. 현재 transaction이 만든 temp는 정상 종료·rollback에서 직접 제거하지만, 소유권을 증명할 수 없는 pre-existing temp나 crash·rollback recovery backup은 자동 삭제하지 않습니다.

referencePng는 선택했을 때만 공식 read-only get_screenshot을 정확히 한 번 추가 호출합니다. 공식 도구는 기본으로 PNG의 URL과 curl 안내를 text로만 돌려주고 긴 변을 1024px로 줄이므로, enableBase64Response: truemaxDimension: 8192로 호출해 node 원래 크기의 PNG를 inline으로 받습니다. 결과의 image block은 정확히 하나여야 하며(곁의 text block은 읽지 않음), JSON/text에 숨긴 image나 다중 image는 거절합니다. 16 MiB compressed, 8192px, 64 MiB decoded 상한 안에서 PNG 전체를 실제 decode한 뒤 byte length와 SHA-256을 확인해 artifact에 보존하며, 단일 링크 node에만 적용됩니다. Section의 여러 Frame은 먼저 반환된 canonical URL별로 수집해야 합니다. PNG bytes는 log·통계·cache key에 포함되지 않으며 outputPaths.referencePng를 명시하지 않으면 디스크에 기록하지 않습니다.

모든 완료 응답에는 다음처럼 요청한 산출물별 quality가 포함됩니다.

{
  "status": "complete",
  "quality": {
    "acquisition": "complete",
    "projection": "exact",
    "theme": "not-requested",
    "assets": "not-requested"
  }
}

acquisitioncomplete | expected-projection | partial | failed, projectionexact | approximated | lossy | failed | not-requested, themecomplete | conflicted | unresolved | not-requested, assetscomplete | partial | failed | not-requested입니다. 검색·탐색의 의도적인 얕은 graph는 expected-projection으로 정상 완료하지만, 포함된 field의 실패나 truncation은 partial입니다. mask/effect fallback은 lossy, absolute layout fallback은 approximated이며 includeDiagnostics: false여도 품질 판정에는 반영됩니다. 기존 status는 요청한 모든 축이 정확하거나 완전할 때만 complete이고, strict: true는 모든 요청 축이 exact/complete가 아니면 quality와 completenessReport를 담은 오류로 거절합니다.

모든 공개 TSX generator는 반환 전에 Rust의 고정된 TypeScript+JSX parser를 통과합니다. parser 오류는 디자인 원문을 노출하지 않고 byte range와 오류 category만 반환합니다. 응답의 fidelity는 생성된 mapping 수가 아니라 수집한 source snapshot에서 독립적으로 계산한 node/text segment/variable/style/asset/layout 기대 집합을 분모로 사용하고, 각 항목이 최종 TSX byte range에서 emitted | flattened | ignored 중 정확히 하나로 추적되었는지와 축별 coverage·typed impact count를 담습니다. component set, non-default variant selector와 inline instance도 최종 변환 후 source identity별 provenance를 다시 만들며, 반복된 동일 text segment는 하나의 mapping을 재사용하지 않고 occurrence별로 소비하고 multiline·중첩 text와 asset identity를 검증합니다. 알 수 없는 codegen warning/error도 각각 최소 approximated/failed로 보수적으로 판정하며, strict는 syntax, source-derived trace coverage, lossy/failed impact를 함께 검사합니다.

브라우저 시각 회귀는 MCP 서버가 임의 명령을 실행하지 않고 소비자 repository가 실제 font/asset/DevupUI 환경으로 actual.png를 만든 뒤 순수 Rust devup-mcp-visual로 비교합니다. renderer pinning, 기본 0.5% threshold, diff PNG와 개인정보 취급 계약은 docs/visual-renderer-contract.md에 있습니다.

렌더링 하네스 — 생성 코드를 Figma가 그린 PNG와 비교

생성 코드를 플러그인의 답안과 줄 단위로 맞춰보면 둘이 일치한다는 것까지는 알 수 있지만, 둘 중 어느 쪽도 Figma가 그리는 그림과 같은지는 말해주지 못합니다. harness/render는 그 질문에 답합니다 — 각 화면을 devup-ui로 빌드해 프레임 크기 그대로 열고, Figma가 같은 프레임을 렌더한 PNG와 픽셀 비교합니다.

cd harness/render && npm install
python scripts/acquire.py            # 모듈·테마·에셋·기준 PNG를 실행 중인 devup-mcp에서 가져옴
node scripts/render.mjs              # 빌드·캡처·비교, 화면별 임계값 초과 시 exit 1

acquire.py는 Figma 호출을 fixtures/local-call-bank에 적립하므로 재실행은 이미 지불한 만큼 무료입니다. 화면마다 자기 테마를 node scope로 받습니다 — 파일 하나에 여러 브랜드 컬렉션이 섞이면 primary 같은 토큰이 서로 덮어써서, 공지 화면이 Figma가 파랑으로 그리는 자리를 보라색으로 그렸습니다. devup-ui는 테마를 빌드 시점에 굽기 때문에 화면들은 필요한 테마별로 묶여 그룹마다 한 번씩 빌드됩니다. 리셋은 생성 코드가 전제하는 @devup-ui/reset-css 그대로입니다.

thresholds.json이 화면별로 Figma와 벌어져도 되는 최대치를 들고 있습니다. 초과하면 실패하고, 밑돌면 그렇다고 알려줍니다(= 수치를 조일 차례). 측정값:

화면 1920 992 / 768 360 / 390
popup 0.83% 2.19% 3.59%
popup (플러그인 답안) 21.06% 7.14% 12.02%
notice 2.33% 4.19% 8.29%
about 4.54% 6.94% 11.26%
report 1.87% · grid 2.96% · keyframes 6.71%

차이가 어디 있는지는 보조 도구가 답합니다 — bands.mjs(가장 많이 어긋난 구간), drift.mjs(단순 이동인지 실제 차이인지), crop.mjs(구간을 기준/캡처 나란히), boxes.mjs(DOM 상자를 Figma 좌표와 대조), elements.mjs(그림이 실제로 몇 픽셀로 나왔는지). 긴 화면을 통째로 줄인 스크린샷은 아무것도 보여주지 않습니다.

text-check.mjs는 픽셀이 아니라 글자를 봅니다. 생성기는 텍스트 노드의 characters를 JSX에 쓰는데, JSX는 공백에 자기 규칙이 있습니다 — 한 문장이 소스 두 줄로 나뉘면 사이에 공백 하나가 들어갑니다. 디자인에 그 공백이 없으면 화면은 디자인에 없는 단어를 찍고, 문단은 Figma가 끊지 않는 자리에서 감깁니다. JSX를 읽어 무엇이 그려질지 추론하는 건 그 규칙을 다시 구현하는 일이고, 그렇게 넘겨짚으면 없는 결함을 만들어냅니다 — 그래서 브라우저가 실제로 찍은 글자characters와 대조합니다. 현재 245개 텍스트 중 3개(같은 문단의 세 폭)가 디자인대로 찍히지 않습니다.

캡처·테마·에셋·빌드 산출물은 커밋하지 않습니다(harness/render/.gitignore). 이 하네스가 찾아낸 결함은 테마 스코프, 컨테이너가 칠하는 그림의 매니페스트 누락, 잘린 fill의 crop 행렬, 파일시스템이 못 받는 레이어 이름, 폭마다 크기가 다른 사진의 파일 공유, 투명도 0 노드의 export 거부, 그리고 positioned child 너머로 CSS가 못 미치는 높이입니다.

Section 링크에서 TSX를 요청하면 먼저 내부 screen frame 후보와 canonical URL을 selection_required로 반환합니다. frameIds로 검토한 frame만 고르거나 allScreens: true로 모든 화면을 시각 순서대로 batch export할 수 있으며 두 옵션은 동시에 사용할 수 없습니다. sourceMap은 생성 TSX/devup.json의 output 위치를 Figma node, variable, style, asset ID에 연결하는 sidecar입니다. assetManifest는 image hash/vector/export provenance를 항상 열거하고, assetRequests로 명시한 항목만 최대 16개·scale 1~4 범위에서 read-only SVG/PNG export합니다. outputPath를 지정하면 binary를 해당 파일로 디코딩하고 응답의 base64를 제거하며, 생략하면 후속 소비를 위해 base64가 memory-only artifact와 해당 MCP 응답에 남을 수 있습니다.

asset의 파일 이름은 기본적으로 레이어 이름입니다 — 플러그인이 그렇게 짓기 때문입니다. 그래서 디자이너가 같은 이름을 준 노드들은 파일 하나를 공유합니다. 같은 그림이면 맞지만 아니면 손실입니다. 한 화면에서 여덟 노드가 Logo.svg 하나를 주장하는데 실제로는 서로 다른 그림 다섯 개였고, 폭마다 그려진 사진은 마지막으로 export된 폭의 파일만 남아 다른 폭에서는 상자와 크기가 어긋난 채 늘어납니다(파일이 상자와 같은 크기이면 object-fit이 무엇이든 결과가 같으므로, 플러그인에서는 이 문제가 드러나지 않습니다).

assetNamesPerNode는 각 asset을 그 노드의 이름으로 지어(Logo-422-6921.svg, Frame 269-422-3392.png) 둘을 함께 없앱니다. 기본값은 true입니다 — 렌더링해 보면 이쪽이 Figma가 그리는 그림에 가깝고(공지 화면 992폭 6.48% → 4.19%, about 992폭 10.79% → 6.94%), 플러그인 golden 268개는 그대로 통과합니다. golden은 CodegenOptions를 직접 쓰고 그 라이브러리 기본값은 여전히 플러그인과 동일하기 때문입니다. 플러그인과 byte 단위로 같은 이름이 필요하면 assetNamesPerNode: false로 끄십시오.

끈 상태에서 서로 다른 그림이 한 파일을 계속 주장하면 첫 번째만 기록하고 나머지는 DEVUP_ASSET_NAME_SHARED diagnostic으로 보고합니다 — 조용히 덮어쓰지 않습니다.

레이어 이름이 파일시스템이 받지 못하는 이름일 때(ic:round-arrow-left처럼 콜론이 든 이름은 Windows가 만들지 못합니다) 전달 시점에 생성 코드와 manifest를 함께 개명해 둘이 어긋나지 않게 합니다. 생성기 자체는 플러그인의 이름을 그대로 쓰므로 golden parity는 유지됩니다.

Section 링크는 전체 subtree를 직접 변환하지 않습니다. selection_required.nextAction에 따라 후보를 확인한 뒤 frameIds 또는 allScreens: true로 화면별 export를 계속하며, 일부 화면 수집이 실패하면 성공한 화면은 유지하고 실패한 node는 failures에 보고합니다.

SECTION 기본 응답은 화면 아티팩트가 아닌 선택 목록입니다. selection.statusselection.count로 목록 조회 상태와 후보 수를 알 수 있으며, 최상위 status: "selection_required"는 아직 화면을 선택해야 한다는 뜻입니다. selection.candidates[]node.name, node.nodeType, node.textPreview, canonicalUrl로 서로 구분할 수 있습니다. 미리보기는 보이는 텍스트만 모아 최대 120자, 전체 최대 2KB로 제한하므로 비어 있거나 짧아도 실제 화면 내용이 없다는 뜻은 아닙니다. nextAction.example에는 첫 후보를 선택하는 devup_figma_export 호출 예시가 들어 있습니다. 예시의 frameIds를 검토한 후보 ID로 바꿔 호출하면 선택한 화면만 수집합니다.

한 화면의 여러 폭 — 반응형 모듈

Section 안의 frame이 mobile / tablet / desktop처럼 breakpoint 이름을 가지면, 그 frame 하나를 요청해도 같은 이름 규칙의 형제 frame이 함께 수집됩니다(Section 자체는 수집 범위 밖이며, 그 이름은 각 frame의 parentName으로 전달됩니다). 이때 tsxresponsiveTsx를 요청하면 결과에 responsiveTsx가 추가됩니다 — 세 폭을 하나의 트리로 접고 폭마다 다른 값을 devup-ui 반응형 배열 [mobile, sm, tablet, lg, pc]로 쓴 모듈입니다. 각 폭이 놓이는 slot은 frame 이름이 아니라 폭으로 정해집니다(≤480 / ≤768 / ≤992 / ≤1280 / 그 이상). 컴포넌트 이름은 componentName이 우선이고, 없으면 Section 이름의 PascalCase에 Page를 붙입니다(aboutAboutPage).

한 폭에만 있는 노드는 다른 폭에서 display: none으로 숨긴 복사본과 병합되며, 이때 복사본은 Section 레이어 순서상 첫 폭의 값을 가집니다 — 그래서 배열의 첫 slot에 desktop 값이 놓일 수 있습니다. 폭마다 줄바꿈 위치만 다른 텍스트는 <Box as="br" display={[...]} />로 쓰고, 컴포넌트 인스턴스의 variant prop이 폭마다 다르면 배열로 쓸 수 없으므로 가장 넓은 폭의 값을 쓰고 responsiveUnrepresented에 보고합니다. 함께 반환되는 responsiveSlots, responsiveImports, responsiveComponents가 slot과 import 목록입니다.

rawPayloadrawSnapshot이 node 트리만 쓰는 것과 달리 수집 전체(variables, styles, stats, assets 포함, referencePng 제외)를 씁니다. 캡처를 fixture로 보관해 오프라인에서 서버와 같은 토큰 이름($gray200, typography="h4")으로 변환하려면 이것이 필요합니다.

시간 트리거 Smart Animate — CSS keyframes

frame에 After delay 트리거로 다른 frame에 Smart animate하는 reaction이 있고, 그 frame이 다시 다음 frame으로 이어지면 하나의 체인입니다(처음 frame으로 돌아오면 루프). 체인의 frame들은 요청한 node의 subtree 밖에 있는 형제 frame이므로, 요청 루트가 하나일 때 snapshot 스크립트가 체인을 따라가며 추가 루트로 함께 수집합니다(다중 루트 요청은 루트 목록을 그대로 둡니다).

변환기는 플러그인의 getReactionProps 규칙대로 frame 사이에서 바뀌는 것 — 위치, 크기, opacity, 첫 fill, 회전(누적 delta) — 을 이름이 같은 자식에서 먼저 찾아 자식마다 animationName={keyframes({...})} / animationDuration / animationTimingFunction / animationFillMode / animationIterationCount(루프면 infinite)로 쓰고, 바뀌는 자식이 없을 때만 frame 자체에 씁니다. 0%는 시작 frame, 각 단계는 도착 시점의 퍼센트에 직전 keyframe과 다른 속성만, 루프는 100%에서 시작 값으로 닫히며 duration은 되돌아가는 구간까지 셉니다. 10ms 미만 timeout은 delay로 쓰지 않습니다. keyframes가 쓰이면 @devup-ui/react에서 import됩니다.

snapshot에 없는 목적지(legacy 경로, 다중 루트 요청)는 조용히 버리지 않고 DEVUP_CODEGEN_ANIMATION_UNREACHABLE diagnostic으로 보고합니다.

Figma 이름 검색

{
  "url": "https://www.figma.com/design/<file-key>/<name>",
  "query": "A : STORY-F-PROOFREAD",
  "nodeTypes": ["PAGE", "SECTION", "FRAME", "COMPONENT_SET"],
  "match": "normalized",
  "limit": 20,
}

검색은 먼저 read-only Plugin API로 실제 figma.root.children page catalog를 얻고, page마다 한 번씩 전환하는 작은 query projection을 병렬 실행합니다. 전체 page snapshot을 응답하지 않으므로 큰 파일에서도 공식 MCP text 상한을 피합니다. 결과는 원문 exact, Unicode NFC·공백·대소문자를 정규화한 exact, prefix, contains 순으로 정렬하고 match: "fuzzy"일 때만 오타 허용 검색을 추가하며, node ID, type, page, 전체 breadcrumb와 후속 devup_figma_export에 그대로 전달할 canonical URL을 포함합니다.

링크 주변 화면 탐색

{
  "url": "https://www.figma.com/design/<file-key>/<name>?node-id=1-2",
  "limit": 50,
  "includeTextPreview": true,
  "refresh": false,
}

요구사항 제목이나 설명 node 링크가 실제 구현 화면이 아닐 때 devup_figma_explore를 먼저 호출합니다. anchor와 같은 공간 묶음의 frame/component 후보를 시각 순서와 canonical URL로 반환하며, 다음 요구사항 제목에서 탐색 범위를 끝냅니다. 같은 파일·옵션에서 이미 수집한 더 큰 탐색 결과는 exact·related-node·superset 범위로 재사용되고, 동시에 들어온 호환 요청도 공식 Figma 호출 하나를 공유합니다. refresh: true는 모든 재사용을 건너뜁니다. 원하는 후보의 canonical URL을 devup_figma_export에 넘겨 정확한 화면만 변환합니다.

탐색과 검색은 변수 catalog를 수집하지 않습니다. 정확한 UI 변환 단계에서 선택 subtree의 모든 보존 필드에 있는 VARIABLE_ALIAS와 paint/text/effect/grid style ID를 재귀적으로 스캔하고, 실제 사용된 ID만 공식 Figma API로 조회합니다. outputs: ["devupJson"]scope: "file"을 함께 준 경우에만 file 전체 로컬 catalog를 수집합니다.

Figma 연결은 direct 하나뿐입니다. sourcePolicy 파라미터는 autodirect 둘 다 같은 동작이었으므로 제거했습니다 — 분기하지 않는 선택지는 호출자에게 틀릴 기회만 주었습니다. direct 경로는 연결과 read-only capability catalog 조회를 각각 30초, 개별 tool 호출을 5분으로 제한합니다. deadline을 넘기면 해당 remote session을 폐기하고 디자인 원문 없이 retryable timeout 단계만 반환합니다.

정확한 node 링크의 UI 변환은 하나 이상의 공식 use_figma 호출 안에서 subtree와 실제 사용 리소스를 수집합니다. 수집 스크립트는 checked-in manifest(devup-ui 변환기가 실제로 읽는 필드만)만 확인하고 — 프로토타입 체인 전체를 훑거나 미분류 필드를 extra에 담지 않습니다 — null/빈 배열/미바인딩 style ID 같은 기본값은 봉투에서 생략합니다. 결과는 항상 텍스트(devupFastSnapshotEnvelope)이며 PNG 같은 바이너리 transport는 없습니다. 한 subtree가 15KB 텍스트 한도를 넘으면 같은 스크립트를 offset을 옮겨 다시 호출하는 방식으로 텍스트 페이지네이션합니다 — 각 라운드는 그 라운드가 보낸 node에서만 리소스를 스캔해 자기 완결적이며, Rust가 여러 라운드의 node와 리소스를 병합합니다. Rust는 schema·대상 ID·node graph·리소스 참조·(페이지 중이 아닐 때의) 자식 완전성을 모두 검증한 뒤에만 결과를 채택합니다. 한 항목이라도 불일치하면 fast 결과 전체를 버리고 기존 cursor 수집을 0부터 재시작합니다. Section multi-root에서는 성공한 root와 resource는 그대로 보존하고 실패하거나 상한을 넘은 root만 legacy로 다시 수집한 뒤 원래 시각 순서로 합칩니다. direct upstream은 연결과 read-only tool catalog를 한 session에서 재사용하고 30초 TTL, 연결 종료 또는 transport 오류 때만 재연결·재검증합니다. 결과의 stats에는 figmaToolCalls, transport(text | text-paginated | legacy-cursor), fallbackUsed, node/variable/style 수와 byte 수만 포함되며 원본 디자인이나 인증 정보는 포함되지 않습니다.

완전성 등급은 다음과 같습니다.

  • full-local-plus-used-remote: 로컬 전체와 사용된 외부 token을 모두 확인
  • used-tokens: 확보한 token만 변환했으며 외부 전체를 보장하지 않음
  • resolved-values-only: 의미 있는 token binding 없이 계산값만 확보

읽기 전용·개인정보 보호

  • upstream 호출은 get_metadata, get_variable_defs, get_design_context, get_code_connect_map, get_screenshot과 내장된 read-only use_figma script로 닫혀 있습니다.
  • 사용자 입력 JavaScript를 받지 않으며 Figma document mutation API를 호출하지 않습니다. figma.io.write는 asset export(devup_figma_exportassetRequests)에만 read-only로 사용하며 Figma 파일을 변경하지 않습니다. fast snapshot/theme envelope는 항상 텍스트로만 반환되며 바이너리 transport를 쓰지 않습니다.
  • stdout에는 MCP frame만 출력하고 trace는 stderr로 보냅니다.
  • access token, refresh token, OAuth code, PKCE verifier는 Debug, trace와 MCP error에 포함하지 않습니다.
  • Figma snapshot과 screenshot을 기본적으로 디스크에 저장하지 않습니다.
  • screenshot, asset, TSX resource는 bounded memory artifact와 같은 TTL을 가지며 resource manifest에는 이름·MIME·크기·hash만 노출됩니다.
  • 호환성 fixture는 고정한 JavaScript 플러그인의 268개 synthetic 입력입니다. 별도의 WQUW-151 회귀 fixture는 공식 MCP에서 read-only로 수집한 디자인 node/텍스트/token 이름만 포함하며 OAuth token, header, callback parameter, 사용자 계정·email은 포함하지 않습니다.

플러그인 호환성 corpus

fixtures/devup-figma-plugindev-five-git/devup-figma-plugin의 고정 commit 243db650f1d635ab5385546a2a297eae4ea93515에서 수집한 54개 test file과 978개 passing-test inventory를 추적합니다. upstream test 252개가 만든 JSON/golden 268쌍은 Rust serde/codegen 경로에서 byte parity를 전부 실행하고, 666개는 같은 동작 영역의 실제 Rust assertion에 연결했습니다. 나머지는 plugin module/codegen handler/iframe/notify/browser download 수명주기 38개와 read-only MCP가 의도적으로 수행하지 않는 Figma document/style/import write 22개입니다. not_ported는 0개이며, 비-parity 항목도 구체적인 MCP 경계 test를 가리킵니다. 즉 정확한 보장은 “268/268 snapshot byte parity, 666개 실행 가능한 대표 Rust assertion 연결, 60개 명시적 runtime/write 경계, 978-entry inventory”이고 JavaScript assertion 978개를 각각 별도 fixture로 복제했다는 뜻은 아닙니다. manifest는 LF로 정규화한 fixture와 snapshot 536개 파일의 SHA-256을 검증하고, coverage registry는 ledger가 실제 Rust test symbol 또는 근거가 있는 비-parity 분류만 참조하도록 강제합니다. 상세 분류와 실행 방법은 fixtures/devup-figma-plugin/README.md를 참고하세요.

실제 Figma JSON contract gate

crates/devup-mcp/tests/live_figma_contract.rs는 기본적으로 ignore됩니다. DEVUP_MCP_LIVE_FIGMA=1을 설정하고 공식 MCP의 fast use_figma 결과를 stdin에 한 줄로 전달하면 실제 payload를 디스크에 쓰거나 출력하지 않고 envelope 무결성, serde round-trip, 요청 context, node/리소스 수와 DevupUI codegen을 검증하고 안전한 count/hash 요약만 출력합니다. 별도의 비-ignore corruption test는 깨진 fast 응답이 legacy metadata 수집으로 원자적으로 폴백하는지 확인합니다.

crates/devup-mcp-figma/tests/explore_script_behavior.mjs는 compile-in explore.js 자체를 mock Figma scene graph에서 실행합니다. 두 단계 이상 중첩된 화면의 parent chain, 화면이 없는 1,000-node Section의 projectionLimit * 8 방문 상한, 필수 node만 남기는 14,000자 이하 fallback을 검증하며 CI의 Node 내장 test runner로 실행됩니다. 제품 binary와 기본 Cargo test에는 JavaScript runtime 의존성이 추가되지 않습니다.

legacy 경로에서 실제 확인된 공식 metadata는 XML text content envelope이며, local 변수/style은 catalog 후 resource 단위로 수집합니다. style의 consumers처럼 단일 field가 공식 MCP의 text 상한(실측 20,480 UTF-8 바이트, 넘는 만큼 잘리고 // truncated to 20kb가 붙음)을 넘을 수 있으므로, base field와 320개 단위의 compact consumer relation을 분리해 읽고 Rust에서 원래 exhaustive JSON shape로 재조립합니다. legacy node snapshot도 byte budget과 cursor를 사용해 같은 상한 아래에서 자동 재개합니다. range의 누락·중복이나 수집 중 목록 변경은 성공으로 숨기지 않고 오류로 처리합니다.

Server module ownership

server/mod.rs는 MCP tool router, service construction, handoff 연결과 ServerHandler만 소유합니다. projection.rs는 TSX/theme/source-map/asset/reference output 생성과 delivery transaction을, validation.rs는 output/schema/artifact capability 입력 검증을 담당합니다. delivery.rs, artifacts.rs, output.rs, resources.rs, quality.rs는 각각 크기 결정, memory artifact, allowlisted filesystem transaction, MCP resource protocol, typed 품질 집계를 담당하며 source-level boundary test가 generator와 filesystem 구현이 router로 되돌아오는 것을 막습니다.

2026-09-01 실제 파일 검증에서는 13개 page 전체 검색으로 [FR-026] 본연체 Section (4217:7743)을 찾고, 그 안의 360×740 화면 10개를 시각 순서대로 인덱싱해 A : STORY-F-PROOFREAD (3879:35518)를 정확한 대상으로 선택했습니다. Section 전체 fast envelope는 8 MiB 안전 상한을 넘어서므로 성공으로 오인하지 않고, 각 화면을 공식 read-only MCP로 개별 수집했습니다. 열 화면은 각각 15210개 node를 가지며 모든 child, styled text segment, 변수 325개와 text style 2~13개의 참조 완전성을 실제 JSON fixture와 DevupUI TSX snapshot으로 검증합니다. 대표 proofread 화면은 공식 read-only MCP 1회, 3개 PNG envelope 청크에서 144개 node, 변수 20개와 text style 11개를 수집했고 폴백은 없었습니다. instance children, concrete boolean property, mixed typography, nested [1. 이름], token binding과 개별 footer stroke도 Rust snapshot/live contract로 검증했습니다. 같은 파일의 전체 theme export는 legacy 공식 read-only 호출 89개를 통해 collection 1개, variable 49개, style 37개, mode 2개를 수집해 42,794자 devup.json을 생성했으며 diagnostics는 0개였습니다. 현재 full-theme fast collector는 같은 collection/variable/style 전체를 단일 read-only use_figma 호출로 수집하고, envelope 검증 실패 시에만 이 legacy 경로를 0부터 다시 시작하도록 contract test로 고정했습니다.

Snapshot 의미와 현재 한계

Figma Remote MCP에서는 JSON_REST_V1 export가 허용되지 않으므로 host object를 그대로 REST JSON으로 만들 수 없습니다. 대신 checked-in property manifest와 runtime prototype/enumerable 탐색을 함께 사용해 모든 발견한 data field의 key와 읽기 결과를 보존합니다. 함수, 순환 node object는 제외하거나 id로 바꾸고, binary asset은 bytes 대신 metadata로 나타내며, 타 plugin private data와 오류를 내는 getter는 읽을 수 없습니다. 단일 값이 byte budget을 넘으면 key를 없애지 않고 { "$truncated": ..., "byteLength": ... }DEVUP_FIELD_VALUE_TRUNCATED를 남기며, characters, styled segment, resource binding처럼 UI 변환에 필요한 값은 우선 보존합니다.

현재 private MVP의 남은 한계는 다음과 같습니다.

  • 공식 get_metadata의 file-level page 목록은 실제 page 전체보다 적게 반환될 수 있습니다. 이름 검색은 Plugin API page catalog와 per-page projection으로 우회하며 실제 13개 page 파일에서 검증했습니다.
  • 매우 큰 computed field(예: vector fillGeometry)는 현재 값 전체 대신 명시적인 byte-length marker로 보존됩니다. 모든 대용량 field 값을 lossless하게 export하는 기능은 후속 wire-format 개선 대상입니다.
  • exact-node fast envelope가 8 MiB 안전 상한을 넘거나 공식 MCP가 image transport를 바꾸면 자동 legacy fallback이 여러 cursor call을 사용하므로 subtree 크기에 따라 시간이 늘어날 수 있습니다.
  • direct OAuth registration은 Figma MCP Catalog 승인이 없는 client_name으로는 거절됩니다. 승인된 이름(기본값 Codex)으로만 등록이 성립하며, 그 등록은 Figma에게 해당 제품으로 기록됩니다.
  • 사용되지 않은 외부 Figma library 변수 전체는 Remote MCP가 제공하지 않을 수 있습니다.
  • node/page theme scope는 로컬 변수 API의 file-wide 결과를 기반으로 하며 세밀한 사용 범위 필터는 후속 보강 대상입니다.
  • vector, mask, image, absolute layout과 일부 effect는 diagnostics를 포함한 제한적 fallback입니다.
  • Figma Remote MCP의 use_figma tool contract가 바뀌면 live smoke test와 adapter 갱신이 필요합니다.

상세 설계는 docs/superpowers/specs/2026-08-30-figma-remote-mcp-design.md를 참고하세요.