Skip to main content
Glama
check-contrast.ts11.7 kB
/** * Check Contrast MCP Tool * Check color contrast compliance with WCAG standards */ import { UnifiedColor } from '../color/unified-color'; import { ColorAnalyzer } from '../color/color-analysis'; import { ToolResponse, ErrorResponse } from '../types/index'; import { createSuccessResponse, createErrorResponse } from '../utils/response'; import { validateColorInput } from '../validation/schemas'; export interface CheckContrastParams { foreground: string; background: string; text_size?: 'normal' | 'large'; standard?: 'WCAG_AA' | 'WCAG_AAA' | 'APCA'; } export interface CheckContrastResponse { foreground: string; background: string; contrast_ratio: number; apca_score?: number; text_size: 'normal' | 'large'; standard: string; compliance: { wcag_aa: boolean; wcag_aaa: boolean; apca_passes?: boolean; passes: boolean; }; recommendations: string[]; alternative_combinations?: | { foreground_adjustments: Array<{ color: string; contrast_ratio: number; apca_score?: number; passes: boolean; }>; background_adjustments: Array<{ color: string; contrast_ratio: number; apca_score?: number; passes: boolean; }>; } | undefined; } /** * Check contrast ratio between foreground and background colors */ export async function checkContrast( params: CheckContrastParams ): Promise<ToolResponse | ErrorResponse> { const startTime = Date.now(); try { // Validate required parameters if (!params.foreground) { return createErrorResponse( 'check_contrast', 'MISSING_PARAMETER', 'Foreground color parameter is required', Date.now() - startTime, { details: { parameter: 'foreground' }, suggestions: ['Provide a foreground color in any supported format'], } ); } if (!params.background) { return createErrorResponse( 'check_contrast', 'MISSING_PARAMETER', 'Background color parameter is required', Date.now() - startTime, { details: { parameter: 'background' }, suggestions: ['Provide a background color in any supported format'], } ); } // Validate color inputs const fgValidation = validateColorInput(params.foreground); if (!fgValidation.isValid) { return createErrorResponse( 'check_contrast', 'INVALID_FOREGROUND_COLOR', `Invalid foreground color format: ${params.foreground}`, Date.now() - startTime, { details: { provided: params.foreground, error: fgValidation.error }, suggestions: [ 'Use a valid color format like #FF0000 or rgb(255, 0, 0)', ], } ); } const bgValidation = validateColorInput(params.background); if (!bgValidation.isValid) { return createErrorResponse( 'check_contrast', 'INVALID_BACKGROUND_COLOR', `Invalid background color format: ${params.background}`, Date.now() - startTime, { details: { provided: params.background, error: bgValidation.error }, suggestions: [ 'Use a valid color format like #FFFFFF or rgb(255, 255, 255)', ], } ); } // Parse colors const foregroundColor = new UnifiedColor(params.foreground); const backgroundColor = new UnifiedColor(params.background); // Set defaults const textSize = params.text_size || 'normal'; const standard = params.standard || 'WCAG_AA'; // Check contrast const contrastResult = ColorAnalyzer.checkContrast( foregroundColor, backgroundColor, textSize, standard ); // Generate recommendations const recommendations: string[] = []; if (!contrastResult.passes) { recommendations.push( 'This color combination does not meet accessibility standards' ); if (contrastResult.ratio < 3.0) { recommendations.push( 'Consider using colors with more contrast difference' ); } // Suggest adjustments if ( foregroundColor.metadata?.brightness && backgroundColor.metadata?.brightness ) { const fgBrightness = foregroundColor.metadata.brightness; const bgBrightness = backgroundColor.metadata.brightness; if (Math.abs(fgBrightness - bgBrightness) < 100) { if (fgBrightness > 127) { recommendations.push('Try using a darker foreground color'); } else { recommendations.push('Try using a lighter foreground color'); } if (bgBrightness > 127) { recommendations.push('Try using a darker background color'); } else { recommendations.push('Try using a lighter background color'); } } } } else { if (contrastResult.wcag_aaa) { recommendations.push('Excellent contrast - meets AAA standards'); } else { recommendations.push('Good contrast - meets AA standards'); } } // Generate alternative combinations const alternativeCombinations = await generateAlternatives( foregroundColor, backgroundColor, textSize, contrastResult.ratio, standard ); // Prepare response const responseData: CheckContrastResponse = { foreground: params.foreground, background: params.background, contrast_ratio: contrastResult.ratio, text_size: textSize, standard, compliance: (() => { const compliance: CheckContrastResponse['compliance'] = { wcag_aa: contrastResult.wcag_aa, wcag_aaa: contrastResult.wcag_aaa, passes: contrastResult.passes, }; if (standard === 'APCA') { compliance.apca_passes = contrastResult.passes; } return compliance; })(), recommendations, alternative_combinations: alternativeCombinations, }; if (contrastResult.apca_score !== undefined) { responseData.apca_score = contrastResult.apca_score; } const executionTime = Date.now() - startTime; // Generate accessibility notes const accessibilityNotes: string[] = []; if (!contrastResult.wcag_aa) { accessibilityNotes.push( `Contrast ratio ${contrastResult.ratio}:1 does not meet WCAG AA standards` ); } if (contrastResult.wcag_aaa) { accessibilityNotes.push( `Excellent contrast ratio ${contrastResult.ratio}:1 meets WCAG AAA standards` ); } return createSuccessResponse( 'check_contrast', responseData, executionTime, { colorSpaceUsed: 'sRGB', accessibilityNotes: accessibilityNotes, recommendations: recommendations.slice(0, 5), } ); } catch (error) { const executionTime = Date.now() - startTime; return createErrorResponse( 'check_contrast', 'CONTRAST_CHECK_ERROR', `Failed to check contrast: ${error instanceof Error ? error.message : 'Unknown error'}`, executionTime, { details: { foreground: params.foreground, background: params.background, }, suggestions: [ 'Check that both colors are in valid formats', 'Try different color formats', ], } ); } } /** * Generate alternative color combinations with better contrast */ async function generateAlternatives( foreground: UnifiedColor, background: UnifiedColor, textSize: 'normal' | 'large', currentRatio: number, standard: 'WCAG_AA' | 'WCAG_AAA' | 'APCA' = 'WCAG_AA' ): Promise<CheckContrastResponse['alternative_combinations']> { const targetRatio = textSize === 'large' ? 3.0 : 4.5; if (currentRatio >= targetRatio) { return undefined; // No alternatives needed } const foregroundAdjustments: Array<{ color: string; contrast_ratio: number; passes: boolean; }> = []; const backgroundAdjustments: Array<{ color: string; contrast_ratio: number; passes: boolean; }> = []; // Generate foreground adjustments (lighter and darker versions) const fgHsl = foreground.hsl; const lightnessAdjustments = [-40, -30, -20, 20, 30, 40]; for (const adjustment of lightnessAdjustments) { const newLightness = Math.max(0, Math.min(100, fgHsl.l + adjustment)); if (Math.abs(newLightness - fgHsl.l) < 5) continue; // Skip minimal changes try { const adjustedFg = UnifiedColor.fromHsl(fgHsl.h, fgHsl.s, newLightness); const contrastResult = ColorAnalyzer.checkContrast( adjustedFg, background, textSize, standard ); const adjustment: { color: string; contrast_ratio: number; passes: boolean; apca_score?: number; } = { color: adjustedFg.hex, contrast_ratio: contrastResult.ratio, passes: contrastResult.passes, }; if (contrastResult.apca_score !== undefined) { adjustment.apca_score = contrastResult.apca_score; } foregroundAdjustments.push(adjustment); } catch { // Skip invalid color combinations } } // Generate background adjustments const bgHsl = background.hsl; for (const adjustment of lightnessAdjustments) { const newLightness = Math.max(0, Math.min(100, bgHsl.l + adjustment)); if (Math.abs(newLightness - bgHsl.l) < 5) continue; // Skip minimal changes try { const adjustedBg = UnifiedColor.fromHsl(bgHsl.h, bgHsl.s, newLightness); const contrastResult = ColorAnalyzer.checkContrast( foreground, adjustedBg, textSize, standard ); const adjustment: { color: string; contrast_ratio: number; passes: boolean; apca_score?: number; } = { color: adjustedBg.hex, contrast_ratio: contrastResult.ratio, passes: contrastResult.passes, }; if (contrastResult.apca_score !== undefined) { adjustment.apca_score = contrastResult.apca_score; } backgroundAdjustments.push(adjustment); } catch { // Skip invalid color combinations } } // Sort by contrast ratio (best first) foregroundAdjustments.sort((a, b) => b.contrast_ratio - a.contrast_ratio); backgroundAdjustments.sort((a, b) => b.contrast_ratio - a.contrast_ratio); return { foreground_adjustments: foregroundAdjustments.slice(0, 5), // Top 5 alternatives background_adjustments: backgroundAdjustments.slice(0, 5), // Top 5 alternatives }; } // Tool definition for MCP registration export const checkContrastTool = { name: 'check_contrast', description: 'Check color contrast compliance with WCAG accessibility standards', parameters: { type: 'object', properties: { foreground: { type: 'string', description: 'Foreground color (typically text color)', }, background: { type: 'string', description: 'Background color', }, text_size: { type: 'string', enum: ['normal', 'large'], description: 'Text size category for WCAG compliance', default: 'normal', }, standard: { type: 'string', enum: ['WCAG_AA', 'WCAG_AAA', 'APCA'], description: 'Accessibility standard to check against', default: 'WCAG_AA', }, }, required: ['foreground', 'background'], }, handler: async (params: unknown) => checkContrast(params as CheckContrastParams), };

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