@trishchuk/coolors-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +39 -0
- package/.env +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +73 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +71 -0
- package/.github/pull_request_template.md +97 -0
- package/.github/workflows/ci.yml +127 -0
- package/.github/workflows/deploy-docs.yml +56 -0
- package/.github/workflows/release.yml +99 -0
- package/.mcp.json +12 -0
- package/.prettierignore +1 -0
- package/CLAUDE.md +201 -0
- package/DOCUMENTATION.md +274 -0
- package/GEMINI.md +54 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/demo/content_based_color.png +0 -0
- package/demo/music-player.html +621 -0
- package/demo/podcast-player.html +903 -0
- package/dist/bin/coolors-mcp.d.ts +1 -0
- package/dist/bin/coolors-mcp.js +154 -0
- package/dist/bin/coolors-mcp.js.map +1 -0
- package/dist/bin/server.d.ts +1 -0
- package/dist/bin/server.js +3292 -0
- package/dist/bin/server.js.map +1 -0
- package/dist/chunk-IQ7NN26V.js +114 -0
- package/dist/chunk-IQ7NN26V.js.map +1 -0
- package/dist/chunk-P3ARRKLS.js +1214 -0
- package/dist/chunk-P3ARRKLS.js.map +1 -0
- package/dist/color/index.d.ts +716 -0
- package/dist/color/index.js +153 -0
- package/dist/color/index.js.map +1 -0
- package/dist/coolors-mcp.d.ts +136 -0
- package/dist/coolors-mcp.js +7 -0
- package/dist/coolors-mcp.js.map +1 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
- package/docs/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
- package/docs/.vitepress/cache/deps/_metadata.json +127 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
- package/docs/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js +12683 -0
- package/docs/.vitepress/cache/deps/chunk-JD3CXNQ6.js.map +7 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js +9719 -0
- package/docs/.vitepress/cache/deps/chunk-SYPOPCWC.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
- package/docs/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
- package/docs/.vitepress/cache/deps/cytoscape.js +30278 -0
- package/docs/.vitepress/cache/deps/cytoscape.js.map +7 -0
- package/docs/.vitepress/cache/deps/dayjs.js +285 -0
- package/docs/.vitepress/cache/deps/dayjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/debug.js +468 -0
- package/docs/.vitepress/cache/deps/debug.js.map +7 -0
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/.vitepress/cache/deps/prismjs.js +1466 -0
- package/docs/.vitepress/cache/deps/prismjs.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js +228 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-bash.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js +142 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-javascript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js +27 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-json.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js +65 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-python.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js +53 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-typescript.js.map +7 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js +73 -0
- package/docs/.vitepress/cache/deps/prismjs_components_prism-yaml.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
- package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1146 -0
- package/docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
- package/docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js +1814 -0
- package/docs/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
- package/docs/.vitepress/cache/deps/vue.js +344 -0
- package/docs/.vitepress/cache/deps/vue.js.map +7 -0
- package/docs/.vitepress/components/ClientGrid.vue +125 -0
- package/docs/.vitepress/components/CodeBlock.vue +231 -0
- package/docs/.vitepress/components/ConfigModal.vue +477 -0
- package/docs/.vitepress/components/DiagramModal.vue +528 -0
- package/docs/.vitepress/components/TroubleshootingModal.vue +472 -0
- package/docs/.vitepress/config.js +162 -0
- package/docs/.vitepress/theme/FundingLayout.vue +251 -0
- package/docs/.vitepress/theme/Layout.vue +134 -0
- package/docs/.vitepress/theme/components/AdBanner.vue +317 -0
- package/docs/.vitepress/theme/components/AdPlaceholder.vue +78 -0
- package/docs/.vitepress/theme/components/FundingEffects.vue +322 -0
- package/docs/.vitepress/theme/components/FundingHero.vue +345 -0
- package/docs/.vitepress/theme/components/SupportSection.vue +511 -0
- package/docs/.vitepress/theme/custom-app.css +339 -0
- package/docs/.vitepress/theme/custom.css +699 -0
- package/docs/.vitepress/theme/index.js +25 -0
- package/docs/README.md +198 -0
- package/docs/concepts/accessibility.md +473 -0
- package/docs/concepts/color-spaces.md +222 -0
- package/docs/concepts/distance-metrics.md +384 -0
- package/docs/concepts/hct.md +261 -0
- package/docs/concepts/image-analysis.md +396 -0
- package/docs/concepts/material-design.md +306 -0
- package/docs/concepts/theme-matching.md +399 -0
- package/docs/examples/basic-colors.md +490 -0
- package/docs/examples/creating-themes.md +898 -0
- package/docs/examples/css-refactoring.md +824 -0
- package/docs/examples/image-extraction.md +882 -0
- package/docs/getting-started.md +366 -0
- package/docs/index.md +190 -0
- package/docs/installation.md +157 -0
- package/docs/tools/README.md +234 -0
- package/docs/tools/accessibility.md +614 -0
- package/docs/tools/color-operations.md +374 -0
- package/docs/tools/image-extraction.md +624 -0
- package/docs/tools/material-design.md +347 -0
- package/docs/tools/theme-matching.md +552 -0
- package/eslint.config.ts +14 -0
- package/examples/theme-matching.md +113 -0
- package/jsr.json +7 -0
- package/mcp-config.json +8 -0
- package/note.md +35 -0
- package/package.json +122 -0
- package/research_results.md +53 -0
- package/src/bin/coolors-mcp.ts +194 -0
- package/src/bin/server.ts +61 -0
- package/src/color/__tests__/conversions-argb.test.ts +198 -0
- package/src/color/__tests__/extract-colors.test.ts +360 -0
- package/src/color/__tests__/image-utils.test.ts +242 -0
- package/src/color/__tests__/reference-colors.test.ts +278 -0
- package/src/color/__tests__/round-trip.test.ts +197 -0
- package/src/color/conversions.test.ts +402 -0
- package/src/color/conversions.ts +393 -0
- package/src/color/dislike/__tests__/dislike-analyzer.test.ts +248 -0
- package/src/color/dislike/dislike-analyzer.ts +114 -0
- package/src/color/extract-colors.ts +228 -0
- package/src/color/hct/__tests__/hct-class.test.ts +232 -0
- package/src/color/hct/harmonization.ts +204 -0
- package/src/color/hct/hct-class.ts +109 -0
- package/src/color/hct/hct-solver.ts +168 -0
- package/src/color/hct/index.ts +39 -0
- package/src/color/hct/tonal-palette.ts +211 -0
- package/src/color/hct/types.ts +88 -0
- package/src/color/image-utils.ts +79 -0
- package/src/color/index.ts +87 -0
- package/src/color/material-theme.ts +157 -0
- package/src/color/metrics.test.ts +276 -0
- package/src/color/metrics.ts +281 -0
- package/src/color/quantize/__tests__/quantizer_celebi.test.ts +195 -0
- package/src/color/quantize/lab_point_provider.ts +55 -0
- package/src/color/quantize/point_provider.ts +27 -0
- package/src/color/quantize/quantizer_celebi.ts +51 -0
- package/src/color/quantize/quantizer_celebi_test.ts +71 -0
- package/src/color/quantize/quantizer_map.ts +47 -0
- package/src/color/quantize/quantizer_wsmeans.ts +232 -0
- package/src/color/quantize/quantizer_wu.ts +472 -0
- package/src/color/score/__tests__/score.test.ts +224 -0
- package/src/color/score/score.ts +175 -0
- package/src/color/types.ts +151 -0
- package/src/color/utils/color_utils.ts +292 -0
- package/src/color/utils/math_utils.ts +145 -0
- package/src/color/utils.test.ts +403 -0
- package/src/color/utils.ts +315 -0
- package/src/constants.ts +5 -0
- package/src/coolors-mcp.ts +37 -0
- package/src/examples/addition.ts +333 -0
- package/src/examples/color-demo.ts +125 -0
- package/src/examples/custom-logger.ts +201 -0
- package/src/examples/oauth-server.ts +113 -0
- package/src/examples/session-context.ts +269 -0
- package/src/session.ts +116 -0
- package/src/theme/__tests__/matcher.test.ts +180 -0
- package/src/theme/__tests__/parser.test.ts +148 -0
- package/src/theme/__tests__/refactor.test.ts +224 -0
- package/src/theme/index.ts +34 -0
- package/src/theme/matcher.ts +395 -0
- package/src/theme/parser.ts +392 -0
- package/src/theme/refactor.ts +360 -0
- package/src/theme/types.ts +152 -0
- package/src/tools/__tests__/gradient-generator.test.ts +206 -0
- package/src/tools/__tests__/palette-with-locks.test.ts +109 -0
- package/src/tools/color-conversion.tool.ts +54 -0
- package/src/tools/color-distance.tool.ts +41 -0
- package/src/tools/colors.ts +31 -0
- package/src/tools/contrast-checker.tool.ts +37 -0
- package/src/tools/dislike-analyzer.tool.ts +247 -0
- package/src/tools/gradient-generator.tool.ts +250 -0
- package/src/tools/image-extraction.tools.ts +289 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/material-theme.tools.ts +250 -0
- package/src/tools/palette-generator.tool.ts +135 -0
- package/src/tools/palette-with-locks.tool.ts +221 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/simple-tools.ts +37 -0
- package/src/tools/theme-matching.tools.ts +334 -0
- package/src/types.ts +182 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +8 -0
- package/vitest.config.js +15 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for color manipulation and validation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { HSL, HSV, RGB } from "./types.js";
|
|
6
|
+
|
|
7
|
+
import { parseColor, rgbToHex, rgbToHsl, rgbToHsv } from "./conversions.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Clamp HSL values to valid range
|
|
11
|
+
*/
|
|
12
|
+
export function clampHsl(hsl: HSL): HSL {
|
|
13
|
+
return {
|
|
14
|
+
h: clamp(hsl.h, 0, 360),
|
|
15
|
+
l: clamp(hsl.l, 0, 100),
|
|
16
|
+
s: clamp(hsl.s, 0, 100),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Clamp HSV values to valid range
|
|
22
|
+
*/
|
|
23
|
+
export function clampHsv(hsv: HSV): HSV {
|
|
24
|
+
return {
|
|
25
|
+
h: clamp(hsv.h, 0, 360),
|
|
26
|
+
s: clamp(hsv.s, 0, 100),
|
|
27
|
+
v: clamp(hsv.v, 0, 100),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clamp RGB values to valid range
|
|
33
|
+
*/
|
|
34
|
+
export function clampRgb(rgb: RGB): RGB {
|
|
35
|
+
return {
|
|
36
|
+
b: clamp(rgb.b, 0, 255),
|
|
37
|
+
g: clamp(rgb.g, 0, 255),
|
|
38
|
+
r: clamp(rgb.r, 0, 255),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Darken a color by a percentage
|
|
44
|
+
* @param rgb The color to darken
|
|
45
|
+
* @param amount Amount to darken (0-100)
|
|
46
|
+
*/
|
|
47
|
+
export function darken(rgb: RGB, amount: number): RGB {
|
|
48
|
+
const hsl = rgbToHsl(rgb);
|
|
49
|
+
hsl.l = clamp(hsl.l - amount, 0, 100);
|
|
50
|
+
return parseColor(formatHsl(hsl)) || rgb;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Desaturate a color by a percentage
|
|
55
|
+
* @param rgb The color to desaturate
|
|
56
|
+
* @param amount Amount to desaturate (0-100)
|
|
57
|
+
*/
|
|
58
|
+
export function desaturate(rgb: RGB, amount: number): RGB {
|
|
59
|
+
const hsl = rgbToHsl(rgb);
|
|
60
|
+
hsl.s = clamp(hsl.s - amount, 0, 100);
|
|
61
|
+
return parseColor(formatHsl(hsl)) || rgb;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format color to various string representations
|
|
66
|
+
*/
|
|
67
|
+
export function formatColor(
|
|
68
|
+
rgb: RGB,
|
|
69
|
+
format: "hex" | "hsl" | "hsv" | "rgb" = "hex",
|
|
70
|
+
): string {
|
|
71
|
+
switch (format) {
|
|
72
|
+
case "hex":
|
|
73
|
+
return rgbToHex(rgb);
|
|
74
|
+
case "hsl":
|
|
75
|
+
return formatHsl(rgbToHsl(rgb));
|
|
76
|
+
case "hsv": {
|
|
77
|
+
const hsv = rgbToHsv(rgb);
|
|
78
|
+
return `hsv(${hsv.h}, ${hsv.s}%, ${hsv.v}%)`;
|
|
79
|
+
}
|
|
80
|
+
case "rgb":
|
|
81
|
+
return formatRgb(rgb);
|
|
82
|
+
default:
|
|
83
|
+
return rgbToHex(rgb);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Format HSL color to string
|
|
89
|
+
*/
|
|
90
|
+
export function formatHsl(hsl: HSL, alpha?: number): string {
|
|
91
|
+
if (alpha !== undefined) {
|
|
92
|
+
return `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${alpha})`;
|
|
93
|
+
}
|
|
94
|
+
return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Format RGB color to string
|
|
99
|
+
*/
|
|
100
|
+
export function formatRgb(rgb: RGB, alpha?: number): string {
|
|
101
|
+
if (alpha !== undefined) {
|
|
102
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
|
|
103
|
+
}
|
|
104
|
+
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get complementary color (opposite on color wheel)
|
|
109
|
+
*/
|
|
110
|
+
export function getComplementary(rgb: RGB): RGB {
|
|
111
|
+
const hsl = rgbToHsl(rgb);
|
|
112
|
+
hsl.h = (hsl.h + 180) % 360;
|
|
113
|
+
return parseColor(formatHsl(hsl)) || rgb;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Calculate contrast ratio between two colors
|
|
118
|
+
* Based on WCAG 2.0 formula
|
|
119
|
+
*/
|
|
120
|
+
export function getContrastRatio(color1: RGB, color2: RGB): number {
|
|
121
|
+
const lum1 = getLuminance(color1);
|
|
122
|
+
const lum2 = getLuminance(color2);
|
|
123
|
+
|
|
124
|
+
const lighter = Math.max(lum1, lum2);
|
|
125
|
+
const darker = Math.min(lum1, lum2);
|
|
126
|
+
|
|
127
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Calculate relative luminance of an RGB color
|
|
132
|
+
* Based on WCAG 2.0 formula
|
|
133
|
+
*/
|
|
134
|
+
export function getLuminance(rgb: RGB): number {
|
|
135
|
+
const rsRGB = rgb.r / 255;
|
|
136
|
+
const gsRGB = rgb.g / 255;
|
|
137
|
+
const bsRGB = rgb.b / 255;
|
|
138
|
+
|
|
139
|
+
const r =
|
|
140
|
+
rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
|
|
141
|
+
const g =
|
|
142
|
+
gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
|
|
143
|
+
const b =
|
|
144
|
+
bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
|
|
145
|
+
|
|
146
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Invert a color
|
|
151
|
+
*/
|
|
152
|
+
export function invertColor(rgb: RGB): RGB {
|
|
153
|
+
return {
|
|
154
|
+
b: 255 - rgb.b,
|
|
155
|
+
g: 255 - rgb.g,
|
|
156
|
+
r: 255 - rgb.r,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if a color is considered "dark"
|
|
162
|
+
* Based on luminance threshold
|
|
163
|
+
*/
|
|
164
|
+
export function isDark(rgb: RGB, threshold: number = 0.5): boolean {
|
|
165
|
+
return getLuminance(rgb) < threshold;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if a color is considered "light"
|
|
170
|
+
* Based on luminance threshold
|
|
171
|
+
*/
|
|
172
|
+
export function isLight(rgb: RGB, threshold: number = 0.5): boolean {
|
|
173
|
+
return getLuminance(rgb) >= threshold;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Validate hexadecimal color string
|
|
178
|
+
*/
|
|
179
|
+
export function isValidHex(hex: string): boolean {
|
|
180
|
+
const cleanHex = hex.replace("#", "");
|
|
181
|
+
return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Validate HSL color values
|
|
186
|
+
*/
|
|
187
|
+
export function isValidHsl(hsl: HSL): boolean {
|
|
188
|
+
return (
|
|
189
|
+
hsl.h >= 0 &&
|
|
190
|
+
hsl.h <= 360 &&
|
|
191
|
+
hsl.s >= 0 &&
|
|
192
|
+
hsl.s <= 100 &&
|
|
193
|
+
hsl.l >= 0 &&
|
|
194
|
+
hsl.l <= 100
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Validate HSV color values
|
|
200
|
+
*/
|
|
201
|
+
export function isValidHsv(hsv: HSV): boolean {
|
|
202
|
+
return (
|
|
203
|
+
hsv.h >= 0 &&
|
|
204
|
+
hsv.h <= 360 &&
|
|
205
|
+
hsv.s >= 0 &&
|
|
206
|
+
hsv.s <= 100 &&
|
|
207
|
+
hsv.v >= 0 &&
|
|
208
|
+
hsv.v <= 100
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Validate RGB color values
|
|
214
|
+
*/
|
|
215
|
+
export function isValidRgb(rgb: RGB): boolean {
|
|
216
|
+
return (
|
|
217
|
+
rgb.r >= 0 &&
|
|
218
|
+
rgb.r <= 255 &&
|
|
219
|
+
rgb.g >= 0 &&
|
|
220
|
+
rgb.g <= 255 &&
|
|
221
|
+
rgb.b >= 0 &&
|
|
222
|
+
rgb.b <= 255
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Lighten a color by a percentage
|
|
228
|
+
* @param rgb The color to lighten
|
|
229
|
+
* @param amount Amount to lighten (0-100)
|
|
230
|
+
*/
|
|
231
|
+
export function lighten(rgb: RGB, amount: number): RGB {
|
|
232
|
+
const hsl = rgbToHsl(rgb);
|
|
233
|
+
hsl.l = clamp(hsl.l + amount, 0, 100);
|
|
234
|
+
return parseColor(formatHsl(hsl)) || rgb;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Check if contrast meets WCAG AA standard
|
|
239
|
+
* Normal text: 4.5:1, Large text: 3:1
|
|
240
|
+
*/
|
|
241
|
+
export function meetsContrastAA(
|
|
242
|
+
color1: RGB,
|
|
243
|
+
color2: RGB,
|
|
244
|
+
largeText: boolean = false,
|
|
245
|
+
): boolean {
|
|
246
|
+
const ratio = getContrastRatio(color1, color2);
|
|
247
|
+
return largeText ? ratio >= 3 : ratio >= 4.5;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if contrast meets WCAG AAA standard
|
|
252
|
+
* Normal text: 7:1, Large text: 4.5:1
|
|
253
|
+
*/
|
|
254
|
+
export function meetsContrastAAA(
|
|
255
|
+
color1: RGB,
|
|
256
|
+
color2: RGB,
|
|
257
|
+
largeText: boolean = false,
|
|
258
|
+
): boolean {
|
|
259
|
+
const ratio = getContrastRatio(color1, color2);
|
|
260
|
+
return largeText ? ratio >= 4.5 : ratio >= 7;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Mix two colors together
|
|
265
|
+
* @param color1 First color
|
|
266
|
+
* @param color2 Second color
|
|
267
|
+
* @param weight Weight of the first color (0-1)
|
|
268
|
+
*/
|
|
269
|
+
export function mixColors(color1: RGB, color2: RGB, weight: number = 0.5): RGB {
|
|
270
|
+
const w = clamp(weight, 0, 1);
|
|
271
|
+
const w2 = 1 - w;
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
b: Math.round(color1.b * w + color2.b * w2),
|
|
275
|
+
g: Math.round(color1.g * w + color2.g * w2),
|
|
276
|
+
r: Math.round(color1.r * w + color2.r * w2),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Generate a random RGB color
|
|
282
|
+
*/
|
|
283
|
+
export function randomColor(): RGB {
|
|
284
|
+
return {
|
|
285
|
+
b: Math.floor(Math.random() * 256),
|
|
286
|
+
g: Math.floor(Math.random() * 256),
|
|
287
|
+
r: Math.floor(Math.random() * 256),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Saturate a color by a percentage
|
|
293
|
+
* @param rgb The color to saturate
|
|
294
|
+
* @param amount Amount to saturate (0-100)
|
|
295
|
+
*/
|
|
296
|
+
export function saturate(rgb: RGB, amount: number): RGB {
|
|
297
|
+
const hsl = rgbToHsl(rgb);
|
|
298
|
+
hsl.s = clamp(hsl.s + amount, 0, 100);
|
|
299
|
+
return parseColor(formatHsl(hsl)) || rgb;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Convert color to grayscale
|
|
304
|
+
*/
|
|
305
|
+
export function toGrayscale(rgb: RGB): RGB {
|
|
306
|
+
const gray = Math.round(0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b);
|
|
307
|
+
return { b: gray, g: gray, r: gray };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clamp a number between min and max values
|
|
312
|
+
*/
|
|
313
|
+
function clamp(value: number, min: number, max: number): number {
|
|
314
|
+
return Math.max(min, Math.min(max, value));
|
|
315
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import { StrictEventEmitter } from "strict-event-emitter-types";
|
|
4
|
+
|
|
5
|
+
import { CoolorsMCPSession } from "./session.js";
|
|
6
|
+
import {
|
|
7
|
+
CoolorsMCPEvents,
|
|
8
|
+
CoolorsMCPSessionAuth,
|
|
9
|
+
ServerOptions,
|
|
10
|
+
Tool,
|
|
11
|
+
ToolParameters,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
export class CoolorsMcp extends (EventEmitter as {
|
|
15
|
+
new (): StrictEventEmitter<EventEmitter, CoolorsMCPEvents>;
|
|
16
|
+
}) {
|
|
17
|
+
#sessions: CoolorsMCPSession[] = [];
|
|
18
|
+
#tools: Tool<CoolorsMCPSessionAuth>[] = [];
|
|
19
|
+
|
|
20
|
+
constructor(public options: ServerOptions<CoolorsMCPSessionAuth>) {
|
|
21
|
+
super();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public addTool<Params extends ToolParameters>(
|
|
25
|
+
tool: Tool<CoolorsMCPSessionAuth, Params>,
|
|
26
|
+
) {
|
|
27
|
+
this.#tools.push(tool as unknown as Tool<CoolorsMCPSessionAuth>);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async start() {
|
|
31
|
+
const transport = new StdioServerTransport();
|
|
32
|
+
const session = new CoolorsMCPSession(this.options, this.#tools);
|
|
33
|
+
this.#sessions.push(session);
|
|
34
|
+
await session.connect(transport);
|
|
35
|
+
this.emit("connect", { session });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example CoolorsMCP server demonstrating core functionality plus streaming output.
|
|
3
|
+
*
|
|
4
|
+
* Features demonstrated:
|
|
5
|
+
* - Basic tool with type-safe parameters
|
|
6
|
+
* - Streaming-enabled tool for incremental output
|
|
7
|
+
* - Advanced tool annotations
|
|
8
|
+
*
|
|
9
|
+
* For a complete project template, see https://github.com/punkpeye/coolors-mcp-boilerplate
|
|
10
|
+
*/
|
|
11
|
+
import { type } from "arktype";
|
|
12
|
+
import * as v from "valibot";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
import { CoolorsMcp } from "../coolors-mcp.js";
|
|
16
|
+
|
|
17
|
+
const server = new CoolorsMcp({
|
|
18
|
+
name: "Addition",
|
|
19
|
+
ping: {
|
|
20
|
+
// enabled: undefined,
|
|
21
|
+
// Automatically enabled/disabled based on transport type
|
|
22
|
+
// Using a longer interval to reduce log noise
|
|
23
|
+
intervalMs: 10000, // default is 5000ms
|
|
24
|
+
// Reduce log verbosity
|
|
25
|
+
logLevel: "debug", // default
|
|
26
|
+
},
|
|
27
|
+
roots: {
|
|
28
|
+
// You can explicitly disable roots support if needed
|
|
29
|
+
// enabled: false,
|
|
30
|
+
},
|
|
31
|
+
version: "1.0.0",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// --- Zod Example ---
|
|
35
|
+
const AddParamsZod = z.object({
|
|
36
|
+
a: z.number().describe("The first number"),
|
|
37
|
+
b: z.number().describe("The second number"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
server.addTool({
|
|
41
|
+
annotations: {
|
|
42
|
+
openWorldHint: false, // This tool doesn't interact with external systems
|
|
43
|
+
readOnlyHint: true, // This tool doesn't modify anything
|
|
44
|
+
title: "Addition (Zod)",
|
|
45
|
+
},
|
|
46
|
+
description: "Add two numbers (using Zod schema)",
|
|
47
|
+
execute: async (args) => {
|
|
48
|
+
// args is typed as { a: number, b: number }
|
|
49
|
+
console.log(`[Zod] Adding ${args.a} and ${args.b}`);
|
|
50
|
+
return String(args.a + args.b);
|
|
51
|
+
},
|
|
52
|
+
name: "add-zod",
|
|
53
|
+
parameters: AddParamsZod,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// --- ArkType Example ---
|
|
57
|
+
const AddParamsArkType = type({
|
|
58
|
+
a: "number",
|
|
59
|
+
b: "number",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
server.addTool({
|
|
63
|
+
annotations: {
|
|
64
|
+
destructiveHint: true, // This would perform destructive operations
|
|
65
|
+
idempotentHint: true, // But operations can be repeated safely
|
|
66
|
+
openWorldHint: true, // Interacts with external systems
|
|
67
|
+
readOnlyHint: false, // Example showing a modifying tool
|
|
68
|
+
title: "Addition (ArkType)",
|
|
69
|
+
},
|
|
70
|
+
description: "Add two numbers (using ArkType schema)",
|
|
71
|
+
execute: async (args, { log }) => {
|
|
72
|
+
// args is typed as { a: number, b: number } based on AddParamsArkType.infer
|
|
73
|
+
console.log(`[ArkType] Adding ${args.a} and ${args.b}`);
|
|
74
|
+
|
|
75
|
+
// Demonstrate long-running operation that might need a timeout
|
|
76
|
+
log.info("Starting calculation with potential delay...");
|
|
77
|
+
|
|
78
|
+
// Simulate a complex calculation process
|
|
79
|
+
if (args.a > 1000 || args.b > 1000) {
|
|
80
|
+
log.warn("Large numbers detected, operation might take longer");
|
|
81
|
+
// In a real implementation, this delay might be a slow operation
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return String(args.a + args.b);
|
|
86
|
+
},
|
|
87
|
+
name: "add-arktype",
|
|
88
|
+
parameters: AddParamsArkType,
|
|
89
|
+
// Will abort execution after 2s
|
|
90
|
+
timeoutMs: 2000,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- Valibot Example ---
|
|
94
|
+
const AddParamsValibot = v.object({
|
|
95
|
+
a: v.number("The first number"),
|
|
96
|
+
b: v.number("The second number"),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
server.addTool({
|
|
100
|
+
annotations: {
|
|
101
|
+
openWorldHint: false,
|
|
102
|
+
readOnlyHint: true,
|
|
103
|
+
title: "Addition (Valibot)",
|
|
104
|
+
},
|
|
105
|
+
description: "Add two numbers (using Valibot schema)",
|
|
106
|
+
execute: async (args) => {
|
|
107
|
+
console.log(`[Valibot] Adding ${args.a} and ${args.b}`);
|
|
108
|
+
return String(args.a + args.b);
|
|
109
|
+
},
|
|
110
|
+
name: "add-valibot",
|
|
111
|
+
parameters: AddParamsValibot,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
server.addResource({
|
|
115
|
+
async load() {
|
|
116
|
+
return {
|
|
117
|
+
text: "Example log content",
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
mimeType: "text/plain",
|
|
121
|
+
name: "Application Logs",
|
|
122
|
+
uri: "file:///logs/app.log",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
server.addTool({
|
|
126
|
+
annotations: {
|
|
127
|
+
openWorldHint: false,
|
|
128
|
+
readOnlyHint: true,
|
|
129
|
+
streamingHint: true,
|
|
130
|
+
},
|
|
131
|
+
description: "Generate a poem line by line with streaming output",
|
|
132
|
+
execute: async (args, context) => {
|
|
133
|
+
const { theme } = args;
|
|
134
|
+
const lines = [
|
|
135
|
+
`Poem about ${theme} - line 1`,
|
|
136
|
+
`Poem about ${theme} - line 2`,
|
|
137
|
+
`Poem about ${theme} - line 3`,
|
|
138
|
+
`Poem about ${theme} - line 4`,
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
await context.streamContent({
|
|
143
|
+
text: line,
|
|
144
|
+
type: "text",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return;
|
|
151
|
+
},
|
|
152
|
+
name: "stream-poem",
|
|
153
|
+
parameters: z.object({
|
|
154
|
+
theme: z.string().describe("Theme for the poem"),
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
server.addTool({
|
|
159
|
+
annotations: {
|
|
160
|
+
openWorldHint: false,
|
|
161
|
+
readOnlyHint: false,
|
|
162
|
+
},
|
|
163
|
+
description: "Test progress reporting without buffering delays",
|
|
164
|
+
execute: async (args, { reportProgress }) => {
|
|
165
|
+
console.log("Testing progress reporting fix for HTTP Stream buffering...");
|
|
166
|
+
|
|
167
|
+
await reportProgress({ progress: 0, total: 100 });
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
169
|
+
|
|
170
|
+
await reportProgress({ progress: 25, total: 100 });
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
172
|
+
|
|
173
|
+
await reportProgress({ progress: 75, total: 100 });
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
175
|
+
|
|
176
|
+
// This progress should be received immediately
|
|
177
|
+
await reportProgress({ progress: 100, total: 100 });
|
|
178
|
+
|
|
179
|
+
return `Buffering test completed for ${args.testCase}`;
|
|
180
|
+
},
|
|
181
|
+
name: "test-buffering-fix",
|
|
182
|
+
parameters: z.object({
|
|
183
|
+
testCase: z.string().describe("Test case description"),
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
server.addPrompt({
|
|
188
|
+
arguments: [
|
|
189
|
+
{
|
|
190
|
+
description: "Git diff or description of changes",
|
|
191
|
+
name: "changes",
|
|
192
|
+
required: true,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
description: "Generate a Git commit message",
|
|
196
|
+
load: async (args) => {
|
|
197
|
+
return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
|
|
198
|
+
},
|
|
199
|
+
name: "git-commit",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
server.addResourceTemplate({
|
|
203
|
+
arguments: [
|
|
204
|
+
{
|
|
205
|
+
description: "Documentation section to retrieve",
|
|
206
|
+
name: "section",
|
|
207
|
+
required: true,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
description: "Get project documentation",
|
|
211
|
+
load: async (args) => {
|
|
212
|
+
const docs = {
|
|
213
|
+
"api-reference":
|
|
214
|
+
"# API Reference\n\n## Authentication\nAll API requests require a valid API key in the Authorization header.\n\n## Endpoints\n- GET /users - List all users\n- POST /users - Create new user",
|
|
215
|
+
deployment:
|
|
216
|
+
"# Deployment Guide\n\nTo deploy this application:\n\n1. Build the project: `npm run build`\n2. Set environment variables\n3. Deploy to your hosting platform",
|
|
217
|
+
"getting-started":
|
|
218
|
+
"# Getting Started\n\nWelcome to our project! Follow these steps to set up your development environment:\n\n1. Clone the repository\n2. Install dependencies with `npm install`\n3. Run `npm start` to begin",
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
text:
|
|
223
|
+
docs[args.section as keyof typeof docs] ||
|
|
224
|
+
"Documentation section not found",
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
mimeType: "text/markdown",
|
|
228
|
+
name: "Project Documentation",
|
|
229
|
+
uriTemplate: "docs://project/{section}",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
server.addTool({
|
|
233
|
+
annotations: {
|
|
234
|
+
openWorldHint: false,
|
|
235
|
+
readOnlyHint: true,
|
|
236
|
+
title: "Get Documentation (Embedded)",
|
|
237
|
+
},
|
|
238
|
+
description:
|
|
239
|
+
"Retrieve project documentation using embedded resources - demonstrates the new embedded() feature",
|
|
240
|
+
execute: async (args) => {
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
resource: await server.embedded(`docs://project/${args.section}`),
|
|
245
|
+
type: "resource",
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
name: "get-documentation",
|
|
251
|
+
parameters: z.object({
|
|
252
|
+
section: z
|
|
253
|
+
.enum(["getting-started", "api-reference", "deployment"])
|
|
254
|
+
.describe("Documentation section to retrieve"),
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Select transport type based on command line arguments
|
|
259
|
+
const transportType = process.argv.includes("--http-stream")
|
|
260
|
+
? "httpStream"
|
|
261
|
+
: "stdio";
|
|
262
|
+
|
|
263
|
+
if (transportType === "httpStream") {
|
|
264
|
+
// Start with HTTP streaming transport
|
|
265
|
+
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 8080;
|
|
266
|
+
|
|
267
|
+
server.start({
|
|
268
|
+
httpStream: {
|
|
269
|
+
port: PORT,
|
|
270
|
+
},
|
|
271
|
+
transportType: "httpStream",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
console.log(
|
|
275
|
+
`HTTP Stream MCP server is running at http://localhost:${PORT}/mcp`,
|
|
276
|
+
);
|
|
277
|
+
console.log("Use StreamableHTTPClientTransport to connect to this server");
|
|
278
|
+
console.log("For example:");
|
|
279
|
+
console.log(`
|
|
280
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
281
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
282
|
+
|
|
283
|
+
const client = new Client(
|
|
284
|
+
{
|
|
285
|
+
name: "example-client",
|
|
286
|
+
version: "1.0.0",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
capabilities: {},
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const transport = new StreamableHTTPClientTransport(
|
|
294
|
+
new URL("http://localhost:${PORT}/mcp"),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
await client.connect(transport);
|
|
298
|
+
`);
|
|
299
|
+
} else if (process.argv.includes("--explicit-ping-config")) {
|
|
300
|
+
server.start({
|
|
301
|
+
transportType: "stdio",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
console.log(
|
|
305
|
+
"Started stdio transport with explicit ping configuration from server options",
|
|
306
|
+
);
|
|
307
|
+
} else if (process.argv.includes("--disable-roots")) {
|
|
308
|
+
// Example of disabling roots at runtime
|
|
309
|
+
const serverWithDisabledRoots = new CoolorsMcp({
|
|
310
|
+
name: "Addition (No Roots)",
|
|
311
|
+
ping: {
|
|
312
|
+
intervalMs: 10000,
|
|
313
|
+
logLevel: "debug",
|
|
314
|
+
},
|
|
315
|
+
roots: {
|
|
316
|
+
enabled: false,
|
|
317
|
+
},
|
|
318
|
+
version: "1.0.0",
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
serverWithDisabledRoots.start({
|
|
322
|
+
transportType: "stdio",
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
console.log("Started stdio transport with roots support disabled");
|
|
326
|
+
} else {
|
|
327
|
+
// Disable by default for:
|
|
328
|
+
server.start({
|
|
329
|
+
transportType: "stdio",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
console.log("Started stdio transport with ping disabled by default");
|
|
333
|
+
}
|