Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_figma_bridge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"crates/devup-mcp-figma/Cargo.toml": "Minor",
"crates/devup-mcp/Cargo.toml": "Minor"
},
"note": "Read a design without spending the quota. Collection runs through the official MCP's use_figma, and that is the call Figma meters, so a handful of screens exhausts the allowance; every script devup-mcp sends uses only the standard Plugin API, so a plugin of our own can serve them. A plugin cannot accept connections, so devup-mcp listens on 127.0.0.1 and the plugin dials in, and a read is routed before the call by asking whether the bridge can serve it rather than by catching a failure, because CapabilityUnavailable and a transport fault arrive as the same error code and a failure cannot tell them apart. No code crosses the socket: the twelve scripts are wrapped into the plugin at build time from the same sources the remote path uses, the build fails on an unsubstituted placeholder, and devup-mcp sends only the name of the script to run. With no plugin attached every call falls through to the remote path unchanged, and DEVUP_FIGMA_BRIDGE_PORT=off closes the door entirely. Three things only a real install revealed are now handled: ws://127.0.0.1 invalidates the whole manifest so the address must be localhost, a plugin that reports no file key registers and serves any key while it is the only one attached rather than leaving the window claiming a connection it never uses, and a fileKey permission entry stops the plugin loading at all. Speed followed: pagination sized for a 20,480-byte truncation collapses to one page over a local socket, the transport now says how many resources one question carries instead of a constant, and a used-resource call that took 21.4 seconds regardless of load turned out to be waiting out a fixed timeout on every individual variable lookup and returning null, which named every bound token after the tail of its id; the file's own variables are read once and indexed, leaving individual lookups only for genuine library ids, and that call now answers in 24 milliseconds with nothing unresolved. Asset export still fails with the same fixed-delay signature and is not fixed here.",
"date": "2026-09-13T23:40:00+09:00"
}
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ jobs:
- run: cargo install cargo-insta --version 1.48.0 --locked
- run: cargo fmt --all -- --check
- run: node --test crates/devup-mcp-figma/tests/explore_script_behavior.mjs
# `plugin/dist/` is committed so a user can import the plugin into Figma
# without running a build. That only stays true if the committed bundle
# is the one this source produces, so rebuild it and refuse a difference.
# The build is reproducible: same input, byte-identical output.
- name: The committed plugin bundle matches its source
if: matrix.os == 'ubuntu-latest'
shell: bash
working-directory: plugin
run: |
npm ci
npm run build
git diff --exit-code -- dist || {
echo "::error::plugin/dist is stale. Run 'npm run build' in plugin/ and commit the result."
exit 1
}
- run: cargo test --locked -p devup-mcp --test stdio_smoke
- run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
- run: cargo insta test --workspace --all-features --check
Expand Down
151 changes: 149 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,57 @@ Figma MCP Catalog에 승인된 client(예: 직접 waitlist로 등록해 발급
devup-mcp가 Figma에 붙는 경로는 하나입니다 — **원격 OAuth (`direct`)**. `devup_figma_auth { action: "login" }`으로 브라우저 인증. Figma MCP Catalog에 승인된 client만 등록할 수 있습니다.
현재 사용 가능한지는 `devup_figma_auth { action: "doctor" }`로 확인하세요.

### 브리지 플러그인 — 한도를 쓰지 않고 읽기

원격 OAuth 경로는 Figma가 **사용량을 셉니다.** devup-mcp의 수집은 화면 하나에 snapshot을 여러 번 부르므로 한도가 금방 바닥납니다.

그래서 **우리가 직접 만든 Figma 플러그인**을 통해 같은 읽기를 할 수 있습니다. 이 경로는 한도를 쓰지 않습니다. 플러그인이 붙어 있으면 스크립트 읽기가 그쪽으로 가고, **안 붙어 있으면 아무 일도 일어나지 않고 그대로 원격 경로로** 갑니다. 설치하지 않은 사람의 동작은 바뀌지 않습니다.

#### 설치 — 빌드할 필요 없습니다

빌드된 플러그인이 저장소에 들어 있습니다(`plugin/dist/`). `npm`도 Node도 필요 없고, 저장소를 받아 두었다면 바로 다음 단계로 갑니다.

**1. Figma 데스크톱 앱에서 불러옵니다**

Figma의 플러그인 등록은 데스크톱 앱의 메뉴 동작이라 스크립트로 대신할 수 없습니다. 이 세 단계만 직접 하시면 됩니다.

1. **Figma 데스크톱 앱**을 엽니다 (브라우저판은 로컬 플러그인을 못 불러옵니다)
2. 메뉴에서 **Plugins → Development → Import plugin from manifest...**
3. 이 저장소의 **`plugin/manifest.json`** 을 선택합니다

한 번만 하면 됩니다. 이후에는 플러그인 목록에 `Devup Bridge`로 남습니다.

> 커밋된 번들이 소스와 어긋나지 않도록 CI가 매번 다시 빌드해 byte 단위로 대조합니다. 플러그인 소스를 고쳤다면 `cd plugin && npm install && npm run build` 후 `dist/`를 함께 커밋해야 합니다.

**2. 읽으려는 파일에서 플러그인을 실행합니다**

Figma에서 대상 파일을 열고 `Devup Bridge`를 실행하면 창이 하나 뜹니다. **그 창이 열려 있는 동안만** 브리지가 동작합니다. 닫으면 다음 읽기부터 원격 경로로 돌아갑니다.

#### 잘 되고 있는지 확인

플러그인 창의 표시등으로 판단합니다.

| 표시 | 뜻 |
|---|---|
| 초록 | devup-mcp에 연결됨. 이 파일의 읽기는 한도를 쓰지 않습니다 |
| 회색 | devup-mcp를 찾지 못함. 2초마다 다시 시도합니다 |

회색에서 안 바뀌면 devup-mcp가 실행 중인지, 그리고 `DEVUP_FIGMA_BRIDGE_PORT=off`로 꺼 두지 않았는지 확인하세요.

#### 알아 두어야 할 것

- **기본으로 켜져 있습니다.** devup-mcp는 시작할 때 `127.0.0.1:1993`에 대기하고, 그 포트를 잡지 못하면 조용히 원격 경로만 씁니다. 끄려면 `DEVUP_FIGMA_BRIDGE_PORT=off`.
- **읽기 전용입니다.** 플러그인이 실행하는 스크립트는 **빌드 시점에 플러그인 안에 박혀 있고**, devup-mcp는 그중 어느 것을 실행할지 **이름만** 보냅니다. 소켓으로 코드가 오가지 않으며 Figma 문서를 바꾸는 호출은 존재하지 않습니다.
- **이 기기에서만 됩니다.** 대기 주소는 `127.0.0.1`이라 다른 기기에서는 붙을 수 없고, 원격 CI에서는 브리지가 없으니 자동으로 원격 경로를 씁니다.
- **파일은 한 번에 하나입니다.** 플러그인이 자기 파일 키를 보고하지 못하는 경우가 있어(Dev Mode 등), 키 없는 플러그인은 **혼자 붙어 있을 때만** 읽기를 받습니다. 두 개 이상이면 어느 파일인지 알 수 없으므로 원격 경로로 넘어갑니다.
- **포트를 바꾸려면 세 곳을 같이 고쳐야 합니다.** Figma는 플러그인이 접속할 수 있는 주소를 manifest에 미리 적어 두게 하고 실행 중에는 바꿀 수 없습니다. `manifest.json`의 `allowedDomains`, `src/code.ts`의 `PORT`, 그리고 환경 변수를 모두 고친 뒤 플러그인을 다시 빌드·설치해야 합니다. 하나만 바꾸면 **아무 말 없이 원격 경로로 돌아가** 한도를 그대로 씁니다.
- **`ws://127.0.0.1`은 쓸 수 없습니다.** Figma의 `allowedDomains`가 그 주소를 거부해 manifest 전체가 무효가 되고 플러그인이 아예 로드되지 않습니다. `localhost`여야 합니다.
- **Figma 창을 뒤로 보내면 느려집니다.** 브라우저가 백그라운드 플러그인을 분당 한 번꼴로 깨우기 때문에, 앞에서 160ms인 snapshot이 뒤에서는 11초가 됩니다. 수집 중에는 Figma를 앞에 두세요.

자세한 내부 구조와 스크립트 생성 방식은 [`plugin/README.md`](plugin/README.md)에 있습니다.

### 로컬 Dev Mode MCP를 쓰지 않는 이유

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 등록 제약 (실측)
Expand Down
7 changes: 6 additions & 1 deletion crates/devup-mcp-figma/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ repository.workspace = true

[dependencies]
async-trait.workspace = true
axum.workspace = true
# `ws` 는 기본 기능이 아니다. 브리지가 플러그인을 받는 데 쓴다.
axum = { workspace = true, features = ["ws"] }
base64.workspace = true
image.workspace = true
keyring.workspace = true
Expand All @@ -29,3 +30,7 @@ webbrowser.workspace = true
[dev-dependencies]
anyhow.workspace = true
tempfile = "3"
# 브리지 서버에 붙는 가짜 플러그인 역할. axum 의 `ws` 가 이미 끌어오는 것과 같은
# 구현이라 의존성 트리가 늘지 않는다.
tokio-tungstenite = "0.28"
futures-util = "0.3"
Loading