google-workspace-mcp 2.1.1 → 2.3.4
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/README.md +17 -2
- package/dist/accounts.d.ts.map +1 -1
- package/dist/accounts.js +1 -0
- package/dist/accounts.js.map +1 -1
- package/dist/excelHelpers.d.ts +108 -0
- package/dist/excelHelpers.d.ts.map +1 -0
- package/dist/excelHelpers.js +343 -0
- package/dist/excelHelpers.js.map +1 -0
- package/dist/securityHelpers.d.ts +118 -0
- package/dist/securityHelpers.d.ts.map +1 -0
- package/dist/securityHelpers.js +437 -0
- package/dist/securityHelpers.js.map +1 -0
- package/dist/server.js +22 -6
- package/dist/server.js.map +1 -1
- package/dist/serverWrapper.d.ts +9 -1
- package/dist/serverWrapper.d.ts.map +1 -1
- package/dist/serverWrapper.js +76 -7
- package/dist/serverWrapper.js.map +1 -1
- package/dist/tools/docs.tools.d.ts.map +1 -1
- package/dist/tools/docs.tools.js +29 -10
- package/dist/tools/docs.tools.js.map +1 -1
- package/dist/tools/drive.tools.d.ts.map +1 -1
- package/dist/tools/drive.tools.js +680 -6
- package/dist/tools/drive.tools.js.map +1 -1
- package/dist/tools/excel.tools.d.ts +3 -0
- package/dist/tools/excel.tools.d.ts.map +1 -0
- package/dist/tools/excel.tools.js +651 -0
- package/dist/tools/excel.tools.js.map +1 -0
- package/dist/tools/forms.tools.d.ts.map +1 -1
- package/dist/tools/forms.tools.js +13 -7
- package/dist/tools/forms.tools.js.map +1 -1
- package/dist/tools/gmail.tools.d.ts.map +1 -1
- package/dist/tools/gmail.tools.js +376 -37
- package/dist/tools/gmail.tools.js.map +1 -1
- package/dist/tools/sheets.tools.d.ts.map +1 -1
- package/dist/tools/sheets.tools.js +138 -4
- package/dist/tools/sheets.tools.js.map +1 -1
- package/dist/tools/slides.tools.d.ts.map +1 -1
- package/dist/tools/slides.tools.js +3 -1
- package/dist/tools/slides.tools.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -6
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// excel.tools.ts - Tools for editing Excel-format files (.xlsx/.xls) on Google Drive
|
|
2
|
+
// These extend Sheets functionality to work with Excel formats without conversion
|
|
3
|
+
import { UserError } from 'fastmcp';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { getErrorMessage } from '../errorHelpers.js';
|
|
6
|
+
import { getDriveFileUrl } from '../urlHelpers.js';
|
|
7
|
+
import * as ExcelHelpers from '../excelHelpers.js';
|
|
8
|
+
import { escapeDriveQuery, wrapSpreadsheetContent } from '../securityHelpers.js';
|
|
9
|
+
export function registerExcelTools(options) {
|
|
10
|
+
const { server, getDriveClient, getAccountEmail } = options;
|
|
11
|
+
// --- List Excel Sheets ---
|
|
12
|
+
server.addTool({
|
|
13
|
+
name: 'listExcelSheets',
|
|
14
|
+
description: 'Lists all sheet/tab names in an Excel-format file (.xlsx or .xls) stored on Google Drive. Use this for spreadsheets that need to stay in Excel format.',
|
|
15
|
+
annotations: {
|
|
16
|
+
title: 'List Excel Sheets',
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
openWorldHint: true,
|
|
19
|
+
},
|
|
20
|
+
parameters: z.object({
|
|
21
|
+
account: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
25
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
26
|
+
}),
|
|
27
|
+
execute: async (args, { log }) => {
|
|
28
|
+
const drive = await getDriveClient(args.account);
|
|
29
|
+
const email = await getAccountEmail(args.account);
|
|
30
|
+
log.info(`Listing sheets in Excel file: ${args.fileId}`);
|
|
31
|
+
try {
|
|
32
|
+
const { workbook, fileName } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
33
|
+
const sheetNames = ExcelHelpers.getSheetNames(workbook);
|
|
34
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
35
|
+
let result = `**Excel File:** ${fileName}\n`;
|
|
36
|
+
result += `**Sheets (${sheetNames.length}):**\n`;
|
|
37
|
+
sheetNames.forEach((name, index) => {
|
|
38
|
+
const info = ExcelHelpers.getSheetInfo(workbook, name);
|
|
39
|
+
result += `${index + 1}. **${name}** (${info.rowCount} rows x ${info.columnCount} columns)\n`;
|
|
40
|
+
});
|
|
41
|
+
result += `\nView in Drive: ${link}`;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = getErrorMessage(error);
|
|
46
|
+
log.error(`Error listing Excel sheets: ${message}`);
|
|
47
|
+
if (error instanceof UserError)
|
|
48
|
+
throw error;
|
|
49
|
+
throw new UserError(`Failed to list Excel sheets: ${message}`);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
// --- Read Excel File ---
|
|
54
|
+
server.addTool({
|
|
55
|
+
name: 'readExcelFile',
|
|
56
|
+
description: 'Reads data from a specific range in an Excel-format file (.xlsx or .xls) on Google Drive. Similar to readSpreadsheet but for files that need to stay in Excel format.',
|
|
57
|
+
annotations: {
|
|
58
|
+
title: 'Read Excel File',
|
|
59
|
+
readOnlyHint: true,
|
|
60
|
+
openWorldHint: true,
|
|
61
|
+
},
|
|
62
|
+
parameters: z.object({
|
|
63
|
+
account: z
|
|
64
|
+
.string()
|
|
65
|
+
.min(1)
|
|
66
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
67
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
68
|
+
range: z
|
|
69
|
+
.string()
|
|
70
|
+
.describe('A1 notation range to read (e.g., "A1:B10"). If only a cell is specified (e.g., "A1"), reads that single cell.'),
|
|
71
|
+
sheetName: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Name of the sheet to read from. If not provided, reads from the first sheet. Use listExcelSheets to see available sheets.'),
|
|
75
|
+
maxRows: z
|
|
76
|
+
.number()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Maximum number of rows to return (default: unlimited). Use this to prevent accidentally reading enormous ranges.'),
|
|
79
|
+
}),
|
|
80
|
+
execute: async (args, { log }) => {
|
|
81
|
+
const drive = await getDriveClient(args.account);
|
|
82
|
+
const email = await getAccountEmail(args.account);
|
|
83
|
+
log.info(`Reading Excel file ${args.fileId}, range: ${args.range}`);
|
|
84
|
+
try {
|
|
85
|
+
const { workbook, fileName } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
86
|
+
const sheet = ExcelHelpers.getWorksheet(workbook, args.sheetName);
|
|
87
|
+
const sheetName = args.sheetName || workbook.SheetNames[0];
|
|
88
|
+
let values = ExcelHelpers.readRange(sheet, args.range);
|
|
89
|
+
const totalRows = values.length;
|
|
90
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
91
|
+
if (values.length === 0 || (values.length === 1 && values[0].length === 0)) {
|
|
92
|
+
return `Range ${args.range} in sheet "${sheetName}" is empty.`;
|
|
93
|
+
}
|
|
94
|
+
// Apply maxRows limit
|
|
95
|
+
let truncatedNotice = '';
|
|
96
|
+
if (args.maxRows && values.length > args.maxRows) {
|
|
97
|
+
values = values.slice(0, args.maxRows);
|
|
98
|
+
truncatedNotice = `\n\n⚠️ Showing ${args.maxRows} of ${totalRows} rows. Increase maxRows to see more.`;
|
|
99
|
+
}
|
|
100
|
+
// Build cell content and wrap with security warning
|
|
101
|
+
let cellContent = '';
|
|
102
|
+
values.forEach((row, index) => {
|
|
103
|
+
cellContent += `Row ${index + 1}: ${JSON.stringify(row)}\n`;
|
|
104
|
+
});
|
|
105
|
+
const wrappedContent = wrapSpreadsheetContent(cellContent, sheetName, args.range);
|
|
106
|
+
let result = `**File:** ${fileName}\n`;
|
|
107
|
+
result += `**Sheet:** ${sheetName}\n`;
|
|
108
|
+
result += `**Range:** ${args.range}\n`;
|
|
109
|
+
result += `**Rows:** ${values.length}${totalRows !== values.length ? ` of ${totalRows}` : ''}\n\n`;
|
|
110
|
+
result += wrappedContent;
|
|
111
|
+
result += `\nView in Drive: ${link}`;
|
|
112
|
+
result += truncatedNotice;
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const message = getErrorMessage(error);
|
|
117
|
+
log.error(`Error reading Excel file ${args.fileId}: ${message}`);
|
|
118
|
+
if (error instanceof UserError)
|
|
119
|
+
throw error;
|
|
120
|
+
throw new UserError(`Failed to read Excel file: ${message}`);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
// --- Write Excel Cell ---
|
|
125
|
+
server.addTool({
|
|
126
|
+
name: 'writeExcelCell',
|
|
127
|
+
description: 'Writes a value to a specific cell in an Excel-format file (.xlsx or .xls) on Google Drive. Use when the file must remain in Excel format.',
|
|
128
|
+
annotations: {
|
|
129
|
+
title: 'Write Excel Cell',
|
|
130
|
+
readOnlyHint: false,
|
|
131
|
+
destructiveHint: true,
|
|
132
|
+
idempotentHint: true,
|
|
133
|
+
openWorldHint: true,
|
|
134
|
+
},
|
|
135
|
+
parameters: z.object({
|
|
136
|
+
account: z
|
|
137
|
+
.string()
|
|
138
|
+
.min(1)
|
|
139
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
140
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
141
|
+
cell: z.string().describe('The cell reference in A1 notation (e.g., "A1", "B5", "AA100").'),
|
|
142
|
+
value: z
|
|
143
|
+
.union([z.string(), z.number(), z.boolean(), z.null()])
|
|
144
|
+
.describe('The value to write to the cell. Use null to clear the cell.'),
|
|
145
|
+
sheetName: z
|
|
146
|
+
.string()
|
|
147
|
+
.optional()
|
|
148
|
+
.describe('Name of the sheet to write to. If not provided, writes to the first sheet. Use listExcelSheets to see available sheets.'),
|
|
149
|
+
}),
|
|
150
|
+
execute: async (args, { log }) => {
|
|
151
|
+
const drive = await getDriveClient(args.account);
|
|
152
|
+
const email = await getAccountEmail(args.account);
|
|
153
|
+
log.info(`Writing to Excel file ${args.fileId}, cell: ${args.cell}`);
|
|
154
|
+
try {
|
|
155
|
+
const { workbook, fileName, mimeType } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
156
|
+
const sheet = ExcelHelpers.getWorksheet(workbook, args.sheetName);
|
|
157
|
+
const sheetName = args.sheetName || workbook.SheetNames[0];
|
|
158
|
+
ExcelHelpers.writeCell(sheet, args.cell, args.value);
|
|
159
|
+
await ExcelHelpers.uploadExcel(drive, args.fileId, workbook, mimeType);
|
|
160
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
161
|
+
return `Successfully wrote to cell ${args.cell.toUpperCase()} in sheet "${sheetName}" of "${fileName}".\nView in Drive: ${link}`;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const message = getErrorMessage(error);
|
|
165
|
+
log.error(`Error writing to Excel file ${args.fileId}: ${message}`);
|
|
166
|
+
if (error instanceof UserError)
|
|
167
|
+
throw error;
|
|
168
|
+
throw new UserError(`Failed to write to Excel file: ${message}`);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
// --- Write Excel Range ---
|
|
173
|
+
server.addTool({
|
|
174
|
+
name: 'writeExcelRange',
|
|
175
|
+
description: 'Writes data to a range in an Excel-format file (.xlsx or .xls) on Google Drive. Similar to writeSpreadsheet but preserves Excel format.',
|
|
176
|
+
annotations: {
|
|
177
|
+
title: 'Write Excel Range',
|
|
178
|
+
readOnlyHint: false,
|
|
179
|
+
destructiveHint: true,
|
|
180
|
+
idempotentHint: true,
|
|
181
|
+
openWorldHint: true,
|
|
182
|
+
},
|
|
183
|
+
parameters: z.object({
|
|
184
|
+
account: z
|
|
185
|
+
.string()
|
|
186
|
+
.min(1)
|
|
187
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
188
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
189
|
+
startCell: z
|
|
190
|
+
.string()
|
|
191
|
+
.describe('The top-left cell of the range to write to in A1 notation (e.g., "A1", "B5"). Data will expand from this cell.'),
|
|
192
|
+
values: z
|
|
193
|
+
.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])))
|
|
194
|
+
.describe('2D array of values to write. Each inner array represents a row.'),
|
|
195
|
+
sheetName: z
|
|
196
|
+
.string()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe('Name of the sheet to write to. If not provided, writes to the first sheet. Use listExcelSheets to see available sheets.'),
|
|
199
|
+
}),
|
|
200
|
+
execute: async (args, { log }) => {
|
|
201
|
+
const drive = await getDriveClient(args.account);
|
|
202
|
+
const email = await getAccountEmail(args.account);
|
|
203
|
+
log.info(`Writing range to Excel file ${args.fileId}, starting at: ${args.startCell}`);
|
|
204
|
+
try {
|
|
205
|
+
const { workbook, fileName, mimeType } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
206
|
+
const sheet = ExcelHelpers.getWorksheet(workbook, args.sheetName);
|
|
207
|
+
const sheetName = args.sheetName || workbook.SheetNames[0];
|
|
208
|
+
const result = ExcelHelpers.writeRange(sheet, args.startCell, args.values);
|
|
209
|
+
await ExcelHelpers.uploadExcel(drive, args.fileId, workbook, mimeType);
|
|
210
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
211
|
+
return `Successfully wrote ${result.updatedCells} cells (${result.updatedRows} rows x ${result.updatedColumns} columns) starting at ${args.startCell.toUpperCase()} in sheet "${sheetName}" of "${fileName}".\nView in Drive: ${link}`;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const message = getErrorMessage(error);
|
|
215
|
+
log.error(`Error writing range to Excel file ${args.fileId}: ${message}`);
|
|
216
|
+
if (error instanceof UserError)
|
|
217
|
+
throw error;
|
|
218
|
+
throw new UserError(`Failed to write range to Excel file: ${message}`);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
// --- Append Excel Rows ---
|
|
223
|
+
server.addTool({
|
|
224
|
+
name: 'appendExcelRows',
|
|
225
|
+
description: 'Appends rows to an Excel-format file (.xlsx or .xls) on Google Drive. Similar to appendSpreadsheetRows but preserves Excel format.',
|
|
226
|
+
annotations: {
|
|
227
|
+
title: 'Append Excel Rows',
|
|
228
|
+
readOnlyHint: false,
|
|
229
|
+
destructiveHint: false,
|
|
230
|
+
idempotentHint: false,
|
|
231
|
+
openWorldHint: true,
|
|
232
|
+
},
|
|
233
|
+
parameters: z.object({
|
|
234
|
+
account: z
|
|
235
|
+
.string()
|
|
236
|
+
.min(1)
|
|
237
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
238
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
239
|
+
values: z
|
|
240
|
+
.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])))
|
|
241
|
+
.describe('2D array of values to append. Each inner array represents a row.'),
|
|
242
|
+
sheetName: z
|
|
243
|
+
.string()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe('Name of the sheet to append to. If not provided, appends to the first sheet. Use listExcelSheets to see available sheets.'),
|
|
246
|
+
startColumn: z
|
|
247
|
+
.string()
|
|
248
|
+
.optional()
|
|
249
|
+
.default('A')
|
|
250
|
+
.describe('Column letter to start appending from (e.g., "A", "B"). Defaults to "A".'),
|
|
251
|
+
}),
|
|
252
|
+
execute: async (args, { log }) => {
|
|
253
|
+
const drive = await getDriveClient(args.account);
|
|
254
|
+
const email = await getAccountEmail(args.account);
|
|
255
|
+
log.info(`Appending rows to Excel file ${args.fileId}`);
|
|
256
|
+
try {
|
|
257
|
+
const { workbook, fileName, mimeType } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
258
|
+
const sheet = ExcelHelpers.getWorksheet(workbook, args.sheetName);
|
|
259
|
+
const sheetName = args.sheetName || workbook.SheetNames[0];
|
|
260
|
+
const result = ExcelHelpers.appendRows(sheet, args.values, args.startColumn);
|
|
261
|
+
await ExcelHelpers.uploadExcel(drive, args.fileId, workbook, mimeType);
|
|
262
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
263
|
+
return `Successfully appended ${result.appendedRows} row(s) starting at row ${result.startingRow} in sheet "${sheetName}" of "${fileName}".\nView in Drive: ${link}`;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
const message = getErrorMessage(error);
|
|
267
|
+
log.error(`Error appending rows to Excel file ${args.fileId}: ${message}`);
|
|
268
|
+
if (error instanceof UserError)
|
|
269
|
+
throw error;
|
|
270
|
+
throw new UserError(`Failed to append rows to Excel file: ${message}`);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
// --- Clear Excel Range ---
|
|
275
|
+
server.addTool({
|
|
276
|
+
name: 'clearExcelRange',
|
|
277
|
+
description: 'Clears values from a range in an Excel-format file (.xlsx or .xls) on Google Drive. Similar to clearSpreadsheetRange but preserves Excel format.',
|
|
278
|
+
annotations: {
|
|
279
|
+
title: 'Clear Excel Range',
|
|
280
|
+
readOnlyHint: false,
|
|
281
|
+
destructiveHint: true,
|
|
282
|
+
idempotentHint: true,
|
|
283
|
+
openWorldHint: true,
|
|
284
|
+
},
|
|
285
|
+
parameters: z.object({
|
|
286
|
+
account: z
|
|
287
|
+
.string()
|
|
288
|
+
.min(1)
|
|
289
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
290
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
291
|
+
range: z.string().describe('A1 notation range to clear (e.g., "A1:B10").'),
|
|
292
|
+
sheetName: z
|
|
293
|
+
.string()
|
|
294
|
+
.optional()
|
|
295
|
+
.describe('Name of the sheet to clear from. If not provided, clears from the first sheet. Use listExcelSheets to see available sheets.'),
|
|
296
|
+
}),
|
|
297
|
+
execute: async (args, { log }) => {
|
|
298
|
+
const drive = await getDriveClient(args.account);
|
|
299
|
+
const email = await getAccountEmail(args.account);
|
|
300
|
+
log.info(`Clearing range ${args.range} in Excel file ${args.fileId}`);
|
|
301
|
+
try {
|
|
302
|
+
const { workbook, fileName, mimeType } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
303
|
+
const sheet = ExcelHelpers.getWorksheet(workbook, args.sheetName);
|
|
304
|
+
const sheetName = args.sheetName || workbook.SheetNames[0];
|
|
305
|
+
const clearedCells = ExcelHelpers.clearRange(sheet, args.range);
|
|
306
|
+
await ExcelHelpers.uploadExcel(drive, args.fileId, workbook, mimeType);
|
|
307
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
308
|
+
return `Successfully cleared ${clearedCells} cells in range ${args.range} in sheet "${sheetName}" of "${fileName}".\nView in Drive: ${link}`;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
const message = getErrorMessage(error);
|
|
312
|
+
log.error(`Error clearing range in Excel file ${args.fileId}: ${message}`);
|
|
313
|
+
if (error instanceof UserError)
|
|
314
|
+
throw error;
|
|
315
|
+
throw new UserError(`Failed to clear range in Excel file: ${message}`);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
// --- Get Excel Info ---
|
|
320
|
+
server.addTool({
|
|
321
|
+
name: 'getExcelInfo',
|
|
322
|
+
description: 'Gets detailed information about an Excel-format file (.xlsx or .xls) on Google Drive, including all sheets and their dimensions.',
|
|
323
|
+
annotations: {
|
|
324
|
+
title: 'Get Excel Info',
|
|
325
|
+
readOnlyHint: true,
|
|
326
|
+
openWorldHint: true,
|
|
327
|
+
},
|
|
328
|
+
parameters: z.object({
|
|
329
|
+
account: z
|
|
330
|
+
.string()
|
|
331
|
+
.min(1)
|
|
332
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
333
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
334
|
+
}),
|
|
335
|
+
execute: async (args, { log }) => {
|
|
336
|
+
const drive = await getDriveClient(args.account);
|
|
337
|
+
const email = await getAccountEmail(args.account);
|
|
338
|
+
log.info(`Getting info for Excel file: ${args.fileId}`);
|
|
339
|
+
try {
|
|
340
|
+
// Get file metadata
|
|
341
|
+
const metadataResponse = await drive.files.get({
|
|
342
|
+
fileId: args.fileId,
|
|
343
|
+
fields: 'id,name,mimeType,size,createdTime,modifiedTime,owners(displayName,emailAddress)',
|
|
344
|
+
});
|
|
345
|
+
const { workbook, fileName, mimeType } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
346
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
347
|
+
const metadata = metadataResponse.data;
|
|
348
|
+
const modifiedDate = metadata.modifiedTime
|
|
349
|
+
? new Date(metadata.modifiedTime).toLocaleString()
|
|
350
|
+
: 'Unknown';
|
|
351
|
+
const owner = metadata.owners?.[0];
|
|
352
|
+
let result = '**Excel File Information:**\n\n';
|
|
353
|
+
result += `**Name:** ${fileName}\n`;
|
|
354
|
+
result += `**ID:** ${args.fileId}\n`;
|
|
355
|
+
result += `**Format:** ${mimeType === ExcelHelpers.EXCEL_MIME_TYPES.xlsx ? 'Excel (.xlsx)' : 'Excel (.xls)'}\n`;
|
|
356
|
+
result += `**Size:** ${metadata.size ? `${parseInt(metadata.size).toLocaleString()} bytes` : 'Unknown'}\n`;
|
|
357
|
+
result += `**Modified:** ${modifiedDate}\n`;
|
|
358
|
+
if (owner) {
|
|
359
|
+
result += `**Owner:** ${owner.displayName} (${owner.emailAddress})\n`;
|
|
360
|
+
}
|
|
361
|
+
result += `**Link:** ${link}\n\n`;
|
|
362
|
+
const sheetNames = ExcelHelpers.getSheetNames(workbook);
|
|
363
|
+
result += `**Sheets (${sheetNames.length}):**\n`;
|
|
364
|
+
sheetNames.forEach((name, index) => {
|
|
365
|
+
const info = ExcelHelpers.getSheetInfo(workbook, name);
|
|
366
|
+
result += `${index + 1}. **${name}**\n`;
|
|
367
|
+
result += ` - Dimensions: ${info.rowCount} rows x ${info.columnCount} columns\n`;
|
|
368
|
+
result += ` - Used Range: ${info.usedRange || 'Empty'}\n`;
|
|
369
|
+
});
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
const message = getErrorMessage(error);
|
|
374
|
+
log.error(`Error getting Excel info ${args.fileId}: ${message}`);
|
|
375
|
+
if (error instanceof UserError)
|
|
376
|
+
throw error;
|
|
377
|
+
throw new UserError(`Failed to get Excel info: ${message}`);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
// --- Add Excel Sheet ---
|
|
382
|
+
server.addTool({
|
|
383
|
+
name: 'addExcelSheet',
|
|
384
|
+
description: 'Adds a new sheet/tab to an Excel-format file (.xlsx or .xls) on Google Drive.',
|
|
385
|
+
annotations: {
|
|
386
|
+
title: 'Add Excel Sheet',
|
|
387
|
+
readOnlyHint: false,
|
|
388
|
+
destructiveHint: false,
|
|
389
|
+
idempotentHint: false,
|
|
390
|
+
openWorldHint: true,
|
|
391
|
+
},
|
|
392
|
+
parameters: z.object({
|
|
393
|
+
account: z
|
|
394
|
+
.string()
|
|
395
|
+
.min(1)
|
|
396
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
397
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
398
|
+
sheetName: z.string().min(1).describe('Name for the new sheet/tab.'),
|
|
399
|
+
}),
|
|
400
|
+
execute: async (args, { log }) => {
|
|
401
|
+
const drive = await getDriveClient(args.account);
|
|
402
|
+
const email = await getAccountEmail(args.account);
|
|
403
|
+
log.info(`Adding sheet "${args.sheetName}" to Excel file ${args.fileId}`);
|
|
404
|
+
try {
|
|
405
|
+
const { workbook, fileName, mimeType } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
406
|
+
ExcelHelpers.addSheet(workbook, args.sheetName);
|
|
407
|
+
await ExcelHelpers.uploadExcel(drive, args.fileId, workbook, mimeType);
|
|
408
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
409
|
+
return `Successfully added sheet "${args.sheetName}" to "${fileName}".\nView in Drive: ${link}`;
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
const message = getErrorMessage(error);
|
|
413
|
+
log.error(`Error adding sheet to Excel file ${args.fileId}: ${message}`);
|
|
414
|
+
if (error instanceof UserError)
|
|
415
|
+
throw error;
|
|
416
|
+
throw new UserError(`Failed to add sheet: ${message}`);
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
// --- Delete Excel Sheet ---
|
|
421
|
+
server.addTool({
|
|
422
|
+
name: 'deleteExcelSheet',
|
|
423
|
+
description: 'Deletes a sheet/tab from an Excel-format file (.xlsx or .xls) on Google Drive. Cannot delete the last sheet.',
|
|
424
|
+
annotations: {
|
|
425
|
+
title: 'Delete Excel Sheet',
|
|
426
|
+
readOnlyHint: false,
|
|
427
|
+
destructiveHint: true,
|
|
428
|
+
idempotentHint: true,
|
|
429
|
+
openWorldHint: true,
|
|
430
|
+
},
|
|
431
|
+
parameters: z.object({
|
|
432
|
+
account: z
|
|
433
|
+
.string()
|
|
434
|
+
.min(1)
|
|
435
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
436
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
437
|
+
sheetName: z.string().min(1).describe('Name of the sheet to delete.'),
|
|
438
|
+
}),
|
|
439
|
+
execute: async (args, { log }) => {
|
|
440
|
+
const drive = await getDriveClient(args.account);
|
|
441
|
+
const email = await getAccountEmail(args.account);
|
|
442
|
+
log.info(`Deleting sheet "${args.sheetName}" from Excel file ${args.fileId}`);
|
|
443
|
+
try {
|
|
444
|
+
const { workbook, fileName, mimeType } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
445
|
+
ExcelHelpers.deleteSheet(workbook, args.sheetName);
|
|
446
|
+
await ExcelHelpers.uploadExcel(drive, args.fileId, workbook, mimeType);
|
|
447
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
448
|
+
return `Successfully deleted sheet "${args.sheetName}" from "${fileName}".\nView in Drive: ${link}`;
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
const message = getErrorMessage(error);
|
|
452
|
+
log.error(`Error deleting sheet from Excel file ${args.fileId}: ${message}`);
|
|
453
|
+
if (error instanceof UserError)
|
|
454
|
+
throw error;
|
|
455
|
+
throw new UserError(`Failed to delete sheet: ${message}`);
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
// --- Rename Excel Sheet ---
|
|
460
|
+
server.addTool({
|
|
461
|
+
name: 'renameExcelSheet',
|
|
462
|
+
description: 'Renames a sheet/tab in an Excel-format file (.xlsx or .xls) on Google Drive.',
|
|
463
|
+
annotations: {
|
|
464
|
+
title: 'Rename Excel Sheet',
|
|
465
|
+
readOnlyHint: false,
|
|
466
|
+
destructiveHint: false,
|
|
467
|
+
idempotentHint: true,
|
|
468
|
+
openWorldHint: true,
|
|
469
|
+
},
|
|
470
|
+
parameters: z.object({
|
|
471
|
+
account: z
|
|
472
|
+
.string()
|
|
473
|
+
.min(1)
|
|
474
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
475
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive.'),
|
|
476
|
+
oldName: z.string().min(1).describe('Current name of the sheet to rename.'),
|
|
477
|
+
newName: z.string().min(1).describe('New name for the sheet.'),
|
|
478
|
+
}),
|
|
479
|
+
execute: async (args, { log }) => {
|
|
480
|
+
const drive = await getDriveClient(args.account);
|
|
481
|
+
const email = await getAccountEmail(args.account);
|
|
482
|
+
log.info(`Renaming sheet "${args.oldName}" to "${args.newName}" in Excel file ${args.fileId}`);
|
|
483
|
+
try {
|
|
484
|
+
const { workbook, fileName, mimeType } = await ExcelHelpers.downloadAndParseExcel(drive, args.fileId);
|
|
485
|
+
ExcelHelpers.renameSheet(workbook, args.oldName, args.newName);
|
|
486
|
+
await ExcelHelpers.uploadExcel(drive, args.fileId, workbook, mimeType);
|
|
487
|
+
const link = getDriveFileUrl(args.fileId, email);
|
|
488
|
+
return `Successfully renamed sheet "${args.oldName}" to "${args.newName}" in "${fileName}".\nView in Drive: ${link}`;
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
const message = getErrorMessage(error);
|
|
492
|
+
log.error(`Error renaming sheet in Excel file ${args.fileId}: ${message}`);
|
|
493
|
+
if (error instanceof UserError)
|
|
494
|
+
throw error;
|
|
495
|
+
throw new UserError(`Failed to rename sheet: ${message}`);
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
// --- Convert Excel to Google Sheets ---
|
|
500
|
+
server.addTool({
|
|
501
|
+
name: 'convertExcelToSheets',
|
|
502
|
+
description: 'Converts an Excel-format file (.xlsx or .xls) on Google Drive to native Google Sheets format for full Sheets API support. Creates a copy; the original is preserved.',
|
|
503
|
+
annotations: {
|
|
504
|
+
title: 'Convert Excel to Google Sheets',
|
|
505
|
+
readOnlyHint: false,
|
|
506
|
+
destructiveHint: false,
|
|
507
|
+
idempotentHint: false,
|
|
508
|
+
openWorldHint: true,
|
|
509
|
+
},
|
|
510
|
+
parameters: z.object({
|
|
511
|
+
account: z
|
|
512
|
+
.string()
|
|
513
|
+
.min(1)
|
|
514
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
515
|
+
fileId: z.string().describe('The ID of the Excel file on Google Drive to convert.'),
|
|
516
|
+
newName: z
|
|
517
|
+
.string()
|
|
518
|
+
.optional()
|
|
519
|
+
.describe('Name for the new Google Sheets file. If not provided, uses the original name without the Excel extension.'),
|
|
520
|
+
parentFolderId: z
|
|
521
|
+
.string()
|
|
522
|
+
.optional()
|
|
523
|
+
.describe('ID of folder where the new Sheets file should be created. If not provided, creates in the same location as the original.'),
|
|
524
|
+
}),
|
|
525
|
+
execute: async (args, { log }) => {
|
|
526
|
+
const drive = await getDriveClient(args.account);
|
|
527
|
+
const email = await getAccountEmail(args.account);
|
|
528
|
+
log.info(`Converting Excel file ${args.fileId} to Google Sheets`);
|
|
529
|
+
try {
|
|
530
|
+
// Get original file metadata
|
|
531
|
+
const originalFile = await drive.files.get({
|
|
532
|
+
fileId: args.fileId,
|
|
533
|
+
fields: 'name,mimeType,parents',
|
|
534
|
+
});
|
|
535
|
+
if (!ExcelHelpers.isExcelFile(originalFile.data.mimeType)) {
|
|
536
|
+
throw new UserError(`File is not an Excel file. MIME type: ${originalFile.data.mimeType}. ` +
|
|
537
|
+
'This tool only converts .xlsx or .xls files.');
|
|
538
|
+
}
|
|
539
|
+
// Determine new name
|
|
540
|
+
let newName = args.newName;
|
|
541
|
+
if (!newName) {
|
|
542
|
+
const originalName = originalFile.data.name || 'Untitled';
|
|
543
|
+
newName = originalName.replace(/\.(xlsx?|xls)$/i, '');
|
|
544
|
+
}
|
|
545
|
+
// Copy the file with conversion to Google Sheets format
|
|
546
|
+
const copyResponse = await drive.files.copy({
|
|
547
|
+
fileId: args.fileId,
|
|
548
|
+
requestBody: {
|
|
549
|
+
name: newName,
|
|
550
|
+
mimeType: 'application/vnd.google-apps.spreadsheet',
|
|
551
|
+
parents: args.parentFolderId ? [args.parentFolderId] : originalFile.data.parents,
|
|
552
|
+
},
|
|
553
|
+
fields: 'id,name,webViewLink',
|
|
554
|
+
});
|
|
555
|
+
const newFile = copyResponse.data;
|
|
556
|
+
const newId = newFile.id || '';
|
|
557
|
+
const sheetsUrl = `https://docs.google.com/spreadsheets/d/${newId}/edit?authuser=${encodeURIComponent(email)}`;
|
|
558
|
+
return `Successfully converted Excel file to Google Sheets!\n\n**Original:** ${originalFile.data.name}\n**New Sheets File:** ${newFile.name} (ID: ${newId})\n**Link:** ${sheetsUrl}\n\nYou can now use the Sheets tools to edit this file with full API support.`;
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
const message = getErrorMessage(error);
|
|
562
|
+
log.error(`Error converting Excel to Sheets ${args.fileId}: ${message}`);
|
|
563
|
+
if (error instanceof UserError)
|
|
564
|
+
throw error;
|
|
565
|
+
throw new UserError(`Failed to convert Excel to Sheets: ${message}`);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
// --- List Excel Files ---
|
|
570
|
+
server.addTool({
|
|
571
|
+
name: 'listExcelFiles',
|
|
572
|
+
description: 'Lists Excel-format files (.xlsx and .xls) in your Google Drive. Use this to find spreadsheets stored in Excel format rather than native Google Sheets.',
|
|
573
|
+
annotations: {
|
|
574
|
+
title: 'List Excel Files',
|
|
575
|
+
readOnlyHint: true,
|
|
576
|
+
openWorldHint: true,
|
|
577
|
+
},
|
|
578
|
+
parameters: z.object({
|
|
579
|
+
account: z
|
|
580
|
+
.string()
|
|
581
|
+
.min(1)
|
|
582
|
+
.describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
|
|
583
|
+
maxResults: z
|
|
584
|
+
.number()
|
|
585
|
+
.int()
|
|
586
|
+
.min(1)
|
|
587
|
+
.max(100)
|
|
588
|
+
.optional()
|
|
589
|
+
.default(20)
|
|
590
|
+
.describe('Maximum number of files to return (1-100).'),
|
|
591
|
+
query: z.string().optional().describe('Search query to filter files by name.'),
|
|
592
|
+
folderId: z
|
|
593
|
+
.string()
|
|
594
|
+
.optional()
|
|
595
|
+
.describe('ID of a specific folder to search in. If not provided, searches all of Drive.'),
|
|
596
|
+
}),
|
|
597
|
+
execute: async (args, { log }) => {
|
|
598
|
+
const drive = await getDriveClient(args.account);
|
|
599
|
+
const email = await getAccountEmail(args.account);
|
|
600
|
+
log.info(`Listing Excel files. Query: ${args.query || 'none'}, Max: ${args.maxResults}`);
|
|
601
|
+
try {
|
|
602
|
+
// Build query for Excel files
|
|
603
|
+
let queryString = `(mimeType='${ExcelHelpers.EXCEL_MIME_TYPES.xlsx}' or mimeType='${ExcelHelpers.EXCEL_MIME_TYPES.xls}') and trashed=false`;
|
|
604
|
+
if (args.query) {
|
|
605
|
+
const safeQuery = escapeDriveQuery(args.query);
|
|
606
|
+
queryString += ` and name contains '${safeQuery}'`;
|
|
607
|
+
}
|
|
608
|
+
if (args.folderId) {
|
|
609
|
+
const safeFolderId = escapeDriveQuery(args.folderId);
|
|
610
|
+
queryString += ` and '${safeFolderId}' in parents`;
|
|
611
|
+
}
|
|
612
|
+
const response = await drive.files.list({
|
|
613
|
+
q: queryString,
|
|
614
|
+
pageSize: args.maxResults,
|
|
615
|
+
orderBy: 'modifiedTime desc',
|
|
616
|
+
fields: 'files(id,name,mimeType,size,modifiedTime,createdTime,owners(displayName,emailAddress))',
|
|
617
|
+
});
|
|
618
|
+
const files = response.data.files ?? [];
|
|
619
|
+
if (files.length === 0) {
|
|
620
|
+
return 'No Excel files found matching your criteria.';
|
|
621
|
+
}
|
|
622
|
+
let result = `Found ${files.length} Excel file(s):\n\n`;
|
|
623
|
+
files.forEach((file, index) => {
|
|
624
|
+
const modifiedDate = file.modifiedTime
|
|
625
|
+
? new Date(file.modifiedTime).toLocaleDateString()
|
|
626
|
+
: 'Unknown';
|
|
627
|
+
const owner = file.owners?.[0]?.displayName || 'Unknown';
|
|
628
|
+
const size = file.size ? `${parseInt(file.size).toLocaleString()} bytes` : 'Unknown';
|
|
629
|
+
const format = file.mimeType === ExcelHelpers.EXCEL_MIME_TYPES.xlsx ? '.xlsx' : '.xls';
|
|
630
|
+
const link = file.id ? getDriveFileUrl(file.id, email) : '';
|
|
631
|
+
result += `${index + 1}. **${file.name}**\n`;
|
|
632
|
+
result += ` ID: ${file.id}\n`;
|
|
633
|
+
result += ` Format: Excel (${format})\n`;
|
|
634
|
+
result += ` Size: ${size}\n`;
|
|
635
|
+
result += ` Modified: ${modifiedDate}\n`;
|
|
636
|
+
result += ` Owner: ${owner}\n`;
|
|
637
|
+
result += ` Link: ${link}\n\n`;
|
|
638
|
+
});
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
const message = getErrorMessage(error);
|
|
643
|
+
log.error(`Error listing Excel files: ${message}`);
|
|
644
|
+
if (error instanceof UserError)
|
|
645
|
+
throw error;
|
|
646
|
+
throw new UserError(`Failed to list Excel files: ${message}`);
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
//# sourceMappingURL=excel.tools.js.map
|