figma-console-mcp 1.22.0 β†’ 1.22.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/README.md CHANGED
@@ -8,7 +8,7 @@
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
- > **πŸ†• 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
+ > **πŸ†• Comprehensive Accessibility Scanning (v1.22.0):** Full-spectrum WCAG coverage across design and code β€” 13 design-side lint rules, component accessibility scorecards with color-blind simulation, code-side scanning via axe-core (104 rules), and design-to-code accessibility parity checking. No rule database to maintain. [See what's new β†’](docs/figma-mcp-vs-figma-console-mcp.md)
12
12
 
13
13
  ## What is this?
14
14
 
@@ -21,6 +21,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
21
21
  - **πŸ”§ Variable management** - Create, update, rename, and delete design tokens
22
22
  - **⚑ Real-time monitoring** - Watch logs as plugins execute
23
23
  - **πŸ“Œ FigJam boards** - Create stickies, flowcharts, tables, and code blocks on collaborative boards
24
+ - **β™Ώ Accessibility scanning** - 13 WCAG design checks, component scorecards, axe-core code scanning, design-to-code parity
24
25
  - **☁️ Cloud Write Relay** - Web AI clients (Claude.ai, v0, Replit) can design in Figma via cloud pairing
25
26
  - **πŸ”„ Four ways to connect** - Remote SSE, Cloud Mode, NPX, or Local Git
26
27
 
@@ -4430,7 +4430,47 @@ figma.ui.onmessage = async (msg) => {
4430
4430
  var variants = isComponentSet ? componentSet.children : [componentSet];
4431
4431
  var variantCount = variants.length;
4432
4432
 
4433
- // 1. State coverage analysis
4433
+ // ---- Classify component as interactive vs presentational ----
4434
+ var interactiveNames = /^(button|link|input|checkbox|radio|switch|toggle|tab|select|slider|dropdown|menu-item|search|combobox|listbox)/i;
4435
+ var presentationalNames = /^(alert|badge|card|avatar|divider|skeleton|tooltip|tag|chip|banner|callout|notification|toast|icon|image|separator|progress|spinner|loader|breadcrumb|label|heading|paragraph|caption|stat|meter|indicator)/i;
4436
+
4437
+ // Parse variant axes from variant names (e.g., "type=success, style=fill" β†’ {type: [...], style: [...]})
4438
+ var variantAxes = {};
4439
+ var hasStateAxis = false;
4440
+ for (var vai = 0; vai < variants.length; vai++) {
4441
+ var vParts = variants[vai].name.split(',');
4442
+ for (var vpi = 0; vpi < vParts.length; vpi++) {
4443
+ var kv = vParts[vpi].trim().split('=');
4444
+ if (kv.length === 2) {
4445
+ var axisName = kv[0].trim().toLowerCase();
4446
+ var axisValue = kv[1].trim().toLowerCase();
4447
+ if (!variantAxes[axisName]) variantAxes[axisName] = [];
4448
+ if (variantAxes[axisName].indexOf(axisValue) === -1) {
4449
+ variantAxes[axisName].push(axisValue);
4450
+ }
4451
+ // Check if this axis contains interaction state values
4452
+ if (axisName === 'state' && /(hover|focus|pressed|disabled|active)/i.test(axisValue)) {
4453
+ hasStateAxis = true;
4454
+ }
4455
+ }
4456
+ }
4457
+ }
4458
+
4459
+ var componentName = componentSet.name || '';
4460
+ var isInteractive = interactiveNames.test(componentName) || hasStateAxis;
4461
+ var isPresentational = !isInteractive && (presentationalNames.test(componentName) || !hasStateAxis);
4462
+ // If ambiguous, check if any variant mentions interaction states
4463
+ if (!isInteractive && !isPresentational) {
4464
+ for (var ami = 0; ami < variants.length; ami++) {
4465
+ if (/(hover|focus|pressed|disabled)/i.test(variants[ami].name)) {
4466
+ isInteractive = true;
4467
+ break;
4468
+ }
4469
+ }
4470
+ if (!isInteractive) isPresentational = true;
4471
+ }
4472
+
4473
+ // 1. Coverage analysis β€” adapts to component classification
4434
4474
  var stateKeywords = {
4435
4475
  'default': /(default|rest|idle|normal|base)/i,
4436
4476
  'hover': /(hover|hovered)/i,
@@ -4440,35 +4480,69 @@ figma.ui.onmessage = async (msg) => {
4440
4480
  'active': /(active|pressed|selected)/i,
4441
4481
  'loading': /(loading|spinner)/i
4442
4482
  };
4483
+
4484
+ var coveredCount = 0;
4485
+ var totalStates = 0;
4486
+ var missingStates = [];
4443
4487
  var statesCovered = {};
4444
4488
  var statesFound = {};
4445
- for (var sk in stateKeywords) {
4446
- statesCovered[sk] = false;
4447
- statesFound[sk] = null;
4448
- }
4449
- for (var vi = 0; vi < variants.length; vi++) {
4450
- var vName = variants[vi].name;
4451
- for (var sk2 in stateKeywords) {
4452
- if (stateKeywords[sk2].test(vName)) {
4453
- statesCovered[sk2] = true;
4454
- if (!statesFound[sk2]) statesFound[sk2] = vName;
4489
+ var coverageLabel = '';
4490
+ var variantAxisCoverage = null;
4491
+
4492
+ if (isInteractive) {
4493
+ // Interactive components: check for interaction states
4494
+ coverageLabel = 'interactive-states';
4495
+ for (var sk in stateKeywords) {
4496
+ statesCovered[sk] = false;
4497
+ statesFound[sk] = null;
4498
+ }
4499
+ for (var vi = 0; vi < variants.length; vi++) {
4500
+ var vName = variants[vi].name;
4501
+ for (var sk2 in stateKeywords) {
4502
+ if (stateKeywords[sk2].test(vName)) {
4503
+ statesCovered[sk2] = true;
4504
+ if (!statesFound[sk2]) statesFound[sk2] = vName;
4505
+ }
4455
4506
  }
4456
4507
  }
4457
- }
4458
- // If only one variant and no state keywords match, assume it's default
4459
- if (variantCount === 1 && !statesCovered['default']) {
4460
- statesCovered['default'] = true;
4461
- statesFound['default'] = variants[0].name;
4462
- }
4463
- var coveredCount = 0;
4464
- var totalStates = 0;
4465
- var missingStates = [];
4466
- for (var sk3 in statesCovered) {
4467
- totalStates++;
4468
- if (statesCovered[sk3]) {
4469
- coveredCount++;
4470
- } else {
4471
- missingStates.push(sk3);
4508
+ if (variantCount === 1 && !statesCovered['default']) {
4509
+ statesCovered['default'] = true;
4510
+ statesFound['default'] = variants[0].name;
4511
+ }
4512
+ for (var sk3 in statesCovered) {
4513
+ totalStates++;
4514
+ if (statesCovered[sk3]) {
4515
+ coveredCount++;
4516
+ } else {
4517
+ missingStates.push(sk3);
4518
+ }
4519
+ }
4520
+ } else {
4521
+ // Presentational components: check variant axis completeness
4522
+ coverageLabel = 'variant-axes';
4523
+ // Calculate expected combinations vs actual
4524
+ var axisNames = [];
4525
+ var axisCounts = [];
4526
+ var expectedCombinations = 1;
4527
+ for (var axName in variantAxes) {
4528
+ axisNames.push(axName);
4529
+ axisCounts.push(variantAxes[axName].length);
4530
+ expectedCombinations *= variantAxes[axName].length;
4531
+ }
4532
+ // Score: actual variants / expected combinations (capped at 100%)
4533
+ var axisCoverageRatio = expectedCombinations > 0 ? Math.min(1, variantCount / expectedCombinations) : 1;
4534
+ coveredCount = variantCount;
4535
+ totalStates = expectedCombinations;
4536
+ variantAxisCoverage = {
4537
+ axes: variantAxes,
4538
+ axisCount: axisNames.length,
4539
+ expectedCombinations: expectedCombinations,
4540
+ actualVariants: variantCount,
4541
+ completeness: Math.round(axisCoverageRatio * 100) + '%'
4542
+ };
4543
+ // For presentational, no "missing states" β€” instead note if combinations are incomplete
4544
+ if (variantCount < expectedCombinations) {
4545
+ missingStates.push(variantCount + '/' + expectedCombinations + ' axis combinations present');
4472
4546
  }
4473
4547
  }
4474
4548
 
@@ -4552,6 +4626,9 @@ figma.ui.onmessage = async (msg) => {
4552
4626
  }
4553
4627
 
4554
4628
  // 4. Target size analysis
4629
+ // WCAG 2.5.8 applies to interactive targets β€” presentational components
4630
+ // (badges, avatars, progress bars) are not tap targets by definition.
4631
+ // Skip target size checking for presentational components to avoid false positives.
4555
4632
  var targetSizeAnalysis = { minWidth: Infinity, minHeight: Infinity, variants: [], issues: [] };
4556
4633
  var minTarget = msg.targetSize || 24; // Default WCAG 2.5.8 minimum
4557
4634
  for (var tvi = 0; tvi < variants.length; tvi++) {
@@ -4562,7 +4639,8 @@ figma.ui.onmessage = async (msg) => {
4562
4639
  targetSizeAnalysis.variants.push({ name: variants[tvi].name, width: tw, height: th });
4563
4640
  if (tw < targetSizeAnalysis.minWidth) targetSizeAnalysis.minWidth = tw;
4564
4641
  if (th < targetSizeAnalysis.minHeight) targetSizeAnalysis.minHeight = th;
4565
- if (tw < minTarget || th < minTarget) {
4642
+ // Only flag target size issues for interactive components
4643
+ if (isInteractive && (tw < minTarget || th < minTarget)) {
4566
4644
  targetSizeAnalysis.issues.push({
4567
4645
  variant: variants[tvi].name,
4568
4646
  width: tw,
@@ -4658,14 +4736,15 @@ figma.ui.onmessage = async (msg) => {
4658
4736
 
4659
4737
  // ---- Compute overall score ----
4660
4738
  var scores = {};
4661
- // State coverage: percentage of states found
4662
- scores.stateCoverage = Math.round((coveredCount / totalStates) * 100);
4739
+ // Coverage: percentage of states (interactive) or axis combinations (presentational)
4740
+ scores.variantCoverage = totalStates > 0 ? Math.round((coveredCount / totalStates) * 100) : 100;
4663
4741
  // Focus indicator: 0 (missing), 50 (exists but no indicator), 100 (good indicator)
4664
- scores.focusIndicator = !focusAnalysis.hasVariant ? 0 : (!focusAnalysis.hasVisibleIndicator ? 50 : 100);
4742
+ // For presentational: N/A β†’ score 100 (don't penalize)
4743
+ scores.focusIndicator = isPresentational ? 100 : (!focusAnalysis.hasVariant ? 0 : (!focusAnalysis.hasVisibleIndicator ? 50 : 100));
4665
4744
  // Color differentiation: 100 if no issues, decremented per issue
4666
4745
  scores.colorDifferentiation = colorDifferentiation.checked === 0 ? 100 : Math.max(0, Math.round(((colorDifferentiation.checked - colorDifferentiation.issues.length) / colorDifferentiation.checked) * 100));
4667
- // Target size: 100 if all pass, 0 if any fail
4668
- scores.targetSize = targetSizeAnalysis.issues.length === 0 ? 100 : Math.max(0, Math.round(((targetSizeAnalysis.variants.length - targetSizeAnalysis.issues.length) / Math.max(1, targetSizeAnalysis.variants.length)) * 100));
4746
+ // Target size: N/A for presentational (not tap targets), scored for interactive
4747
+ scores.targetSize = isPresentational ? 100 : (targetSizeAnalysis.issues.length === 0 ? 100 : Math.max(0, Math.round(((targetSizeAnalysis.variants.length - targetSizeAnalysis.issues.length) / Math.max(1, targetSizeAnalysis.variants.length)) * 100)));
4669
4748
  // Annotations: 0 (nothing), 50 (description only), 100 (has a11y notes)
4670
4749
  scores.annotations = annotations.hasA11yNotes ? 100 : (annotations.hasDescription ? 50 : 0);
4671
4750
  // Color blind: percentage of simulations with no issues
@@ -4675,48 +4754,76 @@ figma.ui.onmessage = async (msg) => {
4675
4754
  }
4676
4755
  scores.colorBlindSafety = colorBlindAnalysis.simulations.length > 0 ? Math.round((cbPassCount / colorBlindAnalysis.simulations.length) * 100) : 100;
4677
4756
 
4678
- // Overall weighted score
4679
- var overall = Math.round(
4680
- scores.stateCoverage * 0.20 +
4681
- scores.focusIndicator * 0.20 +
4682
- scores.colorDifferentiation * 0.15 +
4683
- scores.targetSize * 0.15 +
4684
- scores.annotations * 0.10 +
4685
- scores.colorBlindSafety * 0.20
4686
- );
4757
+ // Overall weighted score β€” weights differ by component classification
4758
+ var overall;
4759
+ if (isInteractive) {
4760
+ // Interactive: focus and states matter most
4761
+ overall = Math.round(
4762
+ scores.variantCoverage * 0.20 +
4763
+ scores.focusIndicator * 0.20 +
4764
+ scores.colorDifferentiation * 0.15 +
4765
+ scores.targetSize * 0.15 +
4766
+ scores.annotations * 0.10 +
4767
+ scores.colorBlindSafety * 0.20
4768
+ );
4769
+ } else {
4770
+ // Presentational: variant completeness and color safety matter most, focus is N/A
4771
+ overall = Math.round(
4772
+ scores.variantCoverage * 0.25 +
4773
+ scores.colorDifferentiation * 0.25 +
4774
+ scores.annotations * 0.15 +
4775
+ scores.colorBlindSafety * 0.25 +
4776
+ scores.targetSize * 0.10
4777
+ );
4778
+ }
4687
4779
 
4688
4780
  // ---- Build response ----
4781
+ var coverageSection;
4782
+ if (isInteractive) {
4783
+ coverageSection = {
4784
+ mode: 'interactive-states',
4785
+ found: statesFound,
4786
+ missing: missingStates,
4787
+ coverage: coveredCount + '/' + totalStates
4788
+ };
4789
+ } else {
4790
+ coverageSection = {
4791
+ mode: 'variant-axes',
4792
+ axes: variantAxisCoverage,
4793
+ coverage: coveredCount + '/' + totalStates
4794
+ };
4795
+ }
4796
+
4689
4797
  var auditResult = {
4690
4798
  component: {
4691
4799
  id: componentSet.id,
4692
4800
  name: componentSet.name,
4693
4801
  type: componentSet.type,
4694
- variantCount: variantCount
4802
+ variantCount: variantCount,
4803
+ classification: isInteractive ? 'interactive' : 'presentational'
4695
4804
  },
4696
4805
  overallScore: overall,
4697
4806
  scores: scores,
4698
- stateCoverage: {
4699
- found: statesFound,
4700
- missing: missingStates,
4701
- coverage: coveredCount + '/' + totalStates
4702
- },
4703
- focusIndicator: focusAnalysis,
4807
+ variantCoverage: coverageSection,
4808
+ focusIndicator: isInteractive ? focusAnalysis : { notApplicable: true, details: 'Focus indicators are not expected for presentational components' },
4704
4809
  colorDifferentiation: colorDifferentiation,
4705
- targetSize: {
4810
+ targetSize: isInteractive ? {
4706
4811
  minimum: minTarget + 'x' + minTarget,
4707
4812
  smallest: targetSizeAnalysis.minWidth + 'x' + targetSizeAnalysis.minHeight,
4708
4813
  issues: targetSizeAnalysis.issues
4709
- },
4814
+ } : { notApplicable: true, details: 'Target size checks apply to interactive components (WCAG 2.5.8 is about tap targets)', smallest: targetSizeAnalysis.minWidth + 'x' + targetSizeAnalysis.minHeight },
4710
4815
  annotations: annotations,
4711
4816
  colorBlindSimulation: colorBlindAnalysis,
4712
4817
  recommendations: []
4713
4818
  };
4714
4819
 
4715
- // Generate recommendations
4716
- if (!focusAnalysis.hasVariant) {
4717
- auditResult.recommendations.push({ priority: 'high', area: 'focus', message: 'Add a focus/focused variant with a visible focus ring (WCAG 2.4.7)' });
4718
- } else if (!focusAnalysis.hasVisibleIndicator) {
4719
- auditResult.recommendations.push({ priority: 'medium', area: 'focus', message: 'Focus variant exists but lacks visible indicator β€” add a border or shadow' });
4820
+ // Generate recommendations β€” classification-aware
4821
+ if (isInteractive) {
4822
+ if (!focusAnalysis.hasVariant) {
4823
+ auditResult.recommendations.push({ priority: 'high', area: 'focus', message: 'Add a focus/focused variant with a visible focus ring (WCAG 2.4.7)' });
4824
+ } else if (!focusAnalysis.hasVisibleIndicator) {
4825
+ auditResult.recommendations.push({ priority: 'medium', area: 'focus', message: 'Focus variant exists but lacks visible indicator β€” add a border or shadow' });
4826
+ }
4720
4827
  }
4721
4828
  if (colorDifferentiation.issues.length > 0) {
4722
4829
  auditResult.recommendations.push({ priority: 'high', area: 'color', message: 'Add non-color indicators (icons, borders, text) to ' + colorDifferentiation.issues.length + ' state variant(s) (WCAG 1.4.1)' });
@@ -4728,16 +4835,23 @@ figma.ui.onmessage = async (msg) => {
4728
4835
  auditResult.recommendations.push({ priority: 'medium', area: 'documentation', message: 'Add a component description with usage guidelines' });
4729
4836
  }
4730
4837
  if (!annotations.hasA11yNotes) {
4731
- auditResult.recommendations.push({ priority: 'medium', area: 'documentation', message: 'Add accessibility notes (ARIA role, keyboard interactions, screen reader behavior)' });
4838
+ var a11yHint = isInteractive
4839
+ ? 'Add accessibility notes (ARIA role, keyboard interactions, screen reader behavior)'
4840
+ : 'Add accessibility notes (ARIA role, live region behavior, semantic usage)';
4841
+ auditResult.recommendations.push({ priority: 'medium', area: 'documentation', message: a11yHint });
4732
4842
  }
4733
4843
  if (colorBlindAnalysis.issues.length > 0) {
4734
4844
  auditResult.recommendations.push({ priority: 'medium', area: 'color-blind', message: colorBlindAnalysis.issues.join('; ') });
4735
4845
  }
4736
- for (var msi = 0; msi < missingStates.length; msi++) {
4737
- var ms = missingStates[msi];
4738
- if (ms === 'focus' || ms === 'disabled') {
4739
- auditResult.recommendations.push({ priority: 'medium', area: 'states', message: 'Consider adding a "' + ms + '" variant for complete interactive state coverage' });
4846
+ if (isInteractive) {
4847
+ for (var msi = 0; msi < missingStates.length; msi++) {
4848
+ var ms = missingStates[msi];
4849
+ if (ms === 'focus' || ms === 'disabled') {
4850
+ auditResult.recommendations.push({ priority: 'medium', area: 'states', message: 'Consider adding a "' + ms + '" variant for complete interactive state coverage' });
4851
+ }
4740
4852
  }
4853
+ } else if (variantAxisCoverage && variantCount < variantAxisCoverage.expectedCombinations) {
4854
+ auditResult.recommendations.push({ priority: 'low', area: 'coverage', message: variantCount + ' of ' + variantAxisCoverage.expectedCombinations + ' axis combinations present β€” consider adding missing variants for completeness' });
4741
4855
  }
4742
4856
 
4743
4857
  console.log('πŸŒ‰ [Desktop Bridge] Component audit complete: score ' + overall + '/100 for "' + componentSet.name + '"');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-console-mcp",
3
- "version": "1.22.0",
3
+ "version": "1.22.1",
4
4
  "description": "MCP server for accessing Figma plugin console logs and screenshots via Cloudflare Workers or local mode",
5
5
  "type": "module",
6
6
  "main": "dist/local.js",