@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
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
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { exportPaletteTool } from "../palette-export.tool.js";
4
+
5
+ describe("exportPaletteTool", () => {
6
+ it("exports CSS custom properties", async () => {
7
+ const result = (await exportPaletteTool.execute({
8
+ colors: ["#6750a4", "#ff6b6b"],
9
+ format: "css",
10
+ names: ["primary", "danger"],
11
+ })) as string;
12
+ expect(result).toContain("--primary: #6750a4;");
13
+ expect(result).toContain("--danger: #ff6b6b;");
14
+ });
15
+
16
+ it("exports SCSS variables", async () => {
17
+ const result = (await exportPaletteTool.execute({
18
+ colors: ["#6750a4"],
19
+ format: "scss",
20
+ names: ["primary"],
21
+ })) as string;
22
+ expect(result).toContain("$primary: #6750a4;");
23
+ });
24
+
25
+ it("exports Tailwind config snippet", async () => {
26
+ const result = (await exportPaletteTool.execute({
27
+ colors: ["#fef3c7", "#fde68a"],
28
+ format: "tailwind",
29
+ prefix: "amber",
30
+ })) as string;
31
+ expect(result).toContain("tailwind.config.js");
32
+ expect(result).toContain('"amber"');
33
+ });
34
+
35
+ it("exports W3C design tokens", async () => {
36
+ const result = (await exportPaletteTool.execute({
37
+ colors: ["#6750a4"],
38
+ format: "tokens",
39
+ names: ["primary"],
40
+ })) as string;
41
+ const parsed = JSON.parse(result);
42
+ expect(parsed.color.primary.$type).toBe("color");
43
+ expect(parsed.color.primary.$value).toBe("#6750a4");
44
+ });
45
+
46
+ it("rejects mismatched names length", async () => {
47
+ const result = await exportPaletteTool.execute({
48
+ colors: ["#000", "#fff"],
49
+ format: "css",
50
+ names: ["only-one"],
51
+ });
52
+ expect(result).toMatch(/length/);
53
+ });
54
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Color Adjust / Mix Tool
3
+ * Exposes the lighten/darken/saturate/desaturate/grayscale/invert/mix utilities
4
+ * via MCP.
5
+ */
6
+
7
+ import { z } from "zod";
8
+
9
+ import {
10
+ darken,
11
+ desaturate,
12
+ invertColor,
13
+ lighten,
14
+ mixColors,
15
+ parseColor,
16
+ rgbToHex,
17
+ saturate,
18
+ toGrayscale,
19
+ } from "../color/index.js";
20
+
21
+ const OPS = [
22
+ "lighten",
23
+ "darken",
24
+ "saturate",
25
+ "desaturate",
26
+ "grayscale",
27
+ "invert",
28
+ "mix",
29
+ ] as const;
30
+
31
+ export const adjustColorTool = {
32
+ description:
33
+ "Adjust a color: lighten, darken, saturate, desaturate, grayscale, invert, or mix with a second color. Amount is 0-100 (percent) for lighten/darken/saturate/desaturate, 0-1 for mix weight.",
34
+ execute: async (args: {
35
+ amount?: number;
36
+ color: string;
37
+ operation: (typeof OPS)[number];
38
+ with?: string;
39
+ }) => {
40
+ const rgb = parseColor(args.color);
41
+ if (!rgb) return `Invalid color format: ${args.color}`;
42
+
43
+ switch (args.operation) {
44
+ case "darken":
45
+ return rgbToHex(darken(rgb, args.amount ?? 10));
46
+ case "desaturate":
47
+ return rgbToHex(desaturate(rgb, args.amount ?? 10));
48
+ case "grayscale":
49
+ return rgbToHex(toGrayscale(rgb));
50
+ case "invert":
51
+ return rgbToHex(invertColor(rgb));
52
+ case "lighten":
53
+ return rgbToHex(lighten(rgb, args.amount ?? 10));
54
+ case "mix": {
55
+ if (!args.with) return "Error: 'with' is required for mix operation";
56
+ const other = parseColor(args.with);
57
+ if (!other) return `Invalid color format: ${args.with}`;
58
+ const weight = args.amount ?? 0.5;
59
+ if (weight < 0 || weight > 1) {
60
+ return "Error: mix amount must be between 0 and 1";
61
+ }
62
+ return rgbToHex(mixColors(rgb, other, weight));
63
+ }
64
+ case "saturate":
65
+ return rgbToHex(saturate(rgb, args.amount ?? 10));
66
+ }
67
+ },
68
+ name: "adjust_color",
69
+ parameters: z.object({
70
+ amount: z
71
+ .number()
72
+ .optional()
73
+ .describe(
74
+ "Amount of change. For lighten/darken/saturate/desaturate: 0-100 (percent). For mix: 0-1 (weight of first color). Default 10 / 0.5.",
75
+ ),
76
+ color: z.string().describe("Color to adjust (hex, rgb, hsl)"),
77
+ operation: z.enum(OPS).describe("Adjustment operation"),
78
+ with: z.string().optional().describe("Second color (only for mix)"),
79
+ }),
80
+ };