Spaces:
Sleeping
Sleeping
| import type { FastMCP } from 'fastmcp'; | |
| import { UserError } from 'fastmcp'; | |
| import { z } from 'zod'; | |
| import { getSheetsClient } from '../../clients.js'; | |
| import { rowColToA1 } from '../../googleSheetsApiHelpers.js'; | |
| /** | |
| * Converts a Google Sheets RGBA color object (0-1 range) to a hex string. | |
| * Returns null if the color is undefined or has no meaningful channels. | |
| */ | |
| function rgbaToHex( | |
| color: { red?: number | null; green?: number | null; blue?: number | null } | null | undefined | |
| ): string | null { | |
| if (!color) return null; | |
| const r = Math.round((color.red ?? 0) * 255); | |
| const g = Math.round((color.green ?? 0) * 255); | |
| const b = Math.round((color.blue ?? 0) * 255); | |
| return `#${r.toString(16).padStart(2, '0').toUpperCase()}${g.toString(16).padStart(2, '0').toUpperCase()}${b.toString(16).padStart(2, '0').toUpperCase()}`; | |
| } | |
| /** | |
| * Extracts a simplified formatting summary from a Google Sheets CellFormat object. | |
| * Only includes properties that are explicitly set (non-default). | |
| */ | |
| function simplifyFormat(fmt: any): Record<string, any> | null { | |
| if (!fmt) return null; | |
| const result: Record<string, any> = {}; | |
| // Text formatting | |
| if (fmt.textFormat) { | |
| const tf: Record<string, any> = {}; | |
| if (fmt.textFormat.bold) tf.bold = true; | |
| if (fmt.textFormat.italic) tf.italic = true; | |
| if (fmt.textFormat.strikethrough) tf.strikethrough = true; | |
| if (fmt.textFormat.underline) tf.underline = true; | |
| if (fmt.textFormat.fontSize != null) tf.fontSize = fmt.textFormat.fontSize; | |
| if (fmt.textFormat.fontFamily) tf.fontFamily = fmt.textFormat.fontFamily; | |
| if (fmt.textFormat.foregroundColorStyle?.rgbColor) { | |
| tf.foregroundColor = rgbaToHex(fmt.textFormat.foregroundColorStyle.rgbColor); | |
| } else if (fmt.textFormat.foregroundColor) { | |
| tf.foregroundColor = rgbaToHex(fmt.textFormat.foregroundColor); | |
| } | |
| if (Object.keys(tf).length > 0) result.textFormat = tf; | |
| } | |
| // Background color | |
| if (fmt.backgroundColorStyle?.rgbColor) { | |
| result.backgroundColor = rgbaToHex(fmt.backgroundColorStyle.rgbColor); | |
| } else if (fmt.backgroundColor) { | |
| result.backgroundColor = rgbaToHex(fmt.backgroundColor); | |
| } | |
| // Alignment | |
| if (fmt.horizontalAlignment) result.horizontalAlignment = fmt.horizontalAlignment; | |
| if (fmt.verticalAlignment) result.verticalAlignment = fmt.verticalAlignment; | |
| // Number format | |
| if (fmt.numberFormat) { | |
| result.numberFormat = { | |
| type: fmt.numberFormat.type, | |
| pattern: fmt.numberFormat.pattern, | |
| }; | |
| } | |
| // Borders | |
| if (fmt.borders) { | |
| const borders: Record<string, any> = {}; | |
| for (const side of ['top', 'bottom', 'left', 'right'] as const) { | |
| if (fmt.borders[side]) { | |
| borders[side] = { | |
| style: fmt.borders[side].style, | |
| ...(fmt.borders[side].colorStyle?.rgbColor | |
| ? { color: rgbaToHex(fmt.borders[side].colorStyle.rgbColor) } | |
| : fmt.borders[side].color | |
| ? { color: rgbaToHex(fmt.borders[side].color) } | |
| : {}), | |
| }; | |
| } | |
| } | |
| if (Object.keys(borders).length > 0) result.borders = borders; | |
| } | |
| // Wrap strategy | |
| if (fmt.wrapStrategy) result.wrapStrategy = fmt.wrapStrategy; | |
| return Object.keys(result).length > 0 ? result : null; | |
| } | |
| export function register(server: FastMCP) { | |
| server.addTool({ | |
| name: 'readCellFormat', | |
| description: | |
| 'Reads the formatting/style of cells in a given range. Returns formatting details like bold, italic, fontSize, fontFamily, colors, alignment, borders, and number format per cell.', | |
| parameters: z.strictObject({ | |
| spreadsheetId: z | |
| .string() | |
| .describe( | |
| 'The spreadsheet ID — the long string between /d/ and /edit in a Google Sheets URL.' | |
| ), | |
| range: z | |
| .string() | |
| .describe('A1 notation range to read formatting from (e.g., "Sheet1!A1:D5" or "A1:B2").'), | |
| }), | |
| execute: async (args, { log }) => { | |
| const sheets = await getSheetsClient(); | |
| log.info( | |
| `Reading cell format for range "${args.range}" in spreadsheet ${args.spreadsheetId}` | |
| ); | |
| try { | |
| const response = await sheets.spreadsheets.get({ | |
| spreadsheetId: args.spreadsheetId, | |
| ranges: [args.range], | |
| includeGridData: true, | |
| fields: | |
| 'sheets.data.rowData.values.userEnteredFormat,sheets.data.startRow,sheets.data.startColumn', | |
| }); | |
| const sheetData = response.data.sheets?.[0]?.data?.[0]; | |
| if (!sheetData?.rowData) { | |
| return JSON.stringify({ range: args.range, cells: [] }, null, 2); | |
| } | |
| const startRow = sheetData.startRow ?? 0; | |
| const startCol = sheetData.startColumn ?? 0; | |
| const cells: Array<{ cell: string; format: Record<string, any> }> = []; | |
| for (let rowIdx = 0; rowIdx < sheetData.rowData.length; rowIdx++) { | |
| const row = sheetData.rowData[rowIdx]; | |
| if (!row.values) continue; | |
| for (let colIdx = 0; colIdx < row.values.length; colIdx++) { | |
| const cellData = row.values[colIdx]; | |
| const fmt = simplifyFormat(cellData?.userEnteredFormat); | |
| if (fmt) { | |
| const cellRef = rowColToA1(startRow + rowIdx, startCol + colIdx); | |
| cells.push({ cell: cellRef, format: fmt }); | |
| } | |
| } | |
| } | |
| return JSON.stringify({ range: args.range, cells }, null, 2); | |
| } catch (error: any) { | |
| log.error( | |
| `Error reading cell format for spreadsheet ${args.spreadsheetId}: ${error.message || error}` | |
| ); | |
| if (error instanceof UserError) throw error; | |
| throw new UserError(`Failed to read cell format: ${error.message || 'Unknown error'}`); | |
| } | |
| }, | |
| }); | |
| } | |