@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
@@ -1,15 +1,22 @@
1
1
  /**
2
2
  * Contrast Checker Tool
3
- * Check WCAG contrast ratio between two colors
3
+ * Check contrast between two colors using WCAG 2.x luminance ratio,
4
+ * APCA (WCAG 3 draft), or both.
4
5
  */
5
6
 
6
7
  import { z } from "zod";
7
8
 
9
+ import { apcaContrast, apcaLevel } from "../color/apca.js";
8
10
  import { getContrastRatio, parseColor } from "../color/index.js";
9
11
 
10
12
  export const contrastCheckerTool = {
11
- description: "Check WCAG contrast ratio between two colors",
12
- execute: async (args: { background: string; foreground: string }) => {
13
+ description:
14
+ "Check contrast between two colors. Supports WCAG 2.x luminance ratio (default), APCA Lc (WCAG 3 draft), or both algorithms side-by-side.",
15
+ execute: async (args: {
16
+ algorithm?: "apca" | "both" | "wcag";
17
+ background: string;
18
+ foreground: string;
19
+ }) => {
13
20
  const fg = parseColor(args.foreground);
14
21
  const bg = parseColor(args.background);
15
22
 
@@ -17,20 +24,52 @@ export const contrastCheckerTool = {
17
24
  return `Invalid color format: ${!fg ? args.foreground : args.background}`;
18
25
  }
19
26
 
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)`;
27
+ const algorithm = args.algorithm ?? "wcag";
28
+
29
+ const wcagBlock = () => {
30
+ const ratio = getContrastRatio(fg, bg);
31
+ const aaLarge = ratio >= 3;
32
+ const aa = ratio >= 4.5;
33
+ const aaaLarge = ratio >= 4.5;
34
+ const aaa = ratio >= 7;
35
+ return `## WCAG 2.x luminance ratio
36
+ Contrast Ratio: ${ratio.toFixed(2)}:1
37
+ - AA (normal text): ${aa ? "✓ Pass" : "✗ Fail"} (need 4.5:1)
38
+ - AA (large text): ${aaLarge ? "✓ Pass" : "✗ Fail"} (need 3:1)
39
+ - AAA (normal text): ${aaa ? "✓ Pass" : "✗ Fail"} (need 7:1)
40
+ - AAA (large text): ${aaaLarge ? "✓ Pass" : "✗ Fail"} (need 4.5:1)`;
41
+ };
42
+
43
+ const apcaBlock = () => {
44
+ const lc = apcaContrast(fg, bg);
45
+ const level = apcaLevel(lc);
46
+ const polarity =
47
+ lc > 0
48
+ ? "dark text on light bg"
49
+ : lc < 0
50
+ ? "light text on dark bg"
51
+ : "no contrast";
52
+ return `## APCA (WCAG 3 draft)
53
+ Lc: ${lc.toFixed(1)} (${polarity})
54
+ - Body text (|Lc| ≥ 75): ${level.body ? "✓ Pass" : "✗ Fail"}
55
+ - Content text (|Lc| ≥ 60): ${level.content ? "✓ Pass" : "✗ Fail"}
56
+ - Large text (|Lc| ≥ 45): ${level.large ? "✓ Pass" : "✗ Fail"}
57
+ - Spot / non-content (|Lc| ≥ 30): ${level.spot ? "✓ Pass" : "✗ Fail"}`;
58
+ };
59
+
60
+ if (algorithm === "wcag") return wcagBlock();
61
+ if (algorithm === "apca") return apcaBlock();
62
+ return `${wcagBlock()}\n\n${apcaBlock()}`;
31
63
  },
32
64
  name: "check_contrast",
33
65
  parameters: z.object({
66
+ algorithm: z
67
+ .enum(["wcag", "apca", "both"])
68
+ .optional()
69
+ .default("wcag")
70
+ .describe(
71
+ "Contrast algorithm: 'wcag' (WCAG 2.x ratio), 'apca' (Lc, WCAG 3 draft), or 'both'",
72
+ ),
34
73
  background: z.string().describe("Background color"),
35
74
  foreground: z.string().describe("Foreground/text color"),
36
75
  }),
@@ -3,9 +3,7 @@
3
3
  * Identifies and fixes universally disliked colors based on color psychology research
4
4
  */
5
5
 
6
- import type { Tool } from "../types.js";
7
-
8
- type McpTool = Tool<any>;
6
+ import { z } from "zod";
9
7
 
10
8
  import { DislikeAnalyzer } from "../color/dislike/dislike-analyzer.js";
11
9
  import { Hct } from "../color/hct/hct-class.js";
@@ -14,14 +12,11 @@ import { parseColor, rgbToArgb } from "../color/index.js";
14
12
  /**
15
13
  * Analyze if a color is universally disliked
16
14
  */
17
- export const analyzeColorLikabilityTool: McpTool = {
15
+ export const analyzeColorLikabilityTool = {
18
16
  description:
19
17
  "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
- };
18
+ execute: async (args: { autoFix?: boolean; color: string }) => {
19
+ const { autoFix = true, color } = args;
25
20
 
26
21
  try {
27
22
  // Parse the input color
@@ -95,40 +90,37 @@ export const analyzeColorLikabilityTool: McpTool = {
95
90
  return `Error analyzing color: ${error instanceof Error ? error.message : String(error)}`;
96
91
  }
97
92
  },
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
93
  name: "analyze_color_likability",
94
+ parameters: z.object({
95
+ autoFix: z
96
+ .boolean()
97
+ .optional()
98
+ .default(true)
99
+ .describe("Automatically return fixed version if disliked"),
100
+ color: z.string().describe("Color to analyze (hex, rgb, hsl, etc.)"),
101
+ }),
114
102
  };
115
103
 
116
104
  /**
117
105
  * Fix multiple disliked colors in a batch
118
106
  */
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;
107
+ type BatchHct =
108
+ | { c: number; h: number; t: number }
109
+ | {
110
+ fixed: { c: number; h: number; t: number };
111
+ original: { c: number; h: number; t: number };
125
112
  };
126
113
 
114
+ export const fixDislikedColorsBatchTool = {
115
+ description: "Analyze and fix multiple colors, returning only liked versions",
116
+ execute: async (args: { colors: string[]; includeAnalysis?: boolean }) => {
117
+ const { colors, includeAnalysis = false } = args;
118
+
127
119
  try {
128
120
  const results: Array<{
129
121
  error?: string;
130
122
  fixed?: string;
131
- hct?: unknown;
123
+ hct?: BatchHct;
132
124
  original: string;
133
125
  wasDisliked?: boolean;
134
126
  }> = [];
@@ -202,14 +194,16 @@ export const fixDislikedColorsBatchTool: McpTool = {
202
194
  output += `- **${result.original}**: ❌ ${result.error}\n`;
203
195
  } else if (result.wasDisliked) {
204
196
  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`;
197
+ if (includeAnalysis && result.hct && "original" in result.hct) {
198
+ const { fixed, original } = result.hct;
199
+ output += ` - Original HCT: (${original.h.toFixed(0)}°, ${original.c.toFixed(0)}, ${original.t.toFixed(0)})\n`;
200
+ output += ` - Fixed HCT: (${fixed.h.toFixed(0)}°, ${fixed.c.toFixed(0)}, ${fixed.t.toFixed(0)})\n`;
208
201
  }
209
202
  } else {
210
203
  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`;
204
+ if (includeAnalysis && result.hct && "h" in result.hct) {
205
+ const { c, h, t } = result.hct;
206
+ output += ` - HCT: (${h.toFixed(0)}°, ${c.toFixed(0)}, ${t.toFixed(0)})\n`;
213
207
  }
214
208
  }
215
209
  }
@@ -225,23 +219,16 @@ export const fixDislikedColorsBatchTool: McpTool = {
225
219
  return `Error processing colors: ${error instanceof Error ? error.message : String(error)}`;
226
220
  }
227
221
  },
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
222
  name: "fix_disliked_colors_batch",
223
+ parameters: z.object({
224
+ colors: z
225
+ .array(z.string())
226
+ .min(1)
227
+ .describe("Array of colors to analyze (hex, rgb, hsl, etc.)"),
228
+ includeAnalysis: z
229
+ .boolean()
230
+ .optional()
231
+ .default(false)
232
+ .describe("Include detailed analysis for each color"),
233
+ }),
247
234
  };
@@ -2,15 +2,7 @@
2
2
  * MCP tools for image color extraction
3
3
  */
4
4
 
5
- import type { Tool } from "../types.js";
6
-
7
- type ImageData = {
8
- data: number[] | Uint8ClampedArray;
9
- height: number;
10
- width: number;
11
- };
12
-
13
- type McpTool = Tool<unknown>;
5
+ import { z } from "zod";
14
6
 
15
7
  import {
16
8
  extractColors,
@@ -19,31 +11,37 @@ import {
19
11
  } from "../color/extract-colors.js";
20
12
  import { generateMaterialTheme } from "../color/material-theme.js";
21
13
 
14
+ const imageDataSchema = z
15
+ .object({
16
+ data: z.array(z.number()).describe("Flat array of RGBA values (0-255)"),
17
+ height: z.number().int().positive().describe("Image height in pixels"),
18
+ width: z.number().int().positive().describe("Image width in pixels"),
19
+ })
20
+ .describe("Image data with RGBA values");
21
+
22
+ type ImageDataInput = z.infer<typeof imageDataSchema>;
23
+
22
24
  /**
23
25
  * Extract dominant colors from image data
24
26
  */
25
- export const extractImageColorsTool: McpTool = {
27
+ export const extractImageColorsTool = {
26
28
  description:
27
29
  "Extract dominant colors from an image. Input should be image data as an array of RGBA values.",
28
- execute: async (args: unknown, _context: unknown) => {
30
+ execute: async (args: {
31
+ format?: "css" | "json" | "palette";
32
+ imageData: ImageDataInput;
33
+ maxColors?: number;
34
+ quality?: "high" | "low" | "medium";
35
+ }) => {
29
36
  const {
30
37
  format = "json",
31
38
  imageData,
32
39
  maxColors = 5,
33
40
  quality = "medium",
34
- } = args as {
35
- format?: string;
36
- imageData: ImageData;
37
- maxColors?: number;
38
- quality?: "high" | "low" | "medium";
39
- };
41
+ } = args;
40
42
 
41
43
  try {
42
- // Convert data array to Uint8ClampedArray if needed
43
- const data =
44
- imageData.data instanceof Uint8ClampedArray
45
- ? imageData.data
46
- : new Uint8ClampedArray(imageData.data);
44
+ const data = new Uint8ClampedArray(imageData.data);
47
45
 
48
46
  const processedImageData = {
49
47
  data,
@@ -75,74 +73,44 @@ export const extractImageColorsTool: McpTool = {
75
73
  return `Error extracting colors: ${error instanceof Error ? error.message : String(error)}`;
76
74
  }
77
75
  },
78
- inputSchema: {
79
- properties: {
80
- format: {
81
- description: "Output format: json, css, or palette (default: json)",
82
- enum: ["json", "css", "palette"],
83
- type: "string",
84
- },
85
- imageData: {
86
- description: "Image data with RGBA values",
87
- properties: {
88
- data: {
89
- description: "Flat array of RGBA values (0-255)",
90
- items: { type: "number" },
91
- type: "array",
92
- },
93
- height: {
94
- description: "Image height in pixels",
95
- type: "number",
96
- },
97
- width: {
98
- description: "Image width in pixels",
99
- type: "number",
100
- },
101
- },
102
- required: ["data", "width", "height"],
103
- type: "object",
104
- },
105
- maxColors: {
106
- description: "Maximum number of colors to extract (default: 5)",
107
- maximum: 20,
108
- minimum: 1,
109
- type: "number",
110
- },
111
- quality: {
112
- description:
113
- "Extraction quality: low, medium, or high (default: medium)",
114
- enum: ["low", "medium", "high"],
115
- type: "string",
116
- },
117
- },
118
- required: ["imageData"],
119
- type: "object",
120
- },
121
76
  name: "extract_image_colors",
77
+ parameters: z.object({
78
+ format: z
79
+ .enum(["json", "css", "palette"])
80
+ .optional()
81
+ .default("json")
82
+ .describe("Output format"),
83
+ imageData: imageDataSchema,
84
+ maxColors: z
85
+ .number()
86
+ .int()
87
+ .min(1)
88
+ .max(20)
89
+ .optional()
90
+ .default(5)
91
+ .describe("Maximum number of colors to extract"),
92
+ quality: z
93
+ .enum(["low", "medium", "high"])
94
+ .optional()
95
+ .default("medium")
96
+ .describe("Extraction quality"),
97
+ }),
122
98
  };
123
99
 
124
100
  /**
125
101
  * Generate a Material Design theme from an image
126
102
  */
127
- export const generateThemeFromImageTool: McpTool = {
103
+ export const generateThemeFromImageTool = {
128
104
  description: "Generate a complete Material Design 3 theme from an image",
129
- execute: async (args: unknown, _context: unknown) => {
130
- const {
131
- imageData,
132
- includeCustomColors = true,
133
- isDark = false,
134
- } = args as {
135
- imageData: ImageData;
136
- includeCustomColors?: boolean;
137
- isDark?: boolean;
138
- };
105
+ execute: async (args: {
106
+ imageData: ImageDataInput;
107
+ includeCustomColors?: boolean;
108
+ isDark?: boolean;
109
+ }) => {
110
+ const { imageData, includeCustomColors = true, isDark = false } = args;
139
111
 
140
112
  try {
141
- // Convert data array to Uint8ClampedArray if needed
142
- const data =
143
- imageData.data instanceof Uint8ClampedArray
144
- ? imageData.data
145
- : new Uint8ClampedArray(imageData.data);
113
+ const data = new Uint8ClampedArray(imageData.data);
146
114
 
147
115
  const processedImageData = {
148
116
  data,
@@ -212,41 +180,20 @@ export const generateThemeFromImageTool: McpTool = {
212
180
  return `Error generating theme: ${error instanceof Error ? error.message : String(error)}`;
213
181
  }
214
182
  },
215
- inputSchema: {
216
- properties: {
217
- imageData: {
218
- description: "Image data with RGBA values",
219
- properties: {
220
- data: {
221
- description: "Flat array of RGBA values (0-255)",
222
- items: { type: "number" },
223
- type: "array",
224
- },
225
- height: {
226
- description: "Image height in pixels",
227
- type: "number",
228
- },
229
- width: {
230
- description: "Image width in pixels",
231
- type: "number",
232
- },
233
- },
234
- required: ["data", "width", "height"],
235
- type: "object",
236
- },
237
- includeCustomColors: {
238
- description: "Include custom colors from image (default: true)",
239
- type: "boolean",
240
- },
241
- isDark: {
242
- description: "Generate dark theme (default: false for light theme)",
243
- type: "boolean",
244
- },
245
- },
246
- required: ["imageData"],
247
- type: "object",
248
- },
249
183
  name: "generate_theme_from_image",
184
+ parameters: z.object({
185
+ imageData: imageDataSchema,
186
+ includeCustomColors: z
187
+ .boolean()
188
+ .optional()
189
+ .default(true)
190
+ .describe("Include custom colors from image"),
191
+ isDark: z
192
+ .boolean()
193
+ .optional()
194
+ .default(false)
195
+ .describe("Generate dark theme (false for light theme)"),
196
+ }),
250
197
  };
251
198
 
252
199
  // Helper functions for formatting
@@ -1,10 +1,21 @@
1
1
  // Tool Registry Index - Export all tools
2
2
 
3
+ export { adjustColorTool } from "./adjust-color.tool.js";
4
+ // Visual cohesion: tonal scales, state colors, palette consistency
5
+ export {
6
+ analyzePaletteConsistencyTool,
7
+ generateSemanticPaletteTool,
8
+ generateStateColorsTool,
9
+ generateTonalScaleTool,
10
+ } from "./cohesion.tools.js";
11
+ // Color-blindness simulation & accessibility audit
12
+ export {
13
+ checkPaletteAccessibilityTool,
14
+ simulateColorBlindnessTool,
15
+ } from "./color-blindness.tool.js";
3
16
  // Color tools
4
17
  export { colorConversionTool } from "./color-conversion.tool.js";
5
18
  export { colorDistanceTool } from "./color-distance.tool.js";
6
- // Legacy tools (if needed)
7
- export { convertColor } from "./colors.js";
8
19
  export { contrastCheckerTool } from "./contrast-checker.tool.js";
9
20
 
10
21
  // Dislike analyzer tools
@@ -27,6 +38,8 @@ export {
27
38
  generateTonalPaletteTool,
28
39
  harmonizeColorsTool,
29
40
  } from "./material-theme.tools.js";
41
+
42
+ export { exportPaletteTool } from "./palette-export.tool.js";
30
43
  export { paletteGeneratorTool } from "./palette-generator.tool.js";
31
44
  export { paletteWithLocksTool } from "./palette-with-locks.tool.js";
32
45
 
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Palette Export Tool
3
+ * Convert a palette of colors into commonly used design-system formats:
4
+ * CSS custom properties, SCSS variables, Tailwind config, or W3C design tokens.
5
+ */
6
+
7
+ import { z } from "zod";
8
+
9
+ import { parseColor, rgbToHex, rgbToHsl } from "../color/index.js";
10
+
11
+ const FORMATS = ["css", "scss", "tailwind", "tokens", "json"] as const;
12
+
13
+ type ExportFormat = (typeof FORMATS)[number];
14
+
15
+ interface NamedColor {
16
+ hex: string;
17
+ name: string;
18
+ }
19
+
20
+ function asCss(items: NamedColor[]): string {
21
+ const body = items.map(({ hex, name }) => ` --${name}: ${hex};`).join("\n");
22
+ return `:root {\n${body}\n}\n`;
23
+ }
24
+
25
+ function asJson(items: NamedColor[]): string {
26
+ const obj: Record<string, { hex: string; hsl: string; rgb: string }> = {};
27
+ for (const { hex, name } of items) {
28
+ const rgb = parseColor(hex)!;
29
+ const hsl = rgbToHsl(rgb);
30
+ obj[name] = {
31
+ hex,
32
+ hsl: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`,
33
+ rgb: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
34
+ };
35
+ }
36
+ return JSON.stringify(obj, null, 2) + "\n";
37
+ }
38
+
39
+ function asScss(items: NamedColor[]): string {
40
+ return items.map(({ hex, name }) => `$${name}: ${hex};`).join("\n") + "\n";
41
+ }
42
+
43
+ function asTailwind(items: NamedColor[], prefix?: string): string {
44
+ const key = prefix ?? "palette";
45
+ const lines = items
46
+ .map(({ hex, name }) => {
47
+ // Drop the prefix from the entry name if it was prefixed
48
+ const entry =
49
+ prefix && name.startsWith(`${prefix}-`)
50
+ ? name.slice(prefix.length + 1)
51
+ : name;
52
+ return ` "${entry}": "${hex}",`;
53
+ })
54
+ .join("\n");
55
+ return `// tailwind.config.js
56
+ module.exports = {
57
+ theme: {
58
+ extend: {
59
+ colors: {
60
+ "${key}": {
61
+ ${lines}
62
+ },
63
+ },
64
+ },
65
+ },
66
+ };
67
+ `;
68
+ }
69
+
70
+ function asTokens(items: NamedColor[], prefix?: string): string {
71
+ // W3C Design Tokens Community Group format (draft).
72
+ const group = prefix ?? "color";
73
+ const obj: Record<string, unknown> = {
74
+ [group]: Object.fromEntries(
75
+ items.map(({ hex, name }) => {
76
+ const entry =
77
+ prefix && name.startsWith(`${prefix}-`)
78
+ ? name.slice(prefix.length + 1)
79
+ : name;
80
+ return [entry, { $type: "color", $value: hex }];
81
+ }),
82
+ ),
83
+ };
84
+ return JSON.stringify(obj, null, 2) + "\n";
85
+ }
86
+
87
+ function defaultName(index: number): string {
88
+ // Tailwind-style 50/100…900 scale up to 10 colors, then numeric.
89
+ const scale = [
90
+ "50",
91
+ "100",
92
+ "200",
93
+ "300",
94
+ "400",
95
+ "500",
96
+ "600",
97
+ "700",
98
+ "800",
99
+ "900",
100
+ ];
101
+ return scale[index] ?? String((index + 1) * 100);
102
+ }
103
+
104
+ function resolveNames(
105
+ colors: string[],
106
+ names?: string[],
107
+ prefix?: string,
108
+ ): NamedColor[] {
109
+ return colors.map((c, i) => {
110
+ const parsed = parseColor(c);
111
+ if (!parsed) throw new Error(`Invalid color: ${c}`);
112
+ const hex = rgbToHex(parsed);
113
+ const raw = names?.[i] ?? defaultName(i);
114
+ const slug = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
115
+ const name = prefix ? `${prefix}-${slug}` : slug;
116
+ return { hex, name };
117
+ });
118
+ }
119
+
120
+ export const exportPaletteTool = {
121
+ description:
122
+ "Export a list of colors as CSS custom properties, SCSS variables, a Tailwind config snippet, W3C design tokens, or JSON.",
123
+ execute: async (args: {
124
+ colors: string[];
125
+ format: ExportFormat;
126
+ names?: string[];
127
+ prefix?: string;
128
+ }) => {
129
+ const { colors, format, names, prefix } = args;
130
+
131
+ if (names && names.length !== colors.length) {
132
+ return `Error: names array length (${names.length}) must match colors length (${colors.length}).`;
133
+ }
134
+
135
+ let items: NamedColor[];
136
+ try {
137
+ items = resolveNames(colors, names, prefix);
138
+ } catch (e) {
139
+ return e instanceof Error ? e.message : String(e);
140
+ }
141
+
142
+ switch (format) {
143
+ case "css":
144
+ return asCss(items);
145
+ case "json":
146
+ return asJson(items);
147
+ case "scss":
148
+ return asScss(items);
149
+ case "tailwind":
150
+ return asTailwind(items, prefix);
151
+ case "tokens":
152
+ return asTokens(items, prefix);
153
+ }
154
+ },
155
+ name: "export_palette",
156
+ parameters: z.object({
157
+ colors: z.array(z.string()).min(1).describe("Palette colors"),
158
+ format: z
159
+ .enum(FORMATS)
160
+ .describe(
161
+ "Output format: css (custom properties), scss (variables), tailwind (config), tokens (W3C design tokens), json",
162
+ ),
163
+ names: z
164
+ .array(z.string())
165
+ .optional()
166
+ .describe(
167
+ "Optional names for each color. Length must match colors. Defaults to a 50/100…900 scale.",
168
+ ),
169
+ prefix: z
170
+ .string()
171
+ .optional()
172
+ .describe("Optional prefix for variable/token names (e.g. 'brand')"),
173
+ }),
174
+ };