@mp3wizard/figma-console-mcp 1.22.5 β†’ 1.23.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.
Files changed (51) hide show
  1. package/README.md +15 -38
  2. package/dist/cloudflare/core/cloud-websocket-connector.js +19 -17
  3. package/dist/cloudflare/core/design-code-tools.js +23 -39
  4. package/dist/cloudflare/core/diff/changelog-formatter.js +275 -0
  5. package/dist/cloudflare/core/diff/diff-engine.js +334 -0
  6. package/dist/cloudflare/core/diff/property-compare.js +36 -0
  7. package/dist/cloudflare/core/diff/version-cache.js +74 -0
  8. package/dist/cloudflare/core/figma-api.js +19 -0
  9. package/dist/cloudflare/core/figma-tools.js +15 -6
  10. package/dist/cloudflare/core/version-tools.js +1014 -0
  11. package/dist/cloudflare/core/websocket-connector.js +24 -18
  12. package/dist/cloudflare/index.js +17 -13
  13. package/dist/core/design-code-tools.d.ts +1 -12
  14. package/dist/core/design-code-tools.d.ts.map +1 -1
  15. package/dist/core/design-code-tools.js +23 -39
  16. package/dist/core/design-code-tools.js.map +1 -1
  17. package/dist/core/diff/changelog-formatter.d.ts +35 -0
  18. package/dist/core/diff/changelog-formatter.d.ts.map +1 -0
  19. package/dist/core/diff/changelog-formatter.js +276 -0
  20. package/dist/core/diff/changelog-formatter.js.map +1 -0
  21. package/dist/core/diff/diff-engine.d.ts +113 -0
  22. package/dist/core/diff/diff-engine.d.ts.map +1 -0
  23. package/dist/core/diff/diff-engine.js +335 -0
  24. package/dist/core/diff/diff-engine.js.map +1 -0
  25. package/dist/core/diff/property-compare.d.ts +19 -0
  26. package/dist/core/diff/property-compare.d.ts.map +1 -0
  27. package/dist/core/diff/property-compare.js +37 -0
  28. package/dist/core/diff/property-compare.js.map +1 -0
  29. package/dist/core/diff/version-cache.d.ts +40 -0
  30. package/dist/core/diff/version-cache.d.ts.map +1 -0
  31. package/dist/core/diff/version-cache.js +75 -0
  32. package/dist/core/diff/version-cache.js.map +1 -0
  33. package/dist/core/figma-api.d.ts +29 -0
  34. package/dist/core/figma-api.d.ts.map +1 -1
  35. package/dist/core/figma-api.js +19 -0
  36. package/dist/core/figma-api.js.map +1 -1
  37. package/dist/core/figma-tools.d.ts.map +1 -1
  38. package/dist/core/figma-tools.js +15 -6
  39. package/dist/core/figma-tools.js.map +1 -1
  40. package/dist/core/version-tools.d.ts +30 -0
  41. package/dist/core/version-tools.d.ts.map +1 -0
  42. package/dist/core/version-tools.js +1015 -0
  43. package/dist/core/version-tools.js.map +1 -0
  44. package/dist/core/websocket-connector.d.ts.map +1 -1
  45. package/dist/core/websocket-connector.js +24 -18
  46. package/dist/core/websocket-connector.js.map +1 -1
  47. package/dist/local.d.ts.map +1 -1
  48. package/dist/local.js +8 -0
  49. package/dist/local.js.map +1 -1
  50. package/figma-desktop-bridge/code.js +1 -1
  51. package/package.json +108 -1
package/README.md CHANGED
@@ -1,17 +1,14 @@
1
1
  # Figma Console MCP Server
2
2
 
3
3
  [![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io/)
4
- [![npm](https://img.shields.io/npm/v/@mp3wizard/figma-console-mcp)](https://www.npmjs.com/package/@mp3wizard/figma-console-mcp)
4
+ [![npm](https://img.shields.io/npm/v/figma-console-mcp)](https://www.npmjs.com/package/figma-console-mcp)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![Security Reviewed](https://img.shields.io/badge/Security-Reviewed-brightgreen)](Security%20review%20report/)
7
6
  [![Documentation](https://img.shields.io/badge/docs-docs.figma--console--mcp.southleft.com-0D9488)](https://docs.figma-console-mcp.southleft.com)
8
7
  [![Sponsor](https://img.shields.io/badge/Sponsor-southleft-ea4aaa?logo=github-sponsors&logoColor=white)](https://github.com/sponsors/southleft)
9
8
 
10
9
  > **Your design system as an API.** Model Context Protocol server that bridges design and developmentβ€”giving AI assistants complete access to Figma for **extraction**, **creation**, and **debugging**.
11
10
 
12
- > **πŸ”’ Security Reviewed Fork:** This fork (`@mp3wizard/figma-console-mcp`) has passed a full security review following OWASP Top 10 and CWE standards, including automated scanning (Semgrep, Trivy, TruffleHog) and manual vulnerability analysis. Review reports are available in the [`Security review report/`](Security%20review%20report/) folder.
13
-
14
- > **πŸ†• High-Fidelity Design-to-Code:** Deep component trees (depth 4), resolved design tokens, interaction state machines with CSS mappings, and codebase-aware component scanning. AI gets everything a senior engineer needs β€” tokens, sizing, states, annotations, and a cross-reference of what already exists in your codebase. [See what's new β†’](docs/figma-mcp-vs-figma-console-mcp.md)
11
+ > **πŸ†• Version History & Time-Series Awareness (v1.23.0):** Six new tools turn a Figma file from a static snapshot into a queryable history β€” list versions, snapshot any past version, diff two versions for component/binding deltas, generate markdown changelogs ready for release notes, and trace exactly when (and by whom) a property or variant was introduced via a binary-search blame walker. Author attribution flows from autosaves, not just labeled releases. [See what's new β†’](CHANGELOG.md#1230---2026-05-09)
15
12
 
16
13
  ## What is this?
17
14
 
@@ -54,9 +51,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
54
51
  | Real-time monitoring (console, selection) | βœ… | ❌ | ❌ |
55
52
  | Desktop Bridge plugin | βœ… | βœ… | ❌ |
56
53
  | Requires Node.js | Yes | **No** | No |
57
- | **Total tools available** | **94+** | **43** | **22** |
54
+ | **Total tools available** | **100+** | **83** | **9** |
58
55
 
59
- > **Bottom line:** Remote SSE is **read-only** with ~38% of the tools. **Cloud Mode** unlocks write access from web AI clients without Node.js. NPX/Local Git gives the full 94+ tools with real-time monitoring.
56
+ > **Bottom line:** Remote SSE is **read-only** with ~38% of the tools. **Cloud Mode** unlocks write access from web AI clients without Node.js. NPX/Local Git gives the full 100+ tools with real-time monitoring.
60
57
 
61
58
  ---
62
59
 
@@ -64,7 +61,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
64
61
 
65
62
  **Best for:** Designers who want full AI-assisted design capabilities.
66
63
 
67
- **What you get:** All 94+ tools including design creation, variable management, and component instantiation.
64
+ **What you get:** All 100+ tools including design creation, variable management, and component instantiation.
68
65
 
69
66
  #### Prerequisites
70
67
 
@@ -77,14 +74,14 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
77
74
  1. Go to [Manage personal access tokens](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens) in Figma Help
78
75
  2. Follow the steps to **create a new personal access token**
79
76
  3. Enter description: `Figma Console MCP`
80
- 4. Set scopes: **File content** (Read), **Variables** (Read), **Comments** (Read and write)
77
+ 4. Set scopes: **File content** (Read), **File versions** (Read), **Variables** (Read), **Comments** (Read and write)
81
78
  5. **Copy the token** β€” you won't see it again! (starts with `figd_`)
82
79
 
83
80
  #### Step 2: Configure Your MCP Client
84
81
 
85
82
  **Claude Code (CLI):**
86
83
  ```bash
87
- claude mcp add figma-console -s user -e FIGMA_ACCESS_TOKEN=figd_YOUR_TOKEN_HERE -e ENABLE_MCP_APPS=true -- npx -y @mp3wizard/figma-console-mcp@latest
84
+ claude mcp add figma-console -s user -e FIGMA_ACCESS_TOKEN=figd_YOUR_TOKEN_HERE -e ENABLE_MCP_APPS=true -- npx -y figma-console-mcp@latest
88
85
  ```
89
86
 
90
87
  **Cursor / Windsurf / Claude Desktop:**
@@ -96,7 +93,7 @@ Add to your MCP config file (see [Where to find your config file](#-where-to-fin
96
93
  "mcpServers": {
97
94
  "figma-console": {
98
95
  "command": "npx",
99
- "args": ["-y", "@mp3wizard/figma-console-mcp@latest"],
96
+ "args": ["-y", "figma-console-mcp@latest"],
100
97
  "env": {
101
98
  "FIGMA_ACCESS_TOKEN": "figd_YOUR_TOKEN_HERE",
102
99
  "ENABLE_MCP_APPS": "true"
@@ -133,7 +130,7 @@ If you're not sure where to put the JSON configuration above, here's where each
133
130
 
134
131
  > One-time setup. The plugin uses a bootloader that dynamically loads fresh code from the MCP server β€” no need to re-import when the server updates.
135
132
 
136
- > **Upgrading from v1.14 or earlier?** Your existing plugin still works, but to get the bootloader benefits (no more re-importing), do one final re-import from `~/.figma-console-mcp/plugin/manifest.json`. The path is created automatically when the MCP server starts. Run `npx @mp3wizard/figma-console-mcp@latest --print-path` to see it. After this one-time upgrade, you're done forever.
133
+ > **Upgrading from v1.14 or earlier?** Your existing plugin still works, but to get the bootloader benefits (no more re-importing), do one final re-import from `~/.figma-console-mcp/plugin/manifest.json`. The path is created automatically when the MCP server starts. Run `npx figma-console-mcp@latest --print-path` to see it. After this one-time upgrade, you're done forever.
137
134
 
138
135
  #### Step 4: Restart Your MCP Client
139
136
 
@@ -159,7 +156,7 @@ Create a simple frame with a blue background
159
156
 
160
157
  **Best for:** Developers who want to modify source code or contribute to the project.
161
158
 
162
- **What you get:** Same 94+ tools as NPX, plus full source code access.
159
+ **What you get:** Same 100+ tools as NPX, plus full source code access.
163
160
 
164
161
  #### Quick Setup
165
162
 
@@ -305,7 +302,7 @@ AI Client β†’ Cloud MCP Server β†’ Durable Object Relay β†’ Desktop Bridge Plugi
305
302
  | Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
306
303
  |---------|-------------------|------------|-----------|------------|
307
304
  | **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
308
- | **Total tools** | **94+** | **43** | **94+** | **22** (read-only) |
305
+ | **Total tools** | **100+** | **83** | **100+** | **9** (read-only) |
309
306
  | **Design creation** | βœ… | βœ… | βœ… | ❌ |
310
307
  | **Variable management** | βœ… | βœ… | βœ… | ❌ |
311
308
  | **Component instantiation** | βœ… | βœ… | βœ… | ❌ |
@@ -320,7 +317,7 @@ AI Client β†’ Cloud MCP Server β†’ Durable Object Relay β†’ Desktop Bridge Plugi
320
317
  | **Automatic updates** | βœ… (`@latest`) | βœ… | Manual (`git pull`) | βœ… |
321
318
  | **Source code access** | ❌ | ❌ | βœ… | ❌ |
322
319
 
323
- > **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 94+ tools.
320
+ > **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 100+ tools.
324
321
 
325
322
  **πŸ“– [Complete Feature Comparison](docs/mode-comparison.md)**
326
323
 
@@ -366,7 +363,7 @@ When you first use design system tools:
366
363
  ### Local Mode - Personal Access Token (Manual)
367
364
 
368
365
  1. Visit https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens
369
- 2. Generate token with scopes: **File content** (Read), **Variables** (Read), **Comments** (Read and write)
366
+ 2. Generate token with scopes: **File content** (Read), **File versions** (Read), **Variables** (Read), **Comments** (Read and write)
370
367
  3. Add to MCP config as `FIGMA_ACCESS_TOKEN` environment variable
371
368
 
372
369
  ---
@@ -654,7 +651,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
654
651
  - The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
655
652
  - The server tries port 9223 first, then automatically falls back through ports 9224–9232 if needed
656
653
  - The plugin scans all ports in the range and connects to every active server it finds
657
- - All 94+ tools work through the WebSocket transport
654
+ - All 100+ tools work through the WebSocket transport
658
655
 
659
656
  **Multiple files:** The WebSocket server supports multiple simultaneous plugin connections β€” one per open Figma file. Each connection is tracked by file key with independent state (selection, document changes, console logs).
660
657
 
@@ -791,11 +788,9 @@ The architecture supports adding new apps with minimal boilerplate β€” each app
791
788
 
792
789
  ## πŸ›€οΈ Roadmap
793
790
 
794
- **Current Status:** v1.22.4 (Stable) - Production-ready with 14 WCAG accessibility rules, Phase B lint checks (disabled variant context + token misuse detection), FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, 94+ tools, Comments API, and MCP Apps
791
+ **Current Status:** v1.17.0 (Stable) - Production-ready with FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, 100+ tools, Comments API, and MCP Apps
795
792
 
796
793
  **Recent Releases:**
797
- - [x] **v1.22.4** - Security: fix 6 Medium hono/node-server CVEs via package.json overrides (CVE-2026-39406 through CVE-2026-39410, GHSA-26pp-8wgv-hjvm, GHSA-xpcf-pg52-r92g)
798
- - [x] **v1.22.3** - Phase B accessibility: disabled variant context check, token misuse detection, WCAG interpretation fixes from accessibility consultant review, rule count 13 to 14
799
794
  - [x] **v1.17.0** - Figma Slides Support: 15 new tools for managing presentations β€” slides, transitions, content, reordering, and navigation. Inspired by Toni Haidamous (PR #11).
800
795
  - [x] **v1.16.0** - FigJam Support: 9 new tools for creating and reading FigJam boards β€” stickies, flowcharts, tables, code blocks, and connection graphs. Community-contributed by klgral and lukemoderwell.
801
796
  - [x] **v1.12.0** - Cloud Write Relay: web AI clients (Claude.ai, v0, Replit, Lovable) can create and modify Figma designs via cloud relay pairing β€” no Node.js required
@@ -840,24 +835,6 @@ npm run build
840
835
 
841
836
  ---
842
837
 
843
- ## πŸ”’ Network Transparency
844
-
845
- All outbound network connections made by this MCP server:
846
-
847
- | Destination | Protocol | Purpose | Data Sent |
848
- |-------------|----------|---------|-----------|
849
- | `api.figma.com` | HTTPS | REST API (files, variables, components, styles, images, comments) | File keys, node IDs, API parameters |
850
- | `www.figma.com` | HTTPS | OAuth 2.0 authorization flow | Client ID, auth codes, refresh tokens |
851
- | `figma-console-mcp.southleft.com` | WSS/HTTPS | Cloud relay for web AI clients (Cloud Mode only) | Metadata only: fileName, fileKey, currentPage |
852
- | Figma S3 CDN | HTTPS | Rendered image downloads (temporary URLs) | None (download only) |
853
- | `localhost:9223-9232` | WS | Desktop Bridge plugin (local only) | Plugin commands/responses |
854
-
855
- **Not present:** telemetry, analytics, tracking, third-party data services, obfuscated code, or environment variable leakage. Full audit available in [`Security review report/`](Security%20review%20report/).
856
-
857
- > **Local Mode users:** `src/local.ts` does not connect to the cloud relay β€” only Figma API and localhost WebSocket.
858
-
859
- ---
860
-
861
838
  ## πŸ“„ License
862
839
 
863
840
  MIT - See [LICENSE](LICENSE) file for details.
@@ -32,25 +32,27 @@ export class CloudWebSocketConnector {
32
32
  return this.sendCommand('GET_VARIABLES_DATA', {}, 10000);
33
33
  }
34
34
  async getVariables(fileKey) {
35
+ // IMPORTANT: bare try/catch with top-level `return`, NO inner IIFE.
36
+ // See issue #68 + the matching note in websocket-connector.ts. The plugin
37
+ // (code.js) wraps every EXECUTE_CODE payload in its own async IIFE; nesting
38
+ // another swallows the inner return and silently drops the variables.
35
39
  const code = `
36
- (async () => {
37
- try {
38
- if (typeof figma === 'undefined') {
39
- throw new Error('Figma API not available in this context');
40
- }
41
- const variables = await figma.variables.getLocalVariablesAsync();
42
- const collections = await figma.variables.getLocalVariableCollectionsAsync();
43
- return {
44
- success: true,
45
- timestamp: Date.now(),
46
- fileMetadata: { fileName: figma.root.name, fileKey: figma.fileKey || null },
47
- variables: variables.map(function(v) { return { id: v.id, name: v.name, key: v.key, resolvedType: v.resolvedType, valuesByMode: v.valuesByMode, variableCollectionId: v.variableCollectionId, scopes: v.scopes, description: v.description, hiddenFromPublishing: v.hiddenFromPublishing }; }),
48
- variableCollections: collections.map(function(c) { return { id: c.id, name: c.name, key: c.key, modes: c.modes, defaultModeId: c.defaultModeId, variableIds: c.variableIds }; })
49
- };
50
- } catch (error) {
51
- return { success: false, error: error.message };
40
+ try {
41
+ if (typeof figma === 'undefined') {
42
+ throw new Error('Figma API not available in this context');
52
43
  }
53
- })()
44
+ const variables = await figma.variables.getLocalVariablesAsync();
45
+ const collections = await figma.variables.getLocalVariableCollectionsAsync();
46
+ return {
47
+ success: true,
48
+ timestamp: Date.now(),
49
+ fileMetadata: { fileName: figma.root.name, fileKey: figma.fileKey || null },
50
+ variables: variables.map(function(v) { return { id: v.id, name: v.name, key: v.key, resolvedType: v.resolvedType, valuesByMode: v.valuesByMode, variableCollectionId: v.variableCollectionId, scopes: v.scopes, description: v.description, hiddenFromPublishing: v.hiddenFromPublishing }; }),
51
+ variableCollections: collections.map(function(c) { return { id: c.id, name: c.name, key: c.key, modes: c.modes, defaultModeId: c.defaultModeId, variableIds: c.variableIds }; })
52
+ };
53
+ } catch (error) {
54
+ return { success: false, error: error.message };
55
+ }
54
56
  `;
55
57
  return this.sendCommand('EXECUTE_CODE', { code, timeout: 30000 }, 32000);
56
58
  }
@@ -12,35 +12,9 @@ const enrichmentService = new EnrichmentService(logger);
12
12
  // ============================================================================
13
13
  // Shared Helpers
14
14
  // ============================================================================
15
- /** Convert Figma RGBA (0-1 floats) to hex string */
16
- export function figmaRGBAToHex(color) {
17
- const r = Math.round(color.r * 255);
18
- const g = Math.round(color.g * 255);
19
- const b = Math.round(color.b * 255);
20
- const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`.toUpperCase();
21
- if (color.a !== undefined && color.a < 1) {
22
- const a = Math.round(color.a * 255);
23
- return `${hex}${a.toString(16).padStart(2, "0")}`;
24
- }
25
- return hex;
26
- }
27
- /** Normalize a color string for comparison (uppercase hex without alpha if fully opaque) */
28
- export function normalizeColor(color) {
29
- let c = color.trim().toUpperCase();
30
- // Strip alpha if fully opaque (FF)
31
- if (c.length === 9 && c.endsWith("FF")) {
32
- c = c.slice(0, 7);
33
- }
34
- // Expand shorthand (#RGB -> #RRGGBB)
35
- if (/^#[0-9A-F]{3}$/.test(c)) {
36
- c = `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
37
- }
38
- return c;
39
- }
40
- /** Compare numeric values with a tolerance */
41
- export function numericClose(a, b, tolerance = 1) {
42
- return Math.abs(a - b) <= tolerance;
43
- }
15
+ // Re-exported from the shared diff module so existing imports continue to work.
16
+ export { figmaRGBAToHex, normalizeColor, numericClose } from "./diff/property-compare.js";
17
+ import { figmaRGBAToHex, normalizeColor, numericClose } from "./diff/property-compare.js";
44
18
  /** Calculate parity score from discrepancy counts */
45
19
  export function calculateParityScore(critical, major, minor, info) {
46
20
  return Math.max(0, 100 - (critical * 15 + major * 8 + minor * 3 + info * 1));
@@ -2180,7 +2154,11 @@ const codeSpecSchema = z.object({
2180
2154
  semanticElement: z.string().optional().describe("Semantic HTML element (e.g., 'button', 'a', 'input')"),
2181
2155
  supportsDisabled: z.boolean().optional().describe("Whether code supports disabled/aria-disabled state"),
2182
2156
  supportsError: z.boolean().optional().describe("Whether code supports aria-invalid/error state"),
2183
- renderedSize: z.tuple([z.number(), z.number()]).optional().describe("Rendered size [width, height] in px"),
2157
+ // NOTE: Use array-with-length, NOT z.tuple β€” tuples emit JSON Schema `items: [...]`
2158
+ // (array of schemas), which Gemini's stricter Function Calling validator rejects with
2159
+ // "is not of type 'object', 'boolean'". See issue #64. A constrained array emits
2160
+ // `items: { type: 'number' }` which all major MCP clients accept.
2161
+ renderedSize: z.array(z.number()).min(2).max(2).optional().describe("Rendered size [width, height] in px"),
2184
2162
  }).optional().describe("Accessibility properties from code. Tip: use figma_scan_code_accessibility with mapToCodeSpec:true to auto-generate this from component HTML."),
2185
2163
  metadata: z.object({
2186
2164
  name: z.string().optional(),
@@ -2350,16 +2328,22 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2350
2328
  logger.warn("Enrichment failed, proceeding without token data");
2351
2329
  }
2352
2330
  }
2331
+ // Cast to the structural CodeSpec interface. The Zod schema infers
2332
+ // `accessibility.renderedSize` as `number[]` (post-#64 fix uses
2333
+ // `z.array(z.number()).min(2).max(2)` for Gemini compat), but at runtime
2334
+ // the validator guarantees exactly two numbers, matching CodeSpec's
2335
+ // `[number, number]`. TypeScript can't bridge the inference gap.
2336
+ const codeSpecTyped = codeSpec;
2353
2337
  // Run all comparators (use nodeForVisual for design properties, nodeForAPI for component API)
2354
2338
  const discrepancies = [];
2355
- compareVisual(nodeForVisual, codeSpec, discrepancies);
2356
- compareSpacing(nodeForVisual, codeSpec, discrepancies);
2357
- compareTypography(nodeForVisual, codeSpec, discrepancies);
2358
- compareTokens(enrichedData, codeSpec, discrepancies);
2359
- compareComponentAPI(nodeForAPI, codeSpec, discrepancies);
2360
- compareAccessibility(node, codeSpec, discrepancies);
2361
- compareNaming(node, codeSpec, discrepancies);
2362
- compareMetadata(node, componentMeta, codeSpec, discrepancies);
2339
+ compareVisual(nodeForVisual, codeSpecTyped, discrepancies);
2340
+ compareSpacing(nodeForVisual, codeSpecTyped, discrepancies);
2341
+ compareTypography(nodeForVisual, codeSpecTyped, discrepancies);
2342
+ compareTokens(enrichedData, codeSpecTyped, discrepancies);
2343
+ compareComponentAPI(nodeForAPI, codeSpecTyped, discrepancies);
2344
+ compareAccessibility(node, codeSpecTyped, discrepancies);
2345
+ compareNaming(node, codeSpecTyped, discrepancies);
2346
+ compareMetadata(node, componentMeta, codeSpecTyped, discrepancies);
2363
2347
  // Sort by severity
2364
2348
  const severityOrder = {
2365
2349
  critical: 0,
@@ -2410,7 +2394,7 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2410
2394
  : [],
2411
2395
  tokenCoverage: enrichedData?.token_coverage,
2412
2396
  },
2413
- codeData: codeSpec,
2397
+ codeData: codeSpecTyped,
2414
2398
  };
2415
2399
  return {
2416
2400
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Pure-function markdown formatter for version diff results.
3
+ *
4
+ * Takes a diff payload (the structured response from figma_diff_versions)
5
+ * plus optional author/label metadata for the from/to versions, and produces
6
+ * a release-notes-style markdown string.
7
+ *
8
+ * Mode-aware:
9
+ * summary β€” header + a single line of counts
10
+ * standard β€” header + page section + per-component counts
11
+ * detailed β€” header + page section + per-component property/binding details
12
+ *
13
+ * Pure: no I/O, no side effects. Trivially testable.
14
+ */
15
+ export function formatChangelogMarkdown(input, mode = "standard") {
16
+ const lines = [];
17
+ // Header
18
+ const title = input.file_name ? `${input.file_name} β€” Change Log` : "Figma File Change Log";
19
+ lines.push(`# ${title}`);
20
+ lines.push("");
21
+ lines.push(`**From:** ${formatVersionRef(input.from_meta, input.from_version_id, false)}`);
22
+ lines.push(`**To:** ${formatVersionRef(input.to_meta, input.to_version_id, true)}`);
23
+ const span = computeSpanDays(input.from_meta?.created_at, input.to_meta?.created_at);
24
+ if (span !== null) {
25
+ lines.push(`**Span:** ${span} day${span === 1 ? "" : "s"}`);
26
+ }
27
+ lines.push("");
28
+ // Summary mode: one-line punch line
29
+ if (mode === "summary") {
30
+ lines.push(formatSummaryLine(input));
31
+ lines.push("");
32
+ appendNotes(lines, input.notes);
33
+ return lines.join("\n").trimEnd() + "\n";
34
+ }
35
+ // Page Structure section (always for standard/detailed)
36
+ appendPageStructureSection(lines, input.page_structure);
37
+ // Components section
38
+ if (input.scoped_nodes && input.scoped_nodes.length > 0) {
39
+ appendComponentsSection(lines, input.scoped_nodes, mode);
40
+ }
41
+ else {
42
+ lines.push("## Components");
43
+ lines.push("");
44
+ lines.push("_No components were scoped for this changelog. Pass `component_ids` to include per-component changes._");
45
+ lines.push("");
46
+ }
47
+ appendNotes(lines, input.notes);
48
+ return lines.join("\n").trimEnd() + "\n";
49
+ }
50
+ // ============================================================================
51
+ // Section formatters
52
+ // ============================================================================
53
+ function appendPageStructureSection(lines, page) {
54
+ const total = page.summary.added + page.summary.removed + page.summary.renamed;
55
+ lines.push("## Page Structure");
56
+ lines.push("");
57
+ if (total === 0) {
58
+ lines.push("_No page-level changes._");
59
+ lines.push("");
60
+ return;
61
+ }
62
+ if (page.pages_added.length > 0) {
63
+ lines.push(`**Added (${page.pages_added.length}):**`);
64
+ for (const p of page.pages_added)
65
+ lines.push(`- ${escapeMd(p.name)} \`${p.id}\``);
66
+ lines.push("");
67
+ }
68
+ if (page.pages_removed.length > 0) {
69
+ lines.push(`**Removed (${page.pages_removed.length}):**`);
70
+ for (const p of page.pages_removed)
71
+ lines.push(`- ${escapeMd(p.name)} \`${p.id}\``);
72
+ lines.push("");
73
+ }
74
+ if (page.pages_renamed.length > 0) {
75
+ lines.push(`**Renamed (${page.pages_renamed.length}):**`);
76
+ for (const r of page.pages_renamed) {
77
+ lines.push(`- \`${r.id}\`: ${escapeMd(r.old_name)} β†’ ${escapeMd(r.new_name)}`);
78
+ }
79
+ lines.push("");
80
+ }
81
+ }
82
+ function appendComponentsSection(lines, scoped, mode) {
83
+ lines.push("## Components");
84
+ lines.push("");
85
+ const withChanges = scoped.filter((n) => n.change_count > 0);
86
+ const unchanged = scoped.filter((n) => n.change_count === 0 && n.notes.length === 0);
87
+ const notFound = scoped.filter((n) => n.notes.some((note) => note.toLowerCase().includes("not found")));
88
+ if (withChanges.length === 0 && unchanged.length === 0 && notFound.length === 0) {
89
+ lines.push("_No scoped components had changes._");
90
+ lines.push("");
91
+ return;
92
+ }
93
+ for (const n of withChanges) {
94
+ appendComponentBlock(lines, n, mode);
95
+ }
96
+ if (unchanged.length > 0) {
97
+ lines.push(`**No changes:** ${unchanged.map((n) => `\`${n.node_name || n.node_id}\``).join(", ")}`);
98
+ lines.push("");
99
+ }
100
+ if (notFound.length > 0) {
101
+ lines.push(`**Not found in either version:** ${notFound.map((n) => `\`${n.node_id || "(empty)"}\``).join(", ")}`);
102
+ lines.push("");
103
+ }
104
+ }
105
+ function appendComponentBlock(lines, n, mode) {
106
+ const heading = n.node_name ? `${n.node_name}` : "(unnamed)";
107
+ lines.push(`### ${escapeMd(heading)} β€” \`${n.node_id}\``);
108
+ lines.push(`**${n.change_count} change${n.change_count === 1 ? "" : "s"}**`);
109
+ // Each subsequent sub-section adds its own single-blank-line separator so
110
+ // we don't end up with two consecutive blank lines when one of these
111
+ // sub-sections is empty. (Earlier versions unconditionally pushed a blank
112
+ // after the change count then again before each sub-section, producing
113
+ // double-blanks in the common case.)
114
+ const bullets = [];
115
+ if (n.name_changed) {
116
+ bullets.push(`- Renamed: \`${escapeMd(n.name_changed.from)}\` β†’ \`${escapeMd(n.name_changed.to)}\``);
117
+ }
118
+ if (n.description_changed) {
119
+ const fromLen = n.description_changed.from.length;
120
+ const toLen = n.description_changed.to.length;
121
+ bullets.push(`- Description changed (${fromLen} β†’ ${toLen} chars)`);
122
+ }
123
+ if (n.children_added.length > 0) {
124
+ const inline = n.children_added.map((c) => `\`${escapeMd(c.name || c.id)}\``).join(", ");
125
+ bullets.push(`- Children added (${n.children_added.length}): ${inline}`);
126
+ }
127
+ if (n.children_removed.length > 0) {
128
+ const inline = n.children_removed.map((c) => `\`${escapeMd(c.name || c.id)}\``).join(", ");
129
+ bullets.push(`- Children removed (${n.children_removed.length}): ${inline}`);
130
+ }
131
+ if (bullets.length > 0) {
132
+ lines.push("");
133
+ lines.push(...bullets);
134
+ }
135
+ if (n.component_properties && hasAnyPropChanges(n.component_properties.summary)) {
136
+ const s = n.component_properties.summary;
137
+ const parts = [];
138
+ if (s.added > 0)
139
+ parts.push(`${s.added} added`);
140
+ if (s.removed > 0)
141
+ parts.push(`${s.removed} removed`);
142
+ if (s.type_changed > 0)
143
+ parts.push(`${s.type_changed} type changed`);
144
+ if (s.default_changed > 0)
145
+ parts.push(`${s.default_changed} default changed`);
146
+ lines.push("");
147
+ lines.push(`**Component properties:** ${parts.join(", ")}`);
148
+ if (mode === "detailed") {
149
+ for (const p of n.component_properties.added) {
150
+ lines.push(`- βž• \`${escapeMd(p.name)}\` (${p.type}, default: \`${stringifyValue(p.default_value)}\`)`);
151
+ }
152
+ for (const p of n.component_properties.removed) {
153
+ lines.push(`- βž– \`${escapeMd(p.name)}\` (${p.type})`);
154
+ }
155
+ for (const t of n.component_properties.type_changed) {
156
+ lines.push(`- πŸ”„ \`${escapeMd(t.name)}\`: ${t.from_type} β†’ ${t.to_type}`);
157
+ }
158
+ for (const d of n.component_properties.default_changed) {
159
+ lines.push(`- βš™οΈ \`${escapeMd(d.name)}\`: \`${stringifyValue(d.from_default)}\` β†’ \`${stringifyValue(d.to_default)}\``);
160
+ }
161
+ }
162
+ }
163
+ if (n.binding_changes.length > 0) {
164
+ lines.push("");
165
+ lines.push(`**Variable bindings (${n.binding_changes.length}):**`);
166
+ if (mode === "detailed") {
167
+ for (const b of n.binding_changes) {
168
+ const arrow = b.change_kind === "added"
169
+ ? `β†’ \`${b.to_variable_id}\``
170
+ : b.change_kind === "removed"
171
+ ? `(removed, was \`${b.from_variable_id}\`)`
172
+ : `\`${b.from_variable_id}\` β†’ \`${b.to_variable_id}\``;
173
+ lines.push(`- ${escapeMd(b.node_name || b.node_id)} β€” \`${b.property}\` ${arrow}`);
174
+ }
175
+ }
176
+ else {
177
+ // standard mode: group by change_kind, give counts
178
+ const counts = { added: 0, removed: 0, rebound: 0 };
179
+ for (const b of n.binding_changes)
180
+ counts[b.change_kind]++;
181
+ const parts = [];
182
+ if (counts.added > 0)
183
+ parts.push(`${counts.added} added`);
184
+ if (counts.removed > 0)
185
+ parts.push(`${counts.removed} removed`);
186
+ if (counts.rebound > 0)
187
+ parts.push(`${counts.rebound} rebound`);
188
+ lines.push(`_${parts.join(", ")}._`);
189
+ }
190
+ }
191
+ lines.push("");
192
+ }
193
+ function appendNotes(lines, notes) {
194
+ if (!notes || notes.length === 0)
195
+ return;
196
+ lines.push("## Notes");
197
+ lines.push("");
198
+ for (const n of notes)
199
+ lines.push(`- ${n}`);
200
+ lines.push("");
201
+ }
202
+ // ============================================================================
203
+ // Helpers
204
+ // ============================================================================
205
+ function formatVersionRef(meta, versionId, isTo) {
206
+ if (meta?.is_head || versionId === "current") {
207
+ const date = meta?.created_at ? formatDate(meta.created_at) : "current state";
208
+ const user = meta?.user_handle ? ` by ${meta.user_handle}` : "";
209
+ return `Current state (last modified ${date}${user})`;
210
+ }
211
+ if (!meta) {
212
+ return `\`${versionId}\` _(metadata not available)_`;
213
+ }
214
+ const label = meta.label && meta.label.trim() !== "" ? `"${meta.label}"` : "_(unlabeled)_";
215
+ const date = meta.created_at ? formatDate(meta.created_at) : "";
216
+ const user = meta.user_handle ? `by ${meta.user_handle}` : "";
217
+ const parts = [label, date, user].filter(Boolean).join(" β€” ");
218
+ return `${parts} \`${versionId}\``;
219
+ }
220
+ function formatSummaryLine(input) {
221
+ const p = input.page_structure.summary;
222
+ const componentCount = input.scoped_nodes?.filter((n) => n.change_count > 0).length ?? 0;
223
+ const totalComponentChanges = input.scoped_nodes?.reduce((acc, n) => acc + n.change_count, 0) ?? 0;
224
+ const parts = [];
225
+ if (p.added > 0)
226
+ parts.push(`${p.added} page${p.added === 1 ? "" : "s"} added`);
227
+ if (p.removed > 0)
228
+ parts.push(`${p.removed} page${p.removed === 1 ? "" : "s"} removed`);
229
+ if (p.renamed > 0)
230
+ parts.push(`${p.renamed} page${p.renamed === 1 ? "" : "s"} renamed`);
231
+ if (componentCount > 0) {
232
+ parts.push(`${componentCount} component${componentCount === 1 ? "" : "s"} with ${totalComponentChanges} change${totalComponentChanges === 1 ? "" : "s"}`);
233
+ }
234
+ if (parts.length === 0)
235
+ return "_No structural changes detected._";
236
+ return parts.join("; ") + ".";
237
+ }
238
+ function computeSpanDays(fromIso, toIso) {
239
+ if (!fromIso || !toIso)
240
+ return null;
241
+ const from = new Date(fromIso).getTime();
242
+ const to = new Date(toIso).getTime();
243
+ if (isNaN(from) || isNaN(to))
244
+ return null;
245
+ return Math.max(0, Math.round((to - from) / (1000 * 60 * 60 * 24)));
246
+ }
247
+ function formatDate(iso) {
248
+ // ISO 8601 is fine as-is for release notes; trim time if it's midnight UTC for cleaner output
249
+ const d = new Date(iso);
250
+ if (isNaN(d.getTime()))
251
+ return iso;
252
+ return d.toISOString().slice(0, 10);
253
+ }
254
+ function escapeMd(s) {
255
+ // Escape characters that would break markdown. Keep light β€” release notes
256
+ // should be readable, not over-escaped.
257
+ return s.replace(/([\\`*_{}\[\]<>])/g, "\\$1");
258
+ }
259
+ function stringifyValue(v) {
260
+ if (v === undefined)
261
+ return "?"; // standard mode strips defaults; render placeholder
262
+ if (v === null)
263
+ return "null";
264
+ if (typeof v === "string")
265
+ return v;
266
+ try {
267
+ return JSON.stringify(v);
268
+ }
269
+ catch {
270
+ return String(v);
271
+ }
272
+ }
273
+ function hasAnyPropChanges(s) {
274
+ return s.added > 0 || s.removed > 0 || s.type_changed > 0 || s.default_changed > 0;
275
+ }