@mp3wizard/figma-console-mcp 1.22.2 → 1.22.5

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.
@@ -3289,6 +3289,7 @@ figma.ui.onmessage = async (msg) => {
3289
3289
  'wcag-non-text-contrast', 'wcag-color-only', 'wcag-focus-indicator',
3290
3290
  'wcag-letter-spacing', 'wcag-paragraph-spacing', 'wcag-image-alt',
3291
3291
  'wcag-heading-hierarchy', 'wcag-reflow', 'wcag-reading-order',
3292
+ 'wcag-disabled-no-context', 'token-misuse',
3292
3293
  'hardcoded-color', 'no-text-style', 'default-name', 'detached-component',
3293
3294
  'no-autolayout', 'empty-container'
3294
3295
  ];
@@ -3299,9 +3300,10 @@ figma.ui.onmessage = async (msg) => {
3299
3300
  'wcag-contrast', 'wcag-text-size', 'wcag-target-size', 'wcag-line-height',
3300
3301
  'wcag-non-text-contrast', 'wcag-color-only', 'wcag-focus-indicator',
3301
3302
  'wcag-letter-spacing', 'wcag-paragraph-spacing', 'wcag-image-alt',
3302
- 'wcag-heading-hierarchy', 'wcag-reflow', 'wcag-reading-order'
3303
+ 'wcag-heading-hierarchy', 'wcag-reflow', 'wcag-reading-order',
3304
+ 'wcag-disabled-no-context'
3303
3305
  ],
3304
- 'design-system': ['hardcoded-color', 'no-text-style', 'default-name', 'detached-component'],
3306
+ 'design-system': ['hardcoded-color', 'no-text-style', 'default-name', 'detached-component', 'token-misuse'],
3305
3307
  'layout': ['no-autolayout', 'empty-container']
3306
3308
  };
3307
3309
 
@@ -3310,15 +3312,17 @@ figma.ui.onmessage = async (msg) => {
3310
3312
  'wcag-target-size': 'critical',
3311
3313
  'wcag-non-text-contrast': 'critical',
3312
3314
  'wcag-color-only': 'critical',
3315
+ 'wcag-focus-indicator': 'critical',
3313
3316
  'wcag-text-size': 'warning',
3314
- 'wcag-line-height': 'warning',
3315
- 'wcag-focus-indicator': 'warning',
3316
3317
  'wcag-letter-spacing': 'warning',
3317
- 'wcag-paragraph-spacing': 'warning',
3318
3318
  'wcag-image-alt': 'warning',
3319
3319
  'wcag-heading-hierarchy': 'warning',
3320
3320
  'wcag-reflow': 'warning',
3321
3321
  'wcag-reading-order': 'warning',
3322
+ 'wcag-disabled-no-context': 'warning',
3323
+ 'wcag-line-height': 'info',
3324
+ 'wcag-paragraph-spacing': 'info',
3325
+ 'token-misuse': 'warning',
3322
3326
  'hardcoded-color': 'warning',
3323
3327
  'no-text-style': 'warning',
3324
3328
  'default-name': 'warning',
@@ -3327,20 +3331,47 @@ figma.ui.onmessage = async (msg) => {
3327
3331
  'empty-container': 'info'
3328
3332
  };
3329
3333
 
3334
+ // WCAG conformance level per rule — lets teams filter by target level (AA vs AAA)
3335
+ var wcagLevelMap = {
3336
+ 'wcag-contrast': 'aa', // 1.4.3 Contrast (Minimum) — Level AA
3337
+ 'wcag-target-size': 'aa', // 2.5.8 Target Size (Minimum) — Level AA
3338
+ 'wcag-non-text-contrast': 'aa', // 1.4.11 Non-text Contrast — Level AA
3339
+ 'wcag-color-only': 'a', // 1.4.1 Use of Color — Level A
3340
+ 'wcag-focus-indicator': 'aa', // 2.4.7 Focus Visible — Level AA
3341
+ 'wcag-text-size': 'best-practice', // Not actually 1.4.4; 12px minimum is a readability best practice
3342
+ 'wcag-line-height': 'best-practice', // 1.4.12 is about supporting user overrides, not requiring specific values
3343
+ 'wcag-letter-spacing': 'best-practice', // Negative spacing actively harms readability
3344
+ 'wcag-paragraph-spacing': 'best-practice', // 1.4.12 is about supporting user overrides
3345
+ 'wcag-image-alt': 'a', // 1.1.1 Non-text Content — Level A
3346
+ 'wcag-heading-hierarchy': 'a', // 1.3.1 Info and Relationships — Level A
3347
+ 'wcag-reflow': 'aa', // 1.4.10 Reflow — Level AA
3348
+ 'wcag-reading-order': 'a', // 1.3.2 Meaningful Sequence — Level A
3349
+ 'wcag-disabled-no-context': 'aa', // 4.1.2 Name, Role, Value — disabled elements need ARIA context
3350
+ 'token-misuse': 'design-system',
3351
+ 'hardcoded-color': 'design-system',
3352
+ 'no-text-style': 'design-system',
3353
+ 'default-name': 'design-system',
3354
+ 'detached-component': 'design-system',
3355
+ 'no-autolayout': 'design-system',
3356
+ 'empty-container': 'design-system'
3357
+ };
3358
+
3330
3359
  var ruleDescriptions = {
3331
- 'wcag-contrast': 'Text does not meet WCAG AA contrast ratio (4.5:1 normal, 3:1 large)',
3332
- 'wcag-text-size': 'Text size is below 12px minimum',
3333
- 'wcag-target-size': 'Interactive element is smaller than 24x24px minimum target size',
3334
- 'wcag-line-height': 'Line height is less than 1.5x the font size',
3335
- 'wcag-non-text-contrast': 'UI component or graphical object does not meet 3:1 contrast ratio against adjacent color (WCAG 1.4.11)',
3336
- 'wcag-color-only': 'Component variants appear to differ only by color without additional visual indicator (WCAG 1.4.1)',
3337
- 'wcag-focus-indicator': 'Interactive component is missing a focus/focused variant or focus indicator is insufficient (WCAG 2.4.7)',
3338
- 'wcag-letter-spacing': 'Negative letter spacing harms readability (WCAG 1.4.12)',
3339
- 'wcag-paragraph-spacing': 'Paragraph spacing is less than 2x the font size (WCAG 1.4.12)',
3340
- 'wcag-image-alt': 'Image or image fill has no description annotation for alternative text (WCAG 1.1.1)',
3341
- 'wcag-heading-hierarchy': 'Heading levels skip a level (e.g., H1 followed by H3) breaking document structure (WCAG 1.3.1)',
3342
- 'wcag-reflow': 'Frame uses fixed positioning without auto-layout, may not reflow for different viewport sizes (WCAG 1.4.10)',
3343
- 'wcag-reading-order': 'Visual position of elements does not match layer order, which may confuse screen readers (WCAG 1.3.2)',
3360
+ 'wcag-contrast': 'Text does not meet WCAG AA contrast ratio (4.5:1 normal, 3:1 large text ≥24px or ≥18.5px bold). Best practice: always target 4.5:1, especially in dark mode.',
3361
+ 'wcag-text-size': 'Text size is below 12px — readability best practice. Note: WCAG 1.4.4 requires supporting 200% text-only zoom (use rem/em units), not a specific minimum size.',
3362
+ 'wcag-target-size': 'Interactive element is smaller than 24x24px minimum target size (WCAG 2.5.8)',
3363
+ 'wcag-line-height': 'Line height is below 1.5x font size — best practice for readability. Note: WCAG 1.4.12 requires that content does not break when users override spacing to 1.5x, not that designs must use 1.5x by default.',
3364
+ 'wcag-non-text-contrast': 'UI component or graphical object does not meet 3:1 contrast ratio against adjacent color. Also applies to borders and chart elements against adjacent elements (WCAG 1.4.11)',
3365
+ 'wcag-color-only': 'Information is conveyed only through color change (e.g., error state uses red border without an error message or icon). Color can supplement but must not be the sole indicator (WCAG 1.4.1)',
3366
+ 'wcag-focus-indicator': 'Interactive component is missing a focus/focused variant or the focus indicator is insufficient. A visible focus state is critical — without it, keyboard users cannot navigate the interface (WCAG 2.4.7)',
3367
+ 'wcag-letter-spacing': 'Negative letter spacing actively harms readability. WCAG 1.4.12 requires content to support user-overridden spacing without breaking.',
3368
+ 'wcag-paragraph-spacing': 'Paragraph spacing is below 2x font size — best practice. WCAG 1.4.12 requires content to support user-overridden spacing to 2x without loss of content, not that designs must use 2x by default.',
3369
+ 'wcag-image-alt': 'Image or image fill has no description annotation for alternative text. All images need alt text; decorative images should be explicitly marked as decorative. Graphs and charts also need long descriptions (e.g., a data table) (WCAG 1.1.1)',
3370
+ 'wcag-heading-hierarchy': 'Heading levels skip a level (e.g., H1 followed by H3). Use H1 through H6 sequentially without skipping levels (WCAG 1.3.1)',
3371
+ 'wcag-reflow': 'Frame uses fixed positioning without auto-layout. Content must support 400% zoom on 1280px viewport (equivalent to 320px minimum width) without horizontal scrolling or loss of content (WCAG 1.4.10)',
3372
+ 'wcag-reading-order': 'Visual position of elements does not match layer order. Keyboard navigation and screen reader order must follow a logical sequence (WCAG 1.3.2)',
3373
+ 'wcag-disabled-no-context': 'Disabled variant has no tooltip, helper text, or annotation explaining why the element is disabled. Use aria-disabled (not HTML disabled) to keep the element focusable for screen readers, and add a tooltip so all users understand the disabled reason.',
3374
+ 'token-misuse': 'Variable name prefix does not match its usage context (e.g., a bg/* token used as a text fill, or a text/* token used as a background). This may cause contrast issues and indicates a misbound token.',
3344
3375
  'hardcoded-color': 'Fill color is not bound to a variable or style',
3345
3376
  'no-text-style': 'Text node is not using a text style',
3346
3377
  'default-name': 'Node has a default Figma name (e.g., "Frame 1")',
@@ -3995,8 +4026,131 @@ figma.ui.onmessage = async (msg) => {
3995
4026
  } catch (e) { /* slot sublayer */ }
3996
4027
  }
3997
4028
 
4029
+ // wcag-disabled-no-context: Disabled variant without tooltip/helper text (Isabella's pattern)
4030
+ if (activeRuleSet['wcag-disabled-no-context'] && nodeType === 'COMPONENT_SET' && !truncated) {
4031
+ try {
4032
+ var csChildren = node.children;
4033
+ if (csChildren) {
4034
+ for (var dci = 0; dci < csChildren.length && !truncated; dci++) {
4035
+ var dcVariant = csChildren[dci];
4036
+ try {
4037
+ var dcName = dcVariant.name || '';
4038
+ if (/(disabled|inactive)/i.test(dcName)) {
4039
+ // Check if disabled variant has tooltip, helper text, or descriptive child
4040
+ var hasContextChild = false;
4041
+ try {
4042
+ if (dcVariant.children) {
4043
+ for (var dcci = 0; dcci < dcVariant.children.length; dcci++) {
4044
+ var dcChild = dcVariant.children[dcci];
4045
+ try {
4046
+ var dcChildName = (dcChild.name || '').toLowerCase();
4047
+ // Look for tooltip, helper text, hint, description, or error message children
4048
+ if (/tooltip|helper|hint|description|message|caption|note|info|why|reason/i.test(dcChildName)) {
4049
+ hasContextChild = true;
4050
+ break;
4051
+ }
4052
+ // Recurse one level for nested tooltip/helper
4053
+ if (dcChild.children) {
4054
+ for (var dcgci = 0; dcgci < dcChild.children.length; dcgci++) {
4055
+ var dcGrandchild = dcChild.children[dcgci];
4056
+ try {
4057
+ if (/tooltip|helper|hint|description|message/i.test(dcGrandchild.name || '')) {
4058
+ hasContextChild = true;
4059
+ break;
4060
+ }
4061
+ } catch (e) { /* skip */ }
4062
+ }
4063
+ if (hasContextChild) break;
4064
+ }
4065
+ } catch (e) { /* skip */ }
4066
+ }
4067
+ }
4068
+ } catch (e) { /* skip */ }
4069
+ // Also check component description for disabled guidance
4070
+ var hasDisabledAnnotation = false;
4071
+ try {
4072
+ var csDesc = (node.description || '').toLowerCase();
4073
+ if (/disabled.*tooltip|disabled.*helper|disabled.*hint|aria-disabled|why.*disabled|disabled.*reason/i.test(csDesc)) {
4074
+ hasDisabledAnnotation = true;
4075
+ }
4076
+ } catch (e) { /* skip */ }
4077
+ if (!hasContextChild && !hasDisabledAnnotation) {
4078
+ if (totalFindings < maxFindings) {
4079
+ findings['wcag-disabled-no-context'].push({
4080
+ id: dcVariant.id,
4081
+ name: nodeName + ' / ' + dcVariant.name,
4082
+ suggestion: 'Disabled elements should remain focusable (use aria-disabled, not HTML disabled). Add a tooltip or helper text explaining why the element is disabled so screen reader users understand the context.'
4083
+ });
4084
+ totalFindings++;
4085
+ } else { truncated = true; }
4086
+ }
4087
+ }
4088
+ } catch (e) { /* skip variant */ }
4089
+ }
4090
+ }
4091
+ } catch (e) { /* slot sublayer */ }
4092
+ }
4093
+
3998
4094
  // ---- Design System checks ----
3999
4095
 
4096
+ // token-misuse: Variable name prefix doesn't match usage context
4097
+ if (activeRuleSet['token-misuse'] && !isPage && !isSection && !truncated) {
4098
+ try {
4099
+ var tmFills = node.fills;
4100
+ if (tmFills && tmFills.length > 0) {
4101
+ for (var tmi = 0; tmi < tmFills.length; tmi++) {
4102
+ var tmFill = tmFills[tmi];
4103
+ if (tmFill.type === 'SOLID' && tmFill.visible !== false) {
4104
+ try {
4105
+ if (tmFill.boundVariables && tmFill.boundVariables.color) {
4106
+ var tmVarId = tmFill.boundVariables.color.id;
4107
+ // Resolve variable name
4108
+ try {
4109
+ var tmVar = figma.variables.getVariableById(tmVarId);
4110
+ if (tmVar) {
4111
+ var tmVarName = tmVar.name.toLowerCase();
4112
+ var isBgToken = /^(bg|background|surface|fill)[\/-]/.test(tmVarName);
4113
+ var isTextNode = nodeType === 'TEXT';
4114
+ // Flag: bg/surface token used as text fill
4115
+ if (isTextNode && isBgToken) {
4116
+ if (totalFindings < maxFindings) {
4117
+ findings['token-misuse'].push({
4118
+ id: nodeId,
4119
+ name: nodeName,
4120
+ variable: tmVar.name,
4121
+ usage: 'text fill',
4122
+ expectedPrefix: 'text/*, fg/*, foreground/*',
4123
+ suggestion: 'This text node uses a background/surface token ("' + tmVar.name + '") as its fill color. This is likely a misbound token — use a text/foreground token instead.'
4124
+ });
4125
+ totalFindings++;
4126
+ } else { truncated = true; }
4127
+ }
4128
+ // Flag: text/foreground token used as frame/shape background
4129
+ var isTextToken = /^(text|fg|foreground|font)[\/-]/.test(tmVarName);
4130
+ var isContainerNode = nodeType === 'FRAME' || nodeType === 'COMPONENT' || nodeType === 'INSTANCE' || nodeType === 'RECTANGLE';
4131
+ if (isContainerNode && isTextToken && !truncated) {
4132
+ if (totalFindings < maxFindings) {
4133
+ findings['token-misuse'].push({
4134
+ id: nodeId,
4135
+ name: nodeName,
4136
+ variable: tmVar.name,
4137
+ usage: 'background fill',
4138
+ expectedPrefix: 'bg/*, background/*, surface/*',
4139
+ suggestion: 'This container uses a text/foreground token ("' + tmVar.name + '") as its background fill. This is likely a misbound token — use a background/surface token instead.'
4140
+ });
4141
+ totalFindings++;
4142
+ } else { truncated = true; }
4143
+ }
4144
+ }
4145
+ } catch (e) { /* can't resolve variable */ }
4146
+ }
4147
+ } catch (e) { /* no bound vars */ }
4148
+ }
4149
+ }
4150
+ }
4151
+ } catch (e) { /* slot sublayer */ }
4152
+ }
4153
+
4000
4154
  // hardcoded-color: Solid fills without variable binding or style
4001
4155
  if (activeRuleSet['hardcoded-color'] && !isPage && !isSection && !truncated) {
4002
4156
  try {
@@ -4190,6 +4344,7 @@ figma.ui.onmessage = async (msg) => {
4190
4344
  categories.push({
4191
4345
  rule: ruleId,
4192
4346
  severity: sev,
4347
+ wcagLevel: wcagLevelMap[ruleId] || null,
4193
4348
  count: findings[ruleId].length,
4194
4349
  description: ruleDescriptions[ruleId],
4195
4350
  nodes: findings[ruleId]
package/package.json CHANGED
@@ -1,102 +1 @@
1
- {
2
- "name": "@mp3wizard/figma-console-mcp",
3
- "version": "1.22.2",
4
- "description": "MCP server for accessing Figma plugin console logs and screenshots via Cloudflare Workers or local mode",
5
- "type": "module",
6
- "main": "dist/local.js",
7
- "types": "dist/local.d.ts",
8
- "bin": {
9
- "figma-console-mcp": "./dist/local.js"
10
- },
11
- "files": [
12
- "dist",
13
- "figma-desktop-bridge",
14
- "README.md",
15
- "LICENSE"
16
- ],
17
- "scripts": {
18
- "prepublishOnly": "npm run build",
19
- "deploy": "wrangler deploy",
20
- "dev": "wrangler dev",
21
- "dev:local": "tsx src/local.ts",
22
- "build": "npm run build:local && npm run build:cloudflare && npm run build:apps",
23
- "build:apps": "cross-env APP_NAME=token-browser vite build && cross-env APP_NAME=design-system-dashboard vite build",
24
- "dev:apps": "vite build --watch",
25
- "build:local": "tsc -p tsconfig.local.json",
26
- "build:cloudflare": "tsc -p tsconfig.cloudflare.json",
27
- "start": "wrangler dev",
28
- "test": "jest",
29
- "test:watch": "jest --watch",
30
- "test:coverage": "jest --coverage",
31
- "format": "biome format --write",
32
- "lint:fix": "biome lint --fix",
33
- "cf-typegen": "wrangler types",
34
- "type-check": "tsc --noEmit"
35
- },
36
- "keywords": [
37
- "mcp",
38
- "figma",
39
- "plugin",
40
- "console",
41
- "debugging",
42
- "ai",
43
- "anthropic",
44
- "claude",
45
- "cloudflare",
46
- "workers"
47
- ],
48
- "author": "Your Name",
49
- "license": "MIT",
50
- "repository": {
51
- "type": "git",
52
- "url": "https://github.com/mp3wizard/figma-console-mcp.git"
53
- },
54
- "engines": {
55
- "node": ">=18.0.0"
56
- },
57
- "overrides": {
58
- "path-to-regexp": ">=8.4.1",
59
- "vite": {
60
- "picomatch": ">=4.0.4"
61
- },
62
- "miniflare": {
63
- "undici": ">=7.24.0"
64
- },
65
- "handlebars": ">=4.7.9",
66
- "brace-expansion": ">=1.1.13",
67
- "lodash": ">=4.18.0",
68
- "picomatch": ">=4.0.4"
69
- },
70
- "dependencies": {
71
- "@cloudflare/puppeteer": "^1.0.4",
72
- "@modelcontextprotocol/ext-apps": "^1.0.1",
73
- "@modelcontextprotocol/sdk": "^1.26.0",
74
- "agents": "^0.7.1",
75
- "axe-core": "^4.11.2",
76
- "chrome-remote-interface": "^0.33.2",
77
- "jsdom": "^29.0.1",
78
- "pino": "^9.5.0",
79
- "pino-pretty": "^13.0.0",
80
- "puppeteer-core": "^23.11.1",
81
- "uuid": "^11.0.3",
82
- "ws": "^8.19.0",
83
- "zod": "^3.25.76"
84
- },
85
- "devDependencies": {
86
- "@biomejs/biome": "^2.2.5",
87
- "@types/jest": "^29.5.14",
88
- "@types/jsdom": "^28.0.1",
89
- "@types/node": "^22.10.2",
90
- "@types/uuid": "^10.0.0",
91
- "@types/ws": "^8.18.1",
92
- "cross-env": "^7.0.3",
93
- "jest": "^29.7.0",
94
- "ts-jest": "^29.2.5",
95
- "tsx": "^4.19.2",
96
- "typescript": "5.9.3",
97
- "vite": ">=6.4.2",
98
- "vite-plugin-singlefile": "^2.0.0",
99
- "wrangler": "^4.42.0",
100
- "zod-to-json-schema": "^3.25.1"
101
- }
102
- }
1
+ {"name":"@mp3wizard/figma-console-mcp","version":"1.22.5","description":"MCP server for accessing Figma plugin console logs and screenshots via Cloudflare Workers or local mode","type":"module","main":"dist/local.js","types":"dist/local.d.ts","bin":{"figma-console-mcp":"./dist/local.js"},"files":["dist","figma-desktop-bridge","README.md","LICENSE"],"scripts":{"prepublishOnly":"npm run build","deploy":"wrangler deploy","dev":"wrangler dev","dev:local":"tsx src/local.ts","build":"npm run build:local && npm run build:cloudflare && npm run build:apps","build:apps":"cross-env APP_NAME=token-browser vite build && cross-env APP_NAME=design-system-dashboard vite build","dev:apps":"vite build --watch","build:local":"tsc -p tsconfig.local.json","build:cloudflare":"tsc -p tsconfig.cloudflare.json","start":"wrangler dev","test":"jest","test:watch":"jest --watch","test:coverage":"jest --coverage","format":"biome format --write","lint:fix":"biome lint --fix","cf-typegen":"wrangler types","type-check":"tsc --noEmit"},"keywords":["mcp","figma","plugin","console","debugging","ai","anthropic","claude","cloudflare","workers"],"author":"Your Name","license":"MIT","repository":{"type":"git","url":"https://github.com/mp3wizard/figma-console-mcp.git"},"engines":{"node":">=18.0.0"},"overrides":{"path-to-regexp":">=8.4.1","vite":{"picomatch":">=4.0.4"},"miniflare":{"undici":">=7.24.0"},"handlebars":">=4.7.9","brace-expansion":">=1.1.13","lodash":">=4.18.0","picomatch":">=4.0.4","hono":">=4.12.12","@hono/node-server":">=1.19.13","basic-ftp":">=5.2.1"},"dependencies":{"@cloudflare/puppeteer":"^1.0.4","@modelcontextprotocol/ext-apps":"^1.0.1","@modelcontextprotocol/sdk":"^1.26.0","agents":"^0.7.1","axe-core":"^4.11.2","chrome-remote-interface":"^0.33.2","jsdom":"^29.0.1","pino":"^9.5.0","pino-pretty":"^13.0.0","puppeteer-core":"^23.11.1","uuid":"^11.0.3","ws":"^8.19.0","zod":"^3.25.76"},"devDependencies":{"@biomejs/biome":"^2.2.5","@types/jest":"^29.5.14","@types/jsdom":"^28.0.1","@types/node":"^22.10.2","@types/uuid":"^10.0.0","@types/ws":"^8.18.1","cross-env":"^7.0.3","jest":"^29.7.0","ts-jest":"^29.2.5","tsx":"^4.19.2","typescript":"5.9.3","vite":">=6.4.2","vite-plugin-singlefile":"^2.0.0","wrangler":"^4.42.0","zod-to-json-schema":"^3.25.1"}}