@trishchuk/coolors-mcp 1.0.1 → 1.1.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.
Files changed (105) hide show
  1. package/.github/workflows/ci.yml +23 -20
  2. package/.github/workflows/deploy-docs.yml +6 -3
  3. package/.github/workflows/release.yml +11 -9
  4. package/README.md +123 -14
  5. package/dist/bin/server.js +997 -256
  6. package/dist/bin/server.js.map +1 -1
  7. package/dist/{chunk-P3ARRKLS.js → chunk-HOMDMKUY.js} +3 -1
  8. package/dist/{chunk-P3ARRKLS.js.map → chunk-HOMDMKUY.js.map} +1 -1
  9. package/dist/{chunk-IQ7NN26V.js → chunk-LHW2ZTOU.js} +14 -2
  10. package/dist/chunk-LHW2ZTOU.js.map +1 -0
  11. package/dist/color/index.js +1 -1
  12. package/dist/coolors-mcp.d.ts +4 -4
  13. package/dist/coolors-mcp.js +1 -1
  14. package/eslint.config.ts +13 -0
  15. package/jsr.json +1 -1
  16. package/package.json +16 -12
  17. package/src/bin/server.ts +13 -1
  18. package/src/color/__tests__/extract-colors.test.ts +20 -30
  19. package/src/color/apca.ts +105 -0
  20. package/src/color/color-blindness.ts +109 -0
  21. package/src/coolors-mcp.ts +1 -1
  22. package/src/session.ts +10 -2
  23. package/src/theme/matcher.ts +1 -1
  24. package/src/theme/refactor.ts +1 -1
  25. package/src/theme/types.ts +3 -0
  26. package/src/tools/__tests__/cohesion.test.ts +97 -0
  27. package/src/tools/__tests__/color-blindness.test.ts +45 -0
  28. package/src/tools/__tests__/color-conversion.test.ts +38 -0
  29. package/src/tools/__tests__/contrast-checker.test.ts +56 -0
  30. package/src/tools/__tests__/palette-export.test.ts +54 -0
  31. package/src/tools/adjust-color.tool.ts +80 -0
  32. package/src/tools/cohesion.tools.ts +380 -0
  33. package/src/tools/color-blindness.tool.ts +168 -0
  34. package/src/tools/color-conversion.tool.ts +1 -1
  35. package/src/tools/contrast-checker.tool.ts +53 -14
  36. package/src/tools/dislike-analyzer.tool.ts +41 -54
  37. package/src/tools/image-extraction.tools.ts +62 -115
  38. package/src/tools/index.ts +15 -2
  39. package/src/tools/palette-export.tool.ts +174 -0
  40. package/src/tools/palette-with-locks.tool.ts +8 -6
  41. package/src/types.ts +2 -3
  42. package/tsconfig.json +12 -2
  43. package/vitest.config.js +1 -3
  44. package/.claude/settings.local.json +0 -35
  45. package/.env +0 -2
  46. package/.mcp.json +0 -12
  47. package/CLAUDE.md +0 -201
  48. package/DOCUMENTATION.md +0 -274
  49. package/GEMINI.md +0 -54
  50. package/TOOLS_UK.md +0 -233
  51. package/demo/content_based_color.png +0 -0
  52. package/demo/music-player.html +0 -621
  53. package/demo/podcast-player.html +0 -903
  54. package/dist/chunk-IQ7NN26V.js.map +0 -1
  55. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +0 -111
  56. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +0 -7
  57. package/docs/.vitepress/cache/deps/_metadata.json +0 -127
  58. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +0 -12
  59. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +0 -7
  60. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +0 -13614
  61. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +0 -7
  62. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +0 -10698
  63. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +0 -7
  64. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +0 -5609
  65. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +0 -7
  66. package/docs/.vitepress/cache/deps/cytoscape.js +0 -36234
  67. package/docs/.vitepress/cache/deps/cytoscape.js.map +0 -7
  68. package/docs/.vitepress/cache/deps/dayjs.js +0 -507
  69. package/docs/.vitepress/cache/deps/dayjs.js.map +0 -7
  70. package/docs/.vitepress/cache/deps/debug.js +0 -512
  71. package/docs/.vitepress/cache/deps/debug.js.map +0 -7
  72. package/docs/.vitepress/cache/deps/package.json +0 -3
  73. package/docs/.vitepress/cache/deps/prismjs.js +0 -1638
  74. package/docs/.vitepress/cache/deps/prismjs.js.map +0 -7
  75. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +0 -235
  76. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +0 -7
  77. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +0 -173
  78. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +0 -7
  79. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +0 -27
  80. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +0 -7
  81. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +0 -72
  82. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +0 -7
  83. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +0 -56
  84. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +0 -7
  85. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +0 -107
  86. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +0 -7
  87. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -5074
  88. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
  89. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -584
  90. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
  91. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +0 -1483
  92. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +0 -7
  93. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +0 -1779
  94. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +0 -7
  95. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +0 -2023
  96. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +0 -7
  97. package/docs/.vitepress/cache/deps/vue.js +0 -344
  98. package/docs/.vitepress/cache/deps/vue.js.map +0 -7
  99. package/examples/theme-matching.md +0 -113
  100. package/mcp-config.json +0 -8
  101. package/note.md +0 -34
  102. package/research_results.md +0 -53
  103. package/src/tools/colors.ts +0 -31
  104. package/src/tools/registry.ts +0 -142
  105. package/src/tools/simple-tools.ts +0 -37
package/src/bin/server.ts CHANGED
@@ -24,16 +24,28 @@ const server = new CoolorsMcp({
24
24
  instructions:
25
25
  "Advanced color operations server with Material Design 3 support, CSS theme matching, image extraction, and accessibility compliance. Uses HCT color space for perceptually accurate operations.",
26
26
  name: "coolors-mcp",
27
- version: "1.0.0",
27
+ version: "1.1.0",
28
28
  });
29
29
 
30
30
  // Core color operations: conversion, distance metrics, accessibility
31
31
  server.addTool(tools.colorConversionTool);
32
32
  server.addTool(tools.colorDistanceTool);
33
33
  server.addTool(tools.contrastCheckerTool);
34
+ server.addTool(tools.adjustColorTool);
34
35
  server.addTool(tools.paletteGeneratorTool);
35
36
  server.addTool(tools.paletteWithLocksTool);
36
37
  server.addTool(tools.gradientGeneratorTool);
38
+ server.addTool(tools.exportPaletteTool);
39
+
40
+ // Color-blindness simulation & accessibility audit
41
+ server.addTool(tools.simulateColorBlindnessTool);
42
+ server.addTool(tools.checkPaletteAccessibilityTool);
43
+
44
+ // Visual cohesion: tonal scales, state colors, palette consistency, semantics
45
+ server.addTool(tools.generateTonalScaleTool);
46
+ server.addTool(tools.generateStateColorsTool);
47
+ server.addTool(tools.analyzePaletteConsistencyTool);
48
+ server.addTool(tools.generateSemanticPaletteTool);
37
49
 
38
50
  // Material Design 3: theme generation, harmonization, tonal palettes
39
51
  server.addTool(tools.generateMaterialThemeTool);
@@ -27,9 +27,8 @@ describe("Color Extraction", () => {
27
27
  });
28
28
 
29
29
  it("should extract colors with default options", async () => {
30
- const { QuantizerCelebi } = await import(
31
- "../quantize/quantizer_celebi.js"
32
- );
30
+ const { QuantizerCelebi } =
31
+ await import("../quantize/quantizer_celebi.js");
33
32
  const { Score } = await import("../score/score.js");
34
33
 
35
34
  // Mock quantizer to return test colors
@@ -106,9 +105,8 @@ describe("Color Extraction", () => {
106
105
  });
107
106
 
108
107
  it("should extract colors without scoring", async () => {
109
- const { QuantizerCelebi } = await import(
110
- "../quantize/quantizer_celebi.js"
111
- );
108
+ const { QuantizerCelebi } =
109
+ await import("../quantize/quantizer_celebi.js");
112
110
  const { Score } = await import("../score/score.js");
113
111
 
114
112
  const mockQuantized = new Map([
@@ -140,9 +138,8 @@ describe("Color Extraction", () => {
140
138
  });
141
139
 
142
140
  it("should handle empty image data", async () => {
143
- const { QuantizerCelebi } = await import(
144
- "../quantize/quantizer_celebi.js"
145
- );
141
+ const { QuantizerCelebi } =
142
+ await import("../quantize/quantizer_celebi.js");
146
143
  const { Score } = await import("../score/score.js");
147
144
 
148
145
  vi.mocked(QuantizerCelebi.quantize).mockReturnValue(new Map());
@@ -159,9 +156,8 @@ describe("Color Extraction", () => {
159
156
  });
160
157
 
161
158
  it("should calculate correct percentages", async () => {
162
- const { QuantizerCelebi } = await import(
163
- "../quantize/quantizer_celebi.js"
164
- );
159
+ const { QuantizerCelebi } =
160
+ await import("../quantize/quantizer_celebi.js");
165
161
  const { Score } = await import("../score/score.js");
166
162
 
167
163
  const mockQuantized = new Map([
@@ -194,9 +190,8 @@ describe("Color Extraction", () => {
194
190
  });
195
191
 
196
192
  it("should extract theme palette with primary color", async () => {
197
- const { QuantizerCelebi } = await import(
198
- "../quantize/quantizer_celebi.js"
199
- );
193
+ const { QuantizerCelebi } =
194
+ await import("../quantize/quantizer_celebi.js");
200
195
  const { Score } = await import("../score/score.js");
201
196
 
202
197
  const mockQuantized = new Map([
@@ -219,9 +214,8 @@ describe("Color Extraction", () => {
219
214
  });
220
215
 
221
216
  it("should extract secondary with different hue", async () => {
222
- const { QuantizerCelebi } = await import(
223
- "../quantize/quantizer_celebi.js"
224
- );
217
+ const { QuantizerCelebi } =
218
+ await import("../quantize/quantizer_celebi.js");
225
219
  const { Score } = await import("../score/score.js");
226
220
 
227
221
  const mockQuantized = new Map([
@@ -251,9 +245,8 @@ describe("Color Extraction", () => {
251
245
  });
252
246
 
253
247
  it("should find neutral color with low chroma", async () => {
254
- const { QuantizerCelebi } = await import(
255
- "../quantize/quantizer_celebi.js"
256
- );
248
+ const { QuantizerCelebi } =
249
+ await import("../quantize/quantizer_celebi.js");
257
250
  const { Score } = await import("../score/score.js");
258
251
 
259
252
  const mockQuantized = new Map([
@@ -280,9 +273,8 @@ describe("Color Extraction", () => {
280
273
  });
281
274
 
282
275
  it("should find error color in red hue range", async () => {
283
- const { QuantizerCelebi } = await import(
284
- "../quantize/quantizer_celebi.js"
285
- );
276
+ const { QuantizerCelebi } =
277
+ await import("../quantize/quantizer_celebi.js");
286
278
  const { Score } = await import("../score/score.js");
287
279
 
288
280
  const primaryArgb = utils.argbFromRgb(103, 80, 164);
@@ -311,9 +303,8 @@ describe("Color Extraction", () => {
311
303
  });
312
304
 
313
305
  it("should throw error for empty image", async () => {
314
- const { QuantizerCelebi } = await import(
315
- "../quantize/quantizer_celebi.js"
316
- );
306
+ const { QuantizerCelebi } =
307
+ await import("../quantize/quantizer_celebi.js");
317
308
  const { Score } = await import("../score/score.js");
318
309
 
319
310
  vi.mocked(QuantizerCelebi.quantize).mockReturnValue(new Map());
@@ -331,9 +322,8 @@ describe("Color Extraction", () => {
331
322
  });
332
323
 
333
324
  it("should handle single color image", async () => {
334
- const { QuantizerCelebi } = await import(
335
- "../quantize/quantizer_celebi.js"
336
- );
325
+ const { QuantizerCelebi } =
326
+ await import("../quantize/quantizer_celebi.js");
337
327
  const { Score } = await import("../score/score.js");
338
328
 
339
329
  const mockQuantized = new Map([
@@ -0,0 +1,105 @@
1
+ /**
2
+ * APCA — Accessible Perceptual Contrast Algorithm (WCAG 3 draft)
3
+ *
4
+ * Implementation of APCA W3 0.1.9 / SAPC by Andrew Somers (Myndex).
5
+ * Returns Lc, an absolute contrast value typically in the range [-108, +106].
6
+ * Sign indicates polarity: positive when text is darker than background
7
+ * ("dark on light"), negative for light text on dark background. The magnitude
8
+ * (|Lc|) maps to readability thresholds via APCA's "Bronze Simple Tables":
9
+ *
10
+ * |Lc| >= 75 body text
11
+ * |Lc| >= 60 content text
12
+ * |Lc| >= 45 large fluent text
13
+ * |Lc| >= 30 non-content / spot text
14
+ * |Lc| < 15 invisible — fails for any text use
15
+ *
16
+ * Reference: https://github.com/Myndex/SAPC-APCA
17
+ */
18
+
19
+ import type { RGB } from "./types.js";
20
+
21
+ // APCA constants (W3-0.1.9 4g)
22
+ const SA98G = {
23
+ blkClmp: 1.414,
24
+ blkThrs: 0.022,
25
+ deltaYmin: 0.0005,
26
+ loBoTclip: -0.6,
27
+ loBoTexp: 0.74,
28
+ loClip: 0.1,
29
+ // Polarity exponents/factors
30
+ normBG: 0.56,
31
+ normTXT: 0.57,
32
+ revBG: 0.62,
33
+ revTXT: 0.65,
34
+ // Soft-clip and scale
35
+ scaleBoW: 1.14,
36
+ scaleWoB: 1.14,
37
+ trailingW: 0.027,
38
+ } as const;
39
+
40
+ const sRGBtrc = 2.4;
41
+ const Rco = 0.2126729;
42
+ const Gco = 0.7151522;
43
+ const Bco = 0.072175;
44
+
45
+ /**
46
+ * Compute APCA Lc for foreground text on background.
47
+ * `text` is the text/foreground color, `bg` is the background color.
48
+ */
49
+ export function apcaContrast(text: RGB, bg: RGB): number {
50
+ let txtY = apcaY(text);
51
+ let bgY = apcaY(bg);
52
+
53
+ // Soft black clamp
54
+ if (txtY <= SA98G.blkThrs) {
55
+ txtY += Math.pow(SA98G.blkThrs - txtY, SA98G.blkClmp);
56
+ }
57
+ if (bgY <= SA98G.blkThrs) {
58
+ bgY += Math.pow(SA98G.blkThrs - bgY, SA98G.blkClmp);
59
+ }
60
+
61
+ if (Math.abs(bgY - txtY) < SA98G.deltaYmin) return 0;
62
+
63
+ let outputContrast: number;
64
+
65
+ if (bgY > txtY) {
66
+ // Normal polarity (dark text on light background) — positive Lc
67
+ const SAPC =
68
+ (Math.pow(bgY, SA98G.normBG) - Math.pow(txtY, SA98G.normTXT)) *
69
+ SA98G.scaleBoW;
70
+ outputContrast = SAPC < SA98G.loClip ? 0 : SAPC - SA98G.trailingW;
71
+ } else {
72
+ // Reverse polarity (light text on dark background) — negative Lc
73
+ const SAPC =
74
+ (Math.pow(bgY, SA98G.revBG) - Math.pow(txtY, SA98G.revTXT)) *
75
+ SA98G.scaleWoB;
76
+ outputContrast = SAPC > -SA98G.loClip ? 0 : SAPC + SA98G.trailingW;
77
+ }
78
+
79
+ return outputContrast * 100;
80
+ }
81
+
82
+ /**
83
+ * Classify an APCA Lc value against the Bronze Simple level thresholds.
84
+ */
85
+ export function apcaLevel(lc: number): {
86
+ body: boolean;
87
+ content: boolean;
88
+ large: boolean;
89
+ spot: boolean;
90
+ } {
91
+ const abs = Math.abs(lc);
92
+ return {
93
+ body: abs >= 75,
94
+ content: abs >= 60,
95
+ large: abs >= 45,
96
+ spot: abs >= 30,
97
+ };
98
+ }
99
+
100
+ function apcaY(rgb: RGB): number {
101
+ const r = Math.pow(rgb.r / 255, sRGBtrc);
102
+ const g = Math.pow(rgb.g / 255, sRGBtrc);
103
+ const b = Math.pow(rgb.b / 255, sRGBtrc);
104
+ return Rco * r + Gco * g + Bco * b;
105
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Color Vision Deficiency (CVD) simulation.
3
+ *
4
+ * Linear-sRGB transformation matrices from Machado, Oliveira, and Fernandes
5
+ * (2009), "A Physiologically-based Model for Simulation of Color Vision
6
+ * Deficiency". Severity 1.0 matrices are used for the dichromatic forms
7
+ * (protanopia/deuteranopia/tritanopia); severity 0.6 matrices are used for the
8
+ * milder anomaly forms. Achromatopsia uses ITU-R BT.709 luminance.
9
+ *
10
+ * Pipeline: sRGB → linear sRGB → matrix → sRGB.
11
+ */
12
+
13
+ import type { RGB } from "./types.js";
14
+
15
+ export type CvdType =
16
+ | "achromatopsia"
17
+ | "deuteranomaly"
18
+ | "deuteranopia"
19
+ | "protanomaly"
20
+ | "protanopia"
21
+ | "tritanomaly"
22
+ | "tritanopia";
23
+
24
+ // Confusion-line matrices applied in linear sRGB space (Machado et al. 2009
25
+ // linear approximation, common in libraries like color-blind / colorjs.io).
26
+ // Each matrix maps linear-sRGB to the visible linear-sRGB for the given CVD.
27
+ const CVD_MATRICES: Record<
28
+ Exclude<CvdType, "achromatopsia">,
29
+ [number, number, number, number, number, number, number, number, number]
30
+ > = {
31
+ deuteranomaly: [
32
+ 0.547494, 0.607765, -0.155259, 0.181692, 0.781742, 0.036566, -0.01041,
33
+ 0.027275, 0.983136,
34
+ ],
35
+ deuteranopia: [
36
+ 0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.01182,
37
+ 0.04294, 0.968881,
38
+ ],
39
+ // Mild forms (-omaly: severity ~0.6, Machado severity 0.6 table)
40
+ protanomaly: [
41
+ 0.458064, 0.679578, -0.137642, 0.092785, 0.846313, 0.060902, -0.007494,
42
+ -0.016807, 1.024301,
43
+ ],
44
+ // Strong forms (-opia: full dichromacy, severity 1.0)
45
+ protanopia: [
46
+ 0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882,
47
+ -0.048116, 1.051998,
48
+ ],
49
+ tritanomaly: [
50
+ 1.193214, -0.109812, -0.083402, 0.058694, 0.901185, 0.040121, -0.005978,
51
+ 0.401901, 0.604077,
52
+ ],
53
+ tritanopia: [
54
+ 1.255528, -0.076749, -0.178779, -0.078411, 0.930809, 0.147602, 0.004733,
55
+ 0.691367, 0.3039,
56
+ ],
57
+ };
58
+
59
+ /**
60
+ * Simulate how a color appears to a viewer with the given color vision
61
+ * deficiency.
62
+ */
63
+ export function simulateCvd(rgb: RGB, type: CvdType): RGB {
64
+ if (type === "achromatopsia") {
65
+ // ITU-R BT.709 luminance (Rec. 709) — perceived brightness.
66
+ const y = 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b;
67
+ const g = Math.max(0, Math.min(255, Math.round(y)));
68
+ return { b: g, g, r: g };
69
+ }
70
+
71
+ const m = CVD_MATRICES[type];
72
+ const r = srgbToLinear(rgb.r);
73
+ const g = srgbToLinear(rgb.g);
74
+ const b = srgbToLinear(rgb.b);
75
+
76
+ const rOut = m[0] * r + m[1] * g + m[2] * b;
77
+ const gOut = m[3] * r + m[4] * g + m[5] * b;
78
+ const bOut = m[6] * r + m[7] * g + m[8] * b;
79
+
80
+ return {
81
+ b: linearToSrgb(bOut),
82
+ g: linearToSrgb(gOut),
83
+ r: linearToSrgb(rOut),
84
+ };
85
+ }
86
+
87
+ function linearToSrgb(c: number): number {
88
+ const v = c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
89
+ return Math.max(0, Math.min(255, Math.round(v * 255)));
90
+ }
91
+
92
+ function srgbToLinear(c: number): number {
93
+ const n = c / 255;
94
+ return n <= 0.04045 ? n / 12.92 : Math.pow((n + 0.055) / 1.055, 2.4);
95
+ }
96
+
97
+ /**
98
+ * Approximate share of the population affected by each CVD type (worldwide,
99
+ * combining male+female prevalence as reported by Colour Blind Awareness).
100
+ */
101
+ export const CVD_PREVALENCE: Record<CvdType, number> = {
102
+ achromatopsia: 0.003,
103
+ deuteranomaly: 5.0,
104
+ deuteranopia: 1.0,
105
+ protanomaly: 1.0,
106
+ protanopia: 1.0,
107
+ tritanomaly: 0.01,
108
+ tritanopia: 0.003,
109
+ };
@@ -1,5 +1,5 @@
1
1
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
- import { EventEmitter } from "events";
2
+ import { EventEmitter } from "node:events";
3
3
  import { StrictEventEmitter } from "strict-event-emitter-types";
4
4
 
5
5
  import { CoolorsMCPSession } from "./session.js";
package/src/session.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  ListToolsRequestSchema,
7
7
  McpError,
8
8
  } from "@modelcontextprotocol/sdk/types.js";
9
- import { EventEmitter } from "events";
9
+ import { EventEmitter } from "node:events";
10
10
  import { StrictEventEmitter } from "strict-event-emitter-types";
11
11
  import { toJsonSchema } from "xsschema";
12
12
 
@@ -99,9 +99,17 @@ export class CoolorsMCPSession extends (EventEmitter as {
99
99
  args = parsed.value;
100
100
  }
101
101
 
102
+ const noop = () => {};
103
+ const context = {
104
+ client: { version: this.#server.getClientVersion() },
105
+ log: { debug: noop, error: noop, info: noop, warn: noop },
106
+ reportProgress: async () => {},
107
+ session: undefined,
108
+ streamContent: async () => {},
109
+ };
102
110
  const result = await tool.execute(
103
111
  args as Parameters<typeof tool.execute>[0],
104
- {} as CoolorsMCPSessionAuth,
112
+ context as Parameters<typeof tool.execute>[1],
105
113
  );
106
114
 
107
115
  if (typeof result === "string") {
@@ -256,7 +256,7 @@ function calculateHctDistance(color1: HCT, color2: HCT): number {
256
256
  * Calculate multi-factor match score
257
257
  */
258
258
  function calculateMatchScore(
259
- inputHct: HCT,
259
+ _inputHct: HCT,
260
260
  candidate: ThemeVariable,
261
261
  distance: number,
262
262
  weights: { accessibility: number; perceptual: number; semantic: number },
@@ -165,7 +165,7 @@ export function refactorCss(
165
165
 
166
166
  const replacements: ColorReplacement[] = [];
167
167
  const warnings: RefactoringWarning[] = [];
168
- let refactoredCss = css;
168
+ let refactoredCss: string;
169
169
  let totalColors = 0;
170
170
  let replacedColors = 0;
171
171
  let totalConfidence = 0;
@@ -142,11 +142,14 @@ export interface ThemeVariable {
142
142
  * Collection of theme variables organized by role
143
143
  */
144
144
  export interface ThemeVariables {
145
+ background?: ThemeVariable[];
145
146
  custom?: Record<string, ThemeVariable[]>;
146
147
  error?: ThemeVariable[];
147
148
  neutral?: ThemeVariable[];
149
+ outline?: ThemeVariable[];
148
150
  primary?: ThemeVariable[];
149
151
  secondary?: ThemeVariable[];
152
+ shadow?: ThemeVariable[];
150
153
  surface?: ThemeVariable[];
151
154
  tertiary?: ThemeVariable[];
152
155
  }
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ analyzePaletteConsistencyTool,
5
+ generateSemanticPaletteTool,
6
+ generateStateColorsTool,
7
+ generateTonalScaleTool,
8
+ } from "../cohesion.tools.js";
9
+
10
+ describe("generateTonalScaleTool", () => {
11
+ it("generates 11 stops by default", async () => {
12
+ const result = (await generateTonalScaleTool.execute({
13
+ seed: "#6750a4",
14
+ })) as string;
15
+ expect(result).toContain("| 50 |");
16
+ expect(result).toContain("| 500 |");
17
+ expect(result).toContain("| 950 |");
18
+ expect(result.match(/^\| \d+ \|/gm)?.length ?? 0).toBeGreaterThanOrEqual(
19
+ 11,
20
+ );
21
+ });
22
+
23
+ it("emits CSS custom properties with the given prefix", async () => {
24
+ const result = (await generateTonalScaleTool.execute({
25
+ name: "brand",
26
+ seed: "#6750a4",
27
+ })) as string;
28
+ expect(result).toContain("--brand-50:");
29
+ expect(result).toContain("--brand-950:");
30
+ });
31
+
32
+ it("rejects invalid colors", async () => {
33
+ const result = await generateTonalScaleTool.execute({ seed: "nope" });
34
+ expect(result).toMatch(/Invalid color format/);
35
+ });
36
+ });
37
+
38
+ describe("generateStateColorsTool", () => {
39
+ it("produces all expected interaction states", async () => {
40
+ const result = (await generateStateColorsTool.execute({
41
+ base: "#6750a4",
42
+ })) as string;
43
+ for (const state of [
44
+ "base",
45
+ "hover",
46
+ "active",
47
+ "pressed",
48
+ "focus",
49
+ "disabled",
50
+ "selected",
51
+ ]) {
52
+ expect(result).toContain(`| ${state} |`);
53
+ }
54
+ });
55
+ });
56
+
57
+ describe("analyzePaletteConsistencyTool", () => {
58
+ it("returns a cohesion score for a small palette", async () => {
59
+ const result = (await analyzePaletteConsistencyTool.execute({
60
+ colors: ["#6750a4", "#7f67be", "#a48dc8"],
61
+ })) as string;
62
+ expect(result).toContain("Overall cohesion");
63
+ });
64
+
65
+ it("flags outliers", async () => {
66
+ const result = (await analyzePaletteConsistencyTool.execute({
67
+ colors: ["#6750a4", "#7f67be", "#a48dc8", "#ff0000"],
68
+ })) as string;
69
+ expect(result).toMatch(/outlier/i);
70
+ });
71
+
72
+ it("requires at least 2 colors", async () => {
73
+ const result = await analyzePaletteConsistencyTool.execute({
74
+ colors: ["#6750a4"],
75
+ });
76
+ expect(result).toMatch(/at least 2/i);
77
+ });
78
+ });
79
+
80
+ describe("generateSemanticPaletteTool", () => {
81
+ it("returns all 7 semantic roles", async () => {
82
+ const result = (await generateSemanticPaletteTool.execute({
83
+ brand: "#6750a4",
84
+ })) as string;
85
+ for (const role of [
86
+ "primary",
87
+ "secondary",
88
+ "tertiary",
89
+ "success",
90
+ "warning",
91
+ "error",
92
+ "info",
93
+ ]) {
94
+ expect(result).toContain(`--color-${role}:`);
95
+ }
96
+ });
97
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { simulateCvd } from "../../color/color-blindness.js";
4
+ import {
5
+ checkPaletteAccessibilityTool,
6
+ simulateColorBlindnessTool,
7
+ } from "../color-blindness.tool.js";
8
+
9
+ describe("simulateCvd", () => {
10
+ it("achromatopsia collapses to grayscale", () => {
11
+ const r = simulateCvd({ b: 70, g: 80, r: 230 }, "achromatopsia");
12
+ expect(r.r).toBe(r.g);
13
+ expect(r.g).toBe(r.b);
14
+ });
15
+
16
+ it("protanopia shifts pure red toward yellow/olive", () => {
17
+ const r = simulateCvd({ b: 0, g: 0, r: 255 }, "protanopia");
18
+ // Protanopes lose long-wavelength sensitivity → red becomes dark/yellow-ish.
19
+ expect(r.r).toBeLessThan(255);
20
+ expect(r.g).toBeGreaterThan(0);
21
+ });
22
+ });
23
+
24
+ describe("simulateColorBlindnessTool", () => {
25
+ it("returns a row per color", async () => {
26
+ const result = (await simulateColorBlindnessTool.execute({
27
+ colors: ["#e63946", "#2a9d8f"],
28
+ types: ["protanopia"],
29
+ })) as string;
30
+ expect(result).toContain("#e63946");
31
+ expect(result).toContain("#2a9d8f");
32
+ expect(result).toContain("protanopia");
33
+ });
34
+ });
35
+
36
+ describe("checkPaletteAccessibilityTool", () => {
37
+ it("audits a 3-color palette across CVD types", async () => {
38
+ const result = (await checkPaletteAccessibilityTool.execute({
39
+ colors: ["#000000", "#888888", "#ffffff"],
40
+ })) as string;
41
+ expect(result).toContain("Palette Accessibility Audit");
42
+ expect(result).toContain("protanopia");
43
+ expect(result).toContain("Summary");
44
+ });
45
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { colorConversionTool } from "../color-conversion.tool.js";
4
+
5
+ describe("colorConversionTool", () => {
6
+ // Regression: previously multiplied by 255 again, producing rgb(26265, ...).
7
+ it("converts hex to rgb without re-scaling", async () => {
8
+ const result = await colorConversionTool.execute({
9
+ color: "#6750a4",
10
+ to: "rgb",
11
+ });
12
+ expect(result).toBe("rgb(103, 80, 164)");
13
+ });
14
+
15
+ it("converts hex to hex (round-trip)", async () => {
16
+ const result = await colorConversionTool.execute({
17
+ color: "#6750a4",
18
+ to: "hex",
19
+ });
20
+ expect(result).toBe("#6750a4");
21
+ });
22
+
23
+ it("converts hex to hsl", async () => {
24
+ const result = await colorConversionTool.execute({
25
+ color: "#6750a4",
26
+ to: "hsl",
27
+ });
28
+ expect(result).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
29
+ });
30
+
31
+ it("rejects invalid colors", async () => {
32
+ const result = await colorConversionTool.execute({
33
+ color: "not-a-color",
34
+ to: "rgb",
35
+ });
36
+ expect(result).toMatch(/Invalid color format/);
37
+ });
38
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { apcaContrast } from "../../color/apca.js";
4
+ import { contrastCheckerTool } from "../contrast-checker.tool.js";
5
+
6
+ describe("apcaContrast", () => {
7
+ // Reference values from Myndex/SAPC-APCA test vectors
8
+ it("matches reference: #888 on #fff ≈ 63", () => {
9
+ const lc = apcaContrast(
10
+ { b: 0x88, g: 0x88, r: 0x88 },
11
+ { b: 255, g: 255, r: 255 },
12
+ );
13
+ expect(lc).toBeCloseTo(63.06, 1);
14
+ });
15
+
16
+ it("matches reference: #000 on #fff ≈ 106", () => {
17
+ const lc = apcaContrast({ b: 0, g: 0, r: 0 }, { b: 255, g: 255, r: 255 });
18
+ expect(lc).toBeCloseTo(106.04, 1);
19
+ });
20
+
21
+ it("matches reference: #fff on #000 ≈ -107", () => {
22
+ const lc = apcaContrast({ b: 255, g: 255, r: 255 }, { b: 0, g: 0, r: 0 });
23
+ expect(lc).toBeCloseTo(-107.28, 1);
24
+ });
25
+ });
26
+
27
+ describe("contrastCheckerTool", () => {
28
+ it("returns WCAG block by default", async () => {
29
+ const result = await contrastCheckerTool.execute({
30
+ background: "#ffffff",
31
+ foreground: "#000000",
32
+ });
33
+ expect(result).toContain("WCAG 2.x");
34
+ expect(result).toContain("21.00:1");
35
+ });
36
+
37
+ it("returns APCA when requested", async () => {
38
+ const result = await contrastCheckerTool.execute({
39
+ algorithm: "apca",
40
+ background: "#ffffff",
41
+ foreground: "#000000",
42
+ });
43
+ expect(result).toContain("APCA");
44
+ expect(result).toContain("Lc:");
45
+ });
46
+
47
+ it("returns both when algorithm=both", async () => {
48
+ const result = await contrastCheckerTool.execute({
49
+ algorithm: "both",
50
+ background: "#ffffff",
51
+ foreground: "#000000",
52
+ });
53
+ expect(result).toContain("WCAG 2.x");
54
+ expect(result).toContain("APCA");
55
+ });
56
+ });