Skip to main content
Glama
generate-linear-gradient.ts12.3 kB
/** * MCP tool for generating linear gradients with precise mathematical control */ import { colord, extend, Colord } from 'colord'; import namesPlugin from 'colord/plugins/names'; import Joi from 'joi'; import { ToolHandler, ToolResponse, ErrorResponse } from '../types/index'; import { createSuccessResponse, createErrorResponse } from '../utils/response'; import { logger } from '../utils/logger'; // Extend colord with names plugin extend([namesPlugin]); interface LinearGradientParams { colors: string[]; positions?: number[]; angle?: number; interpolation?: 'linear' | 'ease' | 'ease_in' | 'ease_out' | 'bezier'; color_space?: 'rgb' | 'hsl' | 'lab' | 'lch'; steps?: number; } interface LinearGradientData { css: string; type: 'linear'; angle: number; colors: Array<{ color: string; position: number; hex: string; rgb: string; hsl: string; }>; interpolation: string; color_space: string; total_stops: number; } const linearGradientSchema = Joi.object({ colors: Joi.array() .items(Joi.string()) .min(2) .max(20) .required() .description('Array of color strings for the gradient'), positions: Joi.array() .items(Joi.number().min(0).max(100)) .optional() .description( 'Stop positions (0-100). If not provided, colors are evenly distributed' ), angle: Joi.number() .min(0) .max(360) .optional() .description('Gradient angle in degrees (0-360, default: 90)'), interpolation: Joi.string() .valid('linear', 'ease', 'ease_in', 'ease_out', 'bezier') .default('linear') .description('Interpolation method for color transitions'), color_space: Joi.string() .valid('rgb', 'hsl', 'lab', 'lch') .default('rgb') .description('Color space for interpolation'), steps: Joi.number() .integer() .min(2) .max(100) .optional() .description( 'Number of steps for stepped gradients (creates discrete color bands)' ), }); /** * Validate and parse colors */ function validateColors( colors: string[] ): Array<{ color: Colord; original: string }> { const validatedColors: Array<{ color: Colord; original: string }> = []; const invalidColors: string[] = []; colors.forEach((colorStr, index) => { try { const color = colord(colorStr); if (!color.isValid()) { invalidColors.push(`${colorStr} at index ${index}`); } else { validatedColors.push({ color, original: colorStr }); } } catch { invalidColors.push(`${colorStr} at index ${index}`); } }); if (invalidColors.length > 0) { throw new Error(`Invalid colors found: ${invalidColors.join(', ')}`); } return validatedColors; } /** * Calculate positions for colors if not provided */ function calculatePositions( colorCount: number, positions?: number[] ): number[] { if (positions) { if (positions.length !== colorCount) { throw new Error( `Position count (${positions.length}) must match color count (${colorCount})` ); } // Validate positions are in ascending order for (let i = 1; i < positions.length; i++) { const current = positions[i]; const previous = positions[i - 1]; if ( current !== undefined && previous !== undefined && current <= previous ) { throw new Error('Positions must be in ascending order'); } } return positions; } // Evenly distribute colors if (colorCount === 1) return [50]; if (colorCount === 2) return [0, 100]; const step = 100 / (colorCount - 1); return Array.from( { length: colorCount }, (_, i) => Math.round(i * step * 100) / 100 ); } /** * Apply interpolation easing to positions */ function applyInterpolation( positions: number[], interpolation: string ): number[] { if (interpolation === 'linear') { return positions; } return positions.map((pos, index) => { if (index === 0 || index === positions.length - 1) { return pos; // Keep first and last positions unchanged } const normalizedPos = pos / 100; let easedPos: number; switch (interpolation) { case 'ease': easedPos = 0.25 * Math.sin(normalizedPos * Math.PI - Math.PI / 2) + 0.25 * normalizedPos + 0.5; break; case 'ease_in': easedPos = normalizedPos * normalizedPos; break; case 'ease_out': easedPos = 1 - (1 - normalizedPos) * (1 - normalizedPos); break; case 'bezier': // Cubic bezier approximation (0.25, 0.1, 0.25, 1) easedPos = normalizedPos * normalizedPos * (3 - 2 * normalizedPos); break; default: easedPos = normalizedPos; } return Math.round(easedPos * 100 * 100) / 100; }); } /** * Generate stepped gradient positions */ function generateSteppedPositions( colors: string[], steps: number ): Array<{ color: string; position: number }> { const steppedColors: Array<{ color: string; position: number }> = []; const stepSize = 100 / steps; for (let i = 0; i < steps; i++) { const colorIndex = Math.floor((i / (steps - 1)) * (colors.length - 1)); const position = i * stepSize; const selectedColor = colors[Math.min(colorIndex, colors.length - 1)]; if (selectedColor) { steppedColors.push({ color: selectedColor, position: Math.round(position * 100) / 100, }); } } return steppedColors; } /** * Generate CSS linear gradient string */ function generateLinearGradientCSS( colors: Array<{ color: Colord; original: string }>, positions: number[], angle: number, steps?: number ): string { let cssStops: string[]; if (steps) { // Generate stepped gradient const steppedColors = generateSteppedPositions( colors.map(c => c.color.toHex()), steps ); cssStops = steppedColors.map( ({ color, position }) => `${color} ${position}%` ); } else { // Generate smooth gradient cssStops = colors.map((colorObj, index) => { const color = colorObj.color.toHex(); const position = positions[index]; return `${color} ${position}%`; }); } return `linear-gradient(${angle}deg, ${cssStops.join(', ')})`; } /** * Generate browser-compatible CSS with fallbacks */ // function generateCompatibleCSS(baseCSS: string): string { // const fallbacks = [ // `background: ${baseCSS};`, // `background: -webkit-${baseCSS};`, // `background: -moz-${baseCSS};`, // `background: -o-${baseCSS};`, // ]; // return fallbacks.join('\n'); // } /** * Generate linear gradient */ async function generateLinearGradient( params: LinearGradientParams ): Promise<ToolResponse | ErrorResponse> { const startTime = Date.now(); try { // Validate parameters const { error, value } = linearGradientSchema.validate(params); if (error) { return createErrorResponse( 'generate_linear_gradient', 'INVALID_PARAMETERS', `Invalid parameters: ${error.details.map(d => d.message).join(', ')}`, startTime, { details: error.details, suggestions: [ 'Ensure colors array has 2-20 valid color strings', 'Check that positions (if provided) match color count and are in ascending order', 'Verify angle is between 0-360 degrees', 'Use supported interpolation methods: linear, ease, ease_in, ease_out, bezier', ], } ); } const validatedParams = value as LinearGradientParams; // Validate colors const validatedColors = validateColors(validatedParams.colors); // Calculate positions const positions = calculatePositions( validatedColors.length, validatedParams.positions ); // Apply interpolation const interpolatedPositions = applyInterpolation( positions, validatedParams.interpolation || 'linear' ); // Generate CSS const angle = validatedParams.angle !== undefined ? validatedParams.angle : 90; const css = generateLinearGradientCSS( validatedColors, interpolatedPositions, angle, validatedParams.steps ); // Prepare response data const colorData = validatedColors.map((colorObj, index) => ({ color: colorObj.original, position: interpolatedPositions[index] || 0, hex: colorObj.color.toHex(), rgb: colorObj.color.toRgbString(), hsl: colorObj.color.toHslString(), })); const data: LinearGradientData = { css, type: 'linear', angle, colors: colorData, interpolation: validatedParams.interpolation || 'linear', color_space: validatedParams.color_space || 'rgb', total_stops: validatedParams.steps || validatedColors.length, }; const executionTime = Date.now() - startTime; // Generate recommendations const recommendations: string[] = []; if (validatedColors.length > 5) { recommendations.push( 'Consider using fewer colors for better performance' ); } if (angle % 45 !== 0) { recommendations.push( 'Consider using multiples of 45° for common gradient angles' ); } if (validatedParams.steps && validatedParams.steps > 20) { recommendations.push( 'High step counts may impact performance on older devices' ); } return createSuccessResponse( 'generate_linear_gradient', data, executionTime, { colorSpaceUsed: validatedParams.color_space || 'rgb', accessibilityNotes: [ 'Ensure sufficient contrast between gradient colors and any overlaid text', 'Test gradient visibility with color vision deficiency simulators', ], recommendations, exportFormats: { css: css, scss: `$gradient: ${css};`, json: { type: 'linear', angle: validatedParams.angle || 90, colors: colorData, css: css, }, }, } ); } catch (error) { logger.error('Error generating linear gradient', { error: error as Error }); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return createErrorResponse( 'generate_linear_gradient', 'GRADIENT_GENERATION_ERROR', errorMessage, startTime, { details: { error: errorMessage }, suggestions: [ 'Check that all colors are in valid formats (hex, rgb, hsl, named)', 'Ensure positions array matches color count if provided', 'Verify all parameters are within valid ranges', ], } ); } } export const generateLinearGradientTool: ToolHandler = { name: 'generate_linear_gradient', description: 'Generate linear gradients with precise mathematical control and CSS output', parameters: { type: 'object', properties: { colors: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 20, description: 'Array of color strings for the gradient', }, positions: { type: 'array', items: { type: 'number', minimum: 0, maximum: 100 }, description: 'Stop positions (0-100). If not provided, colors are evenly distributed', }, angle: { type: 'number', minimum: 0, maximum: 360, default: 90, description: 'Gradient angle in degrees (0-360, default: 90)', }, interpolation: { type: 'string', enum: ['linear', 'ease', 'ease_in', 'ease_out', 'bezier'], default: 'linear', description: 'Interpolation method for color transitions', }, color_space: { type: 'string', enum: ['rgb', 'hsl', 'lab', 'lch'], default: 'rgb', description: 'Color space for interpolation', }, steps: { type: 'number', minimum: 2, maximum: 100, description: 'Number of steps for stepped gradients (creates discrete color bands)', }, }, required: ['colors'], }, handler: async (params: unknown) => generateLinearGradient(params as LinearGradientParams), };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ColorMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server