Spaces:
Sleeping
Sleeping
| import type { FastMCP } from 'fastmcp'; | |
| import { UserError } from 'fastmcp'; | |
| import { z } from 'zod'; | |
| import { getSheetsClient } from '../../clients.js'; | |
| import * as SheetsHelpers from '../../googleSheetsApiHelpers.js'; | |
| const ONE_VALUE_CONDITIONS = [ | |
| 'NUMBER_GREATER', | |
| 'NUMBER_GREATER_THAN_EQ', | |
| 'NUMBER_LESS', | |
| 'NUMBER_LESS_THAN_EQ', | |
| 'NUMBER_EQ', | |
| 'NUMBER_NOT_EQ', | |
| 'CUSTOM_FORMULA', | |
| ] as const; | |
| const TWO_VALUE_CONDITIONS = ['NUMBER_BETWEEN', 'NUMBER_NOT_BETWEEN'] as const; | |
| const NO_VALUE_CONDITIONS = ['BLANK', 'NOT_BLANK'] as const; | |
| const ALL_CONDITION_TYPES = [ | |
| ...ONE_VALUE_CONDITIONS, | |
| ...TWO_VALUE_CONDITIONS, | |
| ...NO_VALUE_CONDITIONS, | |
| ] as const; | |
| export function register(server: FastMCP) { | |
| server.addTool({ | |
| name: 'addConditionalFormatting', | |
| description: | |
| 'Adds a conditional formatting rule to one or more ranges in a spreadsheet. Applies a format (background color, bold, text color, etc.) when cells meet a specified condition. Use CUSTOM_FORMULA for complex conditions like "=$A1>$B1". Note: each call appends a new rule — use deleteConditionalFormatting to remove existing rules before re-adding.', | |
| parameters: z | |
| .object({ | |
| spreadsheetId: z | |
| .string() | |
| .describe( | |
| 'The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.' | |
| ), | |
| sheetName: z | |
| .string() | |
| .optional() | |
| .describe('Name of the sheet/tab. Defaults to the first sheet if not provided.'), | |
| ranges: z | |
| .array(z.string()) | |
| .min(1) | |
| .describe( | |
| 'One or more A1 notation ranges the rule applies to (e.g., ["B2:S68"] or ["A1:A10", "C1:C10"]).' | |
| ), | |
| conditionType: z | |
| .enum(ALL_CONDITION_TYPES) | |
| .describe( | |
| 'The condition type. ' + | |
| 'NUMBER_* types compare cell values numerically. ' + | |
| 'CUSTOM_FORMULA evaluates a formula (e.g., "=$A1>10"). ' + | |
| 'BLANK/NOT_BLANK check whether a cell is empty.' | |
| ), | |
| conditionValues: z | |
| .array(z.string()) | |
| .optional() | |
| .describe( | |
| 'Values for the condition. ' + | |
| 'Omit or pass [] for BLANK and NOT_BLANK. ' + | |
| 'Pass one value for all NUMBER_* single-operand types and CUSTOM_FORMULA. ' + | |
| 'Pass two values for NUMBER_BETWEEN and NUMBER_NOT_BETWEEN (lower bound first).' | |
| ), | |
| backgroundColor: z | |
| .string() | |
| .optional() | |
| .describe('Cell background color as hex (e.g., "#FF9900").'), | |
| bold: z.boolean().optional().describe('Apply bold text formatting.'), | |
| italic: z.boolean().optional().describe('Apply italic text formatting.'), | |
| strikethrough: z.boolean().optional().describe('Apply strikethrough text formatting.'), | |
| underline: z.boolean().optional().describe('Apply underline text formatting.'), | |
| foregroundColor: z | |
| .string() | |
| .optional() | |
| .describe('Text (foreground) color as hex (e.g., "#FF0000").'), | |
| fontSize: z.number().min(1).optional().describe('Font size in points.'), | |
| }) | |
| .refine( | |
| (data) => | |
| data.backgroundColor !== undefined || | |
| data.bold !== undefined || | |
| data.italic !== undefined || | |
| data.strikethrough !== undefined || | |
| data.underline !== undefined || | |
| data.foregroundColor !== undefined || | |
| data.fontSize !== undefined, | |
| { message: 'At least one formatting option must be provided.' } | |
| ) | |
| .refine( | |
| (data) => { | |
| const values = data.conditionValues ?? []; | |
| if ((NO_VALUE_CONDITIONS as readonly string[]).includes(data.conditionType)) { | |
| return values.length === 0; | |
| } | |
| if ((TWO_VALUE_CONDITIONS as readonly string[]).includes(data.conditionType)) { | |
| return values.length === 2; | |
| } | |
| return values.length === 1; | |
| }, | |
| (data) => { | |
| if ((NO_VALUE_CONDITIONS as readonly string[]).includes(data.conditionType)) { | |
| return { message: `${data.conditionType} does not accept condition values.` }; | |
| } | |
| if ((TWO_VALUE_CONDITIONS as readonly string[]).includes(data.conditionType)) { | |
| return { message: `${data.conditionType} requires exactly two condition values.` }; | |
| } | |
| return { message: `${data.conditionType} requires exactly one condition value.` }; | |
| } | |
| ), | |
| execute: async (args, { log }) => { | |
| const sheets = await getSheetsClient(); | |
| log.info(`Adding conditional format rule to spreadsheet ${args.spreadsheetId}`); | |
| try { | |
| const sheetId = await SheetsHelpers.resolveSheetId( | |
| sheets, | |
| args.spreadsheetId, | |
| args.sheetName | |
| ); | |
| const gridRanges = args.ranges.map((r) => SheetsHelpers.parseA1ToGridRange(r, sheetId)); | |
| const conditionValues = (args.conditionValues ?? []).map((v) => ({ | |
| userEnteredValue: v, | |
| })); | |
| const format: Record<string, unknown> = {}; | |
| if (args.backgroundColor) { | |
| const rgb = SheetsHelpers.hexToRgb(args.backgroundColor); | |
| if (!rgb) throw new UserError(`Invalid background color: "${args.backgroundColor}".`); | |
| format.backgroundColor = rgb; | |
| } | |
| const hasTextFormat = | |
| args.bold !== undefined || | |
| args.italic !== undefined || | |
| args.strikethrough !== undefined || | |
| args.underline !== undefined || | |
| args.fontSize !== undefined || | |
| args.foregroundColor !== undefined; | |
| if (hasTextFormat) { | |
| const textFormat: Record<string, unknown> = {}; | |
| if (args.bold !== undefined) textFormat.bold = args.bold; | |
| if (args.italic !== undefined) textFormat.italic = args.italic; | |
| if (args.strikethrough !== undefined) textFormat.strikethrough = args.strikethrough; | |
| if (args.underline !== undefined) textFormat.underline = args.underline; | |
| if (args.fontSize !== undefined) textFormat.fontSize = args.fontSize; | |
| if (args.foregroundColor) { | |
| const rgb = SheetsHelpers.hexToRgb(args.foregroundColor); | |
| if (!rgb) throw new UserError(`Invalid foreground color: "${args.foregroundColor}".`); | |
| textFormat.foregroundColor = rgb; | |
| } | |
| format.textFormat = textFormat; | |
| } | |
| await SheetsHelpers.addConditionalFormatRule( | |
| sheets, | |
| args.spreadsheetId, | |
| gridRanges, | |
| args.conditionType, | |
| conditionValues, | |
| format | |
| ); | |
| return `Successfully added conditional formatting rule to ${args.ranges.join(', ')}.`; | |
| } catch (error: any) { | |
| log.error(`Error adding conditional format rule: ${error.message || error}`); | |
| if (error instanceof UserError) throw error; | |
| throw new UserError( | |
| `Failed to add conditional formatting: ${error.message || 'Unknown error'}` | |
| ); | |
| } | |
| }, | |
| }); | |
| } | |