mcp-google-multi 4.2.0
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/LICENSE +21 -0
- package/README.md +439 -0
- package/dist/accounts.d.ts +10 -0
- package/dist/accounts.js +53 -0
- package/dist/auth.d.ts +15 -0
- package/dist/auth.js +178 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +58 -0
- package/dist/tools/_errors.d.ts +9 -0
- package/dist/tools/_errors.js +42 -0
- package/dist/tools/admin.d.ts +17 -0
- package/dist/tools/admin.js +307 -0
- package/dist/tools/calendar.d.ts +2 -0
- package/dist/tools/calendar.js +399 -0
- package/dist/tools/chat.d.ts +2 -0
- package/dist/tools/chat.js +121 -0
- package/dist/tools/contacts.d.ts +2 -0
- package/dist/tools/contacts.js +368 -0
- package/dist/tools/docs.d.ts +36 -0
- package/dist/tools/docs.js +1123 -0
- package/dist/tools/drive.d.ts +2 -0
- package/dist/tools/drive.js +1082 -0
- package/dist/tools/forms.d.ts +2 -0
- package/dist/tools/forms.js +96 -0
- package/dist/tools/gmail-mime.d.ts +16 -0
- package/dist/tools/gmail-mime.js +64 -0
- package/dist/tools/gmail.d.ts +2 -0
- package/dist/tools/gmail.js +651 -0
- package/dist/tools/meet.d.ts +2 -0
- package/dist/tools/meet.js +133 -0
- package/dist/tools/searchconsole.d.ts +2 -0
- package/dist/tools/searchconsole.js +258 -0
- package/dist/tools/sheets.d.ts +34 -0
- package/dist/tools/sheets.js +1207 -0
- package/dist/tools/tasks.d.ts +2 -0
- package/dist/tools/tasks.js +316 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.js +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { google } from 'googleapis';
|
|
3
|
+
import { ACCOUNTS } from '../accounts.js';
|
|
4
|
+
import { getClient } from '../client.js';
|
|
5
|
+
import { handleGoogleApiError } from './_errors.js';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
9
|
+
import mime from 'mime-types';
|
|
10
|
+
const accountEnum = z.enum(ACCOUNTS);
|
|
11
|
+
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
|
12
|
+
const GOOGLE_WORKSPACE_TYPES = new Set([
|
|
13
|
+
'application/vnd.google-apps.document',
|
|
14
|
+
'application/vnd.google-apps.spreadsheet',
|
|
15
|
+
'application/vnd.google-apps.presentation',
|
|
16
|
+
'application/vnd.google-apps.drawing',
|
|
17
|
+
]);
|
|
18
|
+
// Comment/Reply fields list — Drive API requires explicit `fields` on every call.
|
|
19
|
+
const COMMENT_BASE_FIELDS = 'id,kind,content,htmlContent,createdTime,modifiedTime,resolved,anchor,author,deleted,quotedFileContent';
|
|
20
|
+
const REPLY_SUBFIELDS = 'id,content,action,createdTime,modifiedTime,author,deleted';
|
|
21
|
+
const COMMENT_FIELDS = `${COMMENT_BASE_FIELDS},replies(${REPLY_SUBFIELDS})`;
|
|
22
|
+
const COMMENT_LIST_FIELDS = `nextPageToken,comments(${COMMENT_BASE_FIELDS},replies(${REPLY_SUBFIELDS}))`;
|
|
23
|
+
const REPLY_FIELDS = `kind,htmlContent,${REPLY_SUBFIELDS}`;
|
|
24
|
+
const REPLY_LIST_FIELDS = `nextPageToken,replies(${REPLY_FIELDS})`;
|
|
25
|
+
export function registerDriveTools(server) {
|
|
26
|
+
// ─── Read / search / list ──────────────────────────────────────────────
|
|
27
|
+
server.registerTool('drive_search', {
|
|
28
|
+
description: 'Search files in a Google Drive account',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
account: accountEnum.describe('Google account alias'),
|
|
31
|
+
query: z.string().describe('Drive search syntax, e.g. "name contains \'MoU\'"'),
|
|
32
|
+
maxResults: z.number().min(1).max(100).default(20).optional()
|
|
33
|
+
.describe('Max results to return (default: 20)'),
|
|
34
|
+
driveId: z.string().optional().describe('Optional shared drive ID'),
|
|
35
|
+
},
|
|
36
|
+
}, async ({ account, query, maxResults, driveId }) => {
|
|
37
|
+
try {
|
|
38
|
+
const auth = await getClient(account);
|
|
39
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
40
|
+
const params = {
|
|
41
|
+
q: query,
|
|
42
|
+
pageSize: maxResults ?? 20,
|
|
43
|
+
fields: 'files(id,name,mimeType,modifiedTime,webViewLink,size,parents,driveId)',
|
|
44
|
+
supportsAllDrives: true,
|
|
45
|
+
includeItemsFromAllDrives: true,
|
|
46
|
+
};
|
|
47
|
+
if (driveId) {
|
|
48
|
+
params.driveId = driveId;
|
|
49
|
+
params.corpora = 'drive';
|
|
50
|
+
}
|
|
51
|
+
const res = await drive.files.list(params);
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: JSON.stringify(res.data.files ?? [], null, 2) }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return handleDriveError(error, account);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
server.registerTool('drive_read', {
|
|
61
|
+
description: 'Read the content of a Google Drive file',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
account: accountEnum.describe('Google account alias'),
|
|
64
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
65
|
+
},
|
|
66
|
+
}, async ({ account, fileId }) => {
|
|
67
|
+
try {
|
|
68
|
+
const auth = await getClient(account);
|
|
69
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
70
|
+
const meta = await drive.files.get({
|
|
71
|
+
fileId,
|
|
72
|
+
fields: 'id,name,mimeType,size,webViewLink',
|
|
73
|
+
supportsAllDrives: true,
|
|
74
|
+
});
|
|
75
|
+
const { name, mimeType, size, webViewLink } = meta.data;
|
|
76
|
+
if (mimeType && GOOGLE_WORKSPACE_TYPES.has(mimeType)) {
|
|
77
|
+
const exported = await drive.files.export({
|
|
78
|
+
fileId,
|
|
79
|
+
mimeType: 'text/plain',
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
content: [{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: JSON.stringify({
|
|
85
|
+
id: fileId,
|
|
86
|
+
name,
|
|
87
|
+
mimeType,
|
|
88
|
+
content: String(exported.data),
|
|
89
|
+
}, null, 2),
|
|
90
|
+
}],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (mimeType === 'application/pdf') {
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: JSON.stringify({
|
|
98
|
+
id: fileId,
|
|
99
|
+
name,
|
|
100
|
+
mimeType,
|
|
101
|
+
error: 'binary',
|
|
102
|
+
webViewLink,
|
|
103
|
+
}, null, 2),
|
|
104
|
+
}],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const fileSize = parseInt(size ?? '0', 10);
|
|
108
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: JSON.stringify({
|
|
113
|
+
id: fileId,
|
|
114
|
+
name,
|
|
115
|
+
mimeType,
|
|
116
|
+
error: 'too_large',
|
|
117
|
+
webViewLink,
|
|
118
|
+
}, null, 2),
|
|
119
|
+
}],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (mimeType?.startsWith('text/')) {
|
|
123
|
+
const downloaded = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'text' });
|
|
124
|
+
return {
|
|
125
|
+
content: [{
|
|
126
|
+
type: 'text',
|
|
127
|
+
text: JSON.stringify({
|
|
128
|
+
id: fileId,
|
|
129
|
+
name,
|
|
130
|
+
mimeType,
|
|
131
|
+
content: String(downloaded.data),
|
|
132
|
+
}, null, 2),
|
|
133
|
+
}],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: JSON.stringify({
|
|
140
|
+
id: fileId,
|
|
141
|
+
name,
|
|
142
|
+
mimeType,
|
|
143
|
+
error: 'binary',
|
|
144
|
+
webViewLink,
|
|
145
|
+
}, null, 2),
|
|
146
|
+
}],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return handleDriveError(error, account);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
server.registerTool('drive_list', {
|
|
154
|
+
description: 'List files in a Google Drive folder or root',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
account: accountEnum.describe('Google account alias'),
|
|
157
|
+
folderId: z.string().optional().describe('Folder ID (omit for root)'),
|
|
158
|
+
maxResults: z.number().min(1).max(100).default(50).optional()
|
|
159
|
+
.describe('Max results to return (default: 50)'),
|
|
160
|
+
},
|
|
161
|
+
}, async ({ account, folderId, maxResults }) => {
|
|
162
|
+
try {
|
|
163
|
+
const auth = await getClient(account);
|
|
164
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
165
|
+
// Escape single quotes per Drive query syntax to prevent breaking out of the literal.
|
|
166
|
+
const parent = (folderId ?? 'root').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
167
|
+
const res = await drive.files.list({
|
|
168
|
+
q: `'${parent}' in parents and trashed = false`,
|
|
169
|
+
pageSize: maxResults ?? 50,
|
|
170
|
+
fields: 'files(id,name,mimeType,modifiedTime,webViewLink,size,parents)',
|
|
171
|
+
supportsAllDrives: true,
|
|
172
|
+
includeItemsFromAllDrives: true,
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: 'text', text: JSON.stringify(res.data.files ?? [], null, 2) }],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return handleDriveError(error, account);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
// ─── Write / upload / download ─────────────────────────────────────────
|
|
183
|
+
server.registerTool('drive_upload', {
|
|
184
|
+
description: 'Upload a local file to Google Drive. Pass `convertTo` to import it as a native, editable Google Doc/Sheet/Slides/Drawing instead of storing the raw bytes.',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
account: accountEnum.describe('Google account alias'),
|
|
187
|
+
localPath: z.string().describe('Absolute path to file on disk'),
|
|
188
|
+
filename: z.string().describe('Name as it appears in Drive'),
|
|
189
|
+
mimeType: z.string().optional().describe('Source MIME type of the local file (inferred from extension if omitted). With `convertTo`, this is the format Drive imports from.'),
|
|
190
|
+
convertTo: z.enum([
|
|
191
|
+
'application/vnd.google-apps.document',
|
|
192
|
+
'application/vnd.google-apps.spreadsheet',
|
|
193
|
+
'application/vnd.google-apps.presentation',
|
|
194
|
+
'application/vnd.google-apps.drawing',
|
|
195
|
+
]).optional().describe('Convert the upload into this native Google Workspace type on import (e.g. upload .md/.html/.docx/.txt with convertTo=...google-apps.document to get a real Google Doc). Source must be an importable format. Omit to store the file as-is.'),
|
|
196
|
+
parentFolderId: z.string().optional().describe('Parent folder ID (defaults to My Drive root)'),
|
|
197
|
+
},
|
|
198
|
+
}, async ({ account, localPath, filename, mimeType: mimeTypeArg, convertTo, parentFolderId }) => {
|
|
199
|
+
try {
|
|
200
|
+
const auth = await getClient(account);
|
|
201
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
202
|
+
const resolvedMime = mimeTypeArg ?? (mime.lookup(localPath) || 'application/octet-stream');
|
|
203
|
+
const fileStream = fs.createReadStream(localPath);
|
|
204
|
+
const res = await drive.files.create({
|
|
205
|
+
requestBody: {
|
|
206
|
+
name: filename,
|
|
207
|
+
parents: parentFolderId ? [parentFolderId] : undefined,
|
|
208
|
+
// Setting a google-apps target type makes Drive convert the media on import.
|
|
209
|
+
...(convertTo ? { mimeType: convertTo } : {}),
|
|
210
|
+
},
|
|
211
|
+
media: {
|
|
212
|
+
mimeType: resolvedMime,
|
|
213
|
+
body: fileStream,
|
|
214
|
+
},
|
|
215
|
+
fields: 'id,name,mimeType,webViewLink,size',
|
|
216
|
+
supportsAllDrives: true,
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return handleDriveError(error, account);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
server.registerTool('drive_download', {
|
|
227
|
+
description: 'Download a binary file from Drive to local disk. For Google Workspace formats (Docs, Sheets, Slides), use drive_export instead.',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
account: accountEnum.describe('Google account alias'),
|
|
230
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
231
|
+
savePath: z.string().describe('Absolute directory path to save into'),
|
|
232
|
+
filename: z.string().describe('Filename to save as'),
|
|
233
|
+
},
|
|
234
|
+
}, async ({ account, fileId, savePath, filename }) => {
|
|
235
|
+
try {
|
|
236
|
+
const auth = await getClient(account);
|
|
237
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
238
|
+
const dest = path.join(savePath, path.basename(filename));
|
|
239
|
+
const res = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'stream' });
|
|
240
|
+
// pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
|
|
241
|
+
await pipeline(res.data, fs.createWriteStream(dest));
|
|
242
|
+
const { size } = fs.statSync(dest);
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: 'text', text: JSON.stringify({ savedPath: dest, bytes: size }, null, 2) }],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return handleDriveError(error, account);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
server.registerTool('drive_export', {
|
|
252
|
+
description: 'Export a Google Workspace document (Doc, Sheet, Slide) to a standard format and save to disk. Supported: PDF, DOCX, XLSX, PPTX, TXT, CSV, Markdown (text/markdown for Docs).',
|
|
253
|
+
inputSchema: {
|
|
254
|
+
account: accountEnum.describe('Google account alias'),
|
|
255
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
256
|
+
mimeType: z.string().describe('Target export MIME type (e.g. "application/pdf", "text/markdown", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")'),
|
|
257
|
+
savePath: z.string().describe('Absolute directory path to save into'),
|
|
258
|
+
filename: z.string().describe('Filename to save as'),
|
|
259
|
+
},
|
|
260
|
+
}, async ({ account, fileId, mimeType: exportMime, savePath, filename }) => {
|
|
261
|
+
try {
|
|
262
|
+
const auth = await getClient(account);
|
|
263
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
264
|
+
const dest = path.join(savePath, path.basename(filename));
|
|
265
|
+
const res = await drive.files.export({ fileId, mimeType: exportMime }, { responseType: 'stream' });
|
|
266
|
+
// pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
|
|
267
|
+
await pipeline(res.data, fs.createWriteStream(dest));
|
|
268
|
+
const { size } = fs.statSync(dest);
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: 'text', text: JSON.stringify({ savedPath: dest, bytes: size }, null, 2) }],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return handleDriveError(error, account);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
server.registerTool('drive_create_folder', {
|
|
278
|
+
description: 'Create a new folder in Google Drive',
|
|
279
|
+
inputSchema: {
|
|
280
|
+
account: accountEnum.describe('Google account alias'),
|
|
281
|
+
name: z.string().describe('Folder name'),
|
|
282
|
+
parentFolderId: z.string().optional().describe('Parent folder ID (defaults to My Drive root)'),
|
|
283
|
+
},
|
|
284
|
+
}, async ({ account, name, parentFolderId }) => {
|
|
285
|
+
try {
|
|
286
|
+
const auth = await getClient(account);
|
|
287
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
288
|
+
const res = await drive.files.create({
|
|
289
|
+
requestBody: {
|
|
290
|
+
name,
|
|
291
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
292
|
+
parents: parentFolderId ? [parentFolderId] : undefined,
|
|
293
|
+
},
|
|
294
|
+
fields: 'id,name,webViewLink',
|
|
295
|
+
supportsAllDrives: true,
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
return handleDriveError(error, account);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
server.registerTool('drive_update', {
|
|
306
|
+
description: 'Rename, move, or replace content of a Drive file. Any combination in one call. For untrash, use drive_untrash.',
|
|
307
|
+
inputSchema: {
|
|
308
|
+
account: accountEnum.describe('Google account alias'),
|
|
309
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
310
|
+
newName: z.string().optional().describe('New filename'),
|
|
311
|
+
newParentFolderId: z.string().optional().describe('Move to this folder'),
|
|
312
|
+
localPath: z.string().optional().describe('Replace file content with this local file'),
|
|
313
|
+
mimeType: z.string().optional().describe('MIME type of the replacement file (required if localPath is provided)'),
|
|
314
|
+
},
|
|
315
|
+
}, async ({ account, fileId, newName, newParentFolderId, localPath: localPathArg, mimeType: mimeTypeArg }) => {
|
|
316
|
+
try {
|
|
317
|
+
const auth = await getClient(account);
|
|
318
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
319
|
+
const requestBody = {};
|
|
320
|
+
if (newName)
|
|
321
|
+
requestBody.name = newName;
|
|
322
|
+
const params = {
|
|
323
|
+
fileId,
|
|
324
|
+
requestBody,
|
|
325
|
+
fields: 'id,name,parents,modifiedTime',
|
|
326
|
+
supportsAllDrives: true,
|
|
327
|
+
};
|
|
328
|
+
if (newParentFolderId) {
|
|
329
|
+
const current = await drive.files.get({ fileId, fields: 'parents', supportsAllDrives: true });
|
|
330
|
+
params.removeParents = (current.data.parents ?? []).join(',');
|
|
331
|
+
params.addParents = newParentFolderId;
|
|
332
|
+
}
|
|
333
|
+
if (localPathArg) {
|
|
334
|
+
params.media = {
|
|
335
|
+
mimeType: mimeTypeArg ?? (mime.lookup(localPathArg) || 'application/octet-stream'),
|
|
336
|
+
body: fs.createReadStream(localPathArg),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const res = await drive.files.update(params);
|
|
340
|
+
return {
|
|
341
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
return handleDriveError(error, account);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
// ─── Trash / delete ────────────────────────────────────────────────────
|
|
349
|
+
server.registerTool('drive_delete', {
|
|
350
|
+
description: 'Permanently delete a file or folder from Google Drive. Irreversible. Use drive_trash for recoverable deletion.',
|
|
351
|
+
inputSchema: {
|
|
352
|
+
account: accountEnum.describe('Google account alias'),
|
|
353
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
354
|
+
},
|
|
355
|
+
}, async ({ account, fileId }) => {
|
|
356
|
+
try {
|
|
357
|
+
const auth = await getClient(account);
|
|
358
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
359
|
+
await drive.files.delete({ fileId, supportsAllDrives: true });
|
|
360
|
+
return {
|
|
361
|
+
content: [{ type: 'text', text: JSON.stringify({ deleted: true, fileId }, null, 2) }],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
return handleDriveError(error, account);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
server.registerTool('drive_trash', {
|
|
369
|
+
description: 'Move a file to Google Drive trash. Recoverable from Drive UI or via drive_untrash.',
|
|
370
|
+
inputSchema: {
|
|
371
|
+
account: accountEnum.describe('Google account alias'),
|
|
372
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
373
|
+
},
|
|
374
|
+
}, async ({ account, fileId }) => {
|
|
375
|
+
try {
|
|
376
|
+
const auth = await getClient(account);
|
|
377
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
378
|
+
await drive.files.update({
|
|
379
|
+
fileId,
|
|
380
|
+
requestBody: { trashed: true },
|
|
381
|
+
supportsAllDrives: true,
|
|
382
|
+
});
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: 'text', text: JSON.stringify({ trashed: true, fileId }, null, 2) }],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
return handleDriveError(error, account);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
server.registerTool('drive_untrash', {
|
|
392
|
+
description: 'Restore a trashed file from Google Drive trash back to its previous location.',
|
|
393
|
+
inputSchema: {
|
|
394
|
+
account: accountEnum.describe('Google account alias'),
|
|
395
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
396
|
+
},
|
|
397
|
+
}, async ({ account, fileId }) => {
|
|
398
|
+
try {
|
|
399
|
+
const auth = await getClient(account);
|
|
400
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
401
|
+
const res = await drive.files.update({
|
|
402
|
+
fileId,
|
|
403
|
+
requestBody: { trashed: false },
|
|
404
|
+
fields: 'id,name,trashed,parents',
|
|
405
|
+
supportsAllDrives: true,
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
return handleDriveError(error, account);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
server.registerTool('drive_empty_trash', {
|
|
416
|
+
description: 'Permanently delete every file currently in the account\'s trash. Irreversible.',
|
|
417
|
+
inputSchema: {
|
|
418
|
+
account: accountEnum.describe('Google account alias'),
|
|
419
|
+
},
|
|
420
|
+
}, async ({ account }) => {
|
|
421
|
+
try {
|
|
422
|
+
const auth = await getClient(account);
|
|
423
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
424
|
+
await drive.files.emptyTrash({});
|
|
425
|
+
return {
|
|
426
|
+
content: [{ type: 'text', text: JSON.stringify({ emptied: true }, null, 2) }],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
return handleDriveError(error, account);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
// ─── Copy / move ───────────────────────────────────────────────────────
|
|
434
|
+
server.registerTool('drive_copy', {
|
|
435
|
+
description: 'Duplicate a file in Google Drive (does not work on folders)',
|
|
436
|
+
inputSchema: {
|
|
437
|
+
account: accountEnum.describe('Google account alias'),
|
|
438
|
+
fileId: z.string().describe('Google Drive file ID to copy'),
|
|
439
|
+
newName: z.string().optional().describe('Name for the copy (default: "Copy of <original>")'),
|
|
440
|
+
parentFolderId: z.string().optional().describe('Where to put the copy (default: same folder)'),
|
|
441
|
+
},
|
|
442
|
+
}, async ({ account, fileId, newName, parentFolderId }) => {
|
|
443
|
+
try {
|
|
444
|
+
const auth = await getClient(account);
|
|
445
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
446
|
+
const res = await drive.files.copy({
|
|
447
|
+
fileId,
|
|
448
|
+
requestBody: {
|
|
449
|
+
name: newName,
|
|
450
|
+
parents: parentFolderId ? [parentFolderId] : undefined,
|
|
451
|
+
},
|
|
452
|
+
fields: 'id,name,webViewLink',
|
|
453
|
+
supportsAllDrives: true,
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
return handleDriveError(error, account);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
server.registerTool('drive_move', {
|
|
464
|
+
description: 'Move a file between folders by replacing its parents. To move to multiple parents, list all of them.',
|
|
465
|
+
inputSchema: {
|
|
466
|
+
account: accountEnum.describe('Google account alias'),
|
|
467
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
468
|
+
newParentFolderId: z.string().describe('Destination folder ID'),
|
|
469
|
+
},
|
|
470
|
+
}, async ({ account, fileId, newParentFolderId }) => {
|
|
471
|
+
try {
|
|
472
|
+
const auth = await getClient(account);
|
|
473
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
474
|
+
const current = await drive.files.get({ fileId, fields: 'parents', supportsAllDrives: true });
|
|
475
|
+
const res = await drive.files.update({
|
|
476
|
+
fileId,
|
|
477
|
+
addParents: newParentFolderId,
|
|
478
|
+
removeParents: (current.data.parents ?? []).join(','),
|
|
479
|
+
fields: 'id,name,parents,modifiedTime',
|
|
480
|
+
supportsAllDrives: true,
|
|
481
|
+
});
|
|
482
|
+
return {
|
|
483
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
return handleDriveError(error, account);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
// ─── Permissions / sharing ─────────────────────────────────────────────
|
|
491
|
+
server.registerTool('drive_share', {
|
|
492
|
+
description: 'Share a file or folder with a user, group, domain, or anyone with the link',
|
|
493
|
+
inputSchema: {
|
|
494
|
+
account: accountEnum.describe('Google account alias'),
|
|
495
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
496
|
+
type: z.enum(['user', 'group', 'domain', 'anyone']).describe('Permission type'),
|
|
497
|
+
role: z.enum(['reader', 'commenter', 'writer', 'fileOrganizer', 'organizer', 'owner']).describe('Permission role'),
|
|
498
|
+
emailAddress: z.string().optional().describe('Required when type is "user" or "group"'),
|
|
499
|
+
domain: z.string().optional().describe('Required when type is "domain"'),
|
|
500
|
+
sendNotification: z.boolean().optional().describe('Send notification email (default: true)'),
|
|
501
|
+
emailMessage: z.string().optional().describe('Custom message in notification email'),
|
|
502
|
+
transferOwnership: z.boolean().optional().describe('Transfer ownership to the recipient. Requires role="owner". Recipient must accept ownership.'),
|
|
503
|
+
expirationTime: z.string().optional().describe('RFC 3339 timestamp when access expires. Only valid for role="reader" or "commenter".'),
|
|
504
|
+
},
|
|
505
|
+
}, async ({ account, fileId, type, role, emailAddress, domain, sendNotification, emailMessage, transferOwnership, expirationTime }) => {
|
|
506
|
+
try {
|
|
507
|
+
const auth = await getClient(account);
|
|
508
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
509
|
+
const requestBody = { type, role, emailAddress, domain };
|
|
510
|
+
if (expirationTime)
|
|
511
|
+
requestBody.expirationTime = expirationTime;
|
|
512
|
+
const res = await drive.permissions.create({
|
|
513
|
+
fileId,
|
|
514
|
+
sendNotificationEmail: sendNotification ?? true,
|
|
515
|
+
emailMessage,
|
|
516
|
+
transferOwnership: transferOwnership ?? false,
|
|
517
|
+
supportsAllDrives: true,
|
|
518
|
+
requestBody,
|
|
519
|
+
fields: 'id,type,role,emailAddress,domain,expirationTime',
|
|
520
|
+
});
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
return handleDriveError(error, account);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
server.registerTool('drive_list_permissions', {
|
|
530
|
+
description: 'List all people and groups who have access to a Drive file or folder',
|
|
531
|
+
inputSchema: {
|
|
532
|
+
account: accountEnum.describe('Google account alias'),
|
|
533
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
534
|
+
},
|
|
535
|
+
}, async ({ account, fileId }) => {
|
|
536
|
+
try {
|
|
537
|
+
const auth = await getClient(account);
|
|
538
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
539
|
+
const res = await drive.permissions.list({
|
|
540
|
+
fileId,
|
|
541
|
+
fields: 'permissions(id,type,role,emailAddress,domain,displayName,expirationTime)',
|
|
542
|
+
supportsAllDrives: true,
|
|
543
|
+
});
|
|
544
|
+
return {
|
|
545
|
+
content: [{ type: 'text', text: JSON.stringify(res.data.permissions ?? [], null, 2) }],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
return handleDriveError(error, account);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
server.registerTool('drive_permission_update', {
|
|
553
|
+
description: 'Change the role and/or expirationTime of an existing permission without removing it. Use "removeExpiration=true" to clear an existing expirationTime.',
|
|
554
|
+
inputSchema: {
|
|
555
|
+
account: accountEnum.describe('Google account alias'),
|
|
556
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
557
|
+
permissionId: z.string().describe('Permission ID from drive_list_permissions'),
|
|
558
|
+
role: z.enum(['reader', 'commenter', 'writer', 'fileOrganizer', 'organizer', 'owner']).optional()
|
|
559
|
+
.describe('New role'),
|
|
560
|
+
expirationTime: z.string().optional()
|
|
561
|
+
.describe('New RFC 3339 expiration timestamp. Only valid for role "reader" or "commenter".'),
|
|
562
|
+
removeExpiration: z.boolean().optional()
|
|
563
|
+
.describe('Clear the existing expirationTime'),
|
|
564
|
+
transferOwnership: z.boolean().optional()
|
|
565
|
+
.describe('Promote to owner. Requires role="owner".'),
|
|
566
|
+
},
|
|
567
|
+
}, async ({ account, fileId, permissionId, role, expirationTime, removeExpiration, transferOwnership }) => {
|
|
568
|
+
try {
|
|
569
|
+
const auth = await getClient(account);
|
|
570
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
571
|
+
const requestBody = {};
|
|
572
|
+
if (role)
|
|
573
|
+
requestBody.role = role;
|
|
574
|
+
if (expirationTime)
|
|
575
|
+
requestBody.expirationTime = expirationTime;
|
|
576
|
+
const res = await drive.permissions.update({
|
|
577
|
+
fileId,
|
|
578
|
+
permissionId,
|
|
579
|
+
requestBody,
|
|
580
|
+
removeExpiration: removeExpiration ?? false,
|
|
581
|
+
transferOwnership: transferOwnership ?? false,
|
|
582
|
+
supportsAllDrives: true,
|
|
583
|
+
fields: 'id,type,role,emailAddress,expirationTime',
|
|
584
|
+
});
|
|
585
|
+
return {
|
|
586
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
return handleDriveError(error, account);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
server.registerTool('drive_remove_permission', {
|
|
594
|
+
description: 'Revoke access to a Drive file for a specific permission',
|
|
595
|
+
inputSchema: {
|
|
596
|
+
account: accountEnum.describe('Google account alias'),
|
|
597
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
598
|
+
permissionId: z.string().describe('Permission ID from drive_list_permissions'),
|
|
599
|
+
},
|
|
600
|
+
}, async ({ account, fileId, permissionId }) => {
|
|
601
|
+
try {
|
|
602
|
+
const auth = await getClient(account);
|
|
603
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
604
|
+
await drive.permissions.delete({ fileId, permissionId, supportsAllDrives: true });
|
|
605
|
+
return {
|
|
606
|
+
content: [{ type: 'text', text: JSON.stringify({ removed: true, permissionId }, null, 2) }],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
return handleDriveError(error, account);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
// ─── Comments ──────────────────────────────────────────────────────────
|
|
614
|
+
server.registerTool('drive_comment_create', {
|
|
615
|
+
description: 'Create a comment on a Drive file. Works on Docs, Sheets, Slides, PDFs, and any Drive file. The optional anchor is a JSON string describing the document region (see Drive "Manage comments" guide).',
|
|
616
|
+
inputSchema: {
|
|
617
|
+
account: accountEnum.describe('Google account alias'),
|
|
618
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
619
|
+
content: z.string().describe('Plain text comment content'),
|
|
620
|
+
anchor: z.string().optional().describe('Region anchor (JSON string). Optional.'),
|
|
621
|
+
quotedFileContent: z.object({
|
|
622
|
+
mimeType: z.string(),
|
|
623
|
+
value: z.string(),
|
|
624
|
+
}).optional().describe('Optional reference to quoted file content'),
|
|
625
|
+
},
|
|
626
|
+
}, async ({ account, fileId, content, anchor, quotedFileContent }) => {
|
|
627
|
+
try {
|
|
628
|
+
const auth = await getClient(account);
|
|
629
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
630
|
+
const requestBody = { content };
|
|
631
|
+
if (anchor)
|
|
632
|
+
requestBody.anchor = anchor;
|
|
633
|
+
if (quotedFileContent)
|
|
634
|
+
requestBody.quotedFileContent = quotedFileContent;
|
|
635
|
+
const res = await drive.comments.create({
|
|
636
|
+
fileId,
|
|
637
|
+
requestBody,
|
|
638
|
+
fields: COMMENT_FIELDS,
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
return handleDriveError(error, account);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
server.registerTool('drive_comment_list', {
|
|
649
|
+
description: 'List comments on a Drive file with pagination',
|
|
650
|
+
inputSchema: {
|
|
651
|
+
account: accountEnum.describe('Google account alias'),
|
|
652
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
653
|
+
includeDeleted: z.boolean().optional().describe('Include deleted comments (default: false)'),
|
|
654
|
+
pageSize: z.number().min(1).max(100).optional().describe('Max comments per page (default: 20)'),
|
|
655
|
+
pageToken: z.string().optional().describe('Token from a previous page'),
|
|
656
|
+
startModifiedTime: z.string().optional().describe('Only return comments modified after this RFC 3339 timestamp'),
|
|
657
|
+
},
|
|
658
|
+
}, async ({ account, fileId, includeDeleted, pageSize, pageToken, startModifiedTime }) => {
|
|
659
|
+
try {
|
|
660
|
+
const auth = await getClient(account);
|
|
661
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
662
|
+
const res = await drive.comments.list({
|
|
663
|
+
fileId,
|
|
664
|
+
includeDeleted: includeDeleted ?? false,
|
|
665
|
+
pageSize: pageSize ?? 20,
|
|
666
|
+
pageToken,
|
|
667
|
+
startModifiedTime,
|
|
668
|
+
fields: COMMENT_LIST_FIELDS,
|
|
669
|
+
});
|
|
670
|
+
return {
|
|
671
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
return handleDriveError(error, account);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
server.registerTool('drive_comment_get', {
|
|
679
|
+
description: 'Get a single comment by ID',
|
|
680
|
+
inputSchema: {
|
|
681
|
+
account: accountEnum.describe('Google account alias'),
|
|
682
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
683
|
+
commentId: z.string().describe('Comment ID'),
|
|
684
|
+
includeDeleted: z.boolean().optional(),
|
|
685
|
+
},
|
|
686
|
+
}, async ({ account, fileId, commentId, includeDeleted }) => {
|
|
687
|
+
try {
|
|
688
|
+
const auth = await getClient(account);
|
|
689
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
690
|
+
const res = await drive.comments.get({
|
|
691
|
+
fileId,
|
|
692
|
+
commentId,
|
|
693
|
+
includeDeleted: includeDeleted ?? false,
|
|
694
|
+
fields: COMMENT_FIELDS,
|
|
695
|
+
});
|
|
696
|
+
return {
|
|
697
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
return handleDriveError(error, account);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
server.registerTool('drive_comment_update', {
|
|
705
|
+
description: 'Edit the content of an existing comment (PATCH semantics)',
|
|
706
|
+
inputSchema: {
|
|
707
|
+
account: accountEnum.describe('Google account alias'),
|
|
708
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
709
|
+
commentId: z.string().describe('Comment ID'),
|
|
710
|
+
content: z.string().describe('New plain text content'),
|
|
711
|
+
},
|
|
712
|
+
}, async ({ account, fileId, commentId, content }) => {
|
|
713
|
+
try {
|
|
714
|
+
const auth = await getClient(account);
|
|
715
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
716
|
+
const res = await drive.comments.update({
|
|
717
|
+
fileId,
|
|
718
|
+
commentId,
|
|
719
|
+
requestBody: { content },
|
|
720
|
+
fields: COMMENT_FIELDS,
|
|
721
|
+
});
|
|
722
|
+
return {
|
|
723
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
return handleDriveError(error, account);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
server.registerTool('drive_comment_delete', {
|
|
731
|
+
description: 'Delete a comment from a Drive file',
|
|
732
|
+
inputSchema: {
|
|
733
|
+
account: accountEnum.describe('Google account alias'),
|
|
734
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
735
|
+
commentId: z.string().describe('Comment ID'),
|
|
736
|
+
},
|
|
737
|
+
}, async ({ account, fileId, commentId }) => {
|
|
738
|
+
try {
|
|
739
|
+
const auth = await getClient(account);
|
|
740
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
741
|
+
await drive.comments.delete({ fileId, commentId });
|
|
742
|
+
return {
|
|
743
|
+
content: [{ type: 'text', text: JSON.stringify({ deleted: true, commentId }, null, 2) }],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
return handleDriveError(error, account);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
// ─── Replies ───────────────────────────────────────────────────────────
|
|
751
|
+
server.registerTool('drive_reply_create', {
|
|
752
|
+
description: 'Reply to a comment. Optionally close or reopen the thread by setting action to "resolve" or "reopen".',
|
|
753
|
+
inputSchema: {
|
|
754
|
+
account: accountEnum.describe('Google account alias'),
|
|
755
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
756
|
+
commentId: z.string().describe('Parent comment ID'),
|
|
757
|
+
content: z.string().describe('Reply content (required even when only changing action)'),
|
|
758
|
+
action: z.enum(['resolve', 'reopen']).optional()
|
|
759
|
+
.describe('Optional action to apply to the thread on this reply'),
|
|
760
|
+
},
|
|
761
|
+
}, async ({ account, fileId, commentId, content, action }) => {
|
|
762
|
+
try {
|
|
763
|
+
const auth = await getClient(account);
|
|
764
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
765
|
+
const requestBody = { content };
|
|
766
|
+
if (action)
|
|
767
|
+
requestBody.action = action;
|
|
768
|
+
const res = await drive.replies.create({
|
|
769
|
+
fileId,
|
|
770
|
+
commentId,
|
|
771
|
+
requestBody,
|
|
772
|
+
fields: REPLY_FIELDS,
|
|
773
|
+
});
|
|
774
|
+
return {
|
|
775
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
return handleDriveError(error, account);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
server.registerTool('drive_reply_list', {
|
|
783
|
+
description: 'List replies on a comment with pagination',
|
|
784
|
+
inputSchema: {
|
|
785
|
+
account: accountEnum.describe('Google account alias'),
|
|
786
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
787
|
+
commentId: z.string().describe('Parent comment ID'),
|
|
788
|
+
includeDeleted: z.boolean().optional(),
|
|
789
|
+
pageSize: z.number().min(1).max(100).optional(),
|
|
790
|
+
pageToken: z.string().optional(),
|
|
791
|
+
},
|
|
792
|
+
}, async ({ account, fileId, commentId, includeDeleted, pageSize, pageToken }) => {
|
|
793
|
+
try {
|
|
794
|
+
const auth = await getClient(account);
|
|
795
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
796
|
+
const res = await drive.replies.list({
|
|
797
|
+
fileId,
|
|
798
|
+
commentId,
|
|
799
|
+
includeDeleted: includeDeleted ?? false,
|
|
800
|
+
pageSize: pageSize ?? 20,
|
|
801
|
+
pageToken,
|
|
802
|
+
fields: REPLY_LIST_FIELDS,
|
|
803
|
+
});
|
|
804
|
+
return {
|
|
805
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
return handleDriveError(error, account);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
server.registerTool('drive_reply_update', {
|
|
813
|
+
description: 'Edit the content of an existing reply',
|
|
814
|
+
inputSchema: {
|
|
815
|
+
account: accountEnum.describe('Google account alias'),
|
|
816
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
817
|
+
commentId: z.string().describe('Parent comment ID'),
|
|
818
|
+
replyId: z.string().describe('Reply ID'),
|
|
819
|
+
content: z.string().describe('New plain text content'),
|
|
820
|
+
},
|
|
821
|
+
}, async ({ account, fileId, commentId, replyId, content }) => {
|
|
822
|
+
try {
|
|
823
|
+
const auth = await getClient(account);
|
|
824
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
825
|
+
const res = await drive.replies.update({
|
|
826
|
+
fileId,
|
|
827
|
+
commentId,
|
|
828
|
+
replyId,
|
|
829
|
+
requestBody: { content },
|
|
830
|
+
fields: REPLY_FIELDS,
|
|
831
|
+
});
|
|
832
|
+
return {
|
|
833
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
return handleDriveError(error, account);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
server.registerTool('drive_reply_delete', {
|
|
841
|
+
description: 'Delete a reply from a comment thread',
|
|
842
|
+
inputSchema: {
|
|
843
|
+
account: accountEnum.describe('Google account alias'),
|
|
844
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
845
|
+
commentId: z.string().describe('Parent comment ID'),
|
|
846
|
+
replyId: z.string().describe('Reply ID'),
|
|
847
|
+
},
|
|
848
|
+
}, async ({ account, fileId, commentId, replyId }) => {
|
|
849
|
+
try {
|
|
850
|
+
const auth = await getClient(account);
|
|
851
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
852
|
+
await drive.replies.delete({ fileId, commentId, replyId });
|
|
853
|
+
return {
|
|
854
|
+
content: [{ type: 'text', text: JSON.stringify({ deleted: true, replyId }, null, 2) }],
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
return handleDriveError(error, account);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
// ─── Revisions ─────────────────────────────────────────────────────────
|
|
862
|
+
server.registerTool('drive_revision_list', {
|
|
863
|
+
description: 'List version history of a Drive file',
|
|
864
|
+
inputSchema: {
|
|
865
|
+
account: accountEnum.describe('Google account alias'),
|
|
866
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
867
|
+
pageSize: z.number().min(1).max(200).optional(),
|
|
868
|
+
pageToken: z.string().optional(),
|
|
869
|
+
},
|
|
870
|
+
}, async ({ account, fileId, pageSize, pageToken }) => {
|
|
871
|
+
try {
|
|
872
|
+
const auth = await getClient(account);
|
|
873
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
874
|
+
const res = await drive.revisions.list({
|
|
875
|
+
fileId,
|
|
876
|
+
pageSize: pageSize ?? 50,
|
|
877
|
+
pageToken,
|
|
878
|
+
fields: 'nextPageToken,revisions(id,mimeType,modifiedTime,keepForever,published,lastModifyingUser,size)',
|
|
879
|
+
});
|
|
880
|
+
return {
|
|
881
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
catch (error) {
|
|
885
|
+
return handleDriveError(error, account);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
server.registerTool('drive_revision_update', {
|
|
889
|
+
description: 'Pin a revision (keepForever=true) against the 200-version cap, or change its published state for Docs.',
|
|
890
|
+
inputSchema: {
|
|
891
|
+
account: accountEnum.describe('Google account alias'),
|
|
892
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
893
|
+
revisionId: z.string().describe('Revision ID'),
|
|
894
|
+
keepForever: z.boolean().optional().describe('Pin this revision indefinitely'),
|
|
895
|
+
published: z.boolean().optional().describe('Toggle published state (Docs only)'),
|
|
896
|
+
publishAuto: z.boolean().optional().describe('Auto-publish subsequent revisions'),
|
|
897
|
+
publishedOutsideDomain: z.boolean().optional().describe('Allow publish outside domain'),
|
|
898
|
+
},
|
|
899
|
+
}, async ({ account, fileId, revisionId, keepForever, published, publishAuto, publishedOutsideDomain }) => {
|
|
900
|
+
try {
|
|
901
|
+
const auth = await getClient(account);
|
|
902
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
903
|
+
const requestBody = {};
|
|
904
|
+
if (keepForever !== undefined)
|
|
905
|
+
requestBody.keepForever = keepForever;
|
|
906
|
+
if (published !== undefined)
|
|
907
|
+
requestBody.published = published;
|
|
908
|
+
if (publishAuto !== undefined)
|
|
909
|
+
requestBody.publishAuto = publishAuto;
|
|
910
|
+
if (publishedOutsideDomain !== undefined)
|
|
911
|
+
requestBody.publishedOutsideDomain = publishedOutsideDomain;
|
|
912
|
+
const res = await drive.revisions.update({
|
|
913
|
+
fileId,
|
|
914
|
+
revisionId,
|
|
915
|
+
requestBody,
|
|
916
|
+
fields: 'id,modifiedTime,keepForever,published',
|
|
917
|
+
});
|
|
918
|
+
return {
|
|
919
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
catch (error) {
|
|
923
|
+
return handleDriveError(error, account);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
server.registerTool('drive_revision_delete', {
|
|
927
|
+
description: 'Delete a specific revision of a file',
|
|
928
|
+
inputSchema: {
|
|
929
|
+
account: accountEnum.describe('Google account alias'),
|
|
930
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
931
|
+
revisionId: z.string().describe('Revision ID'),
|
|
932
|
+
},
|
|
933
|
+
}, async ({ account, fileId, revisionId }) => {
|
|
934
|
+
try {
|
|
935
|
+
const auth = await getClient(account);
|
|
936
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
937
|
+
await drive.revisions.delete({ fileId, revisionId });
|
|
938
|
+
return {
|
|
939
|
+
content: [{ type: 'text', text: JSON.stringify({ deleted: true, revisionId }, null, 2) }],
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
return handleDriveError(error, account);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
// ─── Access proposals ──────────────────────────────────────────────────
|
|
947
|
+
server.registerTool('drive_access_proposal_list', {
|
|
948
|
+
description: 'List pending "Request access" proposals on a file. Useful for programmatic triage of share requests from external collaborators.',
|
|
949
|
+
inputSchema: {
|
|
950
|
+
account: accountEnum.describe('Google account alias'),
|
|
951
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
952
|
+
pageSize: z.number().min(1).max(100).optional(),
|
|
953
|
+
pageToken: z.string().optional(),
|
|
954
|
+
},
|
|
955
|
+
}, async ({ account, fileId, pageSize, pageToken }) => {
|
|
956
|
+
try {
|
|
957
|
+
const auth = await getClient(account);
|
|
958
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
959
|
+
const res = await drive.accessproposals.list({
|
|
960
|
+
fileId,
|
|
961
|
+
pageSize: pageSize ?? 20,
|
|
962
|
+
pageToken,
|
|
963
|
+
});
|
|
964
|
+
return {
|
|
965
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
catch (error) {
|
|
969
|
+
return handleDriveError(error, account);
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
server.registerTool('drive_access_proposal_resolve', {
|
|
973
|
+
description: 'Resolve a pending access proposal. Action ACCEPT requires a role array (e.g. ["reader"]).',
|
|
974
|
+
inputSchema: {
|
|
975
|
+
account: accountEnum.describe('Google account alias'),
|
|
976
|
+
fileId: z.string().describe('Google Drive file ID'),
|
|
977
|
+
proposalId: z.string().describe('Access proposal ID'),
|
|
978
|
+
action: z.enum(['ACCEPT', 'DENY']).describe('Whether to accept or deny the proposal'),
|
|
979
|
+
role: z.array(z.enum(['reader', 'commenter', 'writer', 'fileOrganizer'])).optional()
|
|
980
|
+
.describe('Required when action is ACCEPT'),
|
|
981
|
+
view: z.string().optional().describe('Optional view, e.g. "published"'),
|
|
982
|
+
sendNotification: z.boolean().optional()
|
|
983
|
+
.describe('Email the requester about the resolution'),
|
|
984
|
+
},
|
|
985
|
+
}, async ({ account, fileId, proposalId, action, role, view, sendNotification }) => {
|
|
986
|
+
try {
|
|
987
|
+
const auth = await getClient(account);
|
|
988
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
989
|
+
const requestBody = { action };
|
|
990
|
+
if (role)
|
|
991
|
+
requestBody.role = role;
|
|
992
|
+
if (view)
|
|
993
|
+
requestBody.view = view;
|
|
994
|
+
if (sendNotification !== undefined)
|
|
995
|
+
requestBody.sendNotification = sendNotification;
|
|
996
|
+
await drive.accessproposals.resolve({
|
|
997
|
+
fileId,
|
|
998
|
+
proposalId,
|
|
999
|
+
requestBody,
|
|
1000
|
+
});
|
|
1001
|
+
return {
|
|
1002
|
+
content: [{ type: 'text', text: JSON.stringify({ resolved: true, proposalId, action }, null, 2) }],
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
return handleDriveError(error, account);
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
// ─── Shared drives ─────────────────────────────────────────────────────
|
|
1010
|
+
server.registerTool('drive_shared_drives_list', {
|
|
1011
|
+
description: 'List shared drives the account has access to',
|
|
1012
|
+
inputSchema: {
|
|
1013
|
+
account: accountEnum.describe('Google account alias'),
|
|
1014
|
+
pageSize: z.number().min(1).max(100).optional(),
|
|
1015
|
+
pageToken: z.string().optional(),
|
|
1016
|
+
q: z.string().optional().describe('Optional filter expression'),
|
|
1017
|
+
},
|
|
1018
|
+
}, async ({ account, pageSize, pageToken, q }) => {
|
|
1019
|
+
try {
|
|
1020
|
+
const auth = await getClient(account);
|
|
1021
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
1022
|
+
const res = await drive.drives.list({
|
|
1023
|
+
pageSize: pageSize ?? 50,
|
|
1024
|
+
pageToken,
|
|
1025
|
+
q,
|
|
1026
|
+
fields: 'nextPageToken,drives(id,name,colorRgb,createdTime,hidden)',
|
|
1027
|
+
});
|
|
1028
|
+
return {
|
|
1029
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
return handleDriveError(error, account);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
server.registerTool('drive_shared_drive_get', {
|
|
1037
|
+
description: 'Get metadata for a specific shared drive',
|
|
1038
|
+
inputSchema: {
|
|
1039
|
+
account: accountEnum.describe('Google account alias'),
|
|
1040
|
+
driveId: z.string().describe('Shared drive ID'),
|
|
1041
|
+
},
|
|
1042
|
+
}, async ({ account, driveId }) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const auth = await getClient(account);
|
|
1045
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
1046
|
+
const res = await drive.drives.get({
|
|
1047
|
+
driveId,
|
|
1048
|
+
fields: 'id,name,colorRgb,createdTime,hidden,capabilities,restrictions',
|
|
1049
|
+
});
|
|
1050
|
+
return {
|
|
1051
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
return handleDriveError(error, account);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
// ─── About ─────────────────────────────────────────────────────────────
|
|
1059
|
+
server.registerTool('drive_get_about', {
|
|
1060
|
+
description: 'Get Drive storage quota, user display name, and email for an account',
|
|
1061
|
+
inputSchema: {
|
|
1062
|
+
account: accountEnum.describe('Google account alias'),
|
|
1063
|
+
},
|
|
1064
|
+
}, async ({ account }) => {
|
|
1065
|
+
try {
|
|
1066
|
+
const auth = await getClient(account);
|
|
1067
|
+
const drive = google.drive({ version: 'v3', auth });
|
|
1068
|
+
const res = await drive.about.get({
|
|
1069
|
+
fields: 'user,storageQuota',
|
|
1070
|
+
});
|
|
1071
|
+
return {
|
|
1072
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
return handleDriveError(error, account);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
function handleDriveError(error, account) {
|
|
1081
|
+
return handleGoogleApiError(error, account);
|
|
1082
|
+
}
|