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
|
@@ -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
|
-
|
|
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 '${
|
|
145
|
+
queryString += ` and name contains '${safeSearchQuery}'`;
|
|
138
146
|
}
|
|
139
147
|
else if (args.searchIn === 'content') {
|
|
140
|
-
queryString += ` and fullText contains '${
|
|
148
|
+
queryString += ` and fullText contains '${safeSearchQuery}'`;
|
|
141
149
|
}
|
|
142
150
|
else {
|
|
143
|
-
queryString += ` and (name contains '${
|
|
151
|
+
queryString += ` and (name contains '${safeSearchQuery}' or fullText contains '${safeSearchQuery}')`;
|
|
144
152
|
}
|
|
145
153
|
if (args.modifiedAfter) {
|
|
146
|
-
|
|
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
|
-
|
|
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
|