@trishchuk/coolors-mcp 1.0.0 → 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 (140) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +20 -8
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +22 -8
  3. package/.github/pull_request_template.md +33 -8
  4. package/.github/workflows/ci.yml +107 -104
  5. package/.github/workflows/deploy-docs.yml +14 -11
  6. package/.github/workflows/release.yml +25 -23
  7. package/README.md +149 -15
  8. package/dist/bin/server.js +997 -256
  9. package/dist/bin/server.js.map +1 -1
  10. package/dist/{chunk-P3ARRKLS.js → chunk-HOMDMKUY.js} +3 -1
  11. package/dist/{chunk-P3ARRKLS.js.map → chunk-HOMDMKUY.js.map} +1 -1
  12. package/dist/{chunk-IQ7NN26V.js → chunk-LHW2ZTOU.js} +14 -2
  13. package/dist/chunk-LHW2ZTOU.js.map +1 -0
  14. package/dist/color/index.js +1 -1
  15. package/dist/coolors-mcp.d.ts +4 -4
  16. package/dist/coolors-mcp.js +1 -1
  17. package/docs/.vitepress/components/ClientGrid.vue +9 -3
  18. package/docs/.vitepress/components/CodeBlock.vue +51 -44
  19. package/docs/.vitepress/components/ConfigModal.vue +151 -67
  20. package/docs/.vitepress/components/DiagramModal.vue +186 -154
  21. package/docs/.vitepress/components/TroubleshootingModal.vue +101 -96
  22. package/docs/.vitepress/config.js +171 -141
  23. package/docs/.vitepress/theme/FundingLayout.vue +65 -54
  24. package/docs/.vitepress/theme/Layout.vue +21 -21
  25. package/docs/.vitepress/theme/components/AdBanner.vue +73 -52
  26. package/docs/.vitepress/theme/components/AdPlaceholder.vue +3 -3
  27. package/docs/.vitepress/theme/components/FundingEffects.vue +77 -53
  28. package/docs/.vitepress/theme/components/FundingHero.vue +78 -63
  29. package/docs/.vitepress/theme/components/SupportSection.vue +106 -89
  30. package/docs/.vitepress/theme/custom-app.css +19 -12
  31. package/docs/.vitepress/theme/custom.css +33 -25
  32. package/docs/.vitepress/theme/index.js +19 -16
  33. package/docs/concepts/accessibility.md +59 -47
  34. package/docs/concepts/color-spaces.md +28 -6
  35. package/docs/concepts/distance-metrics.md +45 -30
  36. package/docs/concepts/hct.md +30 -27
  37. package/docs/concepts/image-analysis.md +52 -21
  38. package/docs/concepts/material-design.md +43 -17
  39. package/docs/concepts/theme-matching.md +64 -40
  40. package/docs/examples/basic-colors.md +92 -108
  41. package/docs/examples/creating-themes.md +104 -108
  42. package/docs/examples/css-refactoring.md +33 -29
  43. package/docs/examples/image-extraction.md +145 -138
  44. package/docs/getting-started.md +45 -34
  45. package/docs/index.md +5 -1
  46. package/docs/installation.md +15 -1
  47. package/docs/tools/accessibility.md +74 -68
  48. package/docs/tools/image-extraction.md +62 -54
  49. package/docs/tools/theme-matching.md +45 -42
  50. package/eslint.config.ts +13 -0
  51. package/jsr.json +1 -1
  52. package/package.json +17 -13
  53. package/src/bin/server.ts +13 -1
  54. package/src/color/__tests__/extract-colors.test.ts +20 -30
  55. package/src/color/apca.ts +105 -0
  56. package/src/color/color-blindness.ts +109 -0
  57. package/src/coolors-mcp.ts +1 -1
  58. package/src/session.ts +10 -2
  59. package/src/theme/matcher.ts +1 -1
  60. package/src/theme/refactor.ts +1 -1
  61. package/src/theme/types.ts +3 -0
  62. package/src/tools/__tests__/cohesion.test.ts +97 -0
  63. package/src/tools/__tests__/color-blindness.test.ts +45 -0
  64. package/src/tools/__tests__/color-conversion.test.ts +38 -0
  65. package/src/tools/__tests__/contrast-checker.test.ts +56 -0
  66. package/src/tools/__tests__/palette-export.test.ts +54 -0
  67. package/src/tools/adjust-color.tool.ts +80 -0
  68. package/src/tools/cohesion.tools.ts +380 -0
  69. package/src/tools/color-blindness.tool.ts +168 -0
  70. package/src/tools/color-conversion.tool.ts +1 -1
  71. package/src/tools/contrast-checker.tool.ts +53 -14
  72. package/src/tools/dislike-analyzer.tool.ts +41 -54
  73. package/src/tools/image-extraction.tools.ts +62 -115
  74. package/src/tools/index.ts +15 -2
  75. package/src/tools/palette-export.tool.ts +174 -0
  76. package/src/tools/palette-with-locks.tool.ts +8 -6
  77. package/src/types.ts +2 -3
  78. package/tsconfig.json +12 -2
  79. package/vitest.config.js +1 -3
  80. package/.claude/settings.local.json +0 -39
  81. package/.env +0 -2
  82. package/.mcp.json +0 -12
  83. package/CLAUDE.md +0 -201
  84. package/DOCUMENTATION.md +0 -274
  85. package/GEMINI.md +0 -54
  86. package/demo/content_based_color.png +0 -0
  87. package/demo/music-player.html +0 -621
  88. package/demo/podcast-player.html +0 -903
  89. package/dist/chunk-IQ7NN26V.js.map +0 -1
  90. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +0 -93
  91. package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +0 -7
  92. package/docs/.vitepress/cache/deps/_metadata.json +0 -127
  93. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +0 -9
  94. package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +0 -7
  95. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +0 -12683
  96. package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +0 -7
  97. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +0 -9719
  98. package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +0 -7
  99. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +0 -4710
  100. package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +0 -7
  101. package/docs/.vitepress/cache/deps/cytoscape.js +0 -30278
  102. package/docs/.vitepress/cache/deps/cytoscape.js.map +0 -7
  103. package/docs/.vitepress/cache/deps/dayjs.js +0 -285
  104. package/docs/.vitepress/cache/deps/dayjs.js.map +0 -7
  105. package/docs/.vitepress/cache/deps/debug.js +0 -468
  106. package/docs/.vitepress/cache/deps/debug.js.map +0 -7
  107. package/docs/.vitepress/cache/deps/package.json +0 -3
  108. package/docs/.vitepress/cache/deps/prismjs.js +0 -1466
  109. package/docs/.vitepress/cache/deps/prismjs.js.map +0 -7
  110. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +0 -228
  111. package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +0 -7
  112. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +0 -142
  113. package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +0 -7
  114. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +0 -27
  115. package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +0 -7
  116. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +0 -65
  117. package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +0 -7
  118. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +0 -53
  119. package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +0 -7
  120. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +0 -73
  121. package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +0 -7
  122. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4507
  123. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
  124. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -584
  125. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
  126. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +0 -1146
  127. package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +0 -7
  128. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +0 -1667
  129. package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +0 -7
  130. package/docs/.vitepress/cache/deps/vitepress___minisearch.js +0 -1814
  131. package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +0 -7
  132. package/docs/.vitepress/cache/deps/vue.js +0 -344
  133. package/docs/.vitepress/cache/deps/vue.js.map +0 -7
  134. package/examples/theme-matching.md +0 -113
  135. package/mcp-config.json +0 -8
  136. package/note.md +0 -35
  137. package/research_results.md +0 -53
  138. package/src/tools/colors.ts +0 -31
  139. package/src/tools/registry.ts +0 -142
  140. package/src/tools/simple-tools.ts +0 -37
@@ -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
+ };
@@ -9,7 +9,7 @@ import {
9
9
  rgbToHsl,
10
10
  rgbToLab,
11
11
  } from "../color/index.js";
12
- import { HSL, RGB } from "../color/types.js";
12
+ import { RGB } from "../color/types.js";
13
13
 
14
14
  export const paletteWithLocksTool = {
15
15
  description:
@@ -64,7 +64,9 @@ export const paletteWithLocksTool = {
64
64
 
65
65
  let minDistance = Infinity;
66
66
  for (const locked of parsedLocked) {
67
- const dist = colorDistance(candidate, locked, "deltaE2000");
67
+ const dist = colorDistance(candidate, locked, {
68
+ metric: "deltaE2000",
69
+ });
68
70
  if (dist < minDistance) minDistance = dist;
69
71
  }
70
72
 
@@ -135,17 +137,15 @@ export const paletteWithLocksTool = {
135
137
  case "harmony": {
136
138
  // Generate harmonious colors based on locked colors
137
139
  const baseHsl = rgbToHsl(parsedLocked[0]);
138
- const hueStep = 360 / totalColors;
139
140
 
140
141
  for (let i = 0; i < remainingSlots; i++) {
141
- let newHue = baseHsl.h;
142
142
  let attempts = 0;
143
143
  let bestColor: null | RGB = null;
144
144
  let maxMinDistance = 0;
145
145
 
146
146
  // Try different hues to find one that's not too close to locked colors
147
147
  while (attempts < 36) {
148
- newHue = (baseHsl.h + attempts * 10) % 360;
148
+ const newHue = (baseHsl.h + attempts * 10) % 360;
149
149
  const candidate = hslToRgb({
150
150
  h: newHue,
151
151
  l: baseHsl.l + (Math.random() - 0.5) * 20,
@@ -155,7 +155,9 @@ export const paletteWithLocksTool = {
155
155
  // Check minimum distance to all locked colors
156
156
  let minDistance = Infinity;
157
157
  for (const locked of parsedLocked) {
158
- const dist = colorDistance(candidate, locked, "deltaE2000");
158
+ const dist = colorDistance(candidate, locked, {
159
+ metric: "deltaE2000",
160
+ });
159
161
  if (dist < minDistance) minDistance = dist;
160
162
  }
161
163
 
package/src/types.ts CHANGED
@@ -36,8 +36,8 @@ export type Context<T extends CoolorsMCPSessionAuth> = {
36
36
  };
37
37
 
38
38
  export type CoolorsMCPEvents = {
39
- connect: (event: { session: any }) => void;
40
- disconnect: (event: { session: any }) => void;
39
+ connect: (event: { session: unknown }) => void;
40
+ disconnect: (event: { session: unknown }) => void;
41
41
  };
42
42
 
43
43
  export type CoolorsMCPSessionAuth = Record<string, unknown> | undefined;
@@ -164,7 +164,6 @@ export type Tool<
164
164
  | ResourceLink
165
165
  | string
166
166
  | TextContent
167
- | void
168
167
  >;
169
168
  name: string;
170
169
  parameters?: Params;
package/tsconfig.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "extends": "@tsconfig/node22/tsconfig.json",
3
3
  "compilerOptions": {
4
+ "ignoreDeprecations": "6.0",
4
5
  "noEmit": true,
5
6
  "noUnusedLocals": true,
6
- "noUnusedParameters": true
7
- }
7
+ "noUnusedParameters": true,
8
+ "types": ["node"]
9
+ },
10
+ "exclude": [
11
+ "dist",
12
+ "node_modules",
13
+ "src/examples",
14
+ "**/__tests__/**",
15
+ "**/*.test.ts",
16
+ "**/*_test.ts"
17
+ ]
8
18
  }
package/vitest.config.js CHANGED
@@ -2,9 +2,7 @@ import { defineConfig } from "vitest/config";
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
- poolOptions: {
6
- forks: { execArgv: ["--experimental-eventsource"] },
7
- },
5
+ forks: { execArgv: ["--experimental-eventsource"] },
8
6
  exclude: [
9
7
  "**/node_modules/**",
10
8
  "**/dist/**",