diff --git a/.changepacks/changepack_log_status_summary.json b/.changepacks/changepack_log_status_summary.json new file mode 100644 index 00000000..3ffa8728 --- /dev/null +++ b/.changepacks/changepack_log_status_summary.json @@ -0,0 +1,7 @@ +{ + "changes": { + "crates/devup-mcp/Cargo.toml": "Patch" + }, + "note": "devup_figma_auth status: add a one-line summary naming the active path and its metered cost; document responseLimit semantics and large-response client guidance", + "date": "2026-10-20T00:00:00+09:00" +} diff --git a/README.md b/README.md index a1d009e3..8733c2a7 100644 --- a/README.md +++ b/README.md @@ -1181,6 +1181,14 @@ artifact의 `artifactState`는 `expired`(확인된 TTL 만료), `evicted`(확인 오류의 `details.recovery`는 `mode`(`recollect` | `reproject` | `reread-manifest` | `fix-uri`), `cause`, `reprojectable`, `requiresOriginalUrl`을 줍니다. resource 읽기 실패는 계속 `resource not found`이지만 data에 같은 `recovery`가 실립니다 — artifact가 살아 있는데 output이 없으면 `reproject`, 청크 번호가 범위 밖이면 `chunkCount`·`manifestUri`와 함께 `reread-manifest`입니다. artifact의 capability가 요청한 output을 덮지 못해 거절할 때는 `recovery.missing`이 빠진 것(`referencePng`, `asset-captures` 등)을, `nextAction`이 `artifactId` 대신 원래 url로 다시 부르는 인자를 담습니다. 성공 응답의 `cache.reuse`는 이 artifactId로 Figma 호출 없이 더 투영할 수 있는 output(`withArtifactId`)과 원래 url이 필요한 것(`needsUrl`)을 미리 나열합니다. +### 큰 응답·큰 노드를 다루는 법 (클라이언트 가이드) + +- **`responseLimit`/`responseOffset`은 이미 수집된 결과를 자르는 반환 한도**입니다(`compact.rs`가 수집된 노드 목록에 `skip/take`). Figma를 얼마나 읽는지는 바꾸지 않으므로 수집 비용의 상한이 아닙니다. 매우 큰 노드(`outputs:["outline"]`이 오래 걸리는 경우)는 값을 조절하지 말고 `devup_figma_explore`로 내부 프레임을 찾아 그 프레임만 export하거나, 치수만 필요하면 `devup_figma_measure`를 쓰세요. 오래 걸리는 작업은 `jobAction:"cancel"`로 취소할 수 있습니다. +- **짧은 상태 조회:** 폴링에 `responseFields`를 붙이면 필요한 부분만 받습니다. 예: `{"jobId":"","responseFields":["exportJob.state","exportJob.elapsedMs","exportJob.completedCalls","exportJob.nextAction"]}`. 필드 이름은 실제 응답의 키를 따르므로 먼저 한 번 전체 응답으로 확인하세요. `devup_figma_auth`의 `status`는 최상위 `summary` 한 줄이 지금 쓰는 경로(브리지/metered direct)와 비용을 먼저 말합니다. +- **resource 소비:** `delivery:"resource"`(또는 자동 전환)의 결과는 `devup://artifact/{artifactId}/outputs/{outputId}/manifest`를 먼저 읽고, manifest의 `chunkUris`를 순서대로 읽어 이어 붙입니다. 청크 번호가 범위 밖이면 오류의 `recovery`가 manifest를 다시 읽으라고 안내합니다. +- **문자열/객체 반환 차이:** 도구 결과는 `content`의 JSON 텍스트와 `structuredContent`로 같은 내용이 함께 올 수 있고, `read_mcp_resource`의 반환형은 호스트 어댑터마다 `contents[0].text`(문자열)이거나 이미 파싱된 객체일 수 있습니다. `typeof raw === "string" ? JSON.parse(raw) : raw`처럼 정규화해서 읽으세요. 이 차이는 서버가 아니라 호스트 어댑터 몫일 수 있으며, 서버 버그로 확정된 사례는 없습니다. +- **연결 상태 읽기:** `activePath:"direct"`이면서 `paths.bridge.available:false`는 서버 접속 실패가 아니라 "플러그인이 연결되지 않아 metered direct로 읽는 중"이라는 뜻입니다. `summary`와 `paths.bridge.reason`을 보세요. + ### R9 sourceMap 필드와 resolution TSX 항목의 기본 필드는 `nodeId`, `property`, `generatedProperty`, `resolution`입니다. 선택적 `variableId`는 원본 변수 토큰 참조, `styleId`는 typography/style 토큰 참조, `assetId`는 이미지·벡터 자산 참조를 식별할 때 붙습니다. 한 항목에 여러 식별자가 함께 있을 수 있으며 해당 참조가 없으면 생략합니다. `elementIndex`는 그 항목이 매핑한 JSX 여는 태그의 문서 순서 번호(0부터)이고, `devup_ui_validate`가 위반에 붙이는 번호와 같습니다 — 오프셋 없이 둘을 잇는 키입니다. devup.json 매핑은 `jsonPointer`로 테마 위치를 가리키며 TSX 기본 필드가 모두 있는 것은 아닙니다. diff --git a/crates/devup-mcp/src/server/diagnostics/response_diagnostics.rs b/crates/devup-mcp/src/server/diagnostics/response_diagnostics.rs index 6965ea00..162d2d64 100644 --- a/crates/devup-mcp/src/server/diagnostics/response_diagnostics.rs +++ b/crates/devup-mcp/src/server/diagnostics/response_diagnostics.rs @@ -53,7 +53,20 @@ pub fn connection_report( BridgeRole::Connecting | BridgeRole::Unavailable ) }); + let summary = match active_path { + Some(ActivePath::Bridge) => { + "Reading through the Devup Bridge plugin: no login needed and no Figma allowance spent." + } + Some(ActivePath::Direct) if bridge_blocked => { + "Reading through the metered direct path (Figma allowance is spent). The Devup Bridge plugin is not reachable from this process; this is not a server failure - paths.bridge.reason says why." + } + Some(ActivePath::Direct) => { + "Reading through the metered direct path (Figma allowance is spent). No Devup Bridge plugin is attached; running it on the file avoids the cost." + } + None => "Neither the Devup Bridge plugin nor the direct path can serve a read right now.", + }; json!({ + "summary": summary, "connected": active_path.is_some(), "status": if active_path.is_some() { "connected" } else { "disconnected" }, "activePath": active_path, diff --git a/crates/devup-mcp/src/server/diagnostics/tests.rs b/crates/devup-mcp/src/server/diagnostics/tests.rs index e4f550ae..a97304f2 100644 --- a/crates/devup-mcp/src/server/diagnostics/tests.rs +++ b/crates/devup-mcp/src/server/diagnostics/tests.rs @@ -238,6 +238,16 @@ mod tests { direct_only["nextAction"]["requiredArguments"], json!(["url"]) ); + let summary = direct_only["summary"].as_str().unwrap(); + assert!(summary.contains("metered direct path"), "{summary}"); + assert!(!summary.contains("disconnected"), "{summary}"); + assert!( + bridge_only["summary"] + .as_str() + .unwrap() + .contains("no Figma allowance spent") + ); + let neither = connection_report( AuthStatus::Disconnected, @@ -247,6 +257,11 @@ mod tests { assert_eq!(neither["connected"], false); assert_eq!(neither["status"], "disconnected"); assert!(neither["activePath"].is_null()); + assert!( + neither["summary"] + .as_str() + .is_some_and(|s| s.starts_with("Neither")) + ); let options = neither["nextAction"]["options"].as_array().unwrap(); assert_eq!(options[0]["path"], "bridge"); assert_eq!(options[1]["path"], "direct"); diff --git a/crates/devup-mcp/src/server/text/devup-figma-export.md b/crates/devup-mcp/src/server/text/devup-figma-export.md index af083b20..930d32bf 100644 --- a/crates/devup-mcp/src/server/text/devup-figma-export.md +++ b/crates/devup-mcp/src/server/text/devup-figma-export.md @@ -1,3 +1,4 @@ Export a small Figma selection. outputs:[outline] returns a bounded captured tree with logical layout metrics and responseOffset/responseLimit pagination. responseFields or path use a validated field/index/exact-name-or-id grammar (e.g. nodes[name="Cards"].layout), retaining node identity and linking full omitted evidence as resources. For only explicit node dimensions, prefer devup_figma_measure, which bypasses snapshot collection and code generation; the Figma-to-code entry point. url is optional while exactly one Devup Bridge plugin is attached (devup_figma_auth status shows what is attached): omitted, or given as figma-bridge://current, the export reads the file that plugin has open and the node selected in Figma; frameIds without url name frames in it - one id is that frame, several are screens of the selected Section. Never invent a Figma file key to fill url. Asset requests: recommend 1–3 per call, maximum 6; split larger assetRequests before calling. All fresh exports return exportJob (assetJob compatibility alias) within a one-second initial wait when collection is still running, with per-call frame/root IDs, pagination and elapsed time. Slow asset calls also retain per-asset progress. Poll with jobId; use jobAction=resume when paused. Polls return compact asset rows (status, bytes, sha256, fileState); responseFields on a poll selects from its reply, e.g. exportJob.assets for full rows; assetJob only references exportJob. Generated code uses maskImage only for single-colour icons; multi-colour icons stay . Never convert an icon to a mask or recolour it by hand. Jobs retain accepted reads/bytes across client timeouts for 30 minutes in this server process, not across restart. Identical arguments recover a lost job reply. Completed results are retained for 5 minutes. Recommend 1–3 frames per call; allow at most 6 frames and 12 frame-times-output units. Budget roughly 5–20 seconds per frame-output unit (15–60 seconds per frame for three outputs) as a planning heuristic, not a guarantee: paging, complexity and throttling can exceed it and clients commonly time out at 300 seconds. Oversized selections are refused before screen collection; split frameIds into one-frame calls when isolating latency, or poll jobId. Partial per-frame projection failures retain successful frame outputs. projectionIssues always explains reported approximations, unclassified layout loss and missing generated-property provenance, even without includeDiagnostics. mappingComplete=false identifies mapping gaps; mapping-incomplete is not value loss and cannot be exact. projectionEvidence includes source fields and calculations for generated attributes. outputPathResults lists supported keys and diagnostics; frame file keys are frame::, while outputPaths reports actual committed writes. Resource responses offer nextAction.tool/arguments for same-artifact body comparison and sizeEstimate with explicit unmeasured wire overhead; coupled asset/path arguments remain together. quality.assets grades binary collection; assetSummary.description explains collection state. Merge saved batch responses offline with devup-mcp --merge-asset-batches batch1.json batch2.json for cumulative collection, unrequested, failed and conflict counts. SECTION links use two stages: receive selection_required, then run nextAction.example to export a selected screen. Asked to build pages or a site from Figma, follow the devup-implementation-plan workflow (/devup) even if not asked: it plans the URLs, updates dependencies to the latest, and adds interaction states, transitions and metadata; App Router apps may run on vinext with no next dependency. Building pages: with projectRoot, a page write (pageScaffold write, outputPaths under app/, or several frames with outputPaths) needs a confirmed devup_screen_plan and otherwise answers plan_required; pageScaffold then takes its route from the plan. Ask the plan's questions up front only; after confirm never ask the user, record assumptions instead. Excluded and design-fix frames are refused (skill devup-implementation-plan). The `tsx` this returns is devup-ui code, not plain React: its components are compile-time placeholders, `$token` names an entry in the project's devup.json, and a style prop takes a responsive array. Call devup_skills before you write or edit it - it reports which of those conventions this workspace is missing and installs them where your runtime loads skills from. An agent that skips this does not know it is guessing, and this server cannot see the guesses; the gap is repeated as `skillGap` on the response that carries the code. This output is the design, already read. Do not rewrite it from a screenshot or from `get_design_context` - pictures and visual reasoning verify, they do not author. Do not read the node tree and write devup-ui by hand, and do not infer layout from coordinates. Never guess a colour, spacing, radius or typography value: if a value did not come back, say so and stop, because an invented one is indistinguishable from a real one in the code and is the single failure this server exists to prevent. A failed call is a fact to report, not something to route around. Ask only for what you will read: `tsx` is the deliverable, and the response always carries `status`, `quality`, `cache.artifactId`, `collection` and `source` beside it. `outputs` defaults to `["tsx"]`. Add `devupJson` only when the project has no `devup.json` yet, or when you are introducing tokens it does not define - if it already has one, that file is what the code must match, and `devup_project_context` is what reads it. If you already know the node ids you want - a brief named them, or an earlier call did - pass them straight to `frameIds` and skip `devup_figma_explore`; exploring to rediscover ids you are already holding spends a Figma call for nothing. Explore is for when a Section link is all you have. Each output adds its own keys and nothing else - tsx adds `tsx`; componentTsx adds `componentTsx`; responsiveTsx adds `responsiveTsx`, `responsiveSlots` and, where a width asked for something one tree cannot say, `responsiveUnrepresented`; devupJson adds `devupJson`, `themeCounts`, `themeCompleteness`, `conflicts` and `unresolvedVariables`; sourceMap and assetManifest each add the key they name; referencePng adds `referencePng` and, when the Devup Bridge plugin's picture may hold layers get_screenshot leaves out, `referencePngNote` naming them. `rawSnapshot` and `rawPayload` are the collected design in raw form and need `debug: true`. Use them for one question only - a screen looks wrong and you must decide whether the generator is at fault or the design says so - never to implement, since the tsx already carries what they carry. `fidelity` and `completenessReport` appear only when the result is not exact or complete, or when includeDiagnostics is set. tsx expands every instance into primitives while componentTsx keeps them as references, so requesting both gives the same screen twice and the difference between them is each component's body. responsiveTsx merges every width the capture carries into one module whose differing values are devup-ui responsive arrays, and is produced whenever there is more than one width. `assetManifest` lists the assets and their ids and writes nothing. To get files, call again with `assetRequests`, giving each entry an `outputPath` under an allowed write root - and make that call with the original `url` rather than `artifactId`, because an acquisition made without asset capture cannot serve asset requests. By default manifest `path` and TSX references remain placeholders. Opt in with `assetPublicRoot`, an existing absolute local directory served at URL /, together with assetRequests.outputPath in this call (saved output mappings are not part of artifacts): files below that root map to percent-encoded relative URLs in manifest and all TSX outputs. Identical exported bytes share one canonical path/outputPath; inspect the returned paths. assetNamesPerNode remains true by default; matching names alone never trigger deduplication. Reuse a previous acquisition with `artifactId` from `cache` to project further outputs without calling Figma again. Omitted frameIds/allScreens reuse the stored Section selection; explicit selection overrides it. Debug outputs can use resource delivery in auto mode: read the linked resources or follow nextAction to reproject inline. sourceMap is a node/field/generatedProperty/resolution map without offsets; source.generatedOutput identifies the annotated output. Even when reusing artifactId, sourceMap requires tsx, componentTsx or devupJson in outputs in the same call. Example: {"artifactId":"","outputs":["tsx","rawSnapshot","sourceMap"],"debug":true} An asset request targeting a frame by nodeId defaults to the original IMAGE fill when one is available. This keeps hero headings and buttons live instead of baking them into the background image. Explicit renderMode node retains whole-node capture; its renderInfo warns when child text would be baked into the asset. Exports report captured sections/assets with no generated element as DEVUP_CODEGEN_SECTION_COVERAGE projection issues, including without includeDiagnostics. With projectRoot and Figma node identities enabled, each TSX outputResults entry also returns sectionCoverage: pass this original contract to devup_ui_validate to detect deleted sections and asset references. Hidden subtrees, flattened asset internals and component-reference internals are excluded. sectionCoverage is null when identity attributes are disabled or no projectRoot is supplied; static coverage never proves runtime visibility. +responseLimit and responseOffset page the already-captured result (outline nodes, selected paths); they do not limit how much Figma is read. Collection cost follows the size of the selected node, so for a very large node pick a smaller inner frame (devup_figma_explore) or read only dimensions with devup_figma_measure instead of raising or lowering responseLimit. To poll a job cheaply pass responseFields such as ["exportJob.state","exportJob.elapsedMs","exportJob.completedCalls","exportJob.nextAction"].