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.
Files changed (43) hide show
  1. package/README.md +17 -2
  2. package/dist/accounts.d.ts.map +1 -1
  3. package/dist/accounts.js +1 -0
  4. package/dist/accounts.js.map +1 -1
  5. package/dist/excelHelpers.d.ts +108 -0
  6. package/dist/excelHelpers.d.ts.map +1 -0
  7. package/dist/excelHelpers.js +343 -0
  8. package/dist/excelHelpers.js.map +1 -0
  9. package/dist/securityHelpers.d.ts +118 -0
  10. package/dist/securityHelpers.d.ts.map +1 -0
  11. package/dist/securityHelpers.js +437 -0
  12. package/dist/securityHelpers.js.map +1 -0
  13. package/dist/server.js +22 -6
  14. package/dist/server.js.map +1 -1
  15. package/dist/serverWrapper.d.ts +9 -1
  16. package/dist/serverWrapper.d.ts.map +1 -1
  17. package/dist/serverWrapper.js +76 -7
  18. package/dist/serverWrapper.js.map +1 -1
  19. package/dist/tools/docs.tools.d.ts.map +1 -1
  20. package/dist/tools/docs.tools.js +29 -10
  21. package/dist/tools/docs.tools.js.map +1 -1
  22. package/dist/tools/drive.tools.d.ts.map +1 -1
  23. package/dist/tools/drive.tools.js +680 -6
  24. package/dist/tools/drive.tools.js.map +1 -1
  25. package/dist/tools/excel.tools.d.ts +3 -0
  26. package/dist/tools/excel.tools.d.ts.map +1 -0
  27. package/dist/tools/excel.tools.js +651 -0
  28. package/dist/tools/excel.tools.js.map +1 -0
  29. package/dist/tools/forms.tools.d.ts.map +1 -1
  30. package/dist/tools/forms.tools.js +13 -7
  31. package/dist/tools/forms.tools.js.map +1 -1
  32. package/dist/tools/gmail.tools.d.ts.map +1 -1
  33. package/dist/tools/gmail.tools.js +376 -37
  34. package/dist/tools/gmail.tools.js.map +1 -1
  35. package/dist/tools/sheets.tools.d.ts.map +1 -1
  36. package/dist/tools/sheets.tools.js +138 -4
  37. package/dist/tools/sheets.tools.js.map +1 -1
  38. package/dist/tools/slides.tools.d.ts.map +1 -1
  39. package/dist/tools/slides.tools.js +3 -1
  40. package/dist/tools/slides.tools.js.map +1 -1
  41. package/dist/types.d.ts +5 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +12 -6
@@ -1,9 +1,15 @@
1
1
  // drive.tools.ts - Google Drive tool module
2
2
  import { UserError } from 'fastmcp';
3
3
  import { z } from 'zod';
4
+ import { createReadStream, createWriteStream, existsSync, statSync } from 'fs';
5
+ import { basename, dirname } from 'path';
6
+ import { pipeline } from 'stream/promises';
7
+ import mime from 'mime-types';
4
8
  import { AccountDocumentParameters } from '../types.js';
5
9
  import { isGoogleApiError, getErrorMessage } from '../errorHelpers.js';
6
10
  import { addAuthUserToUrl, getDocsUrl, getDriveFileUrl, getDriveFolderUrl } from '../urlHelpers.js';
11
+ import { escapeDriveQuery, validateReadPath, validateWritePath } from '../securityHelpers.js';
12
+ import { getServerConfig } from '../serverWrapper.js';
7
13
  export function registerDriveTools(options) {
8
14
  const { server, getDriveClient, getDocsClient, getAccountEmail } = options;
9
15
  // --- List Google Docs ---
@@ -49,7 +55,8 @@ export function registerDriveTools(options) {
49
55
  try {
50
56
  let queryString = "mimeType='application/vnd.google-apps.document' and trashed=false";
51
57
  if (args.query) {
52
- queryString += ` and (name contains '${args.query}' or fullText contains '${args.query}')`;
58
+ const safeQuery = escapeDriveQuery(args.query);
59
+ queryString += ` and (name contains '${safeQuery}' or fullText contains '${safeQuery}')`;
53
60
  }
54
61
  // Don't use orderBy when query contains fullText search (Google Drive API limitation)
55
62
  const orderBy = args.query
@@ -133,17 +140,20 @@ export function registerDriveTools(options) {
133
140
  log.info(`Searching Google Docs for: "${args.searchQuery}" in ${args.searchIn}`);
134
141
  try {
135
142
  let queryString = "mimeType='application/vnd.google-apps.document' and trashed=false";
143
+ const safeSearchQuery = escapeDriveQuery(args.searchQuery);
136
144
  if (args.searchIn === 'name') {
137
- queryString += ` and name contains '${args.searchQuery}'`;
145
+ queryString += ` and name contains '${safeSearchQuery}'`;
138
146
  }
139
147
  else if (args.searchIn === 'content') {
140
- queryString += ` and fullText contains '${args.searchQuery}'`;
148
+ queryString += ` and fullText contains '${safeSearchQuery}'`;
141
149
  }
142
150
  else {
143
- queryString += ` and (name contains '${args.searchQuery}' or fullText contains '${args.searchQuery}')`;
151
+ queryString += ` and (name contains '${safeSearchQuery}' or fullText contains '${safeSearchQuery}')`;
144
152
  }
145
153
  if (args.modifiedAfter) {
146
- queryString += ` and modifiedTime > '${args.modifiedAfter}'`;
154
+ // modifiedAfter is expected to be an ISO 8601 date, escape it to be safe
155
+ const safeDate = escapeDriveQuery(args.modifiedAfter);
156
+ queryString += ` and modifiedTime > '${safeDate}'`;
147
157
  }
148
158
  // Don't use orderBy when query contains fullText search (Google Drive API limitation)
149
159
  // Only 'name' search doesn't use fullText
@@ -414,7 +424,8 @@ export function registerDriveTools(options) {
414
424
  const email = await getAccountEmail(args.account);
415
425
  log.info(`Listing contents of folder: ${args.folderId}`);
416
426
  try {
417
- let queryString = `'${args.folderId}' in parents and trashed=false`;
427
+ const safeFolderId = escapeDriveQuery(args.folderId);
428
+ let queryString = `'${safeFolderId}' in parents and trashed=false`;
418
429
  if (!args.includeSubfolders && !args.includeFiles) {
419
430
  throw new UserError('At least one of includeSubfolders or includeFiles must be true.');
420
431
  }
@@ -997,5 +1008,668 @@ export function registerDriveTools(options) {
997
1008
  }
998
1009
  },
999
1010
  });
1011
+ // --- Upload File to Drive ---
1012
+ server.addTool({
1013
+ name: 'uploadFileToDrive',
1014
+ description: 'Uploads a local file to Google Drive. Supports any file type. Use this to upload documents, images, PDFs, or any other files.',
1015
+ annotations: {
1016
+ title: 'Upload File to Drive',
1017
+ readOnlyHint: false,
1018
+ destructiveHint: false,
1019
+ idempotentHint: false,
1020
+ openWorldHint: true,
1021
+ },
1022
+ parameters: z.object({
1023
+ account: z
1024
+ .string()
1025
+ .min(1)
1026
+ .describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
1027
+ localPath: z.string().min(1).describe('Absolute path to the local file to upload.'),
1028
+ fileName: z
1029
+ .string()
1030
+ .optional()
1031
+ .describe('Name for the uploaded file in Drive. If not provided, uses the local filename.'),
1032
+ folderId: z
1033
+ .string()
1034
+ .optional()
1035
+ .describe('ID of the folder to upload to. If not provided, uploads to Drive root.'),
1036
+ mimeType: z
1037
+ .string()
1038
+ .optional()
1039
+ .describe('MIME type of the file. If not provided, auto-detected from file extension.'),
1040
+ convertToGoogleFormat: z
1041
+ .boolean()
1042
+ .optional()
1043
+ .default(false)
1044
+ .describe('If true, converts supported files to Google format (e.g., .docx to Google Docs, .xlsx to Google Sheets).'),
1045
+ }),
1046
+ execute: async (args, { log }) => {
1047
+ const drive = await getDriveClient(args.account);
1048
+ const email = await getAccountEmail(args.account);
1049
+ // Validate path for security
1050
+ const pathValidation = validateReadPath(args.localPath, getServerConfig().pathSecurity);
1051
+ if (!pathValidation.valid) {
1052
+ throw new UserError(`Cannot upload from this path: ${pathValidation.error}`);
1053
+ }
1054
+ log.info(`Uploading file from ${pathValidation.resolvedPath}`);
1055
+ try {
1056
+ // Validate file exists
1057
+ if (!existsSync(pathValidation.resolvedPath)) {
1058
+ throw new UserError(`File not found: ${pathValidation.resolvedPath}`);
1059
+ }
1060
+ // Get file stats
1061
+ const stats = statSync(pathValidation.resolvedPath);
1062
+ if (!stats.isFile()) {
1063
+ throw new UserError(`Path is not a file: ${pathValidation.resolvedPath}`);
1064
+ }
1065
+ const fileName = args.fileName || basename(pathValidation.resolvedPath);
1066
+ const detectedMimeType = mime.lookup(pathValidation.resolvedPath) || 'application/octet-stream';
1067
+ const uploadMimeType = args.mimeType || detectedMimeType;
1068
+ // Determine if we should convert to Google format
1069
+ let googleMimeType;
1070
+ if (args.convertToGoogleFormat) {
1071
+ const mimeTypeMap = {
1072
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'application/vnd.google-apps.document',
1073
+ 'application/msword': 'application/vnd.google-apps.document',
1074
+ 'text/plain': 'application/vnd.google-apps.document',
1075
+ 'application/rtf': 'application/vnd.google-apps.document',
1076
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'application/vnd.google-apps.spreadsheet',
1077
+ 'application/vnd.ms-excel': 'application/vnd.google-apps.spreadsheet',
1078
+ 'text/csv': 'application/vnd.google-apps.spreadsheet',
1079
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'application/vnd.google-apps.presentation',
1080
+ 'application/vnd.ms-powerpoint': 'application/vnd.google-apps.presentation',
1081
+ };
1082
+ googleMimeType = mimeTypeMap[uploadMimeType];
1083
+ }
1084
+ const fileMetadata = {
1085
+ name: fileName,
1086
+ mimeType: googleMimeType,
1087
+ };
1088
+ if (args.folderId) {
1089
+ fileMetadata.parents = [args.folderId];
1090
+ }
1091
+ const response = await drive.files.create({
1092
+ requestBody: fileMetadata,
1093
+ media: {
1094
+ mimeType: uploadMimeType,
1095
+ body: createReadStream(pathValidation.resolvedPath),
1096
+ },
1097
+ fields: 'id,name,mimeType,size,webViewLink,webContentLink',
1098
+ });
1099
+ const file = response.data;
1100
+ const fileSizeKB = stats.size > 0 ? Math.round(stats.size / 1024) : 0;
1101
+ // Generate appropriate link
1102
+ let link;
1103
+ if (file.id) {
1104
+ if (file.mimeType === 'application/vnd.google-apps.document') {
1105
+ link = getDocsUrl(file.id, email);
1106
+ }
1107
+ else if (file.mimeType === 'application/vnd.google-apps.spreadsheet') {
1108
+ link = addAuthUserToUrl(`https://docs.google.com/spreadsheets/d/${file.id}/edit`, email);
1109
+ }
1110
+ else if (file.mimeType === 'application/vnd.google-apps.presentation') {
1111
+ link = addAuthUserToUrl(`https://docs.google.com/presentation/d/${file.id}/edit`, email);
1112
+ }
1113
+ else {
1114
+ link = file.webViewLink
1115
+ ? addAuthUserToUrl(file.webViewLink, email)
1116
+ : getDriveFileUrl(file.id, email);
1117
+ }
1118
+ }
1119
+ else {
1120
+ link = file.webViewLink;
1121
+ }
1122
+ let result = `Successfully uploaded "${file.name}" (ID: ${file.id})\n`;
1123
+ result += `Size: ${fileSizeKB} KB\n`;
1124
+ result += `Type: ${file.mimeType}\n`;
1125
+ result += `View Link: ${link}`;
1126
+ if (file.webContentLink) {
1127
+ result += `\nDirect Download: ${file.webContentLink}`;
1128
+ }
1129
+ if (googleMimeType) {
1130
+ result += '\n\nFile was converted to Google format.';
1131
+ }
1132
+ return result;
1133
+ }
1134
+ catch (error) {
1135
+ if (error instanceof UserError)
1136
+ throw error;
1137
+ const message = getErrorMessage(error);
1138
+ log.error(`Error uploading file: ${message}`);
1139
+ const code = isGoogleApiError(error) ? error.code : undefined;
1140
+ if (code === 404)
1141
+ throw new UserError('Destination folder not found. Check the folder ID.');
1142
+ if (code === 403)
1143
+ throw new UserError('Permission denied. Make sure you have write access to the destination folder.');
1144
+ throw new UserError(`Failed to upload file: ${message}`);
1145
+ }
1146
+ },
1147
+ });
1148
+ // --- Download from Drive ---
1149
+ server.addTool({
1150
+ name: 'downloadFromDrive',
1151
+ description: 'Downloads a file from Google Drive to your local filesystem. For Google Docs/Sheets/Slides, exports to a specified format.',
1152
+ annotations: {
1153
+ title: 'Download from Drive',
1154
+ readOnlyHint: true,
1155
+ openWorldHint: true,
1156
+ },
1157
+ parameters: z.object({
1158
+ account: z
1159
+ .string()
1160
+ .min(1)
1161
+ .describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
1162
+ fileId: z.string().min(1).describe('ID of the file to download.'),
1163
+ localPath: z
1164
+ .string()
1165
+ .min(1)
1166
+ .describe('Absolute path where the file should be saved locally.'),
1167
+ exportFormat: z
1168
+ .enum(['pdf', 'docx', 'txt', 'xlsx', 'csv', 'pptx', 'html', 'png', 'jpeg'])
1169
+ .optional()
1170
+ .describe('Export format for Google Docs/Sheets/Slides. Required for native Google files. Options: pdf, docx, txt, xlsx, csv, pptx, html, png, jpeg.'),
1171
+ }),
1172
+ execute: async (args, { log }) => {
1173
+ const drive = await getDriveClient(args.account);
1174
+ // Validate path for security
1175
+ const pathValidation = validateWritePath(args.localPath, getServerConfig().pathSecurity);
1176
+ if (!pathValidation.valid) {
1177
+ throw new UserError(`Cannot download to this path: ${pathValidation.error}`);
1178
+ }
1179
+ log.info(`Downloading file ${args.fileId} to ${pathValidation.resolvedPath}`);
1180
+ try {
1181
+ // Validate destination directory exists
1182
+ const destDir = dirname(pathValidation.resolvedPath);
1183
+ if (!existsSync(destDir)) {
1184
+ throw new UserError(`Destination directory does not exist: ${destDir}`);
1185
+ }
1186
+ // Get file metadata first
1187
+ const metadata = await drive.files.get({
1188
+ fileId: args.fileId,
1189
+ fields: 'id,name,mimeType,size',
1190
+ });
1191
+ const fileMimeType = metadata.data.mimeType;
1192
+ const fileName = metadata.data.name;
1193
+ const isGoogleNative = fileMimeType?.startsWith('application/vnd.google-apps');
1194
+ // Map export formats to MIME types
1195
+ const exportMimeTypes = {
1196
+ pdf: 'application/pdf',
1197
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1198
+ txt: 'text/plain',
1199
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1200
+ csv: 'text/csv',
1201
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1202
+ html: 'text/html',
1203
+ png: 'image/png',
1204
+ jpeg: 'image/jpeg',
1205
+ };
1206
+ let response;
1207
+ if (isGoogleNative) {
1208
+ // Google native files must be exported
1209
+ if (!args.exportFormat) {
1210
+ throw new UserError(`This is a Google ${fileMimeType?.replace('application/vnd.google-apps.', '')} file. ` +
1211
+ 'Please specify an exportFormat (pdf, docx, txt, xlsx, csv, pptx, html, png, jpeg).');
1212
+ }
1213
+ const exportMimeType = exportMimeTypes[args.exportFormat];
1214
+ if (!exportMimeType) {
1215
+ throw new UserError(`Unsupported export format: ${args.exportFormat}. ` +
1216
+ 'Supported formats: pdf, docx, txt, xlsx, csv, pptx, html, png, jpeg.');
1217
+ }
1218
+ response = await drive.files.export({ fileId: args.fileId, mimeType: exportMimeType }, { responseType: 'stream' });
1219
+ }
1220
+ else {
1221
+ // Binary files can be downloaded directly
1222
+ response = await drive.files.get({ fileId: args.fileId, alt: 'media' }, { responseType: 'stream' });
1223
+ }
1224
+ // Write to file
1225
+ const writeStream = createWriteStream(pathValidation.resolvedPath);
1226
+ await pipeline(response.data, writeStream);
1227
+ // Get downloaded file size
1228
+ const downloadedStats = statSync(pathValidation.resolvedPath);
1229
+ const fileSizeKB = Math.round(downloadedStats.size / 1024);
1230
+ let result = `Successfully downloaded "${fileName}" to ${pathValidation.resolvedPath}\n`;
1231
+ result += `Size: ${fileSizeKB} KB`;
1232
+ if (isGoogleNative && args.exportFormat) {
1233
+ result += `\nExported as: ${args.exportFormat.toUpperCase()}`;
1234
+ }
1235
+ return result;
1236
+ }
1237
+ catch (error) {
1238
+ if (error instanceof UserError)
1239
+ throw error;
1240
+ const message = getErrorMessage(error);
1241
+ log.error(`Error downloading file: ${message}`);
1242
+ const code = isGoogleApiError(error) ? error.code : undefined;
1243
+ if (code === 404)
1244
+ throw new UserError('File not found. Check the file ID.');
1245
+ if (code === 403)
1246
+ throw new UserError('Permission denied. Make sure you have access to this file.');
1247
+ throw new UserError(`Failed to download file: ${message}`);
1248
+ }
1249
+ },
1250
+ });
1251
+ // --- Get Shareable Link ---
1252
+ server.addTool({
1253
+ name: 'getShareableLink',
1254
+ description: 'Gets or creates a shareable link for a file with specified permission settings.',
1255
+ annotations: {
1256
+ title: 'Get Shareable Link',
1257
+ readOnlyHint: false,
1258
+ destructiveHint: false,
1259
+ idempotentHint: true,
1260
+ openWorldHint: true,
1261
+ },
1262
+ parameters: z.object({
1263
+ account: z
1264
+ .string()
1265
+ .min(1)
1266
+ .describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
1267
+ fileId: z.string().min(1).describe('ID of the file to share.'),
1268
+ shareWith: z
1269
+ .enum(['anyone', 'anyoneWithLink', 'domain'])
1270
+ .default('anyoneWithLink')
1271
+ .describe('Who can access: "anyone" (public on web), "anyoneWithLink" (anyone with the link), "domain" (your organization).'),
1272
+ role: z
1273
+ .enum(['reader', 'commenter', 'writer'])
1274
+ .default('reader')
1275
+ .describe('Permission level: "reader" (view only), "commenter" (can comment), "writer" (can edit).'),
1276
+ domain: z
1277
+ .string()
1278
+ .optional()
1279
+ .describe('Required when shareWith is "domain". Your organization domain (e.g., "company.com").'),
1280
+ }),
1281
+ execute: async (args, { log }) => {
1282
+ const drive = await getDriveClient(args.account);
1283
+ const email = await getAccountEmail(args.account);
1284
+ log.info(`Creating shareable link for file ${args.fileId}`);
1285
+ try {
1286
+ // Validate domain parameter
1287
+ if (args.shareWith === 'domain' && !args.domain) {
1288
+ throw new UserError('The "domain" parameter is required when shareWith is "domain".');
1289
+ }
1290
+ // Get file info first
1291
+ const fileInfo = await drive.files.get({
1292
+ fileId: args.fileId,
1293
+ fields: 'id,name,mimeType,webViewLink,webContentLink',
1294
+ });
1295
+ const fileName = fileInfo.data.name;
1296
+ const fileMimeType = fileInfo.data.mimeType;
1297
+ // Create the permission
1298
+ const permissionBody = {
1299
+ role: args.role,
1300
+ type: args.shareWith === 'domain' ? 'domain' : 'anyone',
1301
+ };
1302
+ if (args.shareWith === 'domain' && args.domain) {
1303
+ permissionBody.domain = args.domain;
1304
+ }
1305
+ // For "anyoneWithLink", we still use type "anyone" but the link is not discoverable
1306
+ await drive.permissions.create({
1307
+ fileId: args.fileId,
1308
+ requestBody: permissionBody,
1309
+ });
1310
+ // Get the updated file with links
1311
+ const updatedFile = await drive.files.get({
1312
+ fileId: args.fileId,
1313
+ fields: 'id,webViewLink,webContentLink',
1314
+ });
1315
+ // Generate appropriate link
1316
+ let viewLink;
1317
+ if (updatedFile.data.id) {
1318
+ if (fileMimeType === 'application/vnd.google-apps.document') {
1319
+ viewLink = getDocsUrl(updatedFile.data.id, email);
1320
+ }
1321
+ else if (fileMimeType === 'application/vnd.google-apps.spreadsheet') {
1322
+ viewLink = addAuthUserToUrl(`https://docs.google.com/spreadsheets/d/${updatedFile.data.id}/edit`, email);
1323
+ }
1324
+ else if (fileMimeType === 'application/vnd.google-apps.presentation') {
1325
+ viewLink = addAuthUserToUrl(`https://docs.google.com/presentation/d/${updatedFile.data.id}/edit`, email);
1326
+ }
1327
+ else if (fileMimeType === 'application/vnd.google-apps.folder') {
1328
+ viewLink = getDriveFolderUrl(updatedFile.data.id, email);
1329
+ }
1330
+ else {
1331
+ viewLink = updatedFile.data.webViewLink
1332
+ ? addAuthUserToUrl(updatedFile.data.webViewLink, email)
1333
+ : getDriveFileUrl(updatedFile.data.id, email);
1334
+ }
1335
+ }
1336
+ else {
1337
+ viewLink = updatedFile.data.webViewLink;
1338
+ }
1339
+ const shareDescription = args.shareWith === 'anyone'
1340
+ ? 'Anyone on the internet'
1341
+ : args.shareWith === 'domain'
1342
+ ? `Anyone in ${args.domain}`
1343
+ : 'Anyone with the link';
1344
+ const roleDescription = args.role === 'reader'
1345
+ ? 'view'
1346
+ : args.role === 'commenter'
1347
+ ? 'view and comment'
1348
+ : 'view, comment, and edit';
1349
+ let result = `Successfully created shareable link for "${fileName}"\n\n`;
1350
+ result += `**Access:** ${shareDescription} can ${roleDescription}\n`;
1351
+ result += `**View Link:** ${viewLink}`;
1352
+ if (updatedFile.data.webContentLink) {
1353
+ result += `\n**Direct Download:** ${updatedFile.data.webContentLink}`;
1354
+ }
1355
+ return result;
1356
+ }
1357
+ catch (error) {
1358
+ if (error instanceof UserError)
1359
+ throw error;
1360
+ const message = getErrorMessage(error);
1361
+ log.error(`Error creating shareable link: ${message}`);
1362
+ const code = isGoogleApiError(error) ? error.code : undefined;
1363
+ if (code === 404)
1364
+ throw new UserError('File not found. Check the file ID.');
1365
+ if (code === 403)
1366
+ throw new UserError('Permission denied. You may not have permission to share this file, or sharing may be restricted by your organization.');
1367
+ throw new UserError(`Failed to create shareable link: ${message}`);
1368
+ }
1369
+ },
1370
+ });
1371
+ // --- List Recent Files ---
1372
+ server.addTool({
1373
+ name: 'listRecentFiles',
1374
+ description: 'Lists recently modified files across all of Google Drive, not limited to a specific type.',
1375
+ annotations: {
1376
+ title: 'List Recent Files',
1377
+ readOnlyHint: true,
1378
+ openWorldHint: true,
1379
+ },
1380
+ parameters: z.object({
1381
+ account: z
1382
+ .string()
1383
+ .min(1)
1384
+ .describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
1385
+ maxResults: z
1386
+ .number()
1387
+ .int()
1388
+ .min(1)
1389
+ .max(100)
1390
+ .optional()
1391
+ .default(20)
1392
+ .describe('Maximum number of files to return (1-100).'),
1393
+ daysBack: z
1394
+ .number()
1395
+ .int()
1396
+ .min(1)
1397
+ .max(365)
1398
+ .optional()
1399
+ .default(7)
1400
+ .describe('Only show files modified within this many days.'),
1401
+ fileType: z
1402
+ .enum(['all', 'documents', 'spreadsheets', 'presentations', 'folders', 'pdfs', 'images'])
1403
+ .optional()
1404
+ .default('all')
1405
+ .describe('Filter by file type.'),
1406
+ }),
1407
+ execute: async (args, { log }) => {
1408
+ const drive = await getDriveClient(args.account);
1409
+ const email = await getAccountEmail(args.account);
1410
+ log.info(`Listing recent files: ${args.maxResults} results, ${args.daysBack} days back, type: ${args.fileType}`);
1411
+ try {
1412
+ const cutoffDate = new Date();
1413
+ cutoffDate.setDate(cutoffDate.getDate() - args.daysBack);
1414
+ const cutoffDateStr = cutoffDate.toISOString();
1415
+ let queryString = `trashed=false and modifiedTime > '${cutoffDateStr}'`;
1416
+ // Add file type filter
1417
+ const mimeTypeFilters = {
1418
+ documents: "mimeType='application/vnd.google-apps.document'",
1419
+ spreadsheets: "mimeType='application/vnd.google-apps.spreadsheet'",
1420
+ presentations: "mimeType='application/vnd.google-apps.presentation'",
1421
+ folders: "mimeType='application/vnd.google-apps.folder'",
1422
+ pdfs: "mimeType='application/pdf'",
1423
+ images: "(mimeType contains 'image/' or mimeType='application/vnd.google-apps.photo')",
1424
+ };
1425
+ if (args.fileType !== 'all' && mimeTypeFilters[args.fileType]) {
1426
+ queryString += ` and ${mimeTypeFilters[args.fileType]}`;
1427
+ }
1428
+ const response = await drive.files.list({
1429
+ q: queryString,
1430
+ pageSize: args.maxResults,
1431
+ orderBy: 'modifiedTime desc',
1432
+ fields: 'files(id,name,mimeType,size,modifiedTime,webViewLink,owners(displayName),lastModifyingUser(displayName))',
1433
+ });
1434
+ const files = response.data.files ?? [];
1435
+ if (files.length === 0) {
1436
+ return `No files found that were modified in the last ${args.daysBack} day(s)${args.fileType !== 'all' ? ` matching type "${args.fileType}"` : ''}.`;
1437
+ }
1438
+ let result = `${files.length} recently modified file(s) (last ${args.daysBack} day${args.daysBack !== 1 ? 's' : ''}):\n\n`;
1439
+ files.forEach((file, index) => {
1440
+ const modifiedDate = file.modifiedTime
1441
+ ? new Date(file.modifiedTime).toLocaleString()
1442
+ : 'Unknown';
1443
+ const lastModifier = file.lastModifyingUser?.displayName || 'Unknown';
1444
+ // Determine file type label
1445
+ let typeLabel = 'File';
1446
+ if (file.mimeType === 'application/vnd.google-apps.document')
1447
+ typeLabel = 'Doc';
1448
+ else if (file.mimeType === 'application/vnd.google-apps.spreadsheet')
1449
+ typeLabel = 'Sheet';
1450
+ else if (file.mimeType === 'application/vnd.google-apps.presentation')
1451
+ typeLabel = 'Slides';
1452
+ else if (file.mimeType === 'application/vnd.google-apps.folder')
1453
+ typeLabel = 'Folder';
1454
+ else if (file.mimeType === 'application/pdf')
1455
+ typeLabel = 'PDF';
1456
+ else if (file.mimeType?.startsWith('image/'))
1457
+ typeLabel = 'Image';
1458
+ // Generate appropriate link
1459
+ let link;
1460
+ if (file.id) {
1461
+ if (file.mimeType === 'application/vnd.google-apps.document') {
1462
+ link = getDocsUrl(file.id, email);
1463
+ }
1464
+ else if (file.mimeType === 'application/vnd.google-apps.spreadsheet') {
1465
+ link = addAuthUserToUrl(`https://docs.google.com/spreadsheets/d/${file.id}/edit`, email);
1466
+ }
1467
+ else if (file.mimeType === 'application/vnd.google-apps.presentation') {
1468
+ link = addAuthUserToUrl(`https://docs.google.com/presentation/d/${file.id}/edit`, email);
1469
+ }
1470
+ else if (file.mimeType === 'application/vnd.google-apps.folder') {
1471
+ link = getDriveFolderUrl(file.id, email);
1472
+ }
1473
+ else {
1474
+ link = file.webViewLink
1475
+ ? addAuthUserToUrl(file.webViewLink, email)
1476
+ : getDriveFileUrl(file.id, email);
1477
+ }
1478
+ }
1479
+ else {
1480
+ link = file.webViewLink;
1481
+ }
1482
+ result += `${index + 1}. [${typeLabel}] **${file.name}**\n`;
1483
+ result += ` ID: ${file.id}\n`;
1484
+ result += ` Modified: ${modifiedDate} by ${lastModifier}\n`;
1485
+ result += ` Link: ${link}\n\n`;
1486
+ });
1487
+ return result;
1488
+ }
1489
+ catch (error) {
1490
+ const message = getErrorMessage(error);
1491
+ log.error(`Error listing recent files: ${message}`);
1492
+ const code = isGoogleApiError(error) ? error.code : undefined;
1493
+ if (code === 403)
1494
+ throw new UserError('Permission denied. Make sure you have granted Google Drive access to the application.');
1495
+ throw new UserError(`Failed to list recent files: ${message}`);
1496
+ }
1497
+ },
1498
+ });
1499
+ // --- Search Drive ---
1500
+ server.addTool({
1501
+ name: 'searchDrive',
1502
+ description: 'Searches across all files in Google Drive by name, content, or type. More powerful than type-specific search tools.',
1503
+ annotations: {
1504
+ title: 'Search Drive',
1505
+ readOnlyHint: true,
1506
+ openWorldHint: true,
1507
+ },
1508
+ parameters: z.object({
1509
+ account: z
1510
+ .string()
1511
+ .min(1)
1512
+ .describe('The name of the Google account to use. Use listAccounts to see available accounts.'),
1513
+ query: z.string().min(1).describe('Search term to find in file names or content.'),
1514
+ searchIn: z
1515
+ .enum(['name', 'content', 'both'])
1516
+ .optional()
1517
+ .default('both')
1518
+ .describe('Where to search: file names only, file content only, or both.'),
1519
+ fileType: z
1520
+ .enum([
1521
+ 'all',
1522
+ 'documents',
1523
+ 'spreadsheets',
1524
+ 'presentations',
1525
+ 'pdfs',
1526
+ 'images',
1527
+ 'folders',
1528
+ 'videos',
1529
+ 'audio',
1530
+ ])
1531
+ .optional()
1532
+ .default('all')
1533
+ .describe('Filter results by file type.'),
1534
+ inFolder: z
1535
+ .string()
1536
+ .optional()
1537
+ .describe('Folder ID to search within. If not provided, searches entire Drive.'),
1538
+ maxResults: z
1539
+ .number()
1540
+ .int()
1541
+ .min(1)
1542
+ .max(100)
1543
+ .optional()
1544
+ .default(25)
1545
+ .describe('Maximum number of results to return.'),
1546
+ modifiedAfter: z
1547
+ .string()
1548
+ .optional()
1549
+ .describe('Only return files modified after this date (ISO 8601 format, e.g., "2024-01-01").'),
1550
+ }),
1551
+ execute: async (args, { log }) => {
1552
+ const drive = await getDriveClient(args.account);
1553
+ const email = await getAccountEmail(args.account);
1554
+ log.info(`Searching Drive for "${args.query}" in ${args.searchIn}, type: ${args.fileType}`);
1555
+ try {
1556
+ let queryString = 'trashed=false';
1557
+ // Add search query (escaped for security)
1558
+ const safeQuery = escapeDriveQuery(args.query);
1559
+ if (args.searchIn === 'name') {
1560
+ queryString += ` and name contains '${safeQuery}'`;
1561
+ }
1562
+ else if (args.searchIn === 'content') {
1563
+ queryString += ` and fullText contains '${safeQuery}'`;
1564
+ }
1565
+ else {
1566
+ queryString += ` and (name contains '${safeQuery}' or fullText contains '${safeQuery}')`;
1567
+ }
1568
+ // Add file type filter
1569
+ const mimeTypeFilters = {
1570
+ documents: "mimeType='application/vnd.google-apps.document'",
1571
+ spreadsheets: "mimeType='application/vnd.google-apps.spreadsheet'",
1572
+ presentations: "mimeType='application/vnd.google-apps.presentation'",
1573
+ pdfs: "mimeType='application/pdf'",
1574
+ images: "(mimeType contains 'image/' or mimeType='application/vnd.google-apps.photo')",
1575
+ folders: "mimeType='application/vnd.google-apps.folder'",
1576
+ videos: "mimeType contains 'video/'",
1577
+ audio: "mimeType contains 'audio/'",
1578
+ };
1579
+ if (args.fileType !== 'all' && mimeTypeFilters[args.fileType]) {
1580
+ queryString += ` and ${mimeTypeFilters[args.fileType]}`;
1581
+ }
1582
+ // Add folder filter (escaped for security)
1583
+ if (args.inFolder) {
1584
+ const safeFolderId = escapeDriveQuery(args.inFolder);
1585
+ queryString += ` and '${safeFolderId}' in parents`;
1586
+ }
1587
+ // Add date filter (escaped for security)
1588
+ if (args.modifiedAfter) {
1589
+ const safeDate = escapeDriveQuery(args.modifiedAfter);
1590
+ queryString += ` and modifiedTime > '${safeDate}'`;
1591
+ }
1592
+ // Don't use orderBy when query contains fullText search (Google Drive API limitation)
1593
+ const orderBy = args.searchIn === 'name' ? 'modifiedTime desc' : undefined;
1594
+ const response = await drive.files.list({
1595
+ q: queryString,
1596
+ pageSize: args.maxResults,
1597
+ orderBy,
1598
+ fields: 'files(id,name,mimeType,size,modifiedTime,webViewLink,owners(displayName),parents)',
1599
+ });
1600
+ const files = response.data.files ?? [];
1601
+ if (files.length === 0) {
1602
+ let notFoundMsg = `No files found matching "${args.query}"`;
1603
+ if (args.fileType !== 'all')
1604
+ notFoundMsg += ` (type: ${args.fileType})`;
1605
+ if (args.inFolder)
1606
+ notFoundMsg += ' in the specified folder';
1607
+ return notFoundMsg + '.';
1608
+ }
1609
+ let result = `Found ${files.length} file(s) matching "${args.query}":\n\n`;
1610
+ files.forEach((file, index) => {
1611
+ const modifiedDate = file.modifiedTime
1612
+ ? new Date(file.modifiedTime).toLocaleDateString()
1613
+ : 'Unknown';
1614
+ const owner = file.owners?.[0]?.displayName || 'Unknown';
1615
+ // Determine file type label
1616
+ let typeLabel = 'File';
1617
+ if (file.mimeType === 'application/vnd.google-apps.document')
1618
+ typeLabel = 'Doc';
1619
+ else if (file.mimeType === 'application/vnd.google-apps.spreadsheet')
1620
+ typeLabel = 'Sheet';
1621
+ else if (file.mimeType === 'application/vnd.google-apps.presentation')
1622
+ typeLabel = 'Slides';
1623
+ else if (file.mimeType === 'application/vnd.google-apps.folder')
1624
+ typeLabel = 'Folder';
1625
+ else if (file.mimeType === 'application/pdf')
1626
+ typeLabel = 'PDF';
1627
+ else if (file.mimeType?.startsWith('image/'))
1628
+ typeLabel = 'Image';
1629
+ else if (file.mimeType?.startsWith('video/'))
1630
+ typeLabel = 'Video';
1631
+ else if (file.mimeType?.startsWith('audio/'))
1632
+ typeLabel = 'Audio';
1633
+ // Generate appropriate link
1634
+ let link;
1635
+ if (file.id) {
1636
+ if (file.mimeType === 'application/vnd.google-apps.document') {
1637
+ link = getDocsUrl(file.id, email);
1638
+ }
1639
+ else if (file.mimeType === 'application/vnd.google-apps.spreadsheet') {
1640
+ link = addAuthUserToUrl(`https://docs.google.com/spreadsheets/d/${file.id}/edit`, email);
1641
+ }
1642
+ else if (file.mimeType === 'application/vnd.google-apps.presentation') {
1643
+ link = addAuthUserToUrl(`https://docs.google.com/presentation/d/${file.id}/edit`, email);
1644
+ }
1645
+ else if (file.mimeType === 'application/vnd.google-apps.folder') {
1646
+ link = getDriveFolderUrl(file.id, email);
1647
+ }
1648
+ else {
1649
+ link = file.webViewLink
1650
+ ? addAuthUserToUrl(file.webViewLink, email)
1651
+ : getDriveFileUrl(file.id, email);
1652
+ }
1653
+ }
1654
+ else {
1655
+ link = file.webViewLink;
1656
+ }
1657
+ result += `${index + 1}. [${typeLabel}] **${file.name}**\n`;
1658
+ result += ` ID: ${file.id}\n`;
1659
+ result += ` Modified: ${modifiedDate} by ${owner}\n`;
1660
+ result += ` Link: ${link}\n\n`;
1661
+ });
1662
+ return result;
1663
+ }
1664
+ catch (error) {
1665
+ const message = getErrorMessage(error);
1666
+ log.error(`Error searching Drive: ${message}`);
1667
+ const code = isGoogleApiError(error) ? error.code : undefined;
1668
+ if (code === 403)
1669
+ throw new UserError('Permission denied. Make sure you have granted Google Drive access to the application.');
1670
+ throw new UserError(`Failed to search Drive: ${message}`);
1671
+ }
1672
+ },
1673
+ });
1000
1674
  }
1001
1675
  //# sourceMappingURL=drive.tools.js.map