Spaces:
Sleeping
Sleeping
File size: 7,097 Bytes
7dc28be | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | 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'}`
);
}
},
});
}
|