mcp-google-extras 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +80 -0
  3. package/dist/auth.js +225 -0
  4. package/dist/cachedToolsList.js +38 -0
  5. package/dist/clients.js +92 -0
  6. package/dist/googleDocsApiHelpers.js +883 -0
  7. package/dist/googleSheetsApiHelpers.js +808 -0
  8. package/dist/index.js +54 -0
  9. package/dist/logger.js +58 -0
  10. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  11. package/dist/markdown-transformer/index.js +126 -0
  12. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  13. package/dist/tools/docs/addTab.js +92 -0
  14. package/dist/tools/docs/appendToGoogleDoc.js +81 -0
  15. package/dist/tools/docs/comments/addComment.js +83 -0
  16. package/dist/tools/docs/comments/deleteComment.js +30 -0
  17. package/dist/tools/docs/comments/getComment.js +45 -0
  18. package/dist/tools/docs/comments/index.js +14 -0
  19. package/dist/tools/docs/comments/listComments.js +43 -0
  20. package/dist/tools/docs/comments/replyToComment.js +35 -0
  21. package/dist/tools/docs/comments/resolveComment.js +55 -0
  22. package/dist/tools/docs/deleteRange.js +72 -0
  23. package/dist/tools/docs/findAndReplace.js +54 -0
  24. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  25. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  26. package/dist/tools/docs/formatting/index.js +6 -0
  27. package/dist/tools/docs/index.js +38 -0
  28. package/dist/tools/docs/insertImage.js +122 -0
  29. package/dist/tools/docs/insertPageBreak.js +58 -0
  30. package/dist/tools/docs/insertTable.js +53 -0
  31. package/dist/tools/docs/insertTableWithData.js +135 -0
  32. package/dist/tools/docs/insertText.js +61 -0
  33. package/dist/tools/docs/listDocumentTabs.js +60 -0
  34. package/dist/tools/docs/modifyText.js +158 -0
  35. package/dist/tools/docs/readGoogleDoc.js +165 -0
  36. package/dist/tools/docs/renameTab.js +61 -0
  37. package/dist/tools/drive/copyFile.js +63 -0
  38. package/dist/tools/drive/createDocument.js +89 -0
  39. package/dist/tools/drive/createFolder.js +48 -0
  40. package/dist/tools/drive/createFromTemplate.js +82 -0
  41. package/dist/tools/drive/deleteFile.js +72 -0
  42. package/dist/tools/drive/getDocumentInfo.js +48 -0
  43. package/dist/tools/drive/getFolderInfo.js +48 -0
  44. package/dist/tools/drive/index.js +26 -0
  45. package/dist/tools/drive/listFolderContents.js +82 -0
  46. package/dist/tools/drive/listGoogleDocs.js +67 -0
  47. package/dist/tools/drive/moveFile.js +54 -0
  48. package/dist/tools/drive/renameFile.js +39 -0
  49. package/dist/tools/drive/searchGoogleDocs.js +73 -0
  50. package/dist/tools/extras/index.js +7 -0
  51. package/dist/tools/extras/readFile.js +82 -0
  52. package/dist/tools/extras/searchFileContents.js +81 -0
  53. package/dist/tools/index.js +15 -0
  54. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  55. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  56. package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
  57. package/dist/tools/sheets/appendTableRows.js +50 -0
  58. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  59. package/dist/tools/sheets/batchWrite.js +59 -0
  60. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  61. package/dist/tools/sheets/copyFormatting.js +59 -0
  62. package/dist/tools/sheets/createSpreadsheet.js +71 -0
  63. package/dist/tools/sheets/createTable.js +120 -0
  64. package/dist/tools/sheets/deleteChart.js +41 -0
  65. package/dist/tools/sheets/deleteSheet.js +43 -0
  66. package/dist/tools/sheets/deleteTable.js +56 -0
  67. package/dist/tools/sheets/duplicateSheet.js +53 -0
  68. package/dist/tools/sheets/formatCells.js +106 -0
  69. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  70. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  71. package/dist/tools/sheets/getTable.js +48 -0
  72. package/dist/tools/sheets/groupRows.js +62 -0
  73. package/dist/tools/sheets/index.js +66 -0
  74. package/dist/tools/sheets/insertChart.js +225 -0
  75. package/dist/tools/sheets/listGoogleSheets.js +62 -0
  76. package/dist/tools/sheets/listTables.js +55 -0
  77. package/dist/tools/sheets/readCellFormat.js +143 -0
  78. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  79. package/dist/tools/sheets/renameSheet.js +48 -0
  80. package/dist/tools/sheets/setColumnWidths.js +43 -0
  81. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  82. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  83. package/dist/tools/sheets/updateTableRange.js +51 -0
  84. package/dist/tools/sheets/writeSpreadsheet.js +43 -0
  85. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
  86. package/dist/tools/utils/index.js +6 -0
  87. package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
  88. package/dist/types.js +186 -0
  89. package/package.json +47 -0
@@ -0,0 +1,808 @@
1
+ import { UserError } from 'fastmcp';
2
+ // --- Core Helper Functions ---
3
+ /**
4
+ * Converts A1 notation to row/column indices (0-based)
5
+ * Example: "A1" -> {row: 0, col: 0}, "B2" -> {row: 1, col: 1}
6
+ */
7
+ export function a1ToRowCol(a1) {
8
+ const match = a1.match(/^([A-Z]+)(\d+)$/i);
9
+ if (!match) {
10
+ throw new UserError(`Invalid A1 notation: ${a1}. Expected format like "A1" or "B2"`);
11
+ }
12
+ const colStr = match[1].toUpperCase();
13
+ const row = parseInt(match[2], 10) - 1; // Convert to 0-based
14
+ let col = 0;
15
+ for (let i = 0; i < colStr.length; i++) {
16
+ col = col * 26 + (colStr.charCodeAt(i) - 64);
17
+ }
18
+ col -= 1; // Convert to 0-based
19
+ return { row, col };
20
+ }
21
+ /**
22
+ * Converts row/column indices (0-based) to A1 notation
23
+ * Example: {row: 0, col: 0} -> "A1", {row: 1, col: 1} -> "B2"
24
+ */
25
+ export function rowColToA1(row, col) {
26
+ if (row < 0 || col < 0) {
27
+ throw new UserError(`Row and column indices must be non-negative. Got row: ${row}, col: ${col}`);
28
+ }
29
+ let colStr = '';
30
+ let colNum = col + 1; // Convert to 1-based for calculation
31
+ while (colNum > 0) {
32
+ colNum -= 1;
33
+ colStr = String.fromCharCode(65 + (colNum % 26)) + colStr;
34
+ colNum = Math.floor(colNum / 26);
35
+ }
36
+ return `${colStr}${row + 1}`;
37
+ }
38
+ /**
39
+ * Validates and normalizes a range string
40
+ * Examples: "A1" -> "Sheet1!A1", "A1:B2" -> "Sheet1!A1:B2"
41
+ */
42
+ export function normalizeRange(range, sheetName) {
43
+ // If range already contains '!', assume it's already normalized
44
+ if (range.includes('!')) {
45
+ return range;
46
+ }
47
+ // If sheetName is provided, prepend it
48
+ if (sheetName) {
49
+ return `${sheetName}!${range}`;
50
+ }
51
+ // Default to Sheet1 if no sheet name provided
52
+ return `Sheet1!${range}`;
53
+ }
54
+ /**
55
+ * Reads values from a spreadsheet range
56
+ */
57
+ export async function readRange(sheets, spreadsheetId, range, valueRenderOption = 'FORMATTED_VALUE') {
58
+ try {
59
+ const response = await sheets.spreadsheets.values.get({
60
+ spreadsheetId,
61
+ range,
62
+ valueRenderOption,
63
+ });
64
+ return response.data;
65
+ }
66
+ catch (error) {
67
+ if (error.code === 404) {
68
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
69
+ }
70
+ if (error.code === 403) {
71
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have read access.`);
72
+ }
73
+ throw new UserError(`Failed to read range: ${error.message || 'Unknown error'}`);
74
+ }
75
+ }
76
+ /**
77
+ * Writes values to a spreadsheet range
78
+ */
79
+ export async function writeRange(sheets, spreadsheetId, range, values, valueInputOption = 'USER_ENTERED') {
80
+ try {
81
+ const response = await sheets.spreadsheets.values.update({
82
+ spreadsheetId,
83
+ range,
84
+ valueInputOption,
85
+ requestBody: {
86
+ values,
87
+ },
88
+ });
89
+ return response.data;
90
+ }
91
+ catch (error) {
92
+ if (error.code === 404) {
93
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
94
+ }
95
+ if (error.code === 403) {
96
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
97
+ }
98
+ throw new UserError(`Failed to write range: ${error.message || 'Unknown error'}`);
99
+ }
100
+ }
101
+ /**
102
+ * Appends values to the end of a sheet
103
+ */
104
+ export async function appendValues(sheets, spreadsheetId, range, values, valueInputOption = 'USER_ENTERED') {
105
+ try {
106
+ const response = await sheets.spreadsheets.values.append({
107
+ spreadsheetId,
108
+ range,
109
+ valueInputOption,
110
+ insertDataOption: 'INSERT_ROWS',
111
+ requestBody: {
112
+ values,
113
+ },
114
+ });
115
+ return response.data;
116
+ }
117
+ catch (error) {
118
+ if (error.code === 404) {
119
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
120
+ }
121
+ if (error.code === 403) {
122
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
123
+ }
124
+ throw new UserError(`Failed to append values: ${error.message || 'Unknown error'}`);
125
+ }
126
+ }
127
+ /**
128
+ * Clears values from a range
129
+ */
130
+ export async function clearRange(sheets, spreadsheetId, range) {
131
+ try {
132
+ const response = await sheets.spreadsheets.values.clear({
133
+ spreadsheetId,
134
+ range,
135
+ });
136
+ return response.data;
137
+ }
138
+ catch (error) {
139
+ if (error.code === 404) {
140
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
141
+ }
142
+ if (error.code === 403) {
143
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
144
+ }
145
+ throw new UserError(`Failed to clear range: ${error.message || 'Unknown error'}`);
146
+ }
147
+ }
148
+ /**
149
+ * Gets spreadsheet metadata including sheet information
150
+ */
151
+ export async function getSpreadsheetMetadata(sheets, spreadsheetId) {
152
+ try {
153
+ const response = await sheets.spreadsheets.get({
154
+ spreadsheetId,
155
+ includeGridData: false,
156
+ });
157
+ return response.data;
158
+ }
159
+ catch (error) {
160
+ if (error.code === 404) {
161
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
162
+ }
163
+ if (error.code === 403) {
164
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have read access.`);
165
+ }
166
+ throw new UserError(`Failed to get spreadsheet metadata: ${error.message || 'Unknown error'}`);
167
+ }
168
+ }
169
+ /**
170
+ * Creates a new sheet/tab in a spreadsheet
171
+ */
172
+ export async function addSheet(sheets, spreadsheetId, sheetTitle) {
173
+ try {
174
+ const response = await sheets.spreadsheets.batchUpdate({
175
+ spreadsheetId,
176
+ requestBody: {
177
+ requests: [
178
+ {
179
+ addSheet: {
180
+ properties: {
181
+ title: sheetTitle,
182
+ },
183
+ },
184
+ },
185
+ ],
186
+ },
187
+ });
188
+ return response.data;
189
+ }
190
+ catch (error) {
191
+ if (error.code === 404) {
192
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
193
+ }
194
+ if (error.code === 403) {
195
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
196
+ }
197
+ throw new UserError(`Failed to add sheet: ${error.message || 'Unknown error'}`);
198
+ }
199
+ }
200
+ /**
201
+ * Parses A1 notation range to extract sheet name and cell range
202
+ * Returns {sheetName, a1Range} where a1Range is just the cell part (e.g., "A1:B2")
203
+ */
204
+ export function parseRange(range) {
205
+ if (range.includes('!')) {
206
+ const parts = range.split('!');
207
+ return {
208
+ sheetName: parts[0].replace(/^'|'$/g, ''), // Remove quotes if present
209
+ a1Range: parts[1],
210
+ };
211
+ }
212
+ return {
213
+ sheetName: null,
214
+ a1Range: range,
215
+ };
216
+ }
217
+ /**
218
+ * Resolves a sheet name to a numeric sheet ID.
219
+ * If sheetName is null/undefined, returns the first sheet's ID.
220
+ */
221
+ export async function resolveSheetId(sheets, spreadsheetId, sheetName) {
222
+ const metadata = await getSpreadsheetMetadata(sheets, spreadsheetId);
223
+ if (sheetName) {
224
+ const sheet = metadata.sheets?.find((s) => s.properties?.title === sheetName);
225
+ if (!sheet || sheet.properties?.sheetId === undefined || sheet.properties?.sheetId === null) {
226
+ throw new UserError(`Sheet "${sheetName}" not found in spreadsheet.`);
227
+ }
228
+ return sheet.properties.sheetId;
229
+ }
230
+ const firstSheet = metadata.sheets?.[0];
231
+ if (firstSheet?.properties?.sheetId === undefined || firstSheet?.properties?.sheetId === null) {
232
+ throw new UserError('Spreadsheet has no sheets.');
233
+ }
234
+ return firstSheet.properties.sheetId;
235
+ }
236
+ /**
237
+ * Converts column letters to a 0-based column index.
238
+ * Example: "A" -> 0, "B" -> 1, "Z" -> 25, "AA" -> 26
239
+ */
240
+ export function colLettersToIndex(col) {
241
+ let index = 0;
242
+ const upper = col.toUpperCase();
243
+ for (let i = 0; i < upper.length; i++) {
244
+ index = index * 26 + (upper.charCodeAt(i) - 64);
245
+ }
246
+ return index - 1;
247
+ }
248
+ /**
249
+ * Parses an A1-notation cell range string into a Google Sheets GridRange object.
250
+ * Supports:
251
+ * - Standard: "A1", "A1:B2"
252
+ * - Whole rows: "1:1", "1:3"
253
+ * - Whole columns: "A:A", "A:C"
254
+ * When a component is omitted (whole row/column), the corresponding
255
+ * start/end index is left out of the GridRange, which the Sheets API
256
+ * interprets as "unbounded" (i.e., the entire row or column).
257
+ */
258
+ export function parseA1ToGridRange(a1Range, sheetId) {
259
+ // Whole-row pattern: "1:3" or "1"
260
+ const rowOnlyMatch = a1Range.match(/^(\d+)(?::(\d+))?$/);
261
+ if (rowOnlyMatch) {
262
+ const startRow = parseInt(rowOnlyMatch[1], 10) - 1;
263
+ const endRow = rowOnlyMatch[2] ? parseInt(rowOnlyMatch[2], 10) : startRow + 1;
264
+ return {
265
+ sheetId,
266
+ startRowIndex: startRow,
267
+ endRowIndex: endRow,
268
+ // no column indices → entire row
269
+ };
270
+ }
271
+ // Whole-column pattern: "A:C" or "A"
272
+ const colOnlyMatch = a1Range.match(/^([A-Z]+)(?::([A-Z]+))?$/i);
273
+ if (colOnlyMatch && !/\d/.test(a1Range)) {
274
+ const startCol = colLettersToIndex(colOnlyMatch[1]);
275
+ const endCol = colOnlyMatch[2] ? colLettersToIndex(colOnlyMatch[2]) + 1 : startCol + 1;
276
+ return {
277
+ sheetId,
278
+ startColumnIndex: startCol,
279
+ endColumnIndex: endCol,
280
+ // no row indices → entire column
281
+ };
282
+ }
283
+ // Standard A1 pattern: "A1" or "A1:B2"
284
+ const standardMatch = a1Range.match(/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i);
285
+ if (!standardMatch) {
286
+ throw new UserError(`Invalid range format: "${a1Range}". Expected "A1:B2", "1:1" (whole row), or "A:A" (whole column).`);
287
+ }
288
+ const startCol = colLettersToIndex(standardMatch[1]);
289
+ const startRow = parseInt(standardMatch[2], 10) - 1;
290
+ const endCol = standardMatch[3] ? colLettersToIndex(standardMatch[3]) + 1 : startCol + 1;
291
+ const endRow = standardMatch[4] ? parseInt(standardMatch[4], 10) : startRow + 1;
292
+ return {
293
+ sheetId,
294
+ startRowIndex: startRow,
295
+ endRowIndex: endRow,
296
+ startColumnIndex: startCol,
297
+ endColumnIndex: endCol,
298
+ };
299
+ }
300
+ /**
301
+ * Formats cells in a range.
302
+ * Supports standard A1 ranges ("A1:D1"), whole-row ("1:1"), and whole-column ("A:A") notation.
303
+ */
304
+ export async function formatCells(sheets, spreadsheetId, range, format) {
305
+ try {
306
+ // Parse the range to get sheet name and cell range
307
+ const { sheetName, a1Range } = parseRange(range);
308
+ const sheetId = await resolveSheetId(sheets, spreadsheetId, sheetName);
309
+ // Parse A1 range to get row/column indices
310
+ // Supports: "A1:B2" (standard), "1:3" (whole rows), "A:C" (whole columns)
311
+ const gridRange = parseA1ToGridRange(a1Range, sheetId);
312
+ const userEnteredFormat = {};
313
+ if (format.backgroundColor) {
314
+ userEnteredFormat.backgroundColor = {
315
+ red: format.backgroundColor.red,
316
+ green: format.backgroundColor.green,
317
+ blue: format.backgroundColor.blue,
318
+ alpha: 1,
319
+ };
320
+ }
321
+ if (format.textFormat) {
322
+ userEnteredFormat.textFormat = {};
323
+ if (format.textFormat.foregroundColor) {
324
+ userEnteredFormat.textFormat.foregroundColor = {
325
+ red: format.textFormat.foregroundColor.red,
326
+ green: format.textFormat.foregroundColor.green,
327
+ blue: format.textFormat.foregroundColor.blue,
328
+ alpha: 1,
329
+ };
330
+ }
331
+ if (format.textFormat.fontSize !== undefined) {
332
+ userEnteredFormat.textFormat.fontSize = format.textFormat.fontSize;
333
+ }
334
+ if (format.textFormat.bold !== undefined) {
335
+ userEnteredFormat.textFormat.bold = format.textFormat.bold;
336
+ }
337
+ if (format.textFormat.italic !== undefined) {
338
+ userEnteredFormat.textFormat.italic = format.textFormat.italic;
339
+ }
340
+ }
341
+ if (format.horizontalAlignment) {
342
+ userEnteredFormat.horizontalAlignment = format.horizontalAlignment;
343
+ }
344
+ if (format.verticalAlignment) {
345
+ userEnteredFormat.verticalAlignment = format.verticalAlignment;
346
+ }
347
+ if (format.numberFormat) {
348
+ userEnteredFormat.numberFormat = {
349
+ type: format.numberFormat.type,
350
+ pattern: format.numberFormat.pattern ?? '',
351
+ };
352
+ }
353
+ const fields = [
354
+ 'backgroundColor',
355
+ 'textFormat',
356
+ 'horizontalAlignment',
357
+ 'verticalAlignment',
358
+ ...(format.numberFormat ? ['numberFormat'] : []),
359
+ ].join(',');
360
+ const response = await sheets.spreadsheets.batchUpdate({
361
+ spreadsheetId,
362
+ requestBody: {
363
+ requests: [
364
+ {
365
+ repeatCell: {
366
+ range: gridRange,
367
+ cell: {
368
+ userEnteredFormat,
369
+ },
370
+ fields: `userEnteredFormat(${fields})`,
371
+ },
372
+ },
373
+ ],
374
+ },
375
+ });
376
+ return response.data;
377
+ }
378
+ catch (error) {
379
+ if (error.code === 404) {
380
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
381
+ }
382
+ if (error.code === 403) {
383
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
384
+ }
385
+ if (error instanceof UserError)
386
+ throw error;
387
+ throw new UserError(`Failed to format cells: ${error.message || 'Unknown error'}`);
388
+ }
389
+ }
390
+ /**
391
+ * Freezes rows and/or columns in a sheet so they remain visible when scrolling.
392
+ */
393
+ export async function freezeRowsAndColumns(sheets, spreadsheetId, sheetName, frozenRows, frozenColumns) {
394
+ try {
395
+ const sheetId = await resolveSheetId(sheets, spreadsheetId, sheetName);
396
+ const gridProperties = {};
397
+ const fieldParts = [];
398
+ if (frozenRows !== undefined) {
399
+ gridProperties.frozenRowCount = frozenRows;
400
+ fieldParts.push('gridProperties.frozenRowCount');
401
+ }
402
+ if (frozenColumns !== undefined) {
403
+ gridProperties.frozenColumnCount = frozenColumns;
404
+ fieldParts.push('gridProperties.frozenColumnCount');
405
+ }
406
+ const response = await sheets.spreadsheets.batchUpdate({
407
+ spreadsheetId,
408
+ requestBody: {
409
+ requests: [
410
+ {
411
+ updateSheetProperties: {
412
+ properties: {
413
+ sheetId,
414
+ gridProperties,
415
+ },
416
+ fields: fieldParts.join(','),
417
+ },
418
+ },
419
+ ],
420
+ },
421
+ });
422
+ return response.data;
423
+ }
424
+ catch (error) {
425
+ if (error.code === 404) {
426
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
427
+ }
428
+ if (error.code === 403) {
429
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
430
+ }
431
+ if (error instanceof UserError)
432
+ throw error;
433
+ throw new UserError(`Failed to freeze rows/columns: ${error.message || 'Unknown error'}`);
434
+ }
435
+ }
436
+ /**
437
+ * Sets or clears dropdown data validation on a range of cells.
438
+ * When values are provided, creates a ONE_OF_LIST validation rule.
439
+ * When values are omitted or empty, clears any existing validation from the range.
440
+ */
441
+ export async function setDropdownValidation(sheets, spreadsheetId, range, values, strict = true, inputMessage) {
442
+ try {
443
+ const { sheetName, a1Range } = parseRange(range);
444
+ const sheetId = await resolveSheetId(sheets, spreadsheetId, sheetName);
445
+ const gridRange = parseA1ToGridRange(a1Range, sheetId);
446
+ const rule = values && values.length > 0
447
+ ? {
448
+ condition: {
449
+ type: 'ONE_OF_LIST',
450
+ values: values.map((v) => ({ userEnteredValue: v })),
451
+ },
452
+ showCustomUi: true,
453
+ strict,
454
+ inputMessage: inputMessage || null,
455
+ }
456
+ : undefined;
457
+ const response = await sheets.spreadsheets.batchUpdate({
458
+ spreadsheetId,
459
+ requestBody: {
460
+ requests: [
461
+ {
462
+ setDataValidation: {
463
+ range: gridRange,
464
+ rule,
465
+ },
466
+ },
467
+ ],
468
+ },
469
+ });
470
+ return response.data;
471
+ }
472
+ catch (error) {
473
+ if (error.code === 404) {
474
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
475
+ }
476
+ if (error.code === 403) {
477
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
478
+ }
479
+ if (error instanceof UserError)
480
+ throw error;
481
+ throw new UserError(`Failed to set dropdown validation: ${error.message || 'Unknown error'}`);
482
+ }
483
+ }
484
+ /**
485
+ * Sets the width (in pixels) of one or more columns.
486
+ * Each entry may target a single column ("A") or a contiguous range ("A:C").
487
+ */
488
+ export async function setColumnWidths(sheets, spreadsheetId, sheetName, columnWidths) {
489
+ try {
490
+ const sheetId = await resolveSheetId(sheets, spreadsheetId, sheetName);
491
+ const requests = columnWidths.map(({ column, width }) => {
492
+ const colonIdx = column.indexOf(':');
493
+ let startIndex;
494
+ let endIndex;
495
+ if (colonIdx !== -1) {
496
+ startIndex = colLettersToIndex(column.slice(0, colonIdx).trim());
497
+ endIndex = colLettersToIndex(column.slice(colonIdx + 1).trim()) + 1;
498
+ }
499
+ else {
500
+ startIndex = colLettersToIndex(column.trim());
501
+ endIndex = startIndex + 1;
502
+ }
503
+ return {
504
+ updateDimensionProperties: {
505
+ range: {
506
+ sheetId,
507
+ dimension: 'COLUMNS',
508
+ startIndex,
509
+ endIndex,
510
+ },
511
+ properties: {
512
+ pixelSize: width,
513
+ },
514
+ fields: 'pixelSize',
515
+ },
516
+ };
517
+ });
518
+ const response = await sheets.spreadsheets.batchUpdate({
519
+ spreadsheetId,
520
+ requestBody: { requests },
521
+ });
522
+ return response.data;
523
+ }
524
+ catch (error) {
525
+ if (error.code === 404) {
526
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
527
+ }
528
+ if (error.code === 403) {
529
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
530
+ }
531
+ if (error instanceof UserError)
532
+ throw error;
533
+ throw new UserError(`Failed to set column widths: ${error.message || 'Unknown error'}`);
534
+ }
535
+ }
536
+ /**
537
+ * Helper to convert hex color to RGB (0-1 range)
538
+ */
539
+ export function hexToRgb(hex) {
540
+ if (!hex)
541
+ return null;
542
+ let hexClean = hex.startsWith('#') ? hex.slice(1) : hex;
543
+ if (hexClean.length === 3) {
544
+ hexClean = hexClean[0] + hexClean[0] + hexClean[1] + hexClean[1] + hexClean[2] + hexClean[2];
545
+ }
546
+ if (hexClean.length !== 6)
547
+ return null;
548
+ const bigint = parseInt(hexClean, 16);
549
+ if (isNaN(bigint))
550
+ return null;
551
+ return {
552
+ red: ((bigint >> 16) & 255) / 255,
553
+ green: ((bigint >> 8) & 255) / 255,
554
+ blue: (bigint & 255) / 255,
555
+ };
556
+ }
557
+ /**
558
+ * Appends a BooleanRule conditional format rule to a spreadsheet.
559
+ */
560
+ export async function addConditionalFormatRule(sheets, spreadsheetId, ranges, conditionType, conditionValues, format) {
561
+ try {
562
+ await sheets.spreadsheets.batchUpdate({
563
+ spreadsheetId,
564
+ requestBody: {
565
+ requests: [
566
+ {
567
+ addConditionalFormatRule: {
568
+ rule: {
569
+ ranges,
570
+ booleanRule: {
571
+ condition: {
572
+ type: conditionType,
573
+ values: conditionValues,
574
+ },
575
+ format,
576
+ },
577
+ },
578
+ index: 0,
579
+ },
580
+ },
581
+ ],
582
+ },
583
+ });
584
+ }
585
+ catch (error) {
586
+ if (error.code === 404) {
587
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
588
+ }
589
+ if (error.code === 403) {
590
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
591
+ }
592
+ throw new UserError(`Failed to add conditional format rule: ${error.message || 'Unknown error'}`);
593
+ }
594
+ }
595
+ // --- Table Helper Functions ---
596
+ /**
597
+ * Resolves a table name or ID to a table object with sheet context.
598
+ * Searches through all sheets in the spreadsheet.
599
+ */
600
+ export async function resolveTableIdentifier(sheets, spreadsheetId, tableIdentifier) {
601
+ const metadata = await getSpreadsheetMetadata(sheets, spreadsheetId);
602
+ // Search through all sheets for the table
603
+ for (const sheet of metadata.sheets || []) {
604
+ // Check if sheetId exists (can be 0, which is valid for first sheet!)
605
+ if (sheet.properties?.sheetId === null || sheet.properties?.sheetId === undefined) {
606
+ continue;
607
+ }
608
+ const sheetName = sheet.properties.title || 'Unknown';
609
+ const tables = sheet.tables || [];
610
+ for (const table of tables) {
611
+ if (!table)
612
+ continue;
613
+ // Match by tableId (string) or name (case-insensitive)
614
+ const idMatch = table.tableId === tableIdentifier;
615
+ const nameMatch = table.name
616
+ ? table.name.toLowerCase() === tableIdentifier.toLowerCase()
617
+ : false;
618
+ if (idMatch || nameMatch) {
619
+ if (sheet.properties.sheetId === null || sheet.properties.sheetId === undefined) {
620
+ throw new UserError(`Sheet "${sheetName}" has invalid ID.`);
621
+ }
622
+ return {
623
+ table,
624
+ sheetId: sheet.properties.sheetId,
625
+ sheetName,
626
+ };
627
+ }
628
+ }
629
+ }
630
+ throw new UserError(`Table "${tableIdentifier}" not found in spreadsheet. Use listTables to see available tables.`);
631
+ }
632
+ /**
633
+ * Lists all tables across all sheets in a spreadsheet.
634
+ * Optionally filters by sheet name.
635
+ */
636
+ export async function listAllTables(sheets, spreadsheetId, sheetNameFilter) {
637
+ const metadata = await getSpreadsheetMetadata(sheets, spreadsheetId);
638
+ const result = [];
639
+ for (const sheet of metadata.sheets || []) {
640
+ // Check if sheetId exists (can be 0, which is valid for first sheet!)
641
+ if (sheet.properties?.sheetId === null || sheet.properties?.sheetId === undefined) {
642
+ continue;
643
+ }
644
+ // Filter by sheet name if provided
645
+ if (sheetNameFilter && sheet.properties.title !== sheetNameFilter)
646
+ continue;
647
+ const sheetName = sheet.properties.title || 'Unknown';
648
+ const tables = sheet.tables || [];
649
+ for (const table of tables) {
650
+ if (table) {
651
+ result.push({
652
+ table,
653
+ sheetName,
654
+ sheetId: sheet.properties.sheetId,
655
+ });
656
+ }
657
+ }
658
+ }
659
+ return result;
660
+ }
661
+ /**
662
+ * Creates a new table with specified properties.
663
+ */
664
+ export async function createTableHelper(sheets, spreadsheetId, tableDefinition) {
665
+ try {
666
+ const response = await sheets.spreadsheets.batchUpdate({
667
+ spreadsheetId,
668
+ requestBody: {
669
+ requests: [
670
+ {
671
+ addTable: {
672
+ table: {
673
+ name: tableDefinition.name,
674
+ range: tableDefinition.range,
675
+ columnProperties: tableDefinition.columnProperties,
676
+ },
677
+ },
678
+ },
679
+ ],
680
+ },
681
+ });
682
+ const reply = response.data.replies?.[0]?.addTable;
683
+ if (!reply?.table) {
684
+ throw new UserError('Failed to create table - no table returned in response.');
685
+ }
686
+ return reply.table;
687
+ }
688
+ catch (error) {
689
+ if (error.code === 400) {
690
+ throw new UserError(`Invalid table definition: ${error.message}`);
691
+ }
692
+ if (error.code === 403) {
693
+ throw new UserError(`Permission denied. Ensure you have write access to this spreadsheet.`);
694
+ }
695
+ throw error;
696
+ }
697
+ }
698
+ /**
699
+ * Deletes a table by ID.
700
+ */
701
+ export async function deleteTableHelper(sheets, spreadsheetId, tableId) {
702
+ try {
703
+ await sheets.spreadsheets.batchUpdate({
704
+ spreadsheetId,
705
+ requestBody: {
706
+ requests: [
707
+ {
708
+ deleteTable: {
709
+ tableId,
710
+ },
711
+ },
712
+ ],
713
+ },
714
+ });
715
+ }
716
+ catch (error) {
717
+ if (error.code === 404) {
718
+ throw new UserError(`Table not found (ID: ${tableId}).`);
719
+ }
720
+ if (error.code === 403) {
721
+ throw new UserError(`Permission denied. Ensure you have write access to this spreadsheet.`);
722
+ }
723
+ throw error;
724
+ }
725
+ }
726
+ /**
727
+ * Updates a table's range.
728
+ */
729
+ export async function updateTableRangeHelper(sheets, spreadsheetId, tableId, newRange) {
730
+ try {
731
+ const response = await sheets.spreadsheets.batchUpdate({
732
+ spreadsheetId,
733
+ requestBody: {
734
+ requests: [
735
+ {
736
+ updateTable: {
737
+ table: {
738
+ tableId,
739
+ range: newRange,
740
+ },
741
+ fields: 'range',
742
+ },
743
+ },
744
+ ],
745
+ },
746
+ });
747
+ // The Google Sheets API may not return the table object in the response
748
+ // even though the update was successful. Fetch the updated table to return.
749
+ const { table } = await resolveTableIdentifier(sheets, spreadsheetId, tableId);
750
+ return table;
751
+ }
752
+ catch (error) {
753
+ if (error.code === 404) {
754
+ throw new UserError(`Table not found (ID: ${tableId}).`);
755
+ }
756
+ if (error.code === 400) {
757
+ throw new UserError(`Invalid range: ${error.message}`);
758
+ }
759
+ if (error.code === 403) {
760
+ throw new UserError(`Permission denied. Ensure you have write access to this spreadsheet.`);
761
+ }
762
+ throw error;
763
+ }
764
+ }
765
+ /**
766
+ * Appends rows to a table using table-aware insertion.
767
+ * Gets the table's range and appends values after the last data row.
768
+ */
769
+ export async function appendToTableHelper(sheets, spreadsheetId, tableId, values) {
770
+ try {
771
+ // First, get the table to find its range
772
+ const { table, sheetName } = await resolveTableIdentifier(sheets, spreadsheetId, tableId);
773
+ if (!table.range) {
774
+ throw new UserError('Table does not have a range defined.');
775
+ }
776
+ // Calculate the range to append to (start after the table's end row)
777
+ const startRowIndex = table.range.endRowIndex || 0;
778
+ const startColumnIndex = table.range.startColumnIndex || 0;
779
+ const endColumnIndex = table.range.endColumnIndex || 0;
780
+ const range = `${sheetName}!${rowColToA1(startRowIndex, startColumnIndex)}:${rowColToA1(startRowIndex + values.length - 1, endColumnIndex - 1)}`;
781
+ // Append the values using the standard values.append API
782
+ const response = await sheets.spreadsheets.values.append({
783
+ spreadsheetId,
784
+ range,
785
+ valueInputOption: 'USER_ENTERED',
786
+ insertDataOption: 'INSERT_ROWS',
787
+ requestBody: {
788
+ values,
789
+ },
790
+ });
791
+ return {
792
+ rowsAppended: values.length,
793
+ updatedRange: response.data.updates?.updatedRange || range,
794
+ };
795
+ }
796
+ catch (error) {
797
+ if (error.code === 404) {
798
+ throw new UserError(`Table or spreadsheet not found (ID: ${tableId}).`);
799
+ }
800
+ if (error.code === 400) {
801
+ throw new UserError(`Invalid data: ${error.message}`);
802
+ }
803
+ if (error.code === 403) {
804
+ throw new UserError(`Permission denied. Ensure you have write access to this spreadsheet.`);
805
+ }
806
+ throw error;
807
+ }
808
+ }