@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.
- package/dist/auth.js +205 -0
- package/dist/googleDocsApiHelpers.js +617 -0
- package/dist/googleSheetsApiHelpers.js +356 -0
- package/dist/server.js +2215 -0
- package/dist/types.js +107 -0
- package/package.json +1 -1
- package/.repomix/bundles.json +0 -3
- package/SAMPLE_TASKS.md +0 -230
- package/assets/google.docs.mcp.1.gif +0 -0
- package/claude.md +0 -46
- package/docs/index.html +0 -228
- package/google docs mcp.mp4 +0 -0
- package/pages/pages.md +0 -0
- package/repomix-output.txt.xml +0 -4447
- package/src/auth.ts +0 -228
- package/src/backup/auth.ts.bak +0 -101
- package/src/backup/server.ts.bak +0 -481
- package/src/googleDocsApiHelpers.ts +0 -710
- package/src/googleSheetsApiHelpers.ts +0 -427
- package/src/server.ts +0 -2494
- package/src/types.ts +0 -136
- package/tests/helpers.test.js +0 -164
- package/tests/types.test.js +0 -69
- package/tsconfig.json +0 -17
- package/vscode.md +0 -168
|
@@ -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
|
+
}
|