@trishchuk/coolors-mcp 1.0.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 (197) hide show
  1. package/.claude/settings.local.json +39 -0
  2. package/.env +2 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
  5. package/.github/pull_request_template.md +97 -0
  6. package/.github/workflows/ci.yml +127 -0
  7. package/.github/workflows/deploy-docs.yml +56 -0
  8. package/.github/workflows/release.yml +99 -0
  9. package/.mcp.json +12 -0
  10. package/.prettierignore +1 -0
  11. package/CLAUDE.md +201 -0
  12. package/DOCUMENTATION.md +274 -0
  13. package/GEMINI.md +54 -0
  14. package/LICENSE +21 -0
  15. package/README.md +401 -0
  16. package/demo/content_based_color.png +0 -0
  17. package/demo/music-player.html +621 -0
  18. package/demo/podcast-player.html +903 -0
  19. package/dist/bin/coolors-mcp.d.ts +1 -0
  20. package/dist/bin/coolors-mcp.js +154 -0
  21. package/dist/bin/coolors-mcp.js.map +1 -0
  22. package/dist/bin/server.d.ts +1 -0
  23. package/dist/bin/server.js +3292 -0
  24. package/dist/bin/server.js.map +1 -0
  25. package/dist/chunk-IQ7NN26V.js +114 -0
  26. package/dist/chunk-IQ7NN26V.js.map +1 -0
  27. package/dist/chunk-P3ARRKLS.js +1214 -0
  28. package/dist/chunk-P3ARRKLS.js.map +1 -0
  29. package/dist/color/index.d.ts +716 -0
  30. package/dist/color/index.js +153 -0
  31. package/dist/color/index.js.map +1 -0
  32. package/dist/coolors-mcp.d.ts +136 -0
  33. package/dist/coolors-mcp.js +7 -0
  34. package/dist/coolors-mcp.js.map +1 -0
  35. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
  36. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
  37. package/docs/.vitepress/cache/deps/_metadata.json +127 -0
  38. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
  39. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
  40. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
  41. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
  42. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
  43. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
  44. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
  45. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
  46. package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
  47. package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
  48. package/docs/.vitepress/cache/deps/dayjs.js +285 -0
  49. package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
  50. package/docs/.vitepress/cache/deps/debug.js +468 -0
  51. package/docs/.vitepress/cache/deps/debug.js.map +7 -0
  52. package/docs/.vitepress/cache/deps/package.json +3 -0
  53. package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
  54. package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
  55. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
  56. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
  57. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
  58. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
  59. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
  60. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
  61. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
  62. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
  63. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
  64. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
  65. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
  66. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
  67. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
  68. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  69. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
  70. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  71. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
  72. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
  73. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
  74. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
  75. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
  76. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
  77. package/docs/.vitepress/cache/deps/vue.js +344 -0
  78. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  79. package/docs/.vitepress/components/ClientGrid.vue +125 -0
  80. package/docs/.vitepress/components/CodeBlock.vue +231 -0
  81. package/docs/.vitepress/components/ConfigModal.vue +477 -0
  82. package/docs/.vitepress/components/DiagramModal.vue +528 -0
  83. package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
  84. package/docs/.vitepress/config.js +162 -0
  85. package/docs/.vitepress/theme/FundingLayout.vue +251 -0
  86. package/docs/.vitepress/theme/Layout.vue +134 -0
  87. package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
  88. package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
  89. package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
  90. package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
  91. package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
  92. package/docs/.vitepress/theme/custom-app.css +339 -0
  93. package/docs/.vitepress/theme/custom.css +699 -0
  94. package/docs/.vitepress/theme/index.js +25 -0
  95. package/docs/README.md +198 -0
  96. package/docs/concepts/accessibility.md +473 -0
  97. package/docs/concepts/color-spaces.md +222 -0
  98. package/docs/concepts/distance-metrics.md +384 -0
  99. package/docs/concepts/hct.md +261 -0
  100. package/docs/concepts/image-analysis.md +396 -0
  101. package/docs/concepts/material-design.md +306 -0
  102. package/docs/concepts/theme-matching.md +399 -0
  103. package/docs/examples/basic-colors.md +490 -0
  104. package/docs/examples/creating-themes.md +898 -0
  105. package/docs/examples/css-refactoring.md +824 -0
  106. package/docs/examples/image-extraction.md +882 -0
  107. package/docs/getting-started.md +366 -0
  108. package/docs/index.md +190 -0
  109. package/docs/installation.md +157 -0
  110. package/docs/tools/README.md +234 -0
  111. package/docs/tools/accessibility.md +614 -0
  112. package/docs/tools/color-operations.md +374 -0
  113. package/docs/tools/image-extraction.md +624 -0
  114. package/docs/tools/material-design.md +347 -0
  115. package/docs/tools/theme-matching.md +552 -0
  116. package/eslint.config.ts +14 -0
  117. package/examples/theme-matching.md +113 -0
  118. package/jsr.json +7 -0
  119. package/mcp-config.json +8 -0
  120. package/note.md +35 -0
  121. package/package.json +122 -0
  122. package/research_results.md +53 -0
  123. package/src/bin/coolors-mcp.ts +194 -0
  124. package/src/bin/server.ts +61 -0
  125. package/src/color/__tests__/conversions-argb.test.ts +198 -0
  126. package/src/color/__tests__/extract-colors.test.ts +360 -0
  127. package/src/color/__tests__/image-utils.test.ts +242 -0
  128. package/src/color/__tests__/reference-colors.test.ts +278 -0
  129. package/src/color/__tests__/round-trip.test.ts +197 -0
  130. package/src/color/conversions.test.ts +402 -0
  131. package/src/color/conversions.ts +393 -0
  132. package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
  133. package/src/color/dislike/dislike-analyzer.ts +114 -0
  134. package/src/color/extract-colors.ts +228 -0
  135. package/src/color/hct/__tests__/hct-class.test.ts +232 -0
  136. package/src/color/hct/harmonization.ts +204 -0
  137. package/src/color/hct/hct-class.ts +109 -0
  138. package/src/color/hct/hct-solver.ts +168 -0
  139. package/src/color/hct/index.ts +39 -0
  140. package/src/color/hct/tonal-palette.ts +211 -0
  141. package/src/color/hct/types.ts +88 -0
  142. package/src/color/image-utils.ts +79 -0
  143. package/src/color/index.ts +87 -0
  144. package/src/color/material-theme.ts +157 -0
  145. package/src/color/metrics.test.ts +276 -0
  146. package/src/color/metrics.ts +281 -0
  147. package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
  148. package/src/color/quantize/lab_point_provider.ts +55 -0
  149. package/src/color/quantize/point_provider.ts +27 -0
  150. package/src/color/quantize/quantizer_celebi.ts +51 -0
  151. package/src/color/quantize/quantizer_celebi_test.ts +71 -0
  152. package/src/color/quantize/quantizer_map.ts +47 -0
  153. package/src/color/quantize/quantizer_wsmeans.ts +232 -0
  154. package/src/color/quantize/quantizer_wu.ts +472 -0
  155. package/src/color/score/__tests__/score.test.ts +224 -0
  156. package/src/color/score/score.ts +175 -0
  157. package/src/color/types.ts +151 -0
  158. package/src/color/utils/color_utils.ts +292 -0
  159. package/src/color/utils/math_utils.ts +145 -0
  160. package/src/color/utils.test.ts +403 -0
  161. package/src/color/utils.ts +315 -0
  162. package/src/constants.ts +5 -0
  163. package/src/coolors-mcp.ts +37 -0
  164. package/src/examples/addition.ts +333 -0
  165. package/src/examples/color-demo.ts +125 -0
  166. package/src/examples/custom-logger.ts +201 -0
  167. package/src/examples/oauth-server.ts +113 -0
  168. package/src/examples/session-context.ts +269 -0
  169. package/src/session.ts +116 -0
  170. package/src/theme/__tests__/matcher.test.ts +180 -0
  171. package/src/theme/__tests__/parser.test.ts +148 -0
  172. package/src/theme/__tests__/refactor.test.ts +224 -0
  173. package/src/theme/index.ts +34 -0
  174. package/src/theme/matcher.ts +395 -0
  175. package/src/theme/parser.ts +392 -0
  176. package/src/theme/refactor.ts +360 -0
  177. package/src/theme/types.ts +152 -0
  178. package/src/tools/__tests__/gradient-generator.test.ts +206 -0
  179. package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
  180. package/src/tools/color-conversion.tool.ts +54 -0
  181. package/src/tools/color-distance.tool.ts +41 -0
  182. package/src/tools/colors.ts +31 -0
  183. package/src/tools/contrast-checker.tool.ts +37 -0
  184. package/src/tools/dislike-analyzer.tool.ts +247 -0
  185. package/src/tools/gradient-generator.tool.ts +250 -0
  186. package/src/tools/image-extraction.tools.ts +289 -0
  187. package/src/tools/index.ts +39 -0
  188. package/src/tools/material-theme.tools.ts +250 -0
  189. package/src/tools/palette-generator.tool.ts +135 -0
  190. package/src/tools/palette-with-locks.tool.ts +221 -0
  191. package/src/tools/registry.ts +142 -0
  192. package/src/tools/simple-tools.ts +37 -0
  193. package/src/tools/theme-matching.tools.ts +334 -0
  194. package/src/types.ts +182 -0
  195. package/src/utils.ts +22 -0
  196. package/tsconfig.json +8 -0
  197. package/vitest.config.js +15 -0
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { paletteWithLocksTool } from "../palette-with-locks.tool.js";
4
+
5
+ describe("paletteWithLocksTool", () => {
6
+ it("should generate palette with locked colors in harmony mode", async () => {
7
+ const result = await paletteWithLocksTool.execute({
8
+ colorSpace: "hsl",
9
+ lockedColors: ["#ff0000", "#0000ff"],
10
+ mode: "harmony",
11
+ totalColors: 5,
12
+ });
13
+
14
+ expect(result).toContain("Generated palette with 2 locked colors:");
15
+ expect(result).toContain("(locked)");
16
+ expect(result).toContain("Mode: harmony");
17
+ expect(result).toContain("Total colors: 5");
18
+ });
19
+
20
+ it("should generate palette with contrast mode", async () => {
21
+ const result = await paletteWithLocksTool.execute({
22
+ colorSpace: "hsl",
23
+ lockedColors: ["#808080"],
24
+ mode: "contrast",
25
+ totalColors: 4,
26
+ });
27
+
28
+ expect(result).toContain("Generated palette with 1 locked colors:");
29
+ expect(result).toContain("Mode: contrast");
30
+ expect(result).toContain("Total colors: 4");
31
+ });
32
+
33
+ it("should generate gradient between locked colors", async () => {
34
+ const result = await paletteWithLocksTool.execute({
35
+ colorSpace: "lab",
36
+ lockedColors: ["#000000", "#ffffff"],
37
+ mode: "gradient",
38
+ totalColors: 5,
39
+ });
40
+
41
+ expect(result).toContain("Generated palette with 2 locked colors:");
42
+ expect(result).toContain("Mode: gradient");
43
+ expect(result).toContain("Color space: lab");
44
+ });
45
+
46
+ it("should handle gradient with HSL interpolation", async () => {
47
+ const result = await paletteWithLocksTool.execute({
48
+ colorSpace: "hsl",
49
+ lockedColors: ["#ff0000", "#00ff00"],
50
+ mode: "gradient",
51
+ totalColors: 7,
52
+ });
53
+
54
+ expect(result).toContain("Generated palette with 2 locked colors:");
55
+ expect(result).toContain("Mode: gradient");
56
+ expect(result).toContain("Color space: hsl");
57
+ });
58
+
59
+ it("should error when locked colors >= total colors", async () => {
60
+ const result = await paletteWithLocksTool.execute({
61
+ colorSpace: "hsl",
62
+ lockedColors: ["#ff0000", "#00ff00", "#0000ff"],
63
+ mode: "harmony",
64
+ totalColors: 3,
65
+ });
66
+
67
+ expect(result).toContain("Error: Number of locked colors");
68
+ });
69
+
70
+ it("should error on invalid color format", async () => {
71
+ const result = await paletteWithLocksTool.execute({
72
+ colorSpace: "hsl",
73
+ lockedColors: ["invalid", "#00ff00"],
74
+ mode: "harmony",
75
+ totalColors: 5,
76
+ });
77
+
78
+ expect(result).toContain("Invalid color format: invalid");
79
+ });
80
+
81
+ it("should error in gradient mode with only one locked color", async () => {
82
+ const result = await paletteWithLocksTool.execute({
83
+ colorSpace: "hsl",
84
+ lockedColors: ["#ff0000"],
85
+ mode: "gradient",
86
+ totalColors: 5,
87
+ });
88
+
89
+ expect(result).toContain("Gradient mode requires at least 2 locked colors");
90
+ });
91
+
92
+ it("should generate harmonious colors with multiple locked colors", async () => {
93
+ const result = await paletteWithLocksTool.execute({
94
+ colorSpace: "hsl",
95
+ lockedColors: ["#ff0000", "#00ff00", "#0000ff"],
96
+ mode: "harmony",
97
+ totalColors: 6,
98
+ });
99
+
100
+ expect(result).toContain("Generated palette with 3 locked colors:");
101
+ expect(result).toContain("Mode: harmony");
102
+ expect(result).toContain("Total colors: 6");
103
+
104
+ // Should have 6 color entries
105
+ const lines = result.split("\n");
106
+ const colorLines = lines.filter((line) => /^\d+\./.test(line));
107
+ expect(colorLines).toHaveLength(6);
108
+ });
109
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Color Conversion Tool
3
+ * Convert colors between different formats (hex, rgb, hsl, lab, hct)
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ import {
9
+ parseColor,
10
+ rgbToHct,
11
+ rgbToHex,
12
+ rgbToHsl,
13
+ rgbToLab,
14
+ } from "../color/index.js";
15
+
16
+ export const colorConversionTool = {
17
+ description:
18
+ "Convert colors between different formats (hex, rgb, hsl, lab, hct)",
19
+ execute: async (args: {
20
+ color: string;
21
+ to: "hct" | "hex" | "hsl" | "lab" | "rgb";
22
+ }) => {
23
+ const rgb = parseColor(args.color);
24
+ if (!rgb) {
25
+ return `Invalid color format: ${args.color}`;
26
+ }
27
+
28
+ switch (args.to) {
29
+ case "hct": {
30
+ const hct = rgbToHct(rgb);
31
+ return `hct(${hct.h.toFixed(1)}, ${hct.c.toFixed(1)}, ${hct.t.toFixed(1)})`;
32
+ }
33
+ case "hex":
34
+ return rgbToHex(rgb);
35
+ case "hsl": {
36
+ const hsl = rgbToHsl(rgb);
37
+ return `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%)`;
38
+ }
39
+ case "lab": {
40
+ const lab = rgbToLab(rgb);
41
+ return `lab(${lab.l.toFixed(2)}, ${lab.a.toFixed(2)}, ${lab.b.toFixed(2)})`;
42
+ }
43
+ case "rgb":
44
+ return `rgb(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)})`;
45
+ default:
46
+ return `Invalid format: ${args.to}`;
47
+ }
48
+ },
49
+ name: "convert_color",
50
+ parameters: z.object({
51
+ color: z.string().describe("Color to convert (hex, rgb(), or hsl())"),
52
+ to: z.enum(["hex", "rgb", "hsl", "lab", "hct"]).describe("Target format"),
53
+ }),
54
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Color Distance Tool
3
+ * Calculate perceptual distance between two colors using various metrics
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ import { colorDistance, parseColor } from "../color/index.js";
9
+
10
+ export const colorDistanceTool = {
11
+ description:
12
+ "Calculate perceptual distance between two colors using Delta E 2000",
13
+ execute: async (args: {
14
+ color1: string;
15
+ color2: string;
16
+ metric?: "deltaE2000" | "deltaE76" | "deltaE94" | "euclidean" | "weighted";
17
+ }) => {
18
+ const rgb1 = parseColor(args.color1);
19
+ const rgb2 = parseColor(args.color2);
20
+
21
+ if (!rgb1 || !rgb2) {
22
+ return `Invalid color format: ${!rgb1 ? args.color1 : args.color2}`;
23
+ }
24
+
25
+ const distance = colorDistance(rgb1, rgb2, {
26
+ metric: args.metric || "deltaE2000",
27
+ });
28
+
29
+ return `Distance between ${args.color1} and ${args.color2}: ${distance.toFixed(2)}`;
30
+ },
31
+ name: "color_distance",
32
+ parameters: z.object({
33
+ color1: z.string().describe("First color (hex, rgb, or hsl)"),
34
+ color2: z.string().describe("Second color (hex, rgb, or hsl)"),
35
+ metric: z
36
+ .enum(["euclidean", "deltaE76", "deltaE94", "deltaE2000", "weighted"])
37
+ .optional()
38
+ .default("deltaE2000")
39
+ .describe("Distance metric to use"),
40
+ }),
41
+ };
@@ -0,0 +1,31 @@
1
+ import { formatHex, formatHsl, formatRgb, parse } from "culori";
2
+ import { z } from "zod";
3
+
4
+ import { ColorFormat } from "../constants.js";
5
+
6
+ export const convertColor = {
7
+ description: "Convert a color from one format to another",
8
+ execute: async (args: { color: string; to?: ColorFormat }) => {
9
+ const { color, to = ColorFormat.HEX } = args;
10
+ const parsed = parse(color);
11
+ if (!parsed) {
12
+ throw new Error(`Invalid color: ${color}`);
13
+ }
14
+
15
+ switch (to) {
16
+ case ColorFormat.HEX:
17
+ return formatHex(parsed);
18
+ case ColorFormat.HSL:
19
+ return formatHsl(parsed);
20
+ case ColorFormat.RGB:
21
+ return formatRgb(parsed);
22
+ default:
23
+ throw new Error(`Invalid output format: ${to}`);
24
+ }
25
+ },
26
+ name: "convertColor",
27
+ parameters: z.object({
28
+ color: z.string().describe("The color to convert"),
29
+ to: z.nativeEnum(ColorFormat).optional().describe("The output format"),
30
+ }),
31
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Contrast Checker Tool
3
+ * Check WCAG contrast ratio between two colors
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ import { getContrastRatio, parseColor } from "../color/index.js";
9
+
10
+ export const contrastCheckerTool = {
11
+ description: "Check WCAG contrast ratio between two colors",
12
+ execute: async (args: { background: string; foreground: string }) => {
13
+ const fg = parseColor(args.foreground);
14
+ const bg = parseColor(args.background);
15
+
16
+ if (!fg || !bg) {
17
+ return `Invalid color format: ${!fg ? args.foreground : args.background}`;
18
+ }
19
+
20
+ const ratio = getContrastRatio(fg, bg);
21
+ const aa = ratio >= 4.5;
22
+ const aaLarge = ratio >= 3;
23
+ const aaa = ratio >= 7;
24
+ const aaaLarge = ratio >= 4.5;
25
+
26
+ return `Contrast Ratio: ${ratio.toFixed(2)}:1
27
+ WCAG AA: ${aa ? "✓ Pass" : "✗ Fail"} (normal text)
28
+ WCAG AA: ${aaLarge ? "✓ Pass" : "✗ Fail"} (large text)
29
+ WCAG AAA: ${aaa ? "✓ Pass" : "✗ Fail"} (normal text)
30
+ WCAG AAA: ${aaaLarge ? "✓ Pass" : "✗ Fail"} (large text)`;
31
+ },
32
+ name: "check_contrast",
33
+ parameters: z.object({
34
+ background: z.string().describe("Background color"),
35
+ foreground: z.string().describe("Foreground/text color"),
36
+ }),
37
+ };
@@ -0,0 +1,247 @@
1
+ /**
2
+ * MCP Tools for Dislike Color Analysis
3
+ * Identifies and fixes universally disliked colors based on color psychology research
4
+ */
5
+
6
+ import type { Tool } from "../types.js";
7
+
8
+ type McpTool = Tool<any>;
9
+
10
+ import { DislikeAnalyzer } from "../color/dislike/dislike-analyzer.js";
11
+ import { Hct } from "../color/hct/hct-class.js";
12
+ import { parseColor, rgbToArgb } from "../color/index.js";
13
+
14
+ /**
15
+ * Analyze if a color is universally disliked
16
+ */
17
+ export const analyzeColorLikabilityTool: McpTool = {
18
+ description:
19
+ "Check if a color is universally disliked (dark yellow-green associated with biological waste) and get a fixed version if needed",
20
+ execute: async (args: unknown, _context: any) => {
21
+ const { autoFix = true, color } = args as {
22
+ autoFix?: boolean;
23
+ color: string;
24
+ };
25
+
26
+ try {
27
+ // Parse the input color
28
+ const rgb = parseColor(color);
29
+ if (!rgb) {
30
+ return `Error: Invalid color format: ${color}`;
31
+ }
32
+
33
+ // Convert to HCT for analysis
34
+ const argb = rgbToArgb(rgb);
35
+ const hct = Hct.fromInt(argb);
36
+
37
+ // Check if disliked
38
+ const isDisliked = DislikeAnalyzer.isDisliked(hct);
39
+
40
+ let output = `# Color Likability Analysis\n\n`;
41
+ output += `## Input Color: ${color}\n\n`;
42
+ output += `### HCT Values\n`;
43
+ output += `- Hue: ${hct.hue.toFixed(1)}°\n`;
44
+ output += `- Chroma: ${hct.chroma.toFixed(1)}\n`;
45
+ output += `- Tone: ${hct.tone.toFixed(1)}\n\n`;
46
+
47
+ output += `### Analysis Result\n`;
48
+
49
+ if (isDisliked) {
50
+ output += `⚠️ **This color is universally disliked**\n\n`;
51
+ output += `This color falls in the "bile zone" - dark yellow-greens that are universally associated with biological waste and rotting food.\n\n`;
52
+ output += `**Reason:**\n`;
53
+ output += `- Hue is in yellow-green range (90-111°): ${Math.round(hct.hue)}°\n`;
54
+ output += `- Chroma is above threshold (>16): ${Math.round(hct.chroma)}\n`;
55
+ output += `- Tone is dark (<65): ${Math.round(hct.tone)}\n\n`;
56
+
57
+ if (autoFix) {
58
+ const fixed = DislikeAnalyzer.fixIfDisliked(hct);
59
+ const fixedArgb = fixed.toInt();
60
+ const fixedHex =
61
+ "#" +
62
+ [
63
+ (fixedArgb >> 16) & 0xff,
64
+ (fixedArgb >> 8) & 0xff,
65
+ fixedArgb & 0xff,
66
+ ]
67
+ .map((x) => x.toString(16).padStart(2, "0"))
68
+ .join("");
69
+
70
+ output += `### Recommended Fix\n`;
71
+ output += `**Fixed Color:** ${fixedHex}\n`;
72
+ output += `- Hue: ${fixed.hue.toFixed(1)}° (preserved)\n`;
73
+ output += `- Chroma: ${fixed.chroma.toFixed(1)} (preserved)\n`;
74
+ output += `- Tone: ${fixed.tone.toFixed(1)} (lightened to 70)\n\n`;
75
+ output += `The fix preserves the hue and saturation but lightens the color to make it more pleasant.\n`;
76
+ }
77
+ } else {
78
+ output += `✅ **This color is likable**\n\n`;
79
+ output += `This color does not fall in the universally disliked range.\n`;
80
+
81
+ // Provide context about why it's liked
82
+ if (Math.round(hct.hue) >= 90 && Math.round(hct.hue) <= 111) {
83
+ if (Math.round(hct.chroma) <= 16) {
84
+ output += `- Although in yellow-green hue range, the low chroma (${Math.round(hct.chroma)}) makes it neutral and acceptable.\n`;
85
+ } else if (Math.round(hct.tone) >= 65) {
86
+ output += `- Although in yellow-green hue range, the light tone (${Math.round(hct.tone)}) makes it pleasant.\n`;
87
+ }
88
+ } else {
89
+ output += `- Hue (${Math.round(hct.hue)}°) is outside the problematic yellow-green range.\n`;
90
+ }
91
+ }
92
+
93
+ return output;
94
+ } catch (error) {
95
+ return `Error analyzing color: ${error instanceof Error ? error.message : String(error)}`;
96
+ }
97
+ },
98
+ inputSchema: {
99
+ properties: {
100
+ autoFix: {
101
+ description:
102
+ "Automatically return fixed version if disliked (default: true)",
103
+ type: "boolean",
104
+ },
105
+ color: {
106
+ description: "Color to analyze (hex, rgb, hsl, etc.)",
107
+ type: "string",
108
+ },
109
+ },
110
+ required: ["color"],
111
+ type: "object",
112
+ },
113
+ name: "analyze_color_likability",
114
+ };
115
+
116
+ /**
117
+ * Fix multiple disliked colors in a batch
118
+ */
119
+ export const fixDislikedColorsBatchTool: McpTool = {
120
+ description: "Analyze and fix multiple colors, returning only liked versions",
121
+ execute: async (args: unknown, _context: any) => {
122
+ const { colors, includeAnalysis = false } = args as {
123
+ colors: string[];
124
+ includeAnalysis?: boolean;
125
+ };
126
+
127
+ try {
128
+ const results: Array<{
129
+ error?: string;
130
+ fixed?: string;
131
+ hct?: unknown;
132
+ original: string;
133
+ wasDisliked?: boolean;
134
+ }> = [];
135
+ let dislikedCount = 0;
136
+
137
+ for (const color of colors) {
138
+ const rgb = parseColor(color);
139
+ if (!rgb) {
140
+ results.push({
141
+ error: "Invalid color format",
142
+ original: color,
143
+ });
144
+ continue;
145
+ }
146
+
147
+ const argb = rgbToArgb(rgb);
148
+ const hct = Hct.fromInt(argb);
149
+ const isDisliked = DislikeAnalyzer.isDisliked(hct);
150
+
151
+ if (isDisliked) {
152
+ dislikedCount++;
153
+ const fixed = DislikeAnalyzer.fixIfDisliked(hct);
154
+ const fixedArgb = fixed.toInt();
155
+ const fixedHex =
156
+ "#" +
157
+ [
158
+ (fixedArgb >> 16) & 0xff,
159
+ (fixedArgb >> 8) & 0xff,
160
+ fixedArgb & 0xff,
161
+ ]
162
+ .map((x) => x.toString(16).padStart(2, "0"))
163
+ .join("");
164
+
165
+ results.push({
166
+ fixed: fixedHex,
167
+ hct: includeAnalysis
168
+ ? {
169
+ fixed: { c: fixed.chroma, h: fixed.hue, t: fixed.tone },
170
+ original: { c: hct.chroma, h: hct.hue, t: hct.tone },
171
+ }
172
+ : undefined,
173
+ original: color,
174
+ wasDisliked: true,
175
+ });
176
+ } else {
177
+ results.push({
178
+ fixed: color,
179
+ hct: includeAnalysis
180
+ ? {
181
+ c: hct.chroma,
182
+ h: hct.hue,
183
+ t: hct.tone,
184
+ }
185
+ : undefined,
186
+ original: color,
187
+ wasDisliked: false,
188
+ });
189
+ }
190
+ }
191
+
192
+ let output = `# Batch Color Likability Analysis\n\n`;
193
+ output += `## Summary\n`;
194
+ output += `- Total colors: ${colors.length}\n`;
195
+ output += `- Disliked colors found: ${dislikedCount}\n`;
196
+ output += `- Percentage disliked: ${((dislikedCount / colors.length) * 100).toFixed(1)}%\n\n`;
197
+
198
+ output += `## Results\n\n`;
199
+
200
+ for (const result of results) {
201
+ if (result.error) {
202
+ output += `- **${result.original}**: ❌ ${result.error}\n`;
203
+ } else if (result.wasDisliked) {
204
+ output += `- **${result.original}** → **${result.fixed}** (fixed)\n`;
205
+ if (includeAnalysis && result.hct) {
206
+ output += ` - Original HCT: (${result.hct.original.h.toFixed(0)}°, ${result.hct.original.c.toFixed(0)}, ${result.hct.original.t.toFixed(0)})\n`;
207
+ output += ` - Fixed HCT: (${result.hct.fixed.h.toFixed(0)}°, ${result.hct.fixed.c.toFixed(0)}, ${result.hct.fixed.t.toFixed(0)})\n`;
208
+ }
209
+ } else {
210
+ output += `- **${result.original}** ✓ (already liked)\n`;
211
+ if (includeAnalysis && result.hct) {
212
+ output += ` - HCT: (${result.hct.h.toFixed(0)}°, ${result.hct.c.toFixed(0)}, ${result.hct.t.toFixed(0)})\n`;
213
+ }
214
+ }
215
+ }
216
+
217
+ if (dislikedCount > 0) {
218
+ output += `\n## Note\n`;
219
+ output += `Disliked colors have been automatically fixed by lightening their tone to 70 while preserving hue and chroma. `;
220
+ output += `This makes them more pleasant while maintaining their essential character.\n`;
221
+ }
222
+
223
+ return output;
224
+ } catch (error) {
225
+ return `Error processing colors: ${error instanceof Error ? error.message : String(error)}`;
226
+ }
227
+ },
228
+ inputSchema: {
229
+ properties: {
230
+ colors: {
231
+ description: "Array of colors to analyze (hex, rgb, hsl, etc.)",
232
+ items: {
233
+ type: "string",
234
+ },
235
+ type: "array",
236
+ },
237
+ includeAnalysis: {
238
+ description:
239
+ "Include detailed analysis for each color (default: false)",
240
+ type: "boolean",
241
+ },
242
+ },
243
+ required: ["colors"],
244
+ type: "object",
245
+ },
246
+ name: "fix_disliked_colors_batch",
247
+ };