@rog0x/mcp-color-tools 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.
package/src/index.ts ADDED
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { convertColor } from "./tools/color-converter.js";
10
+ import { generatePalette, type PaletteType } from "./tools/palette-generator.js";
11
+ import { checkContrast } from "./tools/contrast-checker.js";
12
+ import { colorMix, type MixOperation } from "./tools/color-mixer.js";
13
+ import { generateGradient, type GradientType, type ColorStop } from "./tools/css-gradient.js";
14
+
15
+ const server = new Server(
16
+ {
17
+ name: "mcp-color-tools",
18
+ version: "1.0.0",
19
+ },
20
+ {
21
+ capabilities: {
22
+ tools: {},
23
+ },
24
+ }
25
+ );
26
+
27
+ // List available tools
28
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
29
+ tools: [
30
+ {
31
+ name: "color_convert",
32
+ description:
33
+ "Convert a color between formats: HEX, RGB, HSL, HSV, CMYK. Accepts any format as input and returns all formats.",
34
+ inputSchema: {
35
+ type: "object" as const,
36
+ properties: {
37
+ color: {
38
+ type: "string",
39
+ description:
40
+ "Color in any format: HEX (#ff0000), RGB (rgb(255,0,0)), HSL (hsl(0,100%,50%)), HSV (hsv(0,100%,100%)), CMYK (cmyk(0,100,100,0))",
41
+ },
42
+ },
43
+ required: ["color"],
44
+ },
45
+ },
46
+ {
47
+ name: "palette_generate",
48
+ description:
49
+ "Generate a color palette from a base color. Types: complementary, analogous, triadic, split-complementary, monochromatic.",
50
+ inputSchema: {
51
+ type: "object" as const,
52
+ properties: {
53
+ color: {
54
+ type: "string",
55
+ description: "Base color in any supported format",
56
+ },
57
+ type: {
58
+ type: "string",
59
+ description:
60
+ "Palette type: complementary, analogous, triadic, split-complementary, monochromatic",
61
+ enum: [
62
+ "complementary",
63
+ "analogous",
64
+ "triadic",
65
+ "split-complementary",
66
+ "monochromatic",
67
+ ],
68
+ },
69
+ },
70
+ required: ["color", "type"],
71
+ },
72
+ },
73
+ {
74
+ name: "contrast_check",
75
+ description:
76
+ "Check WCAG contrast ratio between two colors. Reports AA/AAA compliance for normal text, large text, and UI components.",
77
+ inputSchema: {
78
+ type: "object" as const,
79
+ properties: {
80
+ color1: {
81
+ type: "string",
82
+ description: "First color (foreground) in any supported format",
83
+ },
84
+ color2: {
85
+ type: "string",
86
+ description: "Second color (background) in any supported format",
87
+ },
88
+ },
89
+ required: ["color1", "color2"],
90
+ },
91
+ },
92
+ {
93
+ name: "color_mix",
94
+ description:
95
+ "Mix, blend, lighten, darken, saturate, or desaturate colors. For mix/blend provide 2+ colors; for lighten/darken/saturate/desaturate provide 1 color and an amount.",
96
+ inputSchema: {
97
+ type: "object" as const,
98
+ properties: {
99
+ operation: {
100
+ type: "string",
101
+ description:
102
+ "Operation: mix, blend, lighten, darken, saturate, desaturate",
103
+ enum: [
104
+ "mix",
105
+ "blend",
106
+ "lighten",
107
+ "darken",
108
+ "saturate",
109
+ "desaturate",
110
+ ],
111
+ },
112
+ colors: {
113
+ type: "array",
114
+ items: { type: "string" },
115
+ description: "Array of colors in any supported format",
116
+ },
117
+ weights: {
118
+ type: "array",
119
+ items: { type: "number" },
120
+ description:
121
+ "Optional weights for mixing (same length as colors). Default: equal weights.",
122
+ },
123
+ ratio: {
124
+ type: "number",
125
+ description:
126
+ "Blend ratio from 0 (first color) to 1 (second color). Default: 0.5.",
127
+ },
128
+ amount: {
129
+ type: "number",
130
+ description:
131
+ "Amount for lighten/darken/saturate/desaturate (0-100). Default: 10.",
132
+ },
133
+ },
134
+ required: ["operation", "colors"],
135
+ },
136
+ },
137
+ {
138
+ name: "css_gradient",
139
+ description:
140
+ "Generate CSS gradient code. Supports linear, radial, and conic gradients with direction, stops, and browser prefixes.",
141
+ inputSchema: {
142
+ type: "object" as const,
143
+ properties: {
144
+ type: {
145
+ type: "string",
146
+ description: "Gradient type: linear, radial, conic",
147
+ enum: ["linear", "radial", "conic"],
148
+ },
149
+ colors: {
150
+ type: "array",
151
+ items: {
152
+ oneOf: [
153
+ { type: "string" },
154
+ {
155
+ type: "object",
156
+ properties: {
157
+ color: { type: "string" },
158
+ position: {
159
+ type: "number",
160
+ description: "Stop position as percentage (0-100)",
161
+ },
162
+ },
163
+ required: ["color"],
164
+ },
165
+ ],
166
+ },
167
+ description:
168
+ "Array of colors (strings) or color stops ({color, position}). Minimum 2.",
169
+ },
170
+ direction: {
171
+ type: "string",
172
+ description:
173
+ "Direction/shape. Linear: 'to right', '45deg'. Radial: 'circle at center', 'ellipse at top'. Conic: 'from 0deg at center'.",
174
+ },
175
+ includePrefix: {
176
+ type: "boolean",
177
+ description:
178
+ "Include -webkit- browser prefix. Default: true.",
179
+ },
180
+ },
181
+ required: ["type", "colors"],
182
+ },
183
+ },
184
+ ],
185
+ }));
186
+
187
+ // Handle tool calls
188
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
189
+ const { name, arguments: args } = request.params;
190
+
191
+ try {
192
+ switch (name) {
193
+ case "color_convert": {
194
+ const result = convertColor(args?.color as string);
195
+ return {
196
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
197
+ };
198
+ }
199
+
200
+ case "palette_generate": {
201
+ const result = generatePalette(
202
+ args?.color as string,
203
+ args?.type as PaletteType
204
+ );
205
+ return {
206
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
207
+ };
208
+ }
209
+
210
+ case "contrast_check": {
211
+ const result = checkContrast(
212
+ args?.color1 as string,
213
+ args?.color2 as string
214
+ );
215
+ return {
216
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
217
+ };
218
+ }
219
+
220
+ case "color_mix": {
221
+ const result = colorMix({
222
+ operation: args?.operation as MixOperation,
223
+ colors: args?.colors as string[],
224
+ weights: args?.weights as number[] | undefined,
225
+ ratio: args?.ratio as number | undefined,
226
+ amount: args?.amount as number | undefined,
227
+ });
228
+ return {
229
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
230
+ };
231
+ }
232
+
233
+ case "css_gradient": {
234
+ const result = generateGradient({
235
+ type: args?.type as GradientType,
236
+ colors: args?.colors as (string | ColorStop)[],
237
+ direction: args?.direction as string | undefined,
238
+ includePrefix: args?.includePrefix as boolean | undefined,
239
+ });
240
+ return {
241
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
242
+ };
243
+ }
244
+
245
+ default:
246
+ throw new Error(`Unknown tool: ${name}`);
247
+ }
248
+ } catch (error: unknown) {
249
+ const message = error instanceof Error ? error.message : String(error);
250
+ return {
251
+ content: [{ type: "text", text: `Error: ${message}` }],
252
+ isError: true,
253
+ };
254
+ }
255
+ });
256
+
257
+ // Start server
258
+ async function main() {
259
+ const transport = new StdioServerTransport();
260
+ await server.connect(transport);
261
+ console.error("MCP Color Tools server running on stdio");
262
+ }
263
+
264
+ main().catch(console.error);
@@ -0,0 +1,311 @@
1
+ // Color Converter — convert between HEX, RGB, HSL, HSV, and CMYK formats.
2
+ // All color math is implemented manually without external libraries.
3
+
4
+ export interface RGB {
5
+ r: number;
6
+ g: number;
7
+ b: number;
8
+ }
9
+
10
+ export interface HSL {
11
+ h: number;
12
+ s: number;
13
+ l: number;
14
+ }
15
+
16
+ export interface HSV {
17
+ h: number;
18
+ s: number;
19
+ v: number;
20
+ }
21
+
22
+ export interface CMYK {
23
+ c: number;
24
+ m: number;
25
+ y: number;
26
+ k: number;
27
+ }
28
+
29
+ // --- Parsing ---
30
+
31
+ function parseHex(hex: string): RGB {
32
+ let h = hex.replace(/^#/, "").trim();
33
+ if (h.length === 3) {
34
+ h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
35
+ }
36
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) {
37
+ throw new Error(`Invalid HEX color: ${hex}`);
38
+ }
39
+ return {
40
+ r: parseInt(h.substring(0, 2), 16),
41
+ g: parseInt(h.substring(2, 4), 16),
42
+ b: parseInt(h.substring(4, 6), 16),
43
+ };
44
+ }
45
+
46
+ function parseRgbString(str: string): RGB {
47
+ const match = str.match(
48
+ /rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+\s*)?\)/i
49
+ );
50
+ if (!match) throw new Error(`Invalid RGB string: ${str}`);
51
+ return {
52
+ r: clamp(parseInt(match[1]), 0, 255),
53
+ g: clamp(parseInt(match[2]), 0, 255),
54
+ b: clamp(parseInt(match[3]), 0, 255),
55
+ };
56
+ }
57
+
58
+ function parseHslString(str: string): HSL {
59
+ const match = str.match(
60
+ /hsla?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*(?:,\s*[\d.]+\s*)?\)/i
61
+ );
62
+ if (!match) throw new Error(`Invalid HSL string: ${str}`);
63
+ return {
64
+ h: parseFloat(match[1]) % 360,
65
+ s: clamp(parseFloat(match[2]), 0, 100),
66
+ l: clamp(parseFloat(match[3]), 0, 100),
67
+ };
68
+ }
69
+
70
+ function parseHsvString(str: string): HSV {
71
+ const match = str.match(
72
+ /hsv\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*\)/i
73
+ );
74
+ if (!match) throw new Error(`Invalid HSV string: ${str}`);
75
+ return {
76
+ h: parseFloat(match[1]) % 360,
77
+ s: clamp(parseFloat(match[2]), 0, 100),
78
+ v: clamp(parseFloat(match[3]), 0, 100),
79
+ };
80
+ }
81
+
82
+ function parseCmykString(str: string): CMYK {
83
+ const match = str.match(
84
+ /cmyk\s*\(\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*\)/i
85
+ );
86
+ if (!match) throw new Error(`Invalid CMYK string: ${str}`);
87
+ return {
88
+ c: clamp(parseFloat(match[1]), 0, 100),
89
+ m: clamp(parseFloat(match[2]), 0, 100),
90
+ y: clamp(parseFloat(match[3]), 0, 100),
91
+ k: clamp(parseFloat(match[4]), 0, 100),
92
+ };
93
+ }
94
+
95
+ function clamp(val: number, min: number, max: number): number {
96
+ return Math.max(min, Math.min(max, val));
97
+ }
98
+
99
+ // --- Conversion core ---
100
+
101
+ export function rgbToHex(rgb: RGB): string {
102
+ const toHex = (n: number) =>
103
+ clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
104
+ return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
105
+ }
106
+
107
+ export function rgbToHsl(rgb: RGB): HSL {
108
+ const r = rgb.r / 255;
109
+ const g = rgb.g / 255;
110
+ const b = rgb.b / 255;
111
+ const max = Math.max(r, g, b);
112
+ const min = Math.min(r, g, b);
113
+ const delta = max - min;
114
+ let h = 0;
115
+ let s = 0;
116
+ const l = (max + min) / 2;
117
+
118
+ if (delta !== 0) {
119
+ s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
120
+ if (max === r) {
121
+ h = ((g - b) / delta + (g < b ? 6 : 0)) * 60;
122
+ } else if (max === g) {
123
+ h = ((b - r) / delta + 2) * 60;
124
+ } else {
125
+ h = ((r - g) / delta + 4) * 60;
126
+ }
127
+ }
128
+
129
+ return {
130
+ h: round2(h),
131
+ s: round2(s * 100),
132
+ l: round2(l * 100),
133
+ };
134
+ }
135
+
136
+ export function hslToRgb(hsl: HSL): RGB {
137
+ const h = hsl.h;
138
+ const s = hsl.s / 100;
139
+ const l = hsl.l / 100;
140
+
141
+ if (s === 0) {
142
+ const v = Math.round(l * 255);
143
+ return { r: v, g: v, b: v };
144
+ }
145
+
146
+ const hueToRgb = (p: number, q: number, t: number): number => {
147
+ if (t < 0) t += 1;
148
+ if (t > 1) t -= 1;
149
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
150
+ if (t < 1 / 2) return q;
151
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
152
+ return p;
153
+ };
154
+
155
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
156
+ const p = 2 * l - q;
157
+ const hNorm = h / 360;
158
+
159
+ return {
160
+ r: Math.round(hueToRgb(p, q, hNorm + 1 / 3) * 255),
161
+ g: Math.round(hueToRgb(p, q, hNorm) * 255),
162
+ b: Math.round(hueToRgb(p, q, hNorm - 1 / 3) * 255),
163
+ };
164
+ }
165
+
166
+ export function rgbToHsv(rgb: RGB): HSV {
167
+ const r = rgb.r / 255;
168
+ const g = rgb.g / 255;
169
+ const b = rgb.b / 255;
170
+ const max = Math.max(r, g, b);
171
+ const min = Math.min(r, g, b);
172
+ const delta = max - min;
173
+ let h = 0;
174
+ const s = max === 0 ? 0 : delta / max;
175
+ const v = max;
176
+
177
+ if (delta !== 0) {
178
+ if (max === r) {
179
+ h = ((g - b) / delta + (g < b ? 6 : 0)) * 60;
180
+ } else if (max === g) {
181
+ h = ((b - r) / delta + 2) * 60;
182
+ } else {
183
+ h = ((r - g) / delta + 4) * 60;
184
+ }
185
+ }
186
+
187
+ return {
188
+ h: round2(h),
189
+ s: round2(s * 100),
190
+ v: round2(v * 100),
191
+ };
192
+ }
193
+
194
+ export function hsvToRgb(hsv: HSV): RGB {
195
+ const h = hsv.h;
196
+ const s = hsv.s / 100;
197
+ const v = hsv.v / 100;
198
+ const c = v * s;
199
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
200
+ const m = v - c;
201
+ let r1 = 0,
202
+ g1 = 0,
203
+ b1 = 0;
204
+
205
+ if (h < 60) {
206
+ r1 = c; g1 = x; b1 = 0;
207
+ } else if (h < 120) {
208
+ r1 = x; g1 = c; b1 = 0;
209
+ } else if (h < 180) {
210
+ r1 = 0; g1 = c; b1 = x;
211
+ } else if (h < 240) {
212
+ r1 = 0; g1 = x; b1 = c;
213
+ } else if (h < 300) {
214
+ r1 = x; g1 = 0; b1 = c;
215
+ } else {
216
+ r1 = c; g1 = 0; b1 = x;
217
+ }
218
+
219
+ return {
220
+ r: Math.round((r1 + m) * 255),
221
+ g: Math.round((g1 + m) * 255),
222
+ b: Math.round((b1 + m) * 255),
223
+ };
224
+ }
225
+
226
+ export function rgbToCmyk(rgb: RGB): CMYK {
227
+ const r = rgb.r / 255;
228
+ const g = rgb.g / 255;
229
+ const b = rgb.b / 255;
230
+ const k = 1 - Math.max(r, g, b);
231
+
232
+ if (k === 1) {
233
+ return { c: 0, m: 0, y: 0, k: 100 };
234
+ }
235
+
236
+ return {
237
+ c: round2(((1 - r - k) / (1 - k)) * 100),
238
+ m: round2(((1 - g - k) / (1 - k)) * 100),
239
+ y: round2(((1 - b - k) / (1 - k)) * 100),
240
+ k: round2(k * 100),
241
+ };
242
+ }
243
+
244
+ export function cmykToRgb(cmyk: CMYK): RGB {
245
+ const c = cmyk.c / 100;
246
+ const m = cmyk.m / 100;
247
+ const y = cmyk.y / 100;
248
+ const k = cmyk.k / 100;
249
+
250
+ return {
251
+ r: Math.round(255 * (1 - c) * (1 - k)),
252
+ g: Math.round(255 * (1 - m) * (1 - k)),
253
+ b: Math.round(255 * (1 - y) * (1 - k)),
254
+ };
255
+ }
256
+
257
+ function round2(n: number): number {
258
+ return Math.round(n * 100) / 100;
259
+ }
260
+
261
+ // --- Auto-detect and parse any color format ---
262
+
263
+ export function parseColor(input: string): RGB {
264
+ const trimmed = input.trim();
265
+
266
+ if (/^#?[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(trimmed)) {
267
+ return parseHex(trimmed);
268
+ }
269
+ if (/^rgba?\s*\(/i.test(trimmed)) {
270
+ return parseRgbString(trimmed);
271
+ }
272
+ if (/^hsla?\s*\(/i.test(trimmed)) {
273
+ return hslToRgb(parseHslString(trimmed));
274
+ }
275
+ if (/^hsv\s*\(/i.test(trimmed)) {
276
+ return hsvToRgb(parseHsvString(trimmed));
277
+ }
278
+ if (/^cmyk\s*\(/i.test(trimmed)) {
279
+ return cmykToRgb(parseCmykString(trimmed));
280
+ }
281
+
282
+ throw new Error(
283
+ `Cannot parse color: "${input}". Supported formats: HEX (#ff0000), RGB (rgb(255,0,0)), HSL (hsl(0,100%,50%)), HSV (hsv(0,100%,100%)), CMYK (cmyk(0,100,100,0))`
284
+ );
285
+ }
286
+
287
+ // --- Public tool function ---
288
+
289
+ export interface ConvertResult {
290
+ hex: string;
291
+ rgb: { r: number; g: number; b: number; css: string };
292
+ hsl: { h: number; s: number; l: number; css: string };
293
+ hsv: { h: number; s: number; v: number };
294
+ cmyk: { c: number; m: number; y: number; k: number };
295
+ }
296
+
297
+ export function convertColor(input: string): ConvertResult {
298
+ const rgb = parseColor(input);
299
+ const hsl = rgbToHsl(rgb);
300
+ const hsv = rgbToHsv(rgb);
301
+ const cmyk = rgbToCmyk(rgb);
302
+ const hex = rgbToHex(rgb);
303
+
304
+ return {
305
+ hex,
306
+ rgb: { ...rgb, css: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})` },
307
+ hsl: { ...hsl, css: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)` },
308
+ hsv,
309
+ cmyk,
310
+ };
311
+ }