@brainwavesio/google-docs-mcp 1.0.0 → 1.1.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.
@@ -1,427 +0,0 @@
1
- // src/googleSheetsApiHelpers.ts
2
- import { google, sheets_v4 } from 'googleapis';
3
- import { UserError } from 'fastmcp';
4
-
5
- type Sheets = sheets_v4.Sheets; // Alias for convenience
6
-
7
- // --- Core Helper Functions ---
8
-
9
- /**
10
- * Converts A1 notation to row/column indices (0-based)
11
- * Example: "A1" -> {row: 0, col: 0}, "B2" -> {row: 1, col: 1}
12
- */
13
- export function a1ToRowCol(a1: string): { row: number; col: number } {
14
- const match = a1.match(/^([A-Z]+)(\d+)$/i);
15
- if (!match) {
16
- throw new UserError(`Invalid A1 notation: ${a1}. Expected format like "A1" or "B2"`);
17
- }
18
-
19
- const colStr = match[1].toUpperCase();
20
- const row = parseInt(match[2], 10) - 1; // Convert to 0-based
21
-
22
- let col = 0;
23
- for (let i = 0; i < colStr.length; i++) {
24
- col = col * 26 + (colStr.charCodeAt(i) - 64);
25
- }
26
- col -= 1; // Convert to 0-based
27
-
28
- return { row, col };
29
- }
30
-
31
- /**
32
- * Converts row/column indices (0-based) to A1 notation
33
- * Example: {row: 0, col: 0} -> "A1", {row: 1, col: 1} -> "B2"
34
- */
35
- export function rowColToA1(row: number, col: number): string {
36
- if (row < 0 || col < 0) {
37
- throw new UserError(`Row and column indices must be non-negative. Got row: ${row}, col: ${col}`);
38
- }
39
-
40
- let colStr = '';
41
- let colNum = col + 1; // Convert to 1-based for calculation
42
- while (colNum > 0) {
43
- colNum -= 1;
44
- colStr = String.fromCharCode(65 + (colNum % 26)) + colStr;
45
- colNum = Math.floor(colNum / 26);
46
- }
47
-
48
- return `${colStr}${row + 1}`;
49
- }
50
-
51
- /**
52
- * Validates and normalizes a range string
53
- * Examples: "A1" -> "Sheet1!A1", "A1:B2" -> "Sheet1!A1:B2"
54
- */
55
- export function normalizeRange(range: string, sheetName?: string): string {
56
- // If range already contains '!', assume it's already normalized
57
- if (range.includes('!')) {
58
- return range;
59
- }
60
-
61
- // If sheetName is provided, prepend it
62
- if (sheetName) {
63
- return `${sheetName}!${range}`;
64
- }
65
-
66
- // Default to Sheet1 if no sheet name provided
67
- return `Sheet1!${range}`;
68
- }
69
-
70
- /**
71
- * Reads values from a spreadsheet range
72
- */
73
- export async function readRange(
74
- sheets: Sheets,
75
- spreadsheetId: string,
76
- range: string
77
- ): Promise<sheets_v4.Schema$ValueRange> {
78
- try {
79
- const response = await sheets.spreadsheets.values.get({
80
- spreadsheetId,
81
- range,
82
- });
83
- return response.data;
84
- } catch (error: any) {
85
- if (error.code === 404) {
86
- throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
87
- }
88
- if (error.code === 403) {
89
- throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have read access.`);
90
- }
91
- throw new UserError(`Failed to read range: ${error.message || 'Unknown error'}`);
92
- }
93
- }
94
-
95
- /**
96
- * Writes values to a spreadsheet range
97
- */
98
- export async function writeRange(
99
- sheets: Sheets,
100
- spreadsheetId: string,
101
- range: string,
102
- values: any[][],
103
- valueInputOption: 'RAW' | 'USER_ENTERED' = 'USER_ENTERED'
104
- ): Promise<sheets_v4.Schema$UpdateValuesResponse> {
105
- try {
106
- const response = await sheets.spreadsheets.values.update({
107
- spreadsheetId,
108
- range,
109
- valueInputOption,
110
- requestBody: {
111
- values,
112
- },
113
- });
114
- return response.data;
115
- } catch (error: any) {
116
- if (error.code === 404) {
117
- throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
118
- }
119
- if (error.code === 403) {
120
- throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
121
- }
122
- throw new UserError(`Failed to write range: ${error.message || 'Unknown error'}`);
123
- }
124
- }
125
-
126
- /**
127
- * Appends values to the end of a sheet
128
- */
129
- export async function appendValues(
130
- sheets: Sheets,
131
- spreadsheetId: string,
132
- range: string,
133
- values: any[][],
134
- valueInputOption: 'RAW' | 'USER_ENTERED' = 'USER_ENTERED'
135
- ): Promise<sheets_v4.Schema$AppendValuesResponse> {
136
- try {
137
- const response = await sheets.spreadsheets.values.append({
138
- spreadsheetId,
139
- range,
140
- valueInputOption,
141
- insertDataOption: 'INSERT_ROWS',
142
- requestBody: {
143
- values,
144
- },
145
- });
146
- return response.data;
147
- } catch (error: any) {
148
- if (error.code === 404) {
149
- throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
150
- }
151
- if (error.code === 403) {
152
- throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
153
- }
154
- throw new UserError(`Failed to append values: ${error.message || 'Unknown error'}`);
155
- }
156
- }
157
-
158
- /**
159
- * Clears values from a range
160
- */
161
- export async function clearRange(
162
- sheets: Sheets,
163
- spreadsheetId: string,
164
- range: string
165
- ): Promise<sheets_v4.Schema$ClearValuesResponse> {
166
- try {
167
- const response = await sheets.spreadsheets.values.clear({
168
- spreadsheetId,
169
- range,
170
- });
171
- return response.data;
172
- } catch (error: any) {
173
- if (error.code === 404) {
174
- throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
175
- }
176
- if (error.code === 403) {
177
- throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
178
- }
179
- throw new UserError(`Failed to clear range: ${error.message || 'Unknown error'}`);
180
- }
181
- }
182
-
183
- /**
184
- * Gets spreadsheet metadata including sheet information
185
- */
186
- export async function getSpreadsheetMetadata(
187
- sheets: Sheets,
188
- spreadsheetId: string
189
- ): Promise<sheets_v4.Schema$Spreadsheet> {
190
- try {
191
- const response = await sheets.spreadsheets.get({
192
- spreadsheetId,
193
- includeGridData: false,
194
- });
195
- return response.data;
196
- } catch (error: any) {
197
- if (error.code === 404) {
198
- throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
199
- }
200
- if (error.code === 403) {
201
- throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have read access.`);
202
- }
203
- throw new UserError(`Failed to get spreadsheet metadata: ${error.message || 'Unknown error'}`);
204
- }
205
- }
206
-
207
- /**
208
- * Creates a new sheet/tab in a spreadsheet
209
- */
210
- export async function addSheet(
211
- sheets: Sheets,
212
- spreadsheetId: string,
213
- sheetTitle: string
214
- ): Promise<sheets_v4.Schema$BatchUpdateSpreadsheetResponse> {
215
- try {
216
- const response = await sheets.spreadsheets.batchUpdate({
217
- spreadsheetId,
218
- requestBody: {
219
- requests: [
220
- {
221
- addSheet: {
222
- properties: {
223
- title: sheetTitle,
224
- },
225
- },
226
- },
227
- ],
228
- },
229
- });
230
- return response.data;
231
- } catch (error: any) {
232
- if (error.code === 404) {
233
- throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
234
- }
235
- if (error.code === 403) {
236
- throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
237
- }
238
- throw new UserError(`Failed to add sheet: ${error.message || 'Unknown error'}`);
239
- }
240
- }
241
-
242
- /**
243
- * Parses A1 notation range to extract sheet name and cell range
244
- * Returns {sheetName, a1Range} where a1Range is just the cell part (e.g., "A1:B2")
245
- */
246
- function parseRange(range: string): { sheetName: string | null; a1Range: string } {
247
- if (range.includes('!')) {
248
- const parts = range.split('!');
249
- return {
250
- sheetName: parts[0].replace(/^'|'$/g, ''), // Remove quotes if present
251
- a1Range: parts[1],
252
- };
253
- }
254
- return {
255
- sheetName: null,
256
- a1Range: range,
257
- };
258
- }
259
-
260
- /**
261
- * Formats cells in a range
262
- * Note: This function requires the sheetId. For simplicity, we'll get it from the spreadsheet metadata.
263
- */
264
- export async function formatCells(
265
- sheets: Sheets,
266
- spreadsheetId: string,
267
- range: string,
268
- format: {
269
- backgroundColor?: { red: number; green: number; blue: number };
270
- textFormat?: {
271
- foregroundColor?: { red: number; green: number; blue: number };
272
- fontSize?: number;
273
- bold?: boolean;
274
- italic?: boolean;
275
- };
276
- horizontalAlignment?: 'LEFT' | 'CENTER' | 'RIGHT';
277
- verticalAlignment?: 'TOP' | 'MIDDLE' | 'BOTTOM';
278
- }
279
- ): Promise<sheets_v4.Schema$BatchUpdateSpreadsheetResponse> {
280
- try {
281
- // Parse the range to get sheet name and cell range
282
- const { sheetName, a1Range } = parseRange(range);
283
-
284
- // Get spreadsheet metadata to find sheetId
285
- const metadata = await getSpreadsheetMetadata(sheets, spreadsheetId);
286
- let sheetId: number | undefined;
287
-
288
- if (sheetName) {
289
- // Find the sheet by name
290
- const sheet = metadata.sheets?.find(s => s.properties?.title === sheetName);
291
- if (!sheet || !sheet.properties?.sheetId) {
292
- throw new UserError(`Sheet "${sheetName}" not found in spreadsheet.`);
293
- }
294
- sheetId = sheet.properties.sheetId;
295
- } else {
296
- // Use the first sheet
297
- const firstSheet = metadata.sheets?.[0];
298
- if (!firstSheet?.properties?.sheetId) {
299
- throw new UserError('Spreadsheet has no sheets.');
300
- }
301
- sheetId = firstSheet.properties.sheetId;
302
- }
303
-
304
- if (sheetId === undefined) {
305
- throw new UserError('Could not determine sheet ID.');
306
- }
307
-
308
- // Parse A1 range to get row/column indices
309
- const rangeMatch = a1Range.match(/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i);
310
- if (!rangeMatch) {
311
- throw new UserError(`Invalid range format: ${a1Range}. Expected format like "A1" or "A1:B2"`);
312
- }
313
-
314
- const startCol = rangeMatch[1].toUpperCase();
315
- const startRow = parseInt(rangeMatch[2], 10) - 1; // Convert to 0-based
316
- const endCol = rangeMatch[3] ? rangeMatch[3].toUpperCase() : startCol;
317
- const endRow = rangeMatch[4] ? parseInt(rangeMatch[4], 10) - 1 : startRow; // Convert to 0-based
318
-
319
- // Convert column letters to 0-based indices
320
- function colToIndex(col: string): number {
321
- let index = 0;
322
- for (let i = 0; i < col.length; i++) {
323
- index = index * 26 + (col.charCodeAt(i) - 64);
324
- }
325
- return index - 1;
326
- }
327
-
328
- const startColIndex = colToIndex(startCol);
329
- const endColIndex = colToIndex(endCol);
330
-
331
- const userEnteredFormat: sheets_v4.Schema$CellFormat = {};
332
-
333
- if (format.backgroundColor) {
334
- userEnteredFormat.backgroundColor = {
335
- red: format.backgroundColor.red,
336
- green: format.backgroundColor.green,
337
- blue: format.backgroundColor.blue,
338
- alpha: 1,
339
- };
340
- }
341
-
342
- if (format.textFormat) {
343
- userEnteredFormat.textFormat = {};
344
- if (format.textFormat.foregroundColor) {
345
- userEnteredFormat.textFormat.foregroundColor = {
346
- red: format.textFormat.foregroundColor.red,
347
- green: format.textFormat.foregroundColor.green,
348
- blue: format.textFormat.foregroundColor.blue,
349
- alpha: 1,
350
- };
351
- }
352
- if (format.textFormat.fontSize !== undefined) {
353
- userEnteredFormat.textFormat.fontSize = format.textFormat.fontSize;
354
- }
355
- if (format.textFormat.bold !== undefined) {
356
- userEnteredFormat.textFormat.bold = format.textFormat.bold;
357
- }
358
- if (format.textFormat.italic !== undefined) {
359
- userEnteredFormat.textFormat.italic = format.textFormat.italic;
360
- }
361
- }
362
-
363
- if (format.horizontalAlignment) {
364
- userEnteredFormat.horizontalAlignment = format.horizontalAlignment;
365
- }
366
-
367
- if (format.verticalAlignment) {
368
- userEnteredFormat.verticalAlignment = format.verticalAlignment;
369
- }
370
-
371
- const response = await sheets.spreadsheets.batchUpdate({
372
- spreadsheetId,
373
- requestBody: {
374
- requests: [
375
- {
376
- repeatCell: {
377
- range: {
378
- sheetId: sheetId,
379
- startRowIndex: startRow,
380
- endRowIndex: endRow + 1, // endRowIndex is exclusive
381
- startColumnIndex: startColIndex,
382
- endColumnIndex: endColIndex + 1, // endColumnIndex is exclusive
383
- },
384
- cell: {
385
- userEnteredFormat,
386
- },
387
- fields: 'userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment)',
388
- },
389
- },
390
- ],
391
- },
392
- });
393
-
394
- return response.data;
395
- } catch (error: any) {
396
- if (error.code === 404) {
397
- throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
398
- }
399
- if (error.code === 403) {
400
- throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
401
- }
402
- if (error instanceof UserError) throw error;
403
- throw new UserError(`Failed to format cells: ${error.message || 'Unknown error'}`);
404
- }
405
- }
406
-
407
- /**
408
- * Helper to convert hex color to RGB (0-1 range)
409
- */
410
- export function hexToRgb(hex: string): { red: number; green: number; blue: number } | null {
411
- if (!hex) return null;
412
- let hexClean = hex.startsWith('#') ? hex.slice(1) : hex;
413
-
414
- if (hexClean.length === 3) {
415
- hexClean = hexClean[0] + hexClean[0] + hexClean[1] + hexClean[1] + hexClean[2] + hexClean[2];
416
- }
417
- if (hexClean.length !== 6) return null;
418
- const bigint = parseInt(hexClean, 16);
419
- if (isNaN(bigint)) return null;
420
-
421
- return {
422
- red: ((bigint >> 16) & 255) / 255,
423
- green: ((bigint >> 8) & 255) / 255,
424
- blue: (bigint & 255) / 255,
425
- };
426
- }
427
-