figma-console-mcp 1.22.3 β 1.23.0
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/README.md +14 -15
- package/dist/cloudflare/core/accessibility-tools.js +306 -0
- package/dist/cloudflare/core/cloud-websocket-connector.js +30 -17
- package/dist/cloudflare/core/design-code-tools.js +182 -40
- package/dist/cloudflare/core/diff/changelog-formatter.js +275 -0
- package/dist/cloudflare/core/diff/diff-engine.js +334 -0
- package/dist/cloudflare/core/diff/property-compare.js +36 -0
- package/dist/cloudflare/core/diff/version-cache.js +74 -0
- package/dist/cloudflare/core/figma-api.js +20 -10
- package/dist/cloudflare/core/figma-desktop-connector.js +2 -0
- package/dist/cloudflare/core/figma-tools.js +15 -6
- package/dist/cloudflare/core/version-tools.js +1014 -0
- package/dist/cloudflare/core/websocket-connector.js +35 -18
- package/dist/cloudflare/core/write-tools.js +49 -4
- package/dist/cloudflare/index.js +27 -16
- package/dist/core/design-code-tools.d.ts +1 -12
- package/dist/core/design-code-tools.d.ts.map +1 -1
- package/dist/core/design-code-tools.js +23 -39
- package/dist/core/design-code-tools.js.map +1 -1
- package/dist/core/diff/changelog-formatter.d.ts +35 -0
- package/dist/core/diff/changelog-formatter.d.ts.map +1 -0
- package/dist/core/diff/changelog-formatter.js +276 -0
- package/dist/core/diff/changelog-formatter.js.map +1 -0
- package/dist/core/diff/diff-engine.d.ts +113 -0
- package/dist/core/diff/diff-engine.d.ts.map +1 -0
- package/dist/core/diff/diff-engine.js +335 -0
- package/dist/core/diff/diff-engine.js.map +1 -0
- package/dist/core/diff/property-compare.d.ts +19 -0
- package/dist/core/diff/property-compare.d.ts.map +1 -0
- package/dist/core/diff/property-compare.js +37 -0
- package/dist/core/diff/property-compare.js.map +1 -0
- package/dist/core/diff/version-cache.d.ts +40 -0
- package/dist/core/diff/version-cache.d.ts.map +1 -0
- package/dist/core/diff/version-cache.js +75 -0
- package/dist/core/diff/version-cache.js.map +1 -0
- package/dist/core/figma-api.d.ts +29 -0
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +19 -0
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +15 -6
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/version-tools.d.ts +30 -0
- package/dist/core/version-tools.d.ts.map +1 -0
- package/dist/core/version-tools.js +1015 -0
- package/dist/core/version-tools.js.map +1 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +24 -18
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/write-tools.js +3 -3
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +11 -3
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,20 +8,19 @@
|
|
|
8
8
|
|
|
9
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**.
|
|
10
10
|
|
|
11
|
-
> **π
|
|
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)
|
|
12
12
|
|
|
13
13
|
## What is this?
|
|
14
14
|
|
|
15
15
|
Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
16
16
|
|
|
17
|
-
- **π Plugin debugging** - Capture console logs, errors, and stack traces
|
|
18
|
-
- **πΈ Visual debugging** - Take screenshots for context
|
|
19
17
|
- **π¨ Design system extraction** - Pull variables, components, and styles
|
|
18
|
+
- **πΈ Visual debugging** - Take screenshots for context
|
|
20
19
|
- **βοΈ Design creation** - Create UI components, frames, and layouts directly in Figma
|
|
21
20
|
- **π§ Variable management** - Create, update, rename, and delete design tokens
|
|
22
|
-
- **β‘ Real-time monitoring** - Watch logs
|
|
21
|
+
- **β‘ Real-time monitoring** - Watch console logs from the Desktop Bridge plugin
|
|
23
22
|
- **π FigJam boards** - Create stickies, flowcharts, tables, and code blocks on collaborative boards
|
|
24
|
-
- **βΏ Accessibility scanning** -
|
|
23
|
+
- **βΏ Accessibility scanning** - 14 WCAG design checks with conformance level tagging, component scorecards, axe-core code scanning, design-to-code parity
|
|
25
24
|
- **βοΈ Cloud Write Relay** - Web AI clients (Claude.ai, v0, Replit) can design in Figma via cloud pairing
|
|
26
25
|
- **π Four ways to connect** - Remote SSE, Cloud Mode, NPX, or Local Git
|
|
27
26
|
|
|
@@ -52,9 +51,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
52
51
|
| Real-time monitoring (console, selection) | β
| β | β |
|
|
53
52
|
| Desktop Bridge plugin | β
| β
| β |
|
|
54
53
|
| Requires Node.js | Yes | **No** | No |
|
|
55
|
-
| **Total tools available** | **
|
|
54
|
+
| **Total tools available** | **100+** | **83** | **9** |
|
|
56
55
|
|
|
57
|
-
> **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
|
|
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.
|
|
58
57
|
|
|
59
58
|
---
|
|
60
59
|
|
|
@@ -62,7 +61,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
62
61
|
|
|
63
62
|
**Best for:** Designers who want full AI-assisted design capabilities.
|
|
64
63
|
|
|
65
|
-
**What you get:** All
|
|
64
|
+
**What you get:** All 100+ tools including design creation, variable management, and component instantiation.
|
|
66
65
|
|
|
67
66
|
#### Prerequisites
|
|
68
67
|
|
|
@@ -75,7 +74,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
|
|
|
75
74
|
1. Go to [Manage personal access tokens](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens) in Figma Help
|
|
76
75
|
2. Follow the steps to **create a new personal access token**
|
|
77
76
|
3. Enter description: `Figma Console MCP`
|
|
78
|
-
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)
|
|
79
78
|
5. **Copy the token** β you won't see it again! (starts with `figd_`)
|
|
80
79
|
|
|
81
80
|
#### Step 2: Configure Your MCP Client
|
|
@@ -157,7 +156,7 @@ Create a simple frame with a blue background
|
|
|
157
156
|
|
|
158
157
|
**Best for:** Developers who want to modify source code or contribute to the project.
|
|
159
158
|
|
|
160
|
-
**What you get:** Same
|
|
159
|
+
**What you get:** Same 100+ tools as NPX, plus full source code access.
|
|
161
160
|
|
|
162
161
|
#### Quick Setup
|
|
163
162
|
|
|
@@ -303,7 +302,7 @@ AI Client β Cloud MCP Server β Durable Object Relay β Desktop Bridge Plugi
|
|
|
303
302
|
| Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
|
|
304
303
|
|---------|-------------------|------------|-----------|------------|
|
|
305
304
|
| **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
|
|
306
|
-
| **Total tools** | **
|
|
305
|
+
| **Total tools** | **100+** | **83** | **100+** | **9** (read-only) |
|
|
307
306
|
| **Design creation** | β
| β
| β
| β |
|
|
308
307
|
| **Variable management** | β
| β
| β
| β |
|
|
309
308
|
| **Component instantiation** | β
| β
| β
| β |
|
|
@@ -318,7 +317,7 @@ AI Client β Cloud MCP Server β Durable Object Relay β Desktop Bridge Plugi
|
|
|
318
317
|
| **Automatic updates** | β
(`@latest`) | β
| Manual (`git pull`) | β
|
|
|
319
318
|
| **Source code access** | β | β | β
| β |
|
|
320
319
|
|
|
321
|
-
> **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
|
|
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.
|
|
322
321
|
|
|
323
322
|
**π [Complete Feature Comparison](docs/mode-comparison.md)**
|
|
324
323
|
|
|
@@ -364,7 +363,7 @@ When you first use design system tools:
|
|
|
364
363
|
### Local Mode - Personal Access Token (Manual)
|
|
365
364
|
|
|
366
365
|
1. Visit https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens
|
|
367
|
-
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)
|
|
368
367
|
3. Add to MCP config as `FIGMA_ACCESS_TOKEN` environment variable
|
|
369
368
|
|
|
370
369
|
---
|
|
@@ -652,7 +651,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
|
|
|
652
651
|
- The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
|
|
653
652
|
- The server tries port 9223 first, then automatically falls back through ports 9224β9232 if needed
|
|
654
653
|
- The plugin scans all ports in the range and connects to every active server it finds
|
|
655
|
-
- All
|
|
654
|
+
- All 100+ tools work through the WebSocket transport
|
|
656
655
|
|
|
657
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).
|
|
658
657
|
|
|
@@ -789,7 +788,7 @@ The architecture supports adding new apps with minimal boilerplate β each app
|
|
|
789
788
|
|
|
790
789
|
## π€οΈ Roadmap
|
|
791
790
|
|
|
792
|
-
**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,
|
|
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
|
|
793
792
|
|
|
794
793
|
**Recent Releases:**
|
|
795
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).
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code-side accessibility scanning via axe-core + JSDOM.
|
|
3
|
+
*
|
|
4
|
+
* Delegates all rule logic to axe-core (Deque) β the MCP never owns
|
|
5
|
+
* a rule database. JSDOM provides a lightweight DOM for structural checks
|
|
6
|
+
* (~50 rules: ARIA, semantics, alt text, form labels, headings, landmarks).
|
|
7
|
+
*
|
|
8
|
+
* Visual rules (color contrast, focus-visible) are NOT available via JSDOM β
|
|
9
|
+
* those are handled by the design-side figma_lint_design tool.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { logger } from "./logger.js";
|
|
13
|
+
// Lazy-load axe-core and jsdom to keep them optional
|
|
14
|
+
let axeCore = null;
|
|
15
|
+
let JSDOM = null;
|
|
16
|
+
let depsLoaded = false;
|
|
17
|
+
let depsError = null;
|
|
18
|
+
async function loadDeps() {
|
|
19
|
+
if (depsLoaded)
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
axeCore = await import("axe-core");
|
|
23
|
+
// axe-core's default export structure
|
|
24
|
+
if (axeCore.default)
|
|
25
|
+
axeCore = axeCore.default;
|
|
26
|
+
const jsdomModule = await import("jsdom");
|
|
27
|
+
JSDOM = jsdomModule.JSDOM;
|
|
28
|
+
depsLoaded = true;
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
depsError = `axe-core or jsdom not installed. Run: npm install axe-core jsdom\n${e.message}`;
|
|
32
|
+
throw new Error(depsError);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run axe-core against an HTML string using JSDOM.
|
|
37
|
+
*
|
|
38
|
+
* JSDOM limitations: no computed styles, no layout, no visual rendering.
|
|
39
|
+
* This means ~50-60 structural rules work, but visual rules
|
|
40
|
+
* (color-contrast, focus-visible, etc.) will report as "incomplete".
|
|
41
|
+
*/
|
|
42
|
+
async function scanHtmlWithAxe(html, options = {}) {
|
|
43
|
+
await loadDeps();
|
|
44
|
+
// Wrap HTML fragment in a full document if needed
|
|
45
|
+
const fullHtml = html.includes("<html") || html.includes("<!DOCTYPE")
|
|
46
|
+
? html
|
|
47
|
+
: `<!DOCTYPE html><html lang="en"><head><title>Scan</title></head><body>${html}</body></html>`;
|
|
48
|
+
const dom = new JSDOM(fullHtml, {
|
|
49
|
+
runScripts: "dangerously",
|
|
50
|
+
pretendToBeVisual: true,
|
|
51
|
+
url: "http://localhost",
|
|
52
|
+
});
|
|
53
|
+
const { document, window } = dom.window;
|
|
54
|
+
// Inject axe-core into the JSDOM window
|
|
55
|
+
const axeSource = axeCore.source;
|
|
56
|
+
const scriptEl = document.createElement("script");
|
|
57
|
+
scriptEl.textContent = axeSource;
|
|
58
|
+
document.head.appendChild(scriptEl);
|
|
59
|
+
// Configure axe run options
|
|
60
|
+
const runOptions = {};
|
|
61
|
+
if (options.tags && options.tags.length > 0) {
|
|
62
|
+
runOptions.runOnly = { type: "tag", values: options.tags };
|
|
63
|
+
}
|
|
64
|
+
// Disable rules that require visual rendering (always fail/incomplete in JSDOM)
|
|
65
|
+
if (options.disableVisualRules !== false) {
|
|
66
|
+
runOptions.rules = {
|
|
67
|
+
"color-contrast": { enabled: false },
|
|
68
|
+
"color-contrast-enhanced": { enabled: false },
|
|
69
|
+
"link-in-text-block": { enabled: false },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// Determine scan context
|
|
73
|
+
const context = options.context || document;
|
|
74
|
+
try {
|
|
75
|
+
const results = await window.axe.run(context, runOptions);
|
|
76
|
+
// Clean up
|
|
77
|
+
dom.window.close();
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
dom.window.close();
|
|
82
|
+
throw new Error(`axe-core scan failed: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract a CodeSpec.accessibility object from HTML + axe-core results.
|
|
87
|
+
* This bridges Phase 3 (code scanning) β Phase 4 (parity comparison).
|
|
88
|
+
*
|
|
89
|
+
* Parses the HTML to extract semantic element, ARIA attributes, and states.
|
|
90
|
+
* Uses axe-core results to infer what the code supports.
|
|
91
|
+
*/
|
|
92
|
+
export function axeResultsToCodeSpec(html, axeResults) {
|
|
93
|
+
const spec = {};
|
|
94
|
+
// Parse HTML to extract attributes (lightweight regex-based, no DOM needed)
|
|
95
|
+
const htmlLower = html.toLowerCase();
|
|
96
|
+
// Semantic element: find the root/first meaningful element
|
|
97
|
+
const rootElementMatch = html.match(/<(button|a|input|select|textarea|details|dialog|nav|main|form|label|fieldset)\b/i);
|
|
98
|
+
if (rootElementMatch) {
|
|
99
|
+
spec.semanticElement = rootElementMatch[1].toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const firstElementMatch = html.match(/<(\w+)[\s>]/);
|
|
103
|
+
if (firstElementMatch && !["div", "span", "html", "head", "body", "script", "style", "!doctype"].includes(firstElementMatch[1].toLowerCase())) {
|
|
104
|
+
spec.semanticElement = firstElementMatch[1].toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
spec.semanticElement = "div";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ARIA role
|
|
111
|
+
const roleMatch = html.match(/role=["']([^"']+)["']/i);
|
|
112
|
+
if (roleMatch) {
|
|
113
|
+
spec.role = roleMatch[1];
|
|
114
|
+
}
|
|
115
|
+
// ARIA label
|
|
116
|
+
const ariaLabelMatch = html.match(/aria-label=["']([^"']+)["']/i);
|
|
117
|
+
if (ariaLabelMatch) {
|
|
118
|
+
spec.ariaLabel = ariaLabelMatch[1];
|
|
119
|
+
}
|
|
120
|
+
// Focus visible: check for :focus-visible or :focus in inline styles/class names,
|
|
121
|
+
// or infer from element type (native interactive elements have default focus)
|
|
122
|
+
const nativeFocusElements = ["button", "a", "input", "select", "textarea"];
|
|
123
|
+
const hasFocusCSS = /focus-visible|:focus\b|outline.*focus|ring.*focus|focus.*ring/i.test(html);
|
|
124
|
+
spec.focusVisible = hasFocusCSS || nativeFocusElements.includes(spec.semanticElement || "");
|
|
125
|
+
// Disabled support: only assert true when we find positive evidence.
|
|
126
|
+
// Absence of disabled/aria-disabled in a single HTML snapshot does NOT mean
|
|
127
|
+
// the component lacks disabled support β it may be in a non-disabled state.
|
|
128
|
+
if (/\bdisabled\b|aria-disabled/i.test(htmlLower)) {
|
|
129
|
+
spec.supportsDisabled = true;
|
|
130
|
+
}
|
|
131
|
+
// (leave undefined when not found β absence β lack of support)
|
|
132
|
+
// Error support: same principle β only assert true on positive evidence.
|
|
133
|
+
// A default-state HTML snippet won't have aria-invalid; that doesn't mean
|
|
134
|
+
// the component can't enter an error state.
|
|
135
|
+
if (/aria-invalid|aria-errormessage|aria-describedby.*error/i.test(htmlLower)) {
|
|
136
|
+
spec.supportsError = true;
|
|
137
|
+
}
|
|
138
|
+
// (leave undefined when not found β scan a different state to confirm)
|
|
139
|
+
// Required: check for required or aria-required attributes
|
|
140
|
+
if (/aria-required=["']true["']|required(?!=)/i.test(html)) {
|
|
141
|
+
spec.ariaRequired = true;
|
|
142
|
+
}
|
|
143
|
+
else if (/aria-required=["']false["']/i.test(html)) {
|
|
144
|
+
spec.ariaRequired = false;
|
|
145
|
+
}
|
|
146
|
+
// Keyboard interactions: infer from element type
|
|
147
|
+
const keyboardInteractions = [];
|
|
148
|
+
if (spec.semanticElement === "button" || spec.role === "button") {
|
|
149
|
+
keyboardInteractions.push("Enter", "Space");
|
|
150
|
+
}
|
|
151
|
+
else if (spec.semanticElement === "a" || spec.role === "link") {
|
|
152
|
+
keyboardInteractions.push("Enter");
|
|
153
|
+
}
|
|
154
|
+
else if (spec.semanticElement === "input" || spec.semanticElement === "textarea") {
|
|
155
|
+
keyboardInteractions.push("Tab (focus)", "Type (input)");
|
|
156
|
+
}
|
|
157
|
+
else if (spec.semanticElement === "select" || spec.role === "listbox") {
|
|
158
|
+
keyboardInteractions.push("Arrow keys", "Enter", "Space");
|
|
159
|
+
}
|
|
160
|
+
else if (spec.role === "checkbox" || spec.role === "switch") {
|
|
161
|
+
keyboardInteractions.push("Space");
|
|
162
|
+
}
|
|
163
|
+
else if (spec.role === "tab") {
|
|
164
|
+
keyboardInteractions.push("Arrow keys");
|
|
165
|
+
}
|
|
166
|
+
// Check HTML for custom keyboard handlers
|
|
167
|
+
if (/onkeydown|onkeyup|onkeypress|@keydown|@keyup|v-on:keydown/i.test(html)) {
|
|
168
|
+
if (!keyboardInteractions.includes("Custom key handler")) {
|
|
169
|
+
keyboardInteractions.push("Custom key handler");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (keyboardInteractions.length > 0) {
|
|
173
|
+
spec.keyboardInteractions = keyboardInteractions;
|
|
174
|
+
}
|
|
175
|
+
// Use axe-core results to refine: if certain violations exist, it tells us what's missing
|
|
176
|
+
if (axeResults?.violations) {
|
|
177
|
+
for (const v of axeResults.violations) {
|
|
178
|
+
// If button-name violation exists, the button has no accessible name
|
|
179
|
+
if (v.id === "button-name") {
|
|
180
|
+
spec.ariaLabel = undefined; // Explicitly missing
|
|
181
|
+
}
|
|
182
|
+
// If label violation exists, input lacks a label
|
|
183
|
+
if (v.id === "label") {
|
|
184
|
+
spec.ariaLabel = undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return spec;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Format axe-core results into our standard lint-like output structure.
|
|
192
|
+
*/
|
|
193
|
+
function formatAxeResults(axeResults) {
|
|
194
|
+
const categories = [];
|
|
195
|
+
const severityMap = {
|
|
196
|
+
critical: "critical",
|
|
197
|
+
serious: "critical",
|
|
198
|
+
moderate: "warning",
|
|
199
|
+
minor: "info",
|
|
200
|
+
};
|
|
201
|
+
// Group violations
|
|
202
|
+
for (const violation of axeResults.violations || []) {
|
|
203
|
+
const severity = severityMap[violation.impact] || "warning";
|
|
204
|
+
const nodes = violation.nodes.map((node) => ({
|
|
205
|
+
html: node.html?.substring(0, 200),
|
|
206
|
+
target: node.target,
|
|
207
|
+
failureSummary: node.failureSummary?.substring(0, 300),
|
|
208
|
+
}));
|
|
209
|
+
categories.push({
|
|
210
|
+
rule: violation.id,
|
|
211
|
+
severity,
|
|
212
|
+
count: violation.nodes.length,
|
|
213
|
+
description: violation.help,
|
|
214
|
+
wcagTags: violation.tags.filter((t) => t.startsWith("wcag") || t.startsWith("best-practice")),
|
|
215
|
+
helpUrl: violation.helpUrl,
|
|
216
|
+
nodes: nodes.slice(0, 10), // Cap at 10 per rule
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// Sort: critical first, then by count
|
|
220
|
+
categories.sort((a, b) => {
|
|
221
|
+
const sevOrder = { critical: 0, warning: 1, info: 2 };
|
|
222
|
+
if (sevOrder[a.severity] !== sevOrder[b.severity]) {
|
|
223
|
+
return sevOrder[a.severity] - sevOrder[b.severity];
|
|
224
|
+
}
|
|
225
|
+
return b.count - a.count;
|
|
226
|
+
});
|
|
227
|
+
// Summary
|
|
228
|
+
const summary = { critical: 0, warning: 0, info: 0, total: 0 };
|
|
229
|
+
for (const cat of categories) {
|
|
230
|
+
summary[cat.severity] += cat.count;
|
|
231
|
+
summary.total += cat.count;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
engine: "axe-core",
|
|
235
|
+
version: axeResults.testEngine?.version || "unknown",
|
|
236
|
+
mode: "jsdom-structural",
|
|
237
|
+
note: "JSDOM mode: structural/semantic checks only. Visual rules (color contrast, focus visibility) are disabled β use figma_lint_design for visual accessibility checks.",
|
|
238
|
+
categories,
|
|
239
|
+
summary,
|
|
240
|
+
passes: axeResults.passes?.length || 0,
|
|
241
|
+
incomplete: axeResults.incomplete?.length || 0,
|
|
242
|
+
inapplicable: axeResults.inapplicable?.length || 0,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
export function registerAccessibilityTools(server) {
|
|
246
|
+
server.tool("figma_scan_code_accessibility", "Scan HTML code for accessibility violations using axe-core (Deque). " +
|
|
247
|
+
"Runs structural/semantic checks via JSDOM: ARIA attributes, roles, labels, alt text, " +
|
|
248
|
+
"form labels, heading order, landmarks, semantic HTML, tabindex, duplicate IDs, lang attribute, and ~50 more rules. " +
|
|
249
|
+
"Visual checks (color contrast, focus visibility) are disabled in this mode β use figma_lint_design for visual a11y on the design side. " +
|
|
250
|
+
"Together, these two tools provide full-spectrum accessibility coverage across design and code. " +
|
|
251
|
+
"Pass component HTML directly or use with figma_check_design_parity for design-to-code a11y comparison. " +
|
|
252
|
+
"No Figma connection required β this is a standalone code analysis tool.", {
|
|
253
|
+
html: z.string().describe("HTML string to scan. Can be a full document or a component fragment (will be wrapped in a valid document)."),
|
|
254
|
+
tags: z.array(z.string()).optional().describe("WCAG tag filter. Examples: ['wcag2a'], ['wcag2aa'], ['wcag21aa'], ['wcag22aa'], ['best-practice']. " +
|
|
255
|
+
"Defaults to all structural rules if omitted."),
|
|
256
|
+
context: z.string().optional().describe("CSS selector to scope the scan to a specific element (e.g., '#my-component', '.card'). Scans entire document if omitted."),
|
|
257
|
+
includePassingRules: z.boolean().optional().describe("If true, includes count of passing and incomplete rules in the response (default: false)."),
|
|
258
|
+
mapToCodeSpec: z.boolean().optional().describe("If true, includes a codeSpec.accessibility object auto-extracted from the HTML + scan results. " +
|
|
259
|
+
"Pass this directly into figma_check_design_parity's codeSpec.accessibility field for automated design-to-code a11y parity checking."),
|
|
260
|
+
}, async ({ html, tags, context, includePassingRules, mapToCodeSpec }) => {
|
|
261
|
+
try {
|
|
262
|
+
const axeResults = await scanHtmlWithAxe(html, {
|
|
263
|
+
tags: tags || undefined,
|
|
264
|
+
context: context || undefined,
|
|
265
|
+
});
|
|
266
|
+
const formatted = formatAxeResults(axeResults);
|
|
267
|
+
// Optionally strip pass/incomplete counts to save tokens
|
|
268
|
+
if (!includePassingRules) {
|
|
269
|
+
delete formatted.passes;
|
|
270
|
+
delete formatted.incomplete;
|
|
271
|
+
delete formatted.inapplicable;
|
|
272
|
+
}
|
|
273
|
+
// Auto-generate CodeSpec.accessibility from HTML + results
|
|
274
|
+
if (mapToCodeSpec) {
|
|
275
|
+
formatted.codeSpecAccessibility = axeResultsToCodeSpec(html, axeResults);
|
|
276
|
+
formatted.codeSpecAccessibility._usage = "Pass this object as codeSpec.accessibility in figma_check_design_parity for automated a11y parity checking.";
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: JSON.stringify(formatted, null, 2),
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
const isDepsError = error.message?.includes("not installed");
|
|
289
|
+
logger.error({ error }, "Failed to scan code accessibility");
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: "text",
|
|
294
|
+
text: JSON.stringify({
|
|
295
|
+
error: error.message,
|
|
296
|
+
hint: isDepsError
|
|
297
|
+
? "Install dependencies: npm install axe-core jsdom"
|
|
298
|
+
: "Check that the HTML is valid. For visual accessibility checks, use figma_lint_design instead.",
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
isError: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
}
|
|
@@ -262,6 +264,17 @@ export class CloudWebSocketConnector {
|
|
|
262
264
|
return this.sendCommand('LINT_DESIGN', params, 120000);
|
|
263
265
|
}
|
|
264
266
|
// ============================================================================
|
|
267
|
+
// Component accessibility audit
|
|
268
|
+
// ============================================================================
|
|
269
|
+
async auditComponentAccessibility(nodeId, targetSize) {
|
|
270
|
+
const params = {};
|
|
271
|
+
if (nodeId)
|
|
272
|
+
params.nodeId = nodeId;
|
|
273
|
+
if (targetSize !== undefined)
|
|
274
|
+
params.targetSize = targetSize;
|
|
275
|
+
return this.sendCommand('AUDIT_COMPONENT_ACCESSIBILITY', params, 120000);
|
|
276
|
+
}
|
|
277
|
+
// ============================================================================
|
|
265
278
|
// FigJam operations
|
|
266
279
|
// ============================================================================
|
|
267
280
|
async createSticky(params) {
|