figma-spec-mcp 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/figma/client.d.ts +53 -0
- package/dist/figma/client.d.ts.map +1 -0
- package/dist/figma/client.js +193 -0
- package/dist/figma/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/shared.d.ts +9 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +10 -0
- package/dist/shared.js.map +1 -0
- package/dist/tools/analyze-frame.d.ts +9 -0
- package/dist/tools/analyze-frame.d.ts.map +1 -0
- package/dist/tools/analyze-frame.js +117 -0
- package/dist/tools/analyze-frame.js.map +1 -0
- package/dist/tools/audit-accessibility.d.ts +20 -0
- package/dist/tools/audit-accessibility.d.ts.map +1 -0
- package/dist/tools/audit-accessibility.js +186 -0
- package/dist/tools/audit-accessibility.js.map +1 -0
- package/dist/tools/bridge-to-codebase.d.ts +23 -0
- package/dist/tools/bridge-to-codebase.d.ts.map +1 -0
- package/dist/tools/bridge-to-codebase.js +154 -0
- package/dist/tools/bridge-to-codebase.js.map +1 -0
- package/dist/tools/diff-versions.d.ts +23 -0
- package/dist/tools/diff-versions.d.ts.map +1 -0
- package/dist/tools/diff-versions.js +133 -0
- package/dist/tools/diff-versions.js.map +1 -0
- package/dist/tools/export-images.d.ts +26 -0
- package/dist/tools/export-images.d.ts.map +1 -0
- package/dist/tools/export-images.js +53 -0
- package/dist/tools/export-images.js.map +1 -0
- package/dist/tools/extract-design-tokens.d.ts +20 -0
- package/dist/tools/extract-design-tokens.d.ts.map +1 -0
- package/dist/tools/extract-design-tokens.js +230 -0
- package/dist/tools/extract-design-tokens.js.map +1 -0
- package/dist/tools/extract-flows.d.ts +20 -0
- package/dist/tools/extract-flows.d.ts.map +1 -0
- package/dist/tools/extract-flows.js +127 -0
- package/dist/tools/extract-flows.js.map +1 -0
- package/dist/tools/extract-variants.d.ts +20 -0
- package/dist/tools/extract-variants.d.ts.map +1 -0
- package/dist/tools/extract-variants.js +90 -0
- package/dist/tools/extract-variants.js.map +1 -0
- package/dist/tools/inspect-layout.d.ts +26 -0
- package/dist/tools/inspect-layout.d.ts.map +1 -0
- package/dist/tools/inspect-layout.js +271 -0
- package/dist/tools/inspect-layout.js.map +1 -0
- package/dist/tools/map-to-unity.d.ts +26 -0
- package/dist/tools/map-to-unity.d.ts.map +1 -0
- package/dist/tools/map-to-unity.js +173 -0
- package/dist/tools/map-to-unity.js.map +1 -0
- package/dist/tools/register-all.d.ts +12 -0
- package/dist/tools/register-all.d.ts.map +1 -0
- package/dist/tools/register-all.js +13 -0
- package/dist/tools/register-all.js.map +1 -0
- package/dist/tools/registry.d.ts +13 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/resolve-components.d.ts +20 -0
- package/dist/tools/resolve-components.d.ts.map +1 -0
- package/dist/tools/resolve-components.js +97 -0
- package/dist/tools/resolve-components.js.map +1 -0
- package/dist/tools/simplify-context.d.ts +26 -0
- package/dist/tools/simplify-context.d.ts.map +1 -0
- package/dist/tools/simplify-context.js +198 -0
- package/dist/tools/simplify-context.js.map +1 -0
- package/dist/types/figma.d.ts +132 -0
- package/dist/types/figma.d.ts.map +1 -0
- package/dist/types/figma.js +2 -0
- package/dist/types/figma.js.map +1 -0
- package/dist/types/tools.d.ts +397 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +2 -0
- package/dist/types/tools.js.map +1 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zafer Dace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# figma-spec-mcp
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="assets/banner.png" alt="figma-spec-mcp — Bridge Figma to Game Engines" width="720" />
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
**Engineering-grade Figma specs for AI agents.** Layout audit, design tokens, accessibility checks, prototype flows, version diffs, and platform-ready mapping for Unity, React, SwiftUI, and more — all through MCP.
|
|
12
|
+
|
|
13
|
+
Works with **any MCP-compatible client**: Claude Code, Claude Desktop, Cursor, VS Code + Copilot, Windsurf, Cline, Continue.dev, Zed.
|
|
14
|
+
|
|
15
|
+
> **Security note:** Your Figma access token is passed as a tool argument. Never commit it to version control. Use environment variables or your AI client's secret management to supply it at runtime.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
**1. Get a Figma access token**
|
|
22
|
+
→ [figma.com/developers/api#access-tokens](https://www.figma.com/developers/api#access-tokens)
|
|
23
|
+
|
|
24
|
+
**2. Add to your MCP config** (Claude Desktop, Cursor, VS Code, or any MCP client):
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"figma-spec-mcp": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "figma-spec-mcp@beta"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**3. Restart your AI client and use the tools.**
|
|
37
|
+
|
|
38
|
+
Your file key is in the Figma URL: `figma.com/file/<FILE_KEY>/...`
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why figma-spec-mcp?
|
|
43
|
+
|
|
44
|
+
Most Figma MCP tools forward raw API responses. `figma-spec-mcp` adds stable envelopes, focused derivations, and reusable engineering outputs:
|
|
45
|
+
|
|
46
|
+
- Deterministic JSON responses with a shared response envelope
|
|
47
|
+
- Built-in disk cache with freshness metadata on every result
|
|
48
|
+
- Source traceability for tokens, mappings, and extracted relationships
|
|
49
|
+
- Platform-ready outputs for Unity, codebase mapping, and image export workflows
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Tools
|
|
54
|
+
|
|
55
|
+
- `inspect_layout` — Inspects a Figma frame and returns hierarchy, layout structure, spacing, constraints, annotations, and basic accessibility warnings.
|
|
56
|
+
- `extract_design_tokens` — Extracts color, typography, and spacing tokens from a Figma file and exports them as CSS variables, Style Dictionary JSON, or Tailwind config.
|
|
57
|
+
- `map_to_unity` — Produces a Unity UGUI-oriented mapping with RectTransform data, layout groups, suggested components, notes, and warnings.
|
|
58
|
+
- `resolve_components` — Resolves instance nodes to their backing component definitions and returns source file and source node references.
|
|
59
|
+
- `extract_flows` — Extracts prototype transitions from a page or frame and returns directed flow connections plus a deterministic frame order.
|
|
60
|
+
- `bridge_to_codebase` — Scans a local project and maps Figma component names to likely implementation files using filename heuristics.
|
|
61
|
+
- `diff_versions` — Compares two Figma file versions and reports added, removed, and modified nodes.
|
|
62
|
+
- `extract_variants` — Reads a component set and returns structured variant metadata, parsed properties, dimensions, layout details, fills, and typography.
|
|
63
|
+
- `export_images` — Exports one or more Figma nodes as PNG, JPG, SVG, or PDF and returns the image URLs.
|
|
64
|
+
- `audit_accessibility` — Audits a frame for accessibility issues such as contrast, touch targets, font size, missing alt text, and color-only distinctions.
|
|
65
|
+
- `simplify_context` — Produces a token-efficient, LLM-oriented summary tree by collapsing wrappers, grouping repeated nodes, and truncating deep hierarchies.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
### v0.1 — Core
|
|
72
|
+
- `inspect_layout`, `extract_design_tokens`, `map_to_unity`
|
|
73
|
+
- Disk cache with SHA-256 keying and 1h TTL
|
|
74
|
+
|
|
75
|
+
### v0.2 — Intelligence
|
|
76
|
+
- Token name preservation from Figma styles
|
|
77
|
+
- Depth-limited chunking for large files
|
|
78
|
+
- Mixed/rich text runs extraction
|
|
79
|
+
- Annotation extraction, framework-aware hints (Unity, React, SwiftUI, Web)
|
|
80
|
+
|
|
81
|
+
### v0.3 — Workflows
|
|
82
|
+
- `resolve_components` — multi-file component traversal
|
|
83
|
+
- `extract_flows` — prototype flow graph
|
|
84
|
+
- `bridge_to_codebase` — Figma → repo file matching
|
|
85
|
+
- `diff_versions` — structured version diff
|
|
86
|
+
- `extract_variants` — component set batch extraction
|
|
87
|
+
|
|
88
|
+
### v0.4 — Quality & DX
|
|
89
|
+
- `export_images` — PNG/JPG/SVG/PDF export
|
|
90
|
+
- `audit_accessibility` — WCAG 2.1 contrast, touch targets, font size
|
|
91
|
+
- `simplify_context` — AI-optimized, token-efficient output
|
|
92
|
+
- Tool registry pattern for easy contribution
|
|
93
|
+
- Rate limit handling (429 + Retry-After)
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Response Shape
|
|
98
|
+
|
|
99
|
+
All 11 tools return a consistent top-level envelope:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"schema_version": "0.1.0",
|
|
104
|
+
"source": { "file_key": "abc123", "node_id": "1:23" },
|
|
105
|
+
"freshness": {
|
|
106
|
+
"fresh": true,
|
|
107
|
+
"timestamp": "2026-03-26T10:00:00.000Z",
|
|
108
|
+
"ttl_ms": 3600000
|
|
109
|
+
},
|
|
110
|
+
"warnings": [],
|
|
111
|
+
"data": {}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Tool-specific results live in `data`, and most tools also include low-level cache metadata there.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Caching
|
|
120
|
+
|
|
121
|
+
Responses are cached to disk (default: `$TMPDIR/figma-spec-mcp-cache/`) by file key and request shape with a 1-hour TTL. Cache metadata is included in responses:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
"cache": {
|
|
125
|
+
"cachedAt": "2026-03-26T10:00:00.000Z",
|
|
126
|
+
"expiresAt": "2026-03-26T11:00:00.000Z",
|
|
127
|
+
"fileVersion": "123456789",
|
|
128
|
+
"fresh": true
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
git clone https://github.com/zaferdace/figma-spec-mcp
|
|
138
|
+
cd figma-spec-mcp
|
|
139
|
+
npm install
|
|
140
|
+
npm run build
|
|
141
|
+
node dist/index.js
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Roadmap
|
|
147
|
+
|
|
148
|
+
- [ ] Export to React Native StyleSheet
|
|
149
|
+
- [ ] Export to Flutter ThemeData
|
|
150
|
+
- [ ] Semantic component detection (button/card/nav inference)
|
|
151
|
+
- [ ] Webhook-triggered spec generation
|
|
152
|
+
- [ ] Plugin API companion for live document access
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT © Zafer Dace
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { FigmaComponentResponse, FigmaFileResponse, FigmaNode } from "../types/figma.js";
|
|
2
|
+
export interface CacheMetadata {
|
|
3
|
+
cachedAt: string;
|
|
4
|
+
expiresAt: string;
|
|
5
|
+
fileVersion: string;
|
|
6
|
+
fresh: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface CachedResult<T> {
|
|
9
|
+
data: T;
|
|
10
|
+
cache: CacheMetadata;
|
|
11
|
+
}
|
|
12
|
+
export interface FigmaClientOptions {
|
|
13
|
+
ttlMs?: number;
|
|
14
|
+
cacheDir?: string;
|
|
15
|
+
disableCache?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class FigmaRateLimitError extends Error {
|
|
18
|
+
readonly retryAfterMs: number;
|
|
19
|
+
constructor(retryAfterMs: number);
|
|
20
|
+
}
|
|
21
|
+
export declare class FigmaClient {
|
|
22
|
+
private readonly accessToken;
|
|
23
|
+
private readonly ttlMs;
|
|
24
|
+
private readonly cacheDir;
|
|
25
|
+
private readonly disableCache;
|
|
26
|
+
constructor(accessToken: string, options?: FigmaClientOptions);
|
|
27
|
+
private cacheKey;
|
|
28
|
+
private cachePath;
|
|
29
|
+
private getFileCacheKey;
|
|
30
|
+
private getFileNodesCacheKey;
|
|
31
|
+
private getImagesCacheKey;
|
|
32
|
+
private readCache;
|
|
33
|
+
private writeCache;
|
|
34
|
+
invalidateCache(fileKey: string, nodeId?: string): void;
|
|
35
|
+
private buildCacheMetadata;
|
|
36
|
+
private buildFreshMetadata;
|
|
37
|
+
private request;
|
|
38
|
+
private parseRetryAfterMs;
|
|
39
|
+
getFile(fileKey: string, version?: string): Promise<CachedResult<FigmaFileResponse>>;
|
|
40
|
+
getFileNodes(fileKey: string, nodeIds: string[], fileVersion?: string): Promise<CachedResult<{
|
|
41
|
+
nodes: Record<string, {
|
|
42
|
+
document: FigmaNode;
|
|
43
|
+
} | null>;
|
|
44
|
+
}>>;
|
|
45
|
+
getStyles(fileKey: string): Promise<CachedResult<{
|
|
46
|
+
styles: Record<string, unknown>;
|
|
47
|
+
}>>;
|
|
48
|
+
getComponent(componentKey: string): Promise<CachedResult<FigmaComponentResponse>>;
|
|
49
|
+
getImages(fileKey: string, nodeIds: string[], format: "png" | "jpg" | "svg" | "pdf", scale: number): Promise<CachedResult<{
|
|
50
|
+
images: Record<string, string | null>;
|
|
51
|
+
}>>;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/figma/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAY9F,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC;IACR,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,YAAY,EAAE,MAAM;CAKjC;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;gBAE3B,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB;IAWjE,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,SAAS;IAWjB,OAAO,CAAC,UAAU;IAYlB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAWvD,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,kBAAkB;YAUZ,OAAO;IAoBrB,OAAO,CAAC,iBAAiB;IAkBnB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;IAgBpF,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EAAE,EACjB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,YAAY,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,QAAQ,EAAE,SAAS,CAAA;SAAE,GAAG,IAAI,CAAC,CAAA;KAAE,CAAC,CAAC;IAqB7E,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAetF,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC;IAejF,SAAS,CACb,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EAAE,EACjB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,EACrC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAA;KAAE,CAAC,CAAC;CAkBpE"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
const FIGMA_API_BASE = "https://api.figma.com/v1";
|
|
6
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
7
|
+
export class FigmaRateLimitError extends Error {
|
|
8
|
+
retryAfterMs;
|
|
9
|
+
constructor(retryAfterMs) {
|
|
10
|
+
super(`Figma API rate limited. Retry after ${retryAfterMs}ms`);
|
|
11
|
+
this.name = "FigmaRateLimitError";
|
|
12
|
+
this.retryAfterMs = retryAfterMs;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class FigmaClient {
|
|
16
|
+
accessToken;
|
|
17
|
+
ttlMs;
|
|
18
|
+
cacheDir;
|
|
19
|
+
disableCache;
|
|
20
|
+
constructor(accessToken, options = {}) {
|
|
21
|
+
this.accessToken = accessToken;
|
|
22
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
23
|
+
this.cacheDir = options.cacheDir ?? join(tmpdir(), "figma-spec-cache");
|
|
24
|
+
this.disableCache = options.disableCache ?? false;
|
|
25
|
+
if (!this.disableCache) {
|
|
26
|
+
mkdirSync(this.cacheDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
cacheKey(parts) {
|
|
30
|
+
return createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 16);
|
|
31
|
+
}
|
|
32
|
+
cachePath(key) {
|
|
33
|
+
return join(this.cacheDir, `${key}.json`);
|
|
34
|
+
}
|
|
35
|
+
getFileCacheKey(fileKey, version) {
|
|
36
|
+
return this.cacheKey([fileKey, "file", version ?? "latest"]);
|
|
37
|
+
}
|
|
38
|
+
getFileNodesCacheKey(fileKey, nodeIds, fileVersion) {
|
|
39
|
+
const sortedIds = [...nodeIds].sort();
|
|
40
|
+
return this.cacheKey([fileKey, ...sortedIds, fileVersion ?? "latest"]);
|
|
41
|
+
}
|
|
42
|
+
getImagesCacheKey(fileKey, nodeIds, format, scale) {
|
|
43
|
+
const sortedIds = [...nodeIds].sort();
|
|
44
|
+
return this.cacheKey([fileKey, "images", format, String(scale), ...sortedIds]);
|
|
45
|
+
}
|
|
46
|
+
readCache(key) {
|
|
47
|
+
if (this.disableCache)
|
|
48
|
+
return null;
|
|
49
|
+
const path = this.cachePath(key);
|
|
50
|
+
if (!existsSync(path))
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
writeCache(key, data, fileVersion) {
|
|
60
|
+
if (this.disableCache)
|
|
61
|
+
return;
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const entry = {
|
|
64
|
+
data,
|
|
65
|
+
cachedAt: now,
|
|
66
|
+
expiresAt: now + this.ttlMs,
|
|
67
|
+
fileVersion,
|
|
68
|
+
};
|
|
69
|
+
writeFileSync(this.cachePath(key), JSON.stringify(entry));
|
|
70
|
+
}
|
|
71
|
+
invalidateCache(fileKey, nodeId) {
|
|
72
|
+
if (this.disableCache)
|
|
73
|
+
return;
|
|
74
|
+
const key = nodeId ? this.getFileNodesCacheKey(fileKey, [nodeId]) : this.getFileCacheKey(fileKey);
|
|
75
|
+
const path = this.cachePath(key);
|
|
76
|
+
if (existsSync(path)) {
|
|
77
|
+
unlinkSync(path);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
buildCacheMetadata(entry) {
|
|
81
|
+
return {
|
|
82
|
+
cachedAt: new Date(entry.cachedAt).toISOString(),
|
|
83
|
+
expiresAt: new Date(entry.expiresAt).toISOString(),
|
|
84
|
+
fileVersion: entry.fileVersion,
|
|
85
|
+
fresh: Date.now() < entry.expiresAt,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
buildFreshMetadata(fileVersion) {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
return {
|
|
91
|
+
cachedAt: new Date(now).toISOString(),
|
|
92
|
+
expiresAt: new Date(now + this.ttlMs).toISOString(),
|
|
93
|
+
fileVersion,
|
|
94
|
+
fresh: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async request(path) {
|
|
98
|
+
const response = await fetch(`${FIGMA_API_BASE}${path}`, {
|
|
99
|
+
headers: { "X-Figma-Token": this.accessToken },
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
if (response.status === 429) {
|
|
103
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
104
|
+
const retryAfterMs = this.parseRetryAfterMs(retryAfterHeader);
|
|
105
|
+
throw new FigmaRateLimitError(retryAfterMs);
|
|
106
|
+
}
|
|
107
|
+
const body = await response.text();
|
|
108
|
+
const sanitized = body.replace(/figd_[A-Za-z0-9_-]+/g, "[REDACTED]");
|
|
109
|
+
throw new Error(`Figma API error ${response.status}: ${sanitized}`);
|
|
110
|
+
}
|
|
111
|
+
return response.json();
|
|
112
|
+
}
|
|
113
|
+
parseRetryAfterMs(retryAfterHeader) {
|
|
114
|
+
if (!retryAfterHeader) {
|
|
115
|
+
return 1000;
|
|
116
|
+
}
|
|
117
|
+
const seconds = Number(retryAfterHeader);
|
|
118
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
119
|
+
return seconds * 1000;
|
|
120
|
+
}
|
|
121
|
+
const retryAt = Date.parse(retryAfterHeader);
|
|
122
|
+
if (!Number.isNaN(retryAt)) {
|
|
123
|
+
return Math.max(retryAt - Date.now(), 0);
|
|
124
|
+
}
|
|
125
|
+
return 1000;
|
|
126
|
+
}
|
|
127
|
+
async getFile(fileKey, version) {
|
|
128
|
+
const key = this.getFileCacheKey(fileKey, version);
|
|
129
|
+
const cached = this.readCache(key);
|
|
130
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
131
|
+
return { data: cached.data, cache: this.buildCacheMetadata(cached) };
|
|
132
|
+
}
|
|
133
|
+
const params = version ? `?version=${encodeURIComponent(version)}` : "";
|
|
134
|
+
const data = await this.request(`/files/${fileKey}${params}`);
|
|
135
|
+
this.writeCache(key, data, data.version);
|
|
136
|
+
const entry = this.readCache(key);
|
|
137
|
+
const cache = entry ? this.buildCacheMetadata(entry) : this.buildFreshMetadata(data.version);
|
|
138
|
+
return { data, cache };
|
|
139
|
+
}
|
|
140
|
+
async getFileNodes(fileKey, nodeIds, fileVersion) {
|
|
141
|
+
const key = this.getFileNodesCacheKey(fileKey, nodeIds, fileVersion);
|
|
142
|
+
const cached = this.readCache(key);
|
|
143
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
144
|
+
return { data: cached.data, cache: this.buildCacheMetadata(cached) };
|
|
145
|
+
}
|
|
146
|
+
const ids = nodeIds.join(",");
|
|
147
|
+
const versionParam = fileVersion ? `&version=${encodeURIComponent(fileVersion)}` : "";
|
|
148
|
+
const data = await this.request(`/files/${fileKey}/nodes?ids=${encodeURIComponent(ids)}${versionParam}`);
|
|
149
|
+
const version = fileVersion ?? "unknown";
|
|
150
|
+
this.writeCache(key, data, version);
|
|
151
|
+
const entry = this.readCache(key);
|
|
152
|
+
const cache = entry ? this.buildCacheMetadata(entry) : this.buildFreshMetadata(version);
|
|
153
|
+
return { data, cache };
|
|
154
|
+
}
|
|
155
|
+
async getStyles(fileKey) {
|
|
156
|
+
const key = this.cacheKey([fileKey, "styles"]);
|
|
157
|
+
const cached = this.readCache(key);
|
|
158
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
159
|
+
return { data: cached.data, cache: this.buildCacheMetadata(cached) };
|
|
160
|
+
}
|
|
161
|
+
const data = await this.request(`/files/${fileKey}/styles`);
|
|
162
|
+
this.writeCache(key, data, "unknown");
|
|
163
|
+
const entry = this.readCache(key);
|
|
164
|
+
const cache = entry ? this.buildCacheMetadata(entry) : this.buildFreshMetadata("unknown");
|
|
165
|
+
return { data, cache };
|
|
166
|
+
}
|
|
167
|
+
async getComponent(componentKey) {
|
|
168
|
+
const key = this.cacheKey(["component", componentKey]);
|
|
169
|
+
const cached = this.readCache(key);
|
|
170
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
171
|
+
return { data: cached.data, cache: this.buildCacheMetadata(cached) };
|
|
172
|
+
}
|
|
173
|
+
const data = await this.request(`/components/${componentKey}`);
|
|
174
|
+
this.writeCache(key, data, "unknown");
|
|
175
|
+
const entry = this.readCache(key);
|
|
176
|
+
const cache = entry ? this.buildCacheMetadata(entry) : this.buildFreshMetadata("unknown");
|
|
177
|
+
return { data, cache };
|
|
178
|
+
}
|
|
179
|
+
async getImages(fileKey, nodeIds, format, scale) {
|
|
180
|
+
const key = this.getImagesCacheKey(fileKey, nodeIds, format, scale);
|
|
181
|
+
const cached = this.readCache(key);
|
|
182
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
183
|
+
return { data: cached.data, cache: this.buildCacheMetadata(cached) };
|
|
184
|
+
}
|
|
185
|
+
const ids = nodeIds.join(",");
|
|
186
|
+
const data = await this.request(`/images/${fileKey}?ids=${encodeURIComponent(ids)}&format=${encodeURIComponent(format)}&scale=${encodeURIComponent(String(scale))}`);
|
|
187
|
+
this.writeCache(key, data, "unknown");
|
|
188
|
+
const entry = this.readCache(key);
|
|
189
|
+
const cache = entry ? this.buildCacheMetadata(entry) : this.buildFreshMetadata("unknown");
|
|
190
|
+
return { data, cache };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/figma/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAMA,OAAO,yBAAyB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { SERVER_VERSION } from "./shared.js";
|
|
6
|
+
import "./tools/register-all.js";
|
|
7
|
+
import { executeTool, getToolDefinitions } from "./tools/registry.js";
|
|
8
|
+
const server = new Server({ name: "figma-spec-mcp", version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
9
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
10
|
+
const tools = getToolDefinitions();
|
|
11
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
12
|
+
const { name, arguments: args } = request.params;
|
|
13
|
+
try {
|
|
14
|
+
const result = await executeTool(name, args);
|
|
15
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof Error && error.name === "ZodError") {
|
|
19
|
+
throw new Error(`Invalid input: ${error.message}`, { cause: error });
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
async function main() {
|
|
25
|
+
const transport = new StdioServerTransport();
|
|
26
|
+
await server.connect(transport);
|
|
27
|
+
}
|
|
28
|
+
main().catch((error) => {
|
|
29
|
+
console.error("Fatal error:", error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AACnG,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,yBAAyB,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEtE,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAEhH,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAC1E,MAAM,KAAK,GAAG,kBAAkB,EAAE,CAAC;AAEnC,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IAChE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAEjD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC7C,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAChF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACxD,MAAM,IAAI,KAAK,CAAC,kBAAkB,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CacheMetadata } from "./figma/client.js";
|
|
2
|
+
export declare const SCHEMA_VERSION: "0.1.0";
|
|
3
|
+
export declare const SERVER_VERSION = "1.0.0-beta.1";
|
|
4
|
+
export declare function buildFreshness(cache: CacheMetadata): {
|
|
5
|
+
fresh: boolean;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
ttl_ms: number;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,eAAO,MAAM,cAAc,EAAG,OAAgB,CAAC;AAC/C,eAAO,MAAM,cAAc,iBAAiB,CAAC;AAE7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAM1G"}
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const SCHEMA_VERSION = "0.1.0";
|
|
2
|
+
export const SERVER_VERSION = "1.0.0-beta.1";
|
|
3
|
+
export function buildFreshness(cache) {
|
|
4
|
+
return {
|
|
5
|
+
fresh: cache.fresh,
|
|
6
|
+
timestamp: cache.cachedAt,
|
|
7
|
+
ttl_ms: new Date(cache.expiresAt).getTime() - new Date(cache.cachedAt).getTime(),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAG,OAAgB,CAAC;AAC/C,MAAM,CAAC,MAAM,cAAc,GAAG,cAAc,CAAC;AAE7C,MAAM,UAAU,cAAc,CAAC,KAAoB;IACjD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,SAAS,EAAE,KAAK,CAAC,QAAQ;QACzB,MAAM,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE;KACjF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { AnalyzeFrameInput, AnalyzeFrameResult } from "../types/tools.js";
|
|
3
|
+
export declare const analyzeFrameSchema: z.ZodObject<{
|
|
4
|
+
file_key: z.ZodString;
|
|
5
|
+
node_id: z.ZodString;
|
|
6
|
+
access_token: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export declare function analyzeFrame(input: AnalyzeFrameInput): Promise<AnalyzeFrameResult>;
|
|
9
|
+
//# sourceMappingURL=analyze-frame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-frame.d.ts","sourceRoot":"","sources":["../../src/tools/analyze-frame.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAKnB,MAAM,mBAAmB,CAAC;AAE3B,eAAO,MAAM,kBAAkB;;;;iBAI7B,CAAC;AAmFH,wBAAsB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA0CxF"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FigmaClient } from "../figma/client.js";
|
|
3
|
+
export const analyzeFrameSchema = z.object({
|
|
4
|
+
file_key: z.string().describe("The Figma file key (from the file URL)"),
|
|
5
|
+
node_id: z.string().describe("The node ID of the frame to analyze"),
|
|
6
|
+
access_token: z.string().describe("Your Figma personal access token"),
|
|
7
|
+
});
|
|
8
|
+
function collectComponents(node, results) {
|
|
9
|
+
const isComponent = node.type === "COMPONENT" || node.type === "COMPONENT_SET";
|
|
10
|
+
const isInstance = node.type === "INSTANCE";
|
|
11
|
+
if (isComponent || isInstance) {
|
|
12
|
+
results.push({
|
|
13
|
+
id: node.id,
|
|
14
|
+
name: node.name,
|
|
15
|
+
type: node.type,
|
|
16
|
+
isComponent,
|
|
17
|
+
isInstance,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
node.children?.forEach((child) => collectComponents(child, results));
|
|
21
|
+
}
|
|
22
|
+
function collectLayouts(node, results) {
|
|
23
|
+
if (node.layoutMode && node.layoutMode !== "NONE") {
|
|
24
|
+
results[node.id] = {
|
|
25
|
+
mode: node.layoutMode === "HORIZONTAL" ? "horizontal" : "vertical",
|
|
26
|
+
primaryAxisAlign: node.primaryAxisAlignItems ?? "MIN",
|
|
27
|
+
counterAxisAlign: node.counterAxisAlignItems ?? "MIN",
|
|
28
|
+
padding: {
|
|
29
|
+
top: node.paddingTop ?? 0,
|
|
30
|
+
right: node.paddingRight ?? 0,
|
|
31
|
+
bottom: node.paddingBottom ?? 0,
|
|
32
|
+
left: node.paddingLeft ?? 0,
|
|
33
|
+
},
|
|
34
|
+
gap: node.itemSpacing ?? 0,
|
|
35
|
+
sizing: {
|
|
36
|
+
width: node.primaryAxisSizingMode === "AUTO" ? "hug" : "fixed",
|
|
37
|
+
height: node.counterAxisSizingMode === "AUTO" ? "hug" : "fixed",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
node.children?.forEach((child) => collectLayouts(child, results));
|
|
42
|
+
}
|
|
43
|
+
function collectConstraints(node, results) {
|
|
44
|
+
if (node.constraints && node.absoluteBoundingBox) {
|
|
45
|
+
results[node.id] = {
|
|
46
|
+
horizontal: node.constraints.horizontal,
|
|
47
|
+
vertical: node.constraints.vertical,
|
|
48
|
+
bounds: node.absoluteBoundingBox,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
node.children?.forEach((child) => collectConstraints(child, results));
|
|
52
|
+
}
|
|
53
|
+
function collectAccessibilityWarnings(node, warnings) {
|
|
54
|
+
if (node.type === "TEXT" && node.style) {
|
|
55
|
+
if (node.style.fontSize < 12) {
|
|
56
|
+
warnings.push({
|
|
57
|
+
nodeId: node.id,
|
|
58
|
+
nodeName: node.name,
|
|
59
|
+
severity: "warning",
|
|
60
|
+
message: `Text node "${node.name}" has a font size of ${node.style.fontSize}px, which may be too small for accessibility.`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (node.fills && node.fills.length === 0 && node.type === "FRAME") {
|
|
65
|
+
warnings.push({
|
|
66
|
+
nodeId: node.id,
|
|
67
|
+
nodeName: node.name,
|
|
68
|
+
severity: "info",
|
|
69
|
+
message: `Frame "${node.name}" has no background fill — ensure this is intentional.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
node.children?.forEach((child) => collectAccessibilityWarnings(child, warnings));
|
|
73
|
+
}
|
|
74
|
+
function countNodesByType(node, counts) {
|
|
75
|
+
counts[node.type] = (counts[node.type] ?? 0) + 1;
|
|
76
|
+
node.children?.forEach((child) => countNodesByType(child, counts));
|
|
77
|
+
}
|
|
78
|
+
export async function analyzeFrame(input) {
|
|
79
|
+
const client = new FigmaClient(input.access_token);
|
|
80
|
+
const normalizedId = input.node_id.replace("-", ":");
|
|
81
|
+
const response = await client.getFileNodes(input.file_key, [normalizedId]);
|
|
82
|
+
const nodeData = response.nodes[normalizedId];
|
|
83
|
+
if (!nodeData) {
|
|
84
|
+
throw new Error(`Node "${input.node_id}" not found in file "${input.file_key}"`);
|
|
85
|
+
}
|
|
86
|
+
const frame = nodeData.document;
|
|
87
|
+
const components = [];
|
|
88
|
+
const layouts = {};
|
|
89
|
+
const constraints = {};
|
|
90
|
+
const accessibilityWarnings = [];
|
|
91
|
+
const typeCounts = {};
|
|
92
|
+
collectComponents(frame, components);
|
|
93
|
+
collectLayouts(frame, layouts);
|
|
94
|
+
collectConstraints(frame, constraints);
|
|
95
|
+
collectAccessibilityWarnings(frame, accessibilityWarnings);
|
|
96
|
+
countNodesByType(frame, typeCounts);
|
|
97
|
+
return {
|
|
98
|
+
frameId: frame.id,
|
|
99
|
+
frameName: frame.name,
|
|
100
|
+
dimensions: {
|
|
101
|
+
width: frame.absoluteBoundingBox?.width ?? 0,
|
|
102
|
+
height: frame.absoluteBoundingBox?.height ?? 0,
|
|
103
|
+
},
|
|
104
|
+
components,
|
|
105
|
+
layouts,
|
|
106
|
+
constraints,
|
|
107
|
+
accessibilityWarnings,
|
|
108
|
+
stats: {
|
|
109
|
+
totalNodes: Object.values(typeCounts).reduce((a, b) => a + b, 0),
|
|
110
|
+
componentCount: components.filter((c) => c.isComponent).length,
|
|
111
|
+
instanceCount: components.filter((c) => c.isInstance).length,
|
|
112
|
+
textNodeCount: typeCounts["TEXT"] ?? 0,
|
|
113
|
+
imageNodeCount: typeCounts["RECTANGLE"] ?? 0,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=analyze-frame.js.map
|