mcp-google-multi 4.2.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.
@@ -0,0 +1,1207 @@
1
+ import { z } from 'zod';
2
+ import { google } from 'googleapis';
3
+ import { ACCOUNTS } from '../accounts.js';
4
+ import { getClient } from '../client.js';
5
+ import { handleGoogleApiError } from './_errors.js';
6
+ const accountEnum = z.enum(ACCOUNTS);
7
+ export function registerSheetsTools(server) {
8
+ server.registerTool('sheets_create', {
9
+ description: 'Create a new Google Sheets spreadsheet',
10
+ inputSchema: {
11
+ account: accountEnum.describe('Google account alias'),
12
+ title: z.string().describe('Spreadsheet title'),
13
+ sheetTitles: z.array(z.string()).optional()
14
+ .describe('Optional tab/sheet names (default: one sheet named "Sheet1")'),
15
+ },
16
+ }, async ({ account, title, sheetTitles }) => {
17
+ try {
18
+ const auth = await getClient(account);
19
+ const sheets = google.sheets({ version: 'v4', auth });
20
+ const requestBody = {
21
+ properties: { title },
22
+ };
23
+ if (sheetTitles && sheetTitles.length > 0) {
24
+ requestBody.sheets = sheetTitles.map((t, i) => ({
25
+ properties: { title: t, index: i },
26
+ }));
27
+ }
28
+ const res = await sheets.spreadsheets.create({ requestBody });
29
+ return {
30
+ content: [{ type: 'text', text: JSON.stringify({
31
+ spreadsheetId: res.data.spreadsheetId,
32
+ title: res.data.properties?.title,
33
+ url: res.data.spreadsheetUrl,
34
+ sheets: (res.data.sheets ?? []).map((s) => s.properties?.title),
35
+ }, null, 2) }],
36
+ };
37
+ }
38
+ catch (error) {
39
+ return handleSheetsError(error, account);
40
+ }
41
+ });
42
+ server.registerTool('sheets_get', {
43
+ description: 'Get spreadsheet metadata (title, sheets/tabs, named ranges)',
44
+ inputSchema: {
45
+ account: accountEnum.describe('Google account alias'),
46
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
47
+ },
48
+ }, async ({ account, spreadsheetId }) => {
49
+ try {
50
+ const auth = await getClient(account);
51
+ const sheets = google.sheets({ version: 'v4', auth });
52
+ const res = await sheets.spreadsheets.get({
53
+ spreadsheetId,
54
+ fields: 'spreadsheetId,properties,sheets.properties,namedRanges,spreadsheetUrl',
55
+ });
56
+ return {
57
+ content: [{ type: 'text', text: JSON.stringify({
58
+ spreadsheetId: res.data.spreadsheetId,
59
+ title: res.data.properties?.title,
60
+ url: res.data.spreadsheetUrl,
61
+ sheets: (res.data.sheets ?? []).map((s) => ({
62
+ sheetId: s.properties?.sheetId,
63
+ title: s.properties?.title,
64
+ rowCount: s.properties?.gridProperties?.rowCount,
65
+ columnCount: s.properties?.gridProperties?.columnCount,
66
+ })),
67
+ namedRanges: res.data.namedRanges ?? [],
68
+ }, null, 2) }],
69
+ };
70
+ }
71
+ catch (error) {
72
+ return handleSheetsError(error, account);
73
+ }
74
+ });
75
+ server.registerTool('sheets_read_range', {
76
+ description: 'Read cell values from a spreadsheet range (A1 notation, e.g. "Sheet1!A1:D10")',
77
+ inputSchema: {
78
+ account: accountEnum.describe('Google account alias'),
79
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
80
+ range: z.string().describe('A1 notation range, e.g. "Sheet1!A1:D10" or "A1:B5"'),
81
+ valueRenderOption: z.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA'])
82
+ .default('FORMATTED_VALUE').optional()
83
+ .describe('How to render values (default: FORMATTED_VALUE)'),
84
+ },
85
+ }, async ({ account, spreadsheetId, range, valueRenderOption }) => {
86
+ try {
87
+ const auth = await getClient(account);
88
+ const sheets = google.sheets({ version: 'v4', auth });
89
+ const res = await sheets.spreadsheets.values.get({
90
+ spreadsheetId,
91
+ range,
92
+ valueRenderOption: valueRenderOption ?? 'FORMATTED_VALUE',
93
+ });
94
+ return {
95
+ content: [{ type: 'text', text: JSON.stringify({
96
+ range: res.data.range,
97
+ values: res.data.values ?? [],
98
+ }, null, 2) }],
99
+ };
100
+ }
101
+ catch (error) {
102
+ return handleSheetsError(error, account);
103
+ }
104
+ });
105
+ server.registerTool('sheets_write_range', {
106
+ description: 'Write values to a spreadsheet range',
107
+ inputSchema: {
108
+ account: accountEnum.describe('Google account alias'),
109
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
110
+ range: z.string().describe('A1 notation target range, e.g. "Sheet1!A1:C3"'),
111
+ values: z.array(z.array(z.any())).describe('2D array of values (rows x columns)'),
112
+ valueInputOption: z.enum(['RAW', 'USER_ENTERED']).default('USER_ENTERED').optional()
113
+ .describe('How to interpret values: RAW (literal) or USER_ENTERED (formulas/dates parsed). Default: USER_ENTERED'),
114
+ },
115
+ }, async ({ account, spreadsheetId, range, values, valueInputOption }) => {
116
+ try {
117
+ const auth = await getClient(account);
118
+ const sheets = google.sheets({ version: 'v4', auth });
119
+ const res = await sheets.spreadsheets.values.update({
120
+ spreadsheetId,
121
+ range,
122
+ valueInputOption: valueInputOption ?? 'USER_ENTERED',
123
+ requestBody: { values },
124
+ });
125
+ return {
126
+ content: [{ type: 'text', text: JSON.stringify({
127
+ updatedRange: res.data.updatedRange,
128
+ updatedRows: res.data.updatedRows,
129
+ updatedColumns: res.data.updatedColumns,
130
+ updatedCells: res.data.updatedCells,
131
+ }, null, 2) }],
132
+ };
133
+ }
134
+ catch (error) {
135
+ return handleSheetsError(error, account);
136
+ }
137
+ });
138
+ server.registerTool('sheets_append_rows', {
139
+ description: 'Append rows after the last row of existing data in a spreadsheet',
140
+ inputSchema: {
141
+ account: accountEnum.describe('Google account alias'),
142
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
143
+ range: z.string().describe('A1 notation range to search for the table (e.g. "Sheet1")'),
144
+ values: z.array(z.array(z.any())).describe('2D array of rows to append'),
145
+ valueInputOption: z.enum(['RAW', 'USER_ENTERED']).default('USER_ENTERED').optional()
146
+ .describe('How to interpret values. Default: USER_ENTERED'),
147
+ },
148
+ }, async ({ account, spreadsheetId, range, values, valueInputOption }) => {
149
+ try {
150
+ const auth = await getClient(account);
151
+ const sheets = google.sheets({ version: 'v4', auth });
152
+ const res = await sheets.spreadsheets.values.append({
153
+ spreadsheetId,
154
+ range,
155
+ valueInputOption: valueInputOption ?? 'USER_ENTERED',
156
+ insertDataOption: 'INSERT_ROWS',
157
+ requestBody: { values },
158
+ });
159
+ return {
160
+ content: [{ type: 'text', text: JSON.stringify({
161
+ tableRange: res.data.tableRange,
162
+ updatedRange: res.data.updates?.updatedRange,
163
+ updatedRows: res.data.updates?.updatedRows,
164
+ updatedCells: res.data.updates?.updatedCells,
165
+ }, null, 2) }],
166
+ };
167
+ }
168
+ catch (error) {
169
+ return handleSheetsError(error, account);
170
+ }
171
+ });
172
+ server.registerTool('sheets_clear_range', {
173
+ description: 'Clear values from a range (keeps formatting intact)',
174
+ inputSchema: {
175
+ account: accountEnum.describe('Google account alias'),
176
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
177
+ range: z.string().describe('A1 notation range to clear, e.g. "Sheet1!A1:D10"'),
178
+ },
179
+ }, async ({ account, spreadsheetId, range }) => {
180
+ try {
181
+ const auth = await getClient(account);
182
+ const sheets = google.sheets({ version: 'v4', auth });
183
+ const res = await sheets.spreadsheets.values.clear({
184
+ spreadsheetId,
185
+ range,
186
+ requestBody: {},
187
+ });
188
+ return {
189
+ content: [{ type: 'text', text: JSON.stringify({
190
+ clearedRange: res.data.clearedRange,
191
+ }, null, 2) }],
192
+ };
193
+ }
194
+ catch (error) {
195
+ return handleSheetsError(error, account);
196
+ }
197
+ });
198
+ server.registerTool('sheets_batch_read', {
199
+ description: 'Read multiple ranges from a spreadsheet in one request',
200
+ inputSchema: {
201
+ account: accountEnum.describe('Google account alias'),
202
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
203
+ ranges: z.array(z.string()).describe('Array of A1 notation ranges'),
204
+ valueRenderOption: z.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA'])
205
+ .default('FORMATTED_VALUE').optional()
206
+ .describe('How to render values. Default: FORMATTED_VALUE'),
207
+ },
208
+ }, async ({ account, spreadsheetId, ranges, valueRenderOption }) => {
209
+ try {
210
+ const auth = await getClient(account);
211
+ const sheets = google.sheets({ version: 'v4', auth });
212
+ const res = await sheets.spreadsheets.values.batchGet({
213
+ spreadsheetId,
214
+ ranges,
215
+ valueRenderOption: valueRenderOption ?? 'FORMATTED_VALUE',
216
+ });
217
+ const result = (res.data.valueRanges ?? []).map((vr) => ({
218
+ range: vr.range,
219
+ values: vr.values ?? [],
220
+ }));
221
+ return {
222
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
223
+ };
224
+ }
225
+ catch (error) {
226
+ return handleSheetsError(error, account);
227
+ }
228
+ });
229
+ server.registerTool('sheets_batch_write', {
230
+ description: 'Write to multiple ranges in a spreadsheet in one request',
231
+ inputSchema: {
232
+ account: accountEnum.describe('Google account alias'),
233
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
234
+ data: z.array(z.object({
235
+ range: z.string().describe('A1 notation range'),
236
+ values: z.array(z.array(z.any())).describe('2D array of values'),
237
+ })).describe('Array of { range, values } objects to write'),
238
+ valueInputOption: z.enum(['RAW', 'USER_ENTERED']).default('USER_ENTERED').optional()
239
+ .describe('How to interpret values. Default: USER_ENTERED'),
240
+ },
241
+ }, async ({ account, spreadsheetId, data, valueInputOption }) => {
242
+ try {
243
+ const auth = await getClient(account);
244
+ const sheets = google.sheets({ version: 'v4', auth });
245
+ const res = await sheets.spreadsheets.values.batchUpdate({
246
+ spreadsheetId,
247
+ requestBody: {
248
+ valueInputOption: valueInputOption ?? 'USER_ENTERED',
249
+ data,
250
+ },
251
+ });
252
+ return {
253
+ content: [{ type: 'text', text: JSON.stringify({
254
+ totalUpdatedRows: res.data.totalUpdatedRows,
255
+ totalUpdatedColumns: res.data.totalUpdatedColumns,
256
+ totalUpdatedCells: res.data.totalUpdatedCells,
257
+ totalUpdatedSheets: res.data.totalUpdatedSheets,
258
+ }, null, 2) }],
259
+ };
260
+ }
261
+ catch (error) {
262
+ return handleSheetsError(error, account);
263
+ }
264
+ });
265
+ // ─── Sheet ops ─────────────────────────────────────────────────────────
266
+ server.registerTool('sheets_add_sheet', {
267
+ description: 'Add a new tab/sheet to an existing spreadsheet',
268
+ inputSchema: {
269
+ account: accountEnum.describe('Google account alias'),
270
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
271
+ title: z.string().describe('Name for the new sheet/tab'),
272
+ rowCount: z.number().min(1).default(1000).optional()
273
+ .describe('Number of rows (default: 1000)'),
274
+ columnCount: z.number().min(1).default(26).optional()
275
+ .describe('Number of columns (default: 26)'),
276
+ },
277
+ }, async ({ account, spreadsheetId, title, rowCount, columnCount }) => {
278
+ try {
279
+ const auth = await getClient(account);
280
+ const sheets = google.sheets({ version: 'v4', auth });
281
+ const res = await sheets.spreadsheets.batchUpdate({
282
+ spreadsheetId,
283
+ requestBody: {
284
+ requests: [{
285
+ addSheet: {
286
+ properties: {
287
+ title,
288
+ gridProperties: {
289
+ rowCount: rowCount ?? 1000,
290
+ columnCount: columnCount ?? 26,
291
+ },
292
+ },
293
+ },
294
+ }],
295
+ },
296
+ });
297
+ const added = res.data.replies?.[0]?.addSheet?.properties;
298
+ return {
299
+ content: [{ type: 'text', text: JSON.stringify({
300
+ sheetId: added?.sheetId,
301
+ title: added?.title,
302
+ rowCount: added?.gridProperties?.rowCount,
303
+ columnCount: added?.gridProperties?.columnCount,
304
+ }, null, 2) }],
305
+ };
306
+ }
307
+ catch (error) {
308
+ return handleSheetsError(error, account);
309
+ }
310
+ });
311
+ server.registerTool('sheets_delete_sheet', {
312
+ description: 'Delete a tab/sheet from a spreadsheet by sheetId',
313
+ inputSchema: {
314
+ account: accountEnum.describe('Google account alias'),
315
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
316
+ sheetId: z.number().describe('Sheet ID (integer, from sheets_get)'),
317
+ },
318
+ }, async ({ account, spreadsheetId, sheetId }) => {
319
+ try {
320
+ const auth = await getClient(account);
321
+ const sheets = google.sheets({ version: 'v4', auth });
322
+ await sheets.spreadsheets.batchUpdate({
323
+ spreadsheetId,
324
+ requestBody: { requests: [{ deleteSheet: { sheetId } }] },
325
+ });
326
+ return {
327
+ content: [{ type: 'text', text: JSON.stringify({ deleted: true, sheetId }, null, 2) }],
328
+ };
329
+ }
330
+ catch (error) {
331
+ return handleSheetsError(error, account);
332
+ }
333
+ });
334
+ server.registerTool('sheets_duplicate_sheet', {
335
+ description: 'Duplicate a tab within the same spreadsheet',
336
+ inputSchema: {
337
+ account: accountEnum.describe('Google account alias'),
338
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
339
+ sourceSheetId: z.number().describe('Source sheet ID'),
340
+ newSheetName: z.string().optional().describe('Name for the duplicate (default: "Copy of <source>")'),
341
+ insertSheetIndex: z.number().optional().describe('Where to insert the duplicate (0-based)'),
342
+ },
343
+ }, async ({ account, spreadsheetId, sourceSheetId, newSheetName, insertSheetIndex }) => {
344
+ try {
345
+ const auth = await getClient(account);
346
+ const sheets = google.sheets({ version: 'v4', auth });
347
+ const res = await sheets.spreadsheets.batchUpdate({
348
+ spreadsheetId,
349
+ requestBody: {
350
+ requests: [{
351
+ duplicateSheet: {
352
+ sourceSheetId,
353
+ newSheetName,
354
+ insertSheetIndex,
355
+ },
356
+ }],
357
+ },
358
+ });
359
+ const props = res.data.replies?.[0]?.duplicateSheet?.properties;
360
+ return {
361
+ content: [{ type: 'text', text: JSON.stringify(props ?? {}, null, 2) }],
362
+ };
363
+ }
364
+ catch (error) {
365
+ return handleSheetsError(error, account);
366
+ }
367
+ });
368
+ server.registerTool('sheets_update_sheet_properties', {
369
+ description: 'Rename a tab, change tab color, freeze rows/columns, hide/show, or change grid dimensions',
370
+ inputSchema: {
371
+ account: accountEnum.describe('Google account alias'),
372
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
373
+ sheetId: z.number().describe('Sheet ID'),
374
+ title: z.string().optional().describe('New tab title'),
375
+ index: z.number().optional().describe('New position among tabs'),
376
+ hidden: z.boolean().optional().describe('Hide the tab from the UI'),
377
+ tabColor: rgbColorSchema.optional().describe('Tab color (RGB 0..1)'),
378
+ frozenRowCount: z.number().min(0).optional().describe('Number of frozen header rows'),
379
+ frozenColumnCount: z.number().min(0).optional().describe('Number of frozen header columns'),
380
+ rowCount: z.number().min(1).optional().describe('Total row count'),
381
+ columnCount: z.number().min(1).optional().describe('Total column count'),
382
+ },
383
+ }, async ({ account, spreadsheetId, sheetId, title, index, hidden, tabColor, frozenRowCount, frozenColumnCount, rowCount, columnCount }) => {
384
+ try {
385
+ const auth = await getClient(account);
386
+ const sheets = google.sheets({ version: 'v4', auth });
387
+ const properties = { sheetId };
388
+ const fields = [];
389
+ if (title !== undefined) {
390
+ properties.title = title;
391
+ fields.push('title');
392
+ }
393
+ if (index !== undefined) {
394
+ properties.index = index;
395
+ fields.push('index');
396
+ }
397
+ if (hidden !== undefined) {
398
+ properties.hidden = hidden;
399
+ fields.push('hidden');
400
+ }
401
+ if (tabColor) {
402
+ properties.tabColorStyle = { rgbColor: tabColor };
403
+ fields.push('tabColorStyle');
404
+ }
405
+ const grid = {};
406
+ const gridFields = [];
407
+ if (frozenRowCount !== undefined) {
408
+ grid.frozenRowCount = frozenRowCount;
409
+ gridFields.push('frozenRowCount');
410
+ }
411
+ if (frozenColumnCount !== undefined) {
412
+ grid.frozenColumnCount = frozenColumnCount;
413
+ gridFields.push('frozenColumnCount');
414
+ }
415
+ if (rowCount !== undefined) {
416
+ grid.rowCount = rowCount;
417
+ gridFields.push('rowCount');
418
+ }
419
+ if (columnCount !== undefined) {
420
+ grid.columnCount = columnCount;
421
+ gridFields.push('columnCount');
422
+ }
423
+ if (gridFields.length > 0) {
424
+ properties.gridProperties = grid;
425
+ for (const f of gridFields)
426
+ fields.push(`gridProperties.${f}`);
427
+ }
428
+ if (fields.length === 0) {
429
+ return {
430
+ content: [{ type: 'text', text: JSON.stringify({ error: 'No properties supplied' }, null, 2) }],
431
+ isError: true,
432
+ };
433
+ }
434
+ await sheets.spreadsheets.batchUpdate({
435
+ spreadsheetId,
436
+ requestBody: {
437
+ requests: [{
438
+ updateSheetProperties: {
439
+ properties,
440
+ fields: fields.join(','),
441
+ },
442
+ }],
443
+ },
444
+ });
445
+ return {
446
+ content: [{ type: 'text', text: JSON.stringify({ updated: true, sheetId, applied: fields }, null, 2) }],
447
+ };
448
+ }
449
+ catch (error) {
450
+ return handleSheetsError(error, account);
451
+ }
452
+ });
453
+ // ─── Cell formatting ───────────────────────────────────────────────────
454
+ server.registerTool('sheets_format_cells', {
455
+ description: 'Apply uniform formatting (colors, fonts, alignment, number format) to every cell in a range. Uses RepeatCell with a computed fields mask. For borders use sheets_update_borders.',
456
+ inputSchema: {
457
+ account: accountEnum.describe('Google account alias'),
458
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
459
+ range: gridRangeSchema.describe('Target range (GridRange, half-open indexes)'),
460
+ backgroundColor: rgbColorSchema.optional().describe('Cell background (RGB 0..1)'),
461
+ textFormat: z.object({
462
+ foregroundColor: rgbColorSchema.optional(),
463
+ bold: z.boolean().optional(),
464
+ italic: z.boolean().optional(),
465
+ underline: z.boolean().optional(),
466
+ strikethrough: z.boolean().optional(),
467
+ fontFamily: z.string().optional(),
468
+ fontSize: z.number().min(1).optional(),
469
+ }).optional(),
470
+ horizontalAlignment: z.enum(['LEFT', 'CENTER', 'RIGHT']).optional(),
471
+ verticalAlignment: z.enum(['TOP', 'MIDDLE', 'BOTTOM']).optional(),
472
+ wrapStrategy: z.enum(['OVERFLOW_CELL', 'LEGACY_WRAP', 'CLIP', 'WRAP']).optional(),
473
+ numberFormat: z.object({
474
+ type: z.enum(['TEXT', 'NUMBER', 'PERCENT', 'CURRENCY', 'DATE', 'TIME', 'DATE_TIME', 'SCIENTIFIC']),
475
+ pattern: z.string().optional().describe('Optional format string (e.g. "$#,##0.00")'),
476
+ }).optional(),
477
+ },
478
+ }, async ({ account, spreadsheetId, range, ...format }) => {
479
+ try {
480
+ const auth = await getClient(account);
481
+ const sheets = google.sheets({ version: 'v4', auth });
482
+ const built = buildCellFormat(format);
483
+ if (built.fields.length === 0) {
484
+ return {
485
+ content: [{ type: 'text', text: JSON.stringify({ error: 'No format properties supplied' }, null, 2) }],
486
+ isError: true,
487
+ };
488
+ }
489
+ await sheets.spreadsheets.batchUpdate({
490
+ spreadsheetId,
491
+ requestBody: {
492
+ requests: [{
493
+ repeatCell: {
494
+ range,
495
+ cell: { userEnteredFormat: built.format },
496
+ fields: built.fields,
497
+ },
498
+ }],
499
+ },
500
+ });
501
+ return {
502
+ content: [{ type: 'text', text: JSON.stringify({ formatted: true, applied: built.fields }, null, 2) }],
503
+ };
504
+ }
505
+ catch (error) {
506
+ return handleSheetsError(error, account);
507
+ }
508
+ });
509
+ server.registerTool('sheets_update_borders', {
510
+ description: 'Set borders on a range. Each border specifies style and optional color.',
511
+ inputSchema: {
512
+ account: accountEnum.describe('Google account alias'),
513
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
514
+ range: gridRangeSchema,
515
+ top: borderSchema.optional(),
516
+ bottom: borderSchema.optional(),
517
+ left: borderSchema.optional(),
518
+ right: borderSchema.optional(),
519
+ innerHorizontal: borderSchema.optional(),
520
+ innerVertical: borderSchema.optional(),
521
+ },
522
+ }, async ({ account, spreadsheetId, range, top, bottom, left, right, innerHorizontal, innerVertical }) => {
523
+ try {
524
+ const auth = await getClient(account);
525
+ const sheets = google.sheets({ version: 'v4', auth });
526
+ const borders = { range };
527
+ if (top)
528
+ borders.top = toBorder(top);
529
+ if (bottom)
530
+ borders.bottom = toBorder(bottom);
531
+ if (left)
532
+ borders.left = toBorder(left);
533
+ if (right)
534
+ borders.right = toBorder(right);
535
+ if (innerHorizontal)
536
+ borders.innerHorizontal = toBorder(innerHorizontal);
537
+ if (innerVertical)
538
+ borders.innerVertical = toBorder(innerVertical);
539
+ if (Object.keys(borders).length === 1) {
540
+ return {
541
+ content: [{ type: 'text', text: JSON.stringify({ error: 'At least one border edge must be supplied' }, null, 2) }],
542
+ isError: true,
543
+ };
544
+ }
545
+ await sheets.spreadsheets.batchUpdate({
546
+ spreadsheetId,
547
+ requestBody: { requests: [{ updateBorders: borders }] },
548
+ });
549
+ return {
550
+ content: [{ type: 'text', text: JSON.stringify({ borders_applied: true }, null, 2) }],
551
+ };
552
+ }
553
+ catch (error) {
554
+ return handleSheetsError(error, account);
555
+ }
556
+ });
557
+ server.registerTool('sheets_merge_cells', {
558
+ description: 'Merge a range of cells. MERGE_ALL collapses the range to one cell; MERGE_COLUMNS merges each column; MERGE_ROWS merges each row.',
559
+ inputSchema: {
560
+ account: accountEnum.describe('Google account alias'),
561
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
562
+ range: gridRangeSchema,
563
+ mergeType: z.enum(['MERGE_ALL', 'MERGE_COLUMNS', 'MERGE_ROWS']).default('MERGE_ALL').optional(),
564
+ },
565
+ }, async ({ account, spreadsheetId, range, mergeType }) => {
566
+ try {
567
+ const auth = await getClient(account);
568
+ const sheets = google.sheets({ version: 'v4', auth });
569
+ await sheets.spreadsheets.batchUpdate({
570
+ spreadsheetId,
571
+ requestBody: {
572
+ requests: [{ mergeCells: { range, mergeType: mergeType ?? 'MERGE_ALL' } }],
573
+ },
574
+ });
575
+ return {
576
+ content: [{ type: 'text', text: JSON.stringify({ merged: true }, null, 2) }],
577
+ };
578
+ }
579
+ catch (error) {
580
+ return handleSheetsError(error, account);
581
+ }
582
+ });
583
+ server.registerTool('sheets_unmerge_cells', {
584
+ description: 'Unmerge any merged cells overlapping the given range',
585
+ inputSchema: {
586
+ account: accountEnum.describe('Google account alias'),
587
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
588
+ range: gridRangeSchema,
589
+ },
590
+ }, async ({ account, spreadsheetId, range }) => {
591
+ try {
592
+ const auth = await getClient(account);
593
+ const sheets = google.sheets({ version: 'v4', auth });
594
+ await sheets.spreadsheets.batchUpdate({
595
+ spreadsheetId,
596
+ requestBody: { requests: [{ unmergeCells: { range } }] },
597
+ });
598
+ return {
599
+ content: [{ type: 'text', text: JSON.stringify({ unmerged: true }, null, 2) }],
600
+ };
601
+ }
602
+ catch (error) {
603
+ return handleSheetsError(error, account);
604
+ }
605
+ });
606
+ // ─── Conditional formatting ────────────────────────────────────────────
607
+ server.registerTool('sheets_add_conditional_format_rule', {
608
+ description: 'Add a conditional format rule. Specify either booleanRule (single trigger condition + format) or gradientRule (min/mid/max color stops).',
609
+ inputSchema: {
610
+ account: accountEnum.describe('Google account alias'),
611
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
612
+ ranges: z.array(gridRangeSchema).describe('Ranges the rule applies to'),
613
+ index: z.number().min(0).optional().describe('Where in the rule order to insert (0 = highest priority)'),
614
+ booleanRule: z.object({
615
+ conditionType: z.string().describe(BOOLEAN_CONDITION_TYPE_DESC),
616
+ conditionValues: z.array(z.string()).optional().describe('Values for the condition (e.g. ["100"] for NUMBER_GREATER, or ["=A1>10"] for CUSTOM_FORMULA)'),
617
+ format: z.object({
618
+ backgroundColor: rgbColorSchema.optional(),
619
+ textFormat: z.object({
620
+ foregroundColor: rgbColorSchema.optional(),
621
+ bold: z.boolean().optional(),
622
+ italic: z.boolean().optional(),
623
+ strikethrough: z.boolean().optional(),
624
+ underline: z.boolean().optional(),
625
+ }).optional(),
626
+ }).describe('Format to apply when condition is true. Conditional formatting only supports bold/italic/strikethrough/underline/foregroundColor/backgroundColor.'),
627
+ }).optional(),
628
+ gradientRule: z.object({
629
+ minpoint: gradientStopSchema,
630
+ midpoint: gradientStopSchema.optional(),
631
+ maxpoint: gradientStopSchema,
632
+ }).optional(),
633
+ },
634
+ }, async ({ account, spreadsheetId, ranges, index, booleanRule, gradientRule }) => {
635
+ try {
636
+ if (!booleanRule && !gradientRule) {
637
+ return {
638
+ content: [{ type: 'text', text: JSON.stringify({ error: 'Either booleanRule or gradientRule must be supplied' }, null, 2) }],
639
+ isError: true,
640
+ };
641
+ }
642
+ const auth = await getClient(account);
643
+ const sheets = google.sheets({ version: 'v4', auth });
644
+ const rule = { ranges };
645
+ if (booleanRule) {
646
+ const fmt = {};
647
+ if (booleanRule.format.backgroundColor)
648
+ fmt.backgroundColorStyle = { rgbColor: booleanRule.format.backgroundColor };
649
+ if (booleanRule.format.textFormat) {
650
+ const tf = {};
651
+ const t = booleanRule.format.textFormat;
652
+ if (t.foregroundColor)
653
+ tf.foregroundColorStyle = { rgbColor: t.foregroundColor };
654
+ if (t.bold !== undefined)
655
+ tf.bold = t.bold;
656
+ if (t.italic !== undefined)
657
+ tf.italic = t.italic;
658
+ if (t.strikethrough !== undefined)
659
+ tf.strikethrough = t.strikethrough;
660
+ if (t.underline !== undefined)
661
+ tf.underline = t.underline;
662
+ fmt.textFormat = tf;
663
+ }
664
+ rule.booleanRule = {
665
+ condition: {
666
+ type: booleanRule.conditionType,
667
+ values: (booleanRule.conditionValues ?? []).map(v => ({ userEnteredValue: v })),
668
+ },
669
+ format: fmt,
670
+ };
671
+ }
672
+ if (gradientRule) {
673
+ rule.gradientRule = {
674
+ minpoint: toGradientStop(gradientRule.minpoint),
675
+ midpoint: gradientRule.midpoint ? toGradientStop(gradientRule.midpoint) : undefined,
676
+ maxpoint: toGradientStop(gradientRule.maxpoint),
677
+ };
678
+ }
679
+ await sheets.spreadsheets.batchUpdate({
680
+ spreadsheetId,
681
+ requestBody: {
682
+ requests: [{
683
+ addConditionalFormatRule: { rule, index: index ?? 0 },
684
+ }],
685
+ },
686
+ });
687
+ return {
688
+ content: [{ type: 'text', text: JSON.stringify({ added: true }, null, 2) }],
689
+ };
690
+ }
691
+ catch (error) {
692
+ return handleSheetsError(error, account);
693
+ }
694
+ });
695
+ // ─── Filter / sort / find-replace ──────────────────────────────────────
696
+ server.registerTool('sheets_sort_range', {
697
+ description: 'Sort a range by one or more columns',
698
+ inputSchema: {
699
+ account: accountEnum.describe('Google account alias'),
700
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
701
+ range: gridRangeSchema,
702
+ sortSpecs: z.array(sortSpecSchema).describe('One spec per sort column (in priority order)'),
703
+ },
704
+ }, async ({ account, spreadsheetId, range, sortSpecs }) => {
705
+ try {
706
+ const auth = await getClient(account);
707
+ const sheets = google.sheets({ version: 'v4', auth });
708
+ await sheets.spreadsheets.batchUpdate({
709
+ spreadsheetId,
710
+ requestBody: {
711
+ requests: [{ sortRange: { range, sortSpecs } }],
712
+ },
713
+ });
714
+ return {
715
+ content: [{ type: 'text', text: JSON.stringify({ sorted: true }, null, 2) }],
716
+ };
717
+ }
718
+ catch (error) {
719
+ return handleSheetsError(error, account);
720
+ }
721
+ });
722
+ server.registerTool('sheets_set_basic_filter', {
723
+ description: 'Apply a basic filter to a range. Optionally supply sortSpecs to set the default sort, and filterSpecs to hide rows based on per-column criteria.',
724
+ inputSchema: {
725
+ account: accountEnum.describe('Google account alias'),
726
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
727
+ range: gridRangeSchema,
728
+ sortSpecs: z.array(sortSpecSchema).optional(),
729
+ filterSpecs: z.array(z.object({
730
+ columnIndex: z.number().min(0),
731
+ hiddenValues: z.array(z.string()).optional().describe('Values to hide in this column'),
732
+ conditionType: z.string().optional().describe(BOOLEAN_CONDITION_TYPE_DESC),
733
+ conditionValues: z.array(z.string()).optional(),
734
+ })).optional(),
735
+ },
736
+ }, async ({ account, spreadsheetId, range, sortSpecs, filterSpecs }) => {
737
+ try {
738
+ const auth = await getClient(account);
739
+ const sheets = google.sheets({ version: 'v4', auth });
740
+ const filter = { range };
741
+ if (sortSpecs)
742
+ filter.sortSpecs = sortSpecs;
743
+ if (filterSpecs) {
744
+ filter.filterSpecs = filterSpecs.map(s => {
745
+ const out = { columnIndex: s.columnIndex };
746
+ const criteria = {};
747
+ if (s.hiddenValues)
748
+ criteria.hiddenValues = s.hiddenValues;
749
+ if (s.conditionType) {
750
+ criteria.condition = {
751
+ type: s.conditionType,
752
+ values: (s.conditionValues ?? []).map(v => ({ userEnteredValue: v })),
753
+ };
754
+ }
755
+ out.filterCriteria = criteria;
756
+ return out;
757
+ });
758
+ }
759
+ await sheets.spreadsheets.batchUpdate({
760
+ spreadsheetId,
761
+ requestBody: { requests: [{ setBasicFilter: { filter } }] },
762
+ });
763
+ return {
764
+ content: [{ type: 'text', text: JSON.stringify({ filter_set: true }, null, 2) }],
765
+ };
766
+ }
767
+ catch (error) {
768
+ return handleSheetsError(error, account);
769
+ }
770
+ });
771
+ server.registerTool('sheets_clear_basic_filter', {
772
+ description: 'Remove the basic filter from a sheet',
773
+ inputSchema: {
774
+ account: accountEnum.describe('Google account alias'),
775
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
776
+ sheetId: z.number().describe('Sheet ID'),
777
+ },
778
+ }, async ({ account, spreadsheetId, sheetId }) => {
779
+ try {
780
+ const auth = await getClient(account);
781
+ const sheets = google.sheets({ version: 'v4', auth });
782
+ await sheets.spreadsheets.batchUpdate({
783
+ spreadsheetId,
784
+ requestBody: { requests: [{ clearBasicFilter: { sheetId } }] },
785
+ });
786
+ return {
787
+ content: [{ type: 'text', text: JSON.stringify({ cleared: true }, null, 2) }],
788
+ };
789
+ }
790
+ catch (error) {
791
+ return handleSheetsError(error, account);
792
+ }
793
+ });
794
+ server.registerTool('sheets_find_replace', {
795
+ description: 'Find and replace text across a range, a sheet, or all sheets. Supports case sensitivity, full-cell match, regex, and formula scope.',
796
+ inputSchema: {
797
+ account: accountEnum.describe('Google account alias'),
798
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
799
+ find: z.string().describe('Text to find'),
800
+ replacement: z.string().describe('Replacement text'),
801
+ scope: z.enum(['allSheets', 'sheet', 'range']).describe('Search scope'),
802
+ sheetId: z.number().optional().describe('Required when scope=sheet'),
803
+ range: gridRangeSchema.optional().describe('Required when scope=range'),
804
+ matchCase: z.boolean().optional(),
805
+ matchEntireCell: z.boolean().optional(),
806
+ searchByRegex: z.boolean().optional(),
807
+ includeFormulas: z.boolean().optional(),
808
+ },
809
+ }, async ({ account, spreadsheetId, find, replacement, scope, sheetId, range, matchCase, matchEntireCell, searchByRegex, includeFormulas }) => {
810
+ try {
811
+ const auth = await getClient(account);
812
+ const sheets = google.sheets({ version: 'v4', auth });
813
+ const findReplace = {
814
+ find,
815
+ replacement,
816
+ matchCase: matchCase ?? false,
817
+ matchEntireCell: matchEntireCell ?? false,
818
+ searchByRegex: searchByRegex ?? false,
819
+ includeFormulas: includeFormulas ?? false,
820
+ };
821
+ if (scope === 'allSheets')
822
+ findReplace.allSheets = true;
823
+ else if (scope === 'sheet') {
824
+ if (sheetId === undefined) {
825
+ return { content: [{ type: 'text', text: JSON.stringify({ error: 'sheetId required when scope=sheet' }) }], isError: true };
826
+ }
827
+ findReplace.sheetId = sheetId;
828
+ }
829
+ else {
830
+ if (!range) {
831
+ return { content: [{ type: 'text', text: JSON.stringify({ error: 'range required when scope=range' }) }], isError: true };
832
+ }
833
+ findReplace.range = range;
834
+ }
835
+ const res = await sheets.spreadsheets.batchUpdate({
836
+ spreadsheetId,
837
+ requestBody: { requests: [{ findReplace }] },
838
+ });
839
+ const reply = res.data.replies?.[0]?.findReplace;
840
+ return {
841
+ content: [{ type: 'text', text: JSON.stringify({
842
+ valuesChanged: reply?.valuesChanged ?? 0,
843
+ occurrencesChanged: reply?.occurrencesChanged ?? 0,
844
+ rowsChanged: reply?.rowsChanged ?? 0,
845
+ sheetsChanged: reply?.sheetsChanged ?? 0,
846
+ formulasChanged: reply?.formulasChanged ?? 0,
847
+ }, null, 2) }],
848
+ };
849
+ }
850
+ catch (error) {
851
+ return handleSheetsError(error, account);
852
+ }
853
+ });
854
+ // ─── Dimensions ────────────────────────────────────────────────────────
855
+ server.registerTool('sheets_auto_resize_dimensions', {
856
+ description: 'Auto-resize rows or columns to fit content',
857
+ inputSchema: {
858
+ account: accountEnum.describe('Google account alias'),
859
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
860
+ sheetId: z.number().describe('Sheet ID'),
861
+ dimension: z.enum(['ROWS', 'COLUMNS']).describe('Which dimension to resize'),
862
+ startIndex: z.number().min(0).optional(),
863
+ endIndex: z.number().min(1).optional(),
864
+ },
865
+ }, async ({ account, spreadsheetId, sheetId, dimension, startIndex, endIndex }) => {
866
+ try {
867
+ const auth = await getClient(account);
868
+ const sheets = google.sheets({ version: 'v4', auth });
869
+ const dimensions = { sheetId, dimension };
870
+ if (startIndex !== undefined)
871
+ dimensions.startIndex = startIndex;
872
+ if (endIndex !== undefined)
873
+ dimensions.endIndex = endIndex;
874
+ await sheets.spreadsheets.batchUpdate({
875
+ spreadsheetId,
876
+ requestBody: { requests: [{ autoResizeDimensions: { dimensions } }] },
877
+ });
878
+ return {
879
+ content: [{ type: 'text', text: JSON.stringify({ resized: true }, null, 2) }],
880
+ };
881
+ }
882
+ catch (error) {
883
+ return handleSheetsError(error, account);
884
+ }
885
+ });
886
+ server.registerTool('sheets_insert_dimension', {
887
+ description: 'Insert rows or columns at a position. inheritFromBefore=true copies properties from the row/column before the insertion point.',
888
+ inputSchema: {
889
+ account: accountEnum.describe('Google account alias'),
890
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
891
+ sheetId: z.number().describe('Sheet ID'),
892
+ dimension: z.enum(['ROWS', 'COLUMNS']),
893
+ startIndex: z.number().min(0).describe('0-based, inclusive'),
894
+ endIndex: z.number().min(1).describe('0-based, exclusive'),
895
+ inheritFromBefore: z.boolean().optional(),
896
+ },
897
+ }, async ({ account, spreadsheetId, sheetId, dimension, startIndex, endIndex, inheritFromBefore }) => {
898
+ try {
899
+ const auth = await getClient(account);
900
+ const sheets = google.sheets({ version: 'v4', auth });
901
+ await sheets.spreadsheets.batchUpdate({
902
+ spreadsheetId,
903
+ requestBody: {
904
+ requests: [{
905
+ insertDimension: {
906
+ range: { sheetId, dimension, startIndex, endIndex },
907
+ inheritFromBefore: inheritFromBefore ?? false,
908
+ },
909
+ }],
910
+ },
911
+ });
912
+ return {
913
+ content: [{ type: 'text', text: JSON.stringify({ inserted: true }, null, 2) }],
914
+ };
915
+ }
916
+ catch (error) {
917
+ return handleSheetsError(error, account);
918
+ }
919
+ });
920
+ server.registerTool('sheets_delete_dimension', {
921
+ description: 'Delete rows or columns from a sheet',
922
+ inputSchema: {
923
+ account: accountEnum.describe('Google account alias'),
924
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
925
+ sheetId: z.number().describe('Sheet ID'),
926
+ dimension: z.enum(['ROWS', 'COLUMNS']),
927
+ startIndex: z.number().min(0),
928
+ endIndex: z.number().min(1),
929
+ },
930
+ }, async ({ account, spreadsheetId, sheetId, dimension, startIndex, endIndex }) => {
931
+ try {
932
+ const auth = await getClient(account);
933
+ const sheets = google.sheets({ version: 'v4', auth });
934
+ await sheets.spreadsheets.batchUpdate({
935
+ spreadsheetId,
936
+ requestBody: {
937
+ requests: [{
938
+ deleteDimension: {
939
+ range: { sheetId, dimension, startIndex, endIndex },
940
+ },
941
+ }],
942
+ },
943
+ });
944
+ return {
945
+ content: [{ type: 'text', text: JSON.stringify({ deleted: true }, null, 2) }],
946
+ };
947
+ }
948
+ catch (error) {
949
+ return handleSheetsError(error, account);
950
+ }
951
+ });
952
+ // ─── Data validation ───────────────────────────────────────────────────
953
+ server.registerTool('sheets_set_data_validation', {
954
+ description: 'Apply a data validation rule (dropdown, checkbox, numeric range, etc.) to a range. Pass conditionType=null to clear validation.',
955
+ inputSchema: {
956
+ account: accountEnum.describe('Google account alias'),
957
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
958
+ range: gridRangeSchema,
959
+ conditionType: z.string().describe(BOOLEAN_CONDITION_TYPE_DESC),
960
+ conditionValues: z.array(z.string()).optional().describe('Values per condition type (list items for ONE_OF_LIST, range A1 for ONE_OF_RANGE, etc.)'),
961
+ inputMessage: z.string().optional().describe('Tooltip shown when cell is selected'),
962
+ strict: z.boolean().optional().describe('Reject invalid input (default: false = warning only)'),
963
+ showCustomUi: z.boolean().optional().describe('Show dropdown UI for list-type validations'),
964
+ },
965
+ }, async ({ account, spreadsheetId, range, conditionType, conditionValues, inputMessage, strict, showCustomUi }) => {
966
+ try {
967
+ const auth = await getClient(account);
968
+ const sheets = google.sheets({ version: 'v4', auth });
969
+ const rule = {
970
+ condition: {
971
+ type: conditionType,
972
+ values: (conditionValues ?? []).map(v => ({ userEnteredValue: v })),
973
+ },
974
+ strict: strict ?? false,
975
+ };
976
+ if (inputMessage !== undefined)
977
+ rule.inputMessage = inputMessage;
978
+ if (showCustomUi !== undefined)
979
+ rule.showCustomUi = showCustomUi;
980
+ await sheets.spreadsheets.batchUpdate({
981
+ spreadsheetId,
982
+ requestBody: {
983
+ requests: [{ setDataValidation: { range, rule } }],
984
+ },
985
+ });
986
+ return {
987
+ content: [{ type: 'text', text: JSON.stringify({ validation_set: true }, null, 2) }],
988
+ };
989
+ }
990
+ catch (error) {
991
+ return handleSheetsError(error, account);
992
+ }
993
+ });
994
+ // ─── Named ranges ──────────────────────────────────────────────────────
995
+ server.registerTool('sheets_add_named_range', {
996
+ description: 'Define a named range so formulas can reference it by name',
997
+ inputSchema: {
998
+ account: accountEnum.describe('Google account alias'),
999
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
1000
+ name: z.string().describe('Named range identifier (no spaces)'),
1001
+ range: gridRangeSchema,
1002
+ },
1003
+ }, async ({ account, spreadsheetId, name, range }) => {
1004
+ try {
1005
+ const auth = await getClient(account);
1006
+ const sheets = google.sheets({ version: 'v4', auth });
1007
+ const res = await sheets.spreadsheets.batchUpdate({
1008
+ spreadsheetId,
1009
+ requestBody: {
1010
+ requests: [{ addNamedRange: { namedRange: { name, range } } }],
1011
+ },
1012
+ });
1013
+ const added = res.data.replies?.[0]?.addNamedRange?.namedRange;
1014
+ return {
1015
+ content: [{ type: 'text', text: JSON.stringify(added ?? {}, null, 2) }],
1016
+ };
1017
+ }
1018
+ catch (error) {
1019
+ return handleSheetsError(error, account);
1020
+ }
1021
+ });
1022
+ server.registerTool('sheets_delete_named_range', {
1023
+ description: 'Delete a named range by its ID',
1024
+ inputSchema: {
1025
+ account: accountEnum.describe('Google account alias'),
1026
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
1027
+ namedRangeId: z.string().describe('Named range ID (from sheets_get response)'),
1028
+ },
1029
+ }, async ({ account, spreadsheetId, namedRangeId }) => {
1030
+ try {
1031
+ const auth = await getClient(account);
1032
+ const sheets = google.sheets({ version: 'v4', auth });
1033
+ await sheets.spreadsheets.batchUpdate({
1034
+ spreadsheetId,
1035
+ requestBody: { requests: [{ deleteNamedRange: { namedRangeId } }] },
1036
+ });
1037
+ return {
1038
+ content: [{ type: 'text', text: JSON.stringify({ deleted: true, namedRangeId }, null, 2) }],
1039
+ };
1040
+ }
1041
+ catch (error) {
1042
+ return handleSheetsError(error, account);
1043
+ }
1044
+ });
1045
+ // ─── Values: batchClear + generic batchUpdate ──────────────────────────
1046
+ server.registerTool('sheets_batch_clear', {
1047
+ description: 'Clear values from multiple ranges in one request',
1048
+ inputSchema: {
1049
+ account: accountEnum.describe('Google account alias'),
1050
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
1051
+ ranges: z.array(z.string()).describe('Array of A1 notation ranges'),
1052
+ },
1053
+ }, async ({ account, spreadsheetId, ranges }) => {
1054
+ try {
1055
+ const auth = await getClient(account);
1056
+ const sheets = google.sheets({ version: 'v4', auth });
1057
+ const res = await sheets.spreadsheets.values.batchClear({
1058
+ spreadsheetId,
1059
+ requestBody: { ranges },
1060
+ });
1061
+ return {
1062
+ content: [{ type: 'text', text: JSON.stringify({
1063
+ clearedRanges: res.data.clearedRanges ?? [],
1064
+ }, null, 2) }],
1065
+ };
1066
+ }
1067
+ catch (error) {
1068
+ return handleSheetsError(error, account);
1069
+ }
1070
+ });
1071
+ server.registerTool('sheets_batch_update', {
1072
+ description: 'Generic spreadsheets.batchUpdate pass-through. Accepts the full Request union (~70 types). Use this for advanced operations not covered by a dedicated tool. See https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets/request',
1073
+ inputSchema: {
1074
+ account: accountEnum.describe('Google account alias'),
1075
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
1076
+ requests: z.array(z.record(z.string(), z.any())).describe('Array of Request objects. Each object has exactly one key (the request type) like {repeatCell: {...}}, {addChart: {...}}, {updateBanding: {...}}, etc.'),
1077
+ includeSpreadsheetInResponse: z.boolean().optional(),
1078
+ responseRanges: z.array(z.string()).optional(),
1079
+ responseIncludeGridData: z.boolean().optional(),
1080
+ },
1081
+ }, async ({ account, spreadsheetId, requests, includeSpreadsheetInResponse, responseRanges, responseIncludeGridData }) => {
1082
+ try {
1083
+ const auth = await getClient(account);
1084
+ const sheets = google.sheets({ version: 'v4', auth });
1085
+ const res = await sheets.spreadsheets.batchUpdate({
1086
+ spreadsheetId,
1087
+ requestBody: {
1088
+ requests: requests,
1089
+ includeSpreadsheetInResponse,
1090
+ responseRanges,
1091
+ responseIncludeGridData,
1092
+ },
1093
+ });
1094
+ return {
1095
+ content: [{ type: 'text', text: JSON.stringify({
1096
+ replies: res.data.replies ?? [],
1097
+ }, null, 2) }],
1098
+ };
1099
+ }
1100
+ catch (error) {
1101
+ return handleSheetsError(error, account);
1102
+ }
1103
+ });
1104
+ }
1105
+ // ─── Shared schemas & helpers ────────────────────────────────────────────
1106
+ const rgbColorSchema = z.object({
1107
+ red: z.number().min(0).max(1).optional(),
1108
+ green: z.number().min(0).max(1).optional(),
1109
+ blue: z.number().min(0).max(1).optional(),
1110
+ alpha: z.number().min(0).max(1).optional(),
1111
+ });
1112
+ const sortSpecSchema = z.object({
1113
+ dimensionIndex: z.number().min(0).describe('Column index within the range (0-based)'),
1114
+ sortOrder: z.enum(['ASCENDING', 'DESCENDING']).default('ASCENDING'),
1115
+ });
1116
+ // BooleanCondition.type — kept open (z.string) because Google's enum surface is large and may grow.
1117
+ const BOOLEAN_CONDITION_TYPE_DESC = 'BooleanCondition.type. Common values: NUMBER_GREATER, NUMBER_BETWEEN, TEXT_CONTAINS, TEXT_EQ, DATE_BEFORE, BLANK, NOT_BLANK, CUSTOM_FORMULA, BOOLEAN, ONE_OF_LIST, ONE_OF_RANGE.';
1118
+ const gridRangeSchema = z.object({
1119
+ sheetId: z.number().describe('Sheet ID'),
1120
+ startRowIndex: z.number().min(0).optional().describe('0-based, inclusive. Omit = from row 0.'),
1121
+ endRowIndex: z.number().min(0).optional().describe('0-based, exclusive. Omit = to last row.'),
1122
+ startColumnIndex: z.number().min(0).optional(),
1123
+ endColumnIndex: z.number().min(0).optional(),
1124
+ });
1125
+ const borderSchema = z.object({
1126
+ style: z.enum(['DOTTED', 'DASHED', 'SOLID', 'SOLID_MEDIUM', 'SOLID_THICK', 'NONE', 'DOUBLE']),
1127
+ color: rgbColorSchema.optional(),
1128
+ });
1129
+ const gradientStopSchema = z.object({
1130
+ type: z.enum(['MIN', 'MAX', 'NUMBER', 'PERCENT', 'PERCENTILE']),
1131
+ value: z.string().optional().describe('Required when type is NUMBER/PERCENT/PERCENTILE'),
1132
+ color: rgbColorSchema,
1133
+ });
1134
+ function toBorder(b) {
1135
+ const out = { style: b.style };
1136
+ if (b.color)
1137
+ out.colorStyle = { rgbColor: b.color };
1138
+ return out;
1139
+ }
1140
+ function toGradientStop(s) {
1141
+ const out = { type: s.type, colorStyle: { rgbColor: s.color } };
1142
+ if (s.value !== undefined)
1143
+ out.value = s.value;
1144
+ return out;
1145
+ }
1146
+ export function buildCellFormat(input) {
1147
+ const format = {};
1148
+ const fields = [];
1149
+ if (input.backgroundColor) {
1150
+ format.backgroundColorStyle = { rgbColor: input.backgroundColor };
1151
+ fields.push('userEnteredFormat.backgroundColorStyle');
1152
+ }
1153
+ if (input.textFormat) {
1154
+ const tf = {};
1155
+ const t = input.textFormat;
1156
+ if (t.foregroundColor) {
1157
+ tf.foregroundColorStyle = { rgbColor: t.foregroundColor };
1158
+ fields.push('userEnteredFormat.textFormat.foregroundColorStyle');
1159
+ }
1160
+ if (t.bold !== undefined) {
1161
+ tf.bold = t.bold;
1162
+ fields.push('userEnteredFormat.textFormat.bold');
1163
+ }
1164
+ if (t.italic !== undefined) {
1165
+ tf.italic = t.italic;
1166
+ fields.push('userEnteredFormat.textFormat.italic');
1167
+ }
1168
+ if (t.underline !== undefined) {
1169
+ tf.underline = t.underline;
1170
+ fields.push('userEnteredFormat.textFormat.underline');
1171
+ }
1172
+ if (t.strikethrough !== undefined) {
1173
+ tf.strikethrough = t.strikethrough;
1174
+ fields.push('userEnteredFormat.textFormat.strikethrough');
1175
+ }
1176
+ if (t.fontFamily) {
1177
+ tf.fontFamily = t.fontFamily;
1178
+ fields.push('userEnteredFormat.textFormat.fontFamily');
1179
+ }
1180
+ if (t.fontSize !== undefined) {
1181
+ tf.fontSize = t.fontSize;
1182
+ fields.push('userEnteredFormat.textFormat.fontSize');
1183
+ }
1184
+ if (Object.keys(tf).length > 0)
1185
+ format.textFormat = tf;
1186
+ }
1187
+ if (input.horizontalAlignment) {
1188
+ format.horizontalAlignment = input.horizontalAlignment;
1189
+ fields.push('userEnteredFormat.horizontalAlignment');
1190
+ }
1191
+ if (input.verticalAlignment) {
1192
+ format.verticalAlignment = input.verticalAlignment;
1193
+ fields.push('userEnteredFormat.verticalAlignment');
1194
+ }
1195
+ if (input.wrapStrategy) {
1196
+ format.wrapStrategy = input.wrapStrategy;
1197
+ fields.push('userEnteredFormat.wrapStrategy');
1198
+ }
1199
+ if (input.numberFormat) {
1200
+ format.numberFormat = { type: input.numberFormat.type, pattern: input.numberFormat.pattern };
1201
+ fields.push('userEnteredFormat.numberFormat');
1202
+ }
1203
+ return { format, fields: fields.join(',') };
1204
+ }
1205
+ function handleSheetsError(error, account) {
1206
+ return handleGoogleApiError(error, account);
1207
+ }