@brainwavesio/google-docs-mcp 1.0.0 → 1.0.1

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,356 @@
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) {
58
+ try {
59
+ const response = await sheets.spreadsheets.values.get({
60
+ spreadsheetId,
61
+ range,
62
+ });
63
+ return response.data;
64
+ }
65
+ catch (error) {
66
+ if (error.code === 404) {
67
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
68
+ }
69
+ if (error.code === 403) {
70
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have read access.`);
71
+ }
72
+ throw new UserError(`Failed to read range: ${error.message || 'Unknown error'}`);
73
+ }
74
+ }
75
+ /**
76
+ * Writes values to a spreadsheet range
77
+ */
78
+ export async function writeRange(sheets, spreadsheetId, range, values, valueInputOption = 'USER_ENTERED') {
79
+ try {
80
+ const response = await sheets.spreadsheets.values.update({
81
+ spreadsheetId,
82
+ range,
83
+ valueInputOption,
84
+ requestBody: {
85
+ values,
86
+ },
87
+ });
88
+ return response.data;
89
+ }
90
+ catch (error) {
91
+ if (error.code === 404) {
92
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
93
+ }
94
+ if (error.code === 403) {
95
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
96
+ }
97
+ throw new UserError(`Failed to write range: ${error.message || 'Unknown error'}`);
98
+ }
99
+ }
100
+ /**
101
+ * Appends values to the end of a sheet
102
+ */
103
+ export async function appendValues(sheets, spreadsheetId, range, values, valueInputOption = 'USER_ENTERED') {
104
+ try {
105
+ const response = await sheets.spreadsheets.values.append({
106
+ spreadsheetId,
107
+ range,
108
+ valueInputOption,
109
+ insertDataOption: 'INSERT_ROWS',
110
+ requestBody: {
111
+ values,
112
+ },
113
+ });
114
+ return response.data;
115
+ }
116
+ catch (error) {
117
+ if (error.code === 404) {
118
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
119
+ }
120
+ if (error.code === 403) {
121
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
122
+ }
123
+ throw new UserError(`Failed to append values: ${error.message || 'Unknown error'}`);
124
+ }
125
+ }
126
+ /**
127
+ * Clears values from a range
128
+ */
129
+ export async function clearRange(sheets, spreadsheetId, range) {
130
+ try {
131
+ const response = await sheets.spreadsheets.values.clear({
132
+ spreadsheetId,
133
+ range,
134
+ });
135
+ return response.data;
136
+ }
137
+ catch (error) {
138
+ if (error.code === 404) {
139
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
140
+ }
141
+ if (error.code === 403) {
142
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
143
+ }
144
+ throw new UserError(`Failed to clear range: ${error.message || 'Unknown error'}`);
145
+ }
146
+ }
147
+ /**
148
+ * Gets spreadsheet metadata including sheet information
149
+ */
150
+ export async function getSpreadsheetMetadata(sheets, spreadsheetId) {
151
+ try {
152
+ const response = await sheets.spreadsheets.get({
153
+ spreadsheetId,
154
+ includeGridData: false,
155
+ });
156
+ return response.data;
157
+ }
158
+ catch (error) {
159
+ if (error.code === 404) {
160
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
161
+ }
162
+ if (error.code === 403) {
163
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have read access.`);
164
+ }
165
+ throw new UserError(`Failed to get spreadsheet metadata: ${error.message || 'Unknown error'}`);
166
+ }
167
+ }
168
+ /**
169
+ * Creates a new sheet/tab in a spreadsheet
170
+ */
171
+ export async function addSheet(sheets, spreadsheetId, sheetTitle) {
172
+ try {
173
+ const response = await sheets.spreadsheets.batchUpdate({
174
+ spreadsheetId,
175
+ requestBody: {
176
+ requests: [
177
+ {
178
+ addSheet: {
179
+ properties: {
180
+ title: sheetTitle,
181
+ },
182
+ },
183
+ },
184
+ ],
185
+ },
186
+ });
187
+ return response.data;
188
+ }
189
+ catch (error) {
190
+ if (error.code === 404) {
191
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
192
+ }
193
+ if (error.code === 403) {
194
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
195
+ }
196
+ throw new UserError(`Failed to add sheet: ${error.message || 'Unknown error'}`);
197
+ }
198
+ }
199
+ /**
200
+ * Parses A1 notation range to extract sheet name and cell range
201
+ * Returns {sheetName, a1Range} where a1Range is just the cell part (e.g., "A1:B2")
202
+ */
203
+ function parseRange(range) {
204
+ if (range.includes('!')) {
205
+ const parts = range.split('!');
206
+ return {
207
+ sheetName: parts[0].replace(/^'|'$/g, ''), // Remove quotes if present
208
+ a1Range: parts[1],
209
+ };
210
+ }
211
+ return {
212
+ sheetName: null,
213
+ a1Range: range,
214
+ };
215
+ }
216
+ /**
217
+ * Formats cells in a range
218
+ * Note: This function requires the sheetId. For simplicity, we'll get it from the spreadsheet metadata.
219
+ */
220
+ export async function formatCells(sheets, spreadsheetId, range, format) {
221
+ try {
222
+ // Parse the range to get sheet name and cell range
223
+ const { sheetName, a1Range } = parseRange(range);
224
+ // Get spreadsheet metadata to find sheetId
225
+ const metadata = await getSpreadsheetMetadata(sheets, spreadsheetId);
226
+ let sheetId;
227
+ if (sheetName) {
228
+ // Find the sheet by name
229
+ const sheet = metadata.sheets?.find(s => s.properties?.title === sheetName);
230
+ if (!sheet || !sheet.properties?.sheetId) {
231
+ throw new UserError(`Sheet "${sheetName}" not found in spreadsheet.`);
232
+ }
233
+ sheetId = sheet.properties.sheetId;
234
+ }
235
+ else {
236
+ // Use the first sheet
237
+ const firstSheet = metadata.sheets?.[0];
238
+ if (!firstSheet?.properties?.sheetId) {
239
+ throw new UserError('Spreadsheet has no sheets.');
240
+ }
241
+ sheetId = firstSheet.properties.sheetId;
242
+ }
243
+ if (sheetId === undefined) {
244
+ throw new UserError('Could not determine sheet ID.');
245
+ }
246
+ // Parse A1 range to get row/column indices
247
+ const rangeMatch = a1Range.match(/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i);
248
+ if (!rangeMatch) {
249
+ throw new UserError(`Invalid range format: ${a1Range}. Expected format like "A1" or "A1:B2"`);
250
+ }
251
+ const startCol = rangeMatch[1].toUpperCase();
252
+ const startRow = parseInt(rangeMatch[2], 10) - 1; // Convert to 0-based
253
+ const endCol = rangeMatch[3] ? rangeMatch[3].toUpperCase() : startCol;
254
+ const endRow = rangeMatch[4] ? parseInt(rangeMatch[4], 10) - 1 : startRow; // Convert to 0-based
255
+ // Convert column letters to 0-based indices
256
+ function colToIndex(col) {
257
+ let index = 0;
258
+ for (let i = 0; i < col.length; i++) {
259
+ index = index * 26 + (col.charCodeAt(i) - 64);
260
+ }
261
+ return index - 1;
262
+ }
263
+ const startColIndex = colToIndex(startCol);
264
+ const endColIndex = colToIndex(endCol);
265
+ const userEnteredFormat = {};
266
+ if (format.backgroundColor) {
267
+ userEnteredFormat.backgroundColor = {
268
+ red: format.backgroundColor.red,
269
+ green: format.backgroundColor.green,
270
+ blue: format.backgroundColor.blue,
271
+ alpha: 1,
272
+ };
273
+ }
274
+ if (format.textFormat) {
275
+ userEnteredFormat.textFormat = {};
276
+ if (format.textFormat.foregroundColor) {
277
+ userEnteredFormat.textFormat.foregroundColor = {
278
+ red: format.textFormat.foregroundColor.red,
279
+ green: format.textFormat.foregroundColor.green,
280
+ blue: format.textFormat.foregroundColor.blue,
281
+ alpha: 1,
282
+ };
283
+ }
284
+ if (format.textFormat.fontSize !== undefined) {
285
+ userEnteredFormat.textFormat.fontSize = format.textFormat.fontSize;
286
+ }
287
+ if (format.textFormat.bold !== undefined) {
288
+ userEnteredFormat.textFormat.bold = format.textFormat.bold;
289
+ }
290
+ if (format.textFormat.italic !== undefined) {
291
+ userEnteredFormat.textFormat.italic = format.textFormat.italic;
292
+ }
293
+ }
294
+ if (format.horizontalAlignment) {
295
+ userEnteredFormat.horizontalAlignment = format.horizontalAlignment;
296
+ }
297
+ if (format.verticalAlignment) {
298
+ userEnteredFormat.verticalAlignment = format.verticalAlignment;
299
+ }
300
+ const response = await sheets.spreadsheets.batchUpdate({
301
+ spreadsheetId,
302
+ requestBody: {
303
+ requests: [
304
+ {
305
+ repeatCell: {
306
+ range: {
307
+ sheetId: sheetId,
308
+ startRowIndex: startRow,
309
+ endRowIndex: endRow + 1, // endRowIndex is exclusive
310
+ startColumnIndex: startColIndex,
311
+ endColumnIndex: endColIndex + 1, // endColumnIndex is exclusive
312
+ },
313
+ cell: {
314
+ userEnteredFormat,
315
+ },
316
+ fields: 'userEnteredFormat(backgroundColor,textFormat,horizontalAlignment,verticalAlignment)',
317
+ },
318
+ },
319
+ ],
320
+ },
321
+ });
322
+ return response.data;
323
+ }
324
+ catch (error) {
325
+ if (error.code === 404) {
326
+ throw new UserError(`Spreadsheet not found (ID: ${spreadsheetId}). Check the ID.`);
327
+ }
328
+ if (error.code === 403) {
329
+ throw new UserError(`Permission denied for spreadsheet (ID: ${spreadsheetId}). Ensure you have write access.`);
330
+ }
331
+ if (error instanceof UserError)
332
+ throw error;
333
+ throw new UserError(`Failed to format cells: ${error.message || 'Unknown error'}`);
334
+ }
335
+ }
336
+ /**
337
+ * Helper to convert hex color to RGB (0-1 range)
338
+ */
339
+ export function hexToRgb(hex) {
340
+ if (!hex)
341
+ return null;
342
+ let hexClean = hex.startsWith('#') ? hex.slice(1) : hex;
343
+ if (hexClean.length === 3) {
344
+ hexClean = hexClean[0] + hexClean[0] + hexClean[1] + hexClean[1] + hexClean[2] + hexClean[2];
345
+ }
346
+ if (hexClean.length !== 6)
347
+ return null;
348
+ const bigint = parseInt(hexClean, 16);
349
+ if (isNaN(bigint))
350
+ return null;
351
+ return {
352
+ red: ((bigint >> 16) & 255) / 255,
353
+ green: ((bigint >> 8) & 255) / 255,
354
+ blue: (bigint & 255) / 255,
355
+ };
356
+ }