@triffon/google-docs-mcp 1.11.3-triffon

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 (165) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +567 -0
  3. package/dist/auth.js +327 -0
  4. package/dist/cachedToolsList.js +38 -0
  5. package/dist/clients.js +180 -0
  6. package/dist/config.js +9 -0
  7. package/dist/downloadProxy.js +82 -0
  8. package/dist/driveQueryUtils.js +7 -0
  9. package/dist/firestoreTokenStorage.js +32 -0
  10. package/dist/googleDocsApiHelpers.js +1257 -0
  11. package/dist/googleSheetsApiHelpers.js +849 -0
  12. package/dist/index.js +240 -0
  13. package/dist/landingPage.js +103 -0
  14. package/dist/logger.js +58 -0
  15. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  16. package/dist/markdown-transformer/index.js +128 -0
  17. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  18. package/dist/remoteWrapper.js +72 -0
  19. package/dist/tools/calendar/createEvent.js +89 -0
  20. package/dist/tools/calendar/deleteEvent.js +49 -0
  21. package/dist/tools/calendar/helpers.js +19 -0
  22. package/dist/tools/calendar/index.js +12 -0
  23. package/dist/tools/calendar/listEvents.js +84 -0
  24. package/dist/tools/calendar/quickAddEvent.js +55 -0
  25. package/dist/tools/calendar/updateEvent.js +84 -0
  26. package/dist/tools/docs/addTab.js +84 -0
  27. package/dist/tools/docs/appendTableRows.js +89 -0
  28. package/dist/tools/docs/appendToGoogleDoc.js +85 -0
  29. package/dist/tools/docs/cloneTable.js +159 -0
  30. package/dist/tools/docs/comments/addComment.js +83 -0
  31. package/dist/tools/docs/comments/deleteComment.js +30 -0
  32. package/dist/tools/docs/comments/getComment.js +45 -0
  33. package/dist/tools/docs/comments/index.js +14 -0
  34. package/dist/tools/docs/comments/listComments.js +43 -0
  35. package/dist/tools/docs/comments/replyToComment.js +35 -0
  36. package/dist/tools/docs/comments/resolveComment.js +55 -0
  37. package/dist/tools/docs/deleteRange.js +61 -0
  38. package/dist/tools/docs/deleteTableRows.js +62 -0
  39. package/dist/tools/docs/findAndReplace.js +54 -0
  40. package/dist/tools/docs/findElement.js +43 -0
  41. package/dist/tools/docs/findSectionsByHeading.js +46 -0
  42. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  43. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  44. package/dist/tools/docs/formatting/batchApplyTextStyle.js +86 -0
  45. package/dist/tools/docs/formatting/index.js +16 -0
  46. package/dist/tools/docs/formatting/updateTableBorders.js +81 -0
  47. package/dist/tools/docs/formatting/updateTableCellStyle.js +85 -0
  48. package/dist/tools/docs/formatting/updateTableColumnWidth.js +51 -0
  49. package/dist/tools/docs/formatting/updateTableRowStyle.js +74 -0
  50. package/dist/tools/docs/getTableStructure.js +48 -0
  51. package/dist/tools/docs/index.js +66 -0
  52. package/dist/tools/docs/insertDateChip.js +82 -0
  53. package/dist/tools/docs/insertImage.js +112 -0
  54. package/dist/tools/docs/insertPageBreak.js +47 -0
  55. package/dist/tools/docs/insertPerson.js +53 -0
  56. package/dist/tools/docs/insertRichLink.js +58 -0
  57. package/dist/tools/docs/insertSectionBreak.js +60 -0
  58. package/dist/tools/docs/insertTable.js +42 -0
  59. package/dist/tools/docs/insertTableWithData.js +125 -0
  60. package/dist/tools/docs/insertText.js +49 -0
  61. package/dist/tools/docs/listDocumentTables.js +47 -0
  62. package/dist/tools/docs/listDocumentTabs.js +59 -0
  63. package/dist/tools/docs/listSmartChips.js +41 -0
  64. package/dist/tools/docs/modifyText.js +147 -0
  65. package/dist/tools/docs/readGoogleDoc.js +164 -0
  66. package/dist/tools/docs/renameTab.js +47 -0
  67. package/dist/tools/docs/replaceTableRowData.js +55 -0
  68. package/dist/tools/docs/smartChipHelpers.js +71 -0
  69. package/dist/tools/docs/structureHelpers.js +250 -0
  70. package/dist/tools/docs/tabFieldMasks.js +48 -0
  71. package/dist/tools/docs/tableRowDataHelpers.js +53 -0
  72. package/dist/tools/docs/updateSectionStyle.js +148 -0
  73. package/dist/tools/drive/copyFile.js +63 -0
  74. package/dist/tools/drive/createDocument.js +105 -0
  75. package/dist/tools/drive/createFolder.js +48 -0
  76. package/dist/tools/drive/createFromTemplate.js +82 -0
  77. package/dist/tools/drive/deleteFile.js +72 -0
  78. package/dist/tools/drive/downloadFile.js +266 -0
  79. package/dist/tools/drive/getDocumentInfo.js +48 -0
  80. package/dist/tools/drive/getFolderInfo.js +48 -0
  81. package/dist/tools/drive/index.js +34 -0
  82. package/dist/tools/drive/listDriveFiles.js +129 -0
  83. package/dist/tools/drive/listFolderContents.js +83 -0
  84. package/dist/tools/drive/listGoogleDocs.js +70 -0
  85. package/dist/tools/drive/moveFile.js +54 -0
  86. package/dist/tools/drive/renameFile.js +39 -0
  87. package/dist/tools/drive/savePathGuard.js +86 -0
  88. package/dist/tools/drive/searchDriveFiles.js +148 -0
  89. package/dist/tools/drive/searchGoogleDocs.js +77 -0
  90. package/dist/tools/drive/setFilePermission.js +68 -0
  91. package/dist/tools/gmail/createDraft.js +58 -0
  92. package/dist/tools/gmail/deleteDraft.js +37 -0
  93. package/dist/tools/gmail/getDraft.js +52 -0
  94. package/dist/tools/gmail/getMessage.js +92 -0
  95. package/dist/tools/gmail/helpers.js +113 -0
  96. package/dist/tools/gmail/index.js +28 -0
  97. package/dist/tools/gmail/listDrafts.js +74 -0
  98. package/dist/tools/gmail/listLabels.js +31 -0
  99. package/dist/tools/gmail/listMessages.js +87 -0
  100. package/dist/tools/gmail/modifyMessageLabels.js +54 -0
  101. package/dist/tools/gmail/sendDraft.js +42 -0
  102. package/dist/tools/gmail/sendEmail.js +56 -0
  103. package/dist/tools/gmail/trashMessage.js +37 -0
  104. package/dist/tools/gmail/triageInbox.js +147 -0
  105. package/dist/tools/gmail/updateDraft.js +60 -0
  106. package/dist/tools/index.js +64 -0
  107. package/dist/tools/script/appsScriptShared.js +96 -0
  108. package/dist/tools/script/createAppsScriptProject.js +74 -0
  109. package/dist/tools/script/getAppsScriptContent.js +56 -0
  110. package/dist/tools/script/index.js +14 -0
  111. package/dist/tools/script/updateAppsScriptContent.js +56 -0
  112. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  113. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  114. package/dist/tools/sheets/appendSpreadsheetRows.js +44 -0
  115. package/dist/tools/sheets/appendTableRows.js +51 -0
  116. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  117. package/dist/tools/sheets/autoResizeRows.js +63 -0
  118. package/dist/tools/sheets/batchWrite.js +61 -0
  119. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  120. package/dist/tools/sheets/comments/commentAnchor.js +95 -0
  121. package/dist/tools/sheets/comments/createSheetsCellNote.js +33 -0
  122. package/dist/tools/sheets/comments/createSheetsComment.js +168 -0
  123. package/dist/tools/sheets/comments/deleteSheetsComment.js +32 -0
  124. package/dist/tools/sheets/comments/getSheetsComment.js +51 -0
  125. package/dist/tools/sheets/comments/index.js +16 -0
  126. package/dist/tools/sheets/comments/listSheetsComments.js +177 -0
  127. package/dist/tools/sheets/comments/replyToSheetsComment.js +37 -0
  128. package/dist/tools/sheets/comments/resolveSheetsComment.js +53 -0
  129. package/dist/tools/sheets/copyFormatting.js +59 -0
  130. package/dist/tools/sheets/copySheetTo.js +36 -0
  131. package/dist/tools/sheets/createSpreadsheet.js +72 -0
  132. package/dist/tools/sheets/createTable.js +120 -0
  133. package/dist/tools/sheets/deleteChart.js +41 -0
  134. package/dist/tools/sheets/deleteConditionalFormatting.js +46 -0
  135. package/dist/tools/sheets/deleteSheet.js +43 -0
  136. package/dist/tools/sheets/deleteTable.js +56 -0
  137. package/dist/tools/sheets/duplicateSheet.js +53 -0
  138. package/dist/tools/sheets/formatCells.js +122 -0
  139. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  140. package/dist/tools/sheets/getConditionalFormatting.js +98 -0
  141. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  142. package/dist/tools/sheets/getTable.js +48 -0
  143. package/dist/tools/sheets/groupRows.js +62 -0
  144. package/dist/tools/sheets/index.js +84 -0
  145. package/dist/tools/sheets/insertChart.js +225 -0
  146. package/dist/tools/sheets/listGoogleSheets.js +66 -0
  147. package/dist/tools/sheets/listTables.js +55 -0
  148. package/dist/tools/sheets/protectRange.js +59 -0
  149. package/dist/tools/sheets/readCellFormat.js +143 -0
  150. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  151. package/dist/tools/sheets/renameSheet.js +48 -0
  152. package/dist/tools/sheets/setCellBorders.js +94 -0
  153. package/dist/tools/sheets/setColumnWidths.js +43 -0
  154. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  155. package/dist/tools/sheets/setRowHeights.js +63 -0
  156. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  157. package/dist/tools/sheets/updateTableRange.js +51 -0
  158. package/dist/tools/sheets/writeSpreadsheet.js +45 -0
  159. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +95 -0
  160. package/dist/tools/utils/index.js +8 -0
  161. package/dist/tools/utils/replaceDocumentWithMarkdown.js +161 -0
  162. package/dist/tools/utils/replaceRangeWithMarkdown.js +72 -0
  163. package/dist/types.js +208 -0
  164. package/dist/upstreamAuth.js +62 -0
  165. package/package.json +51 -0
@@ -0,0 +1,266 @@
1
+ import { UserError, imageContent } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { getDriveClient } from '../../clients.js';
7
+ import { requestClients } from '../../remoteWrapper.js';
8
+ import { createDownloadToken } from '../../downloadProxy.js';
9
+ import { ensureWithinDownloadRoots, parseDownloadRoots } from './savePathGuard.js';
10
+ const isRemote = process.env.MCP_TRANSPORT === 'httpStream';
11
+ export const WORKSPACE_EXPORT_DEFAULTS = {
12
+ 'application/vnd.google-apps.document': 'text/markdown',
13
+ 'application/vnd.google-apps.spreadsheet': 'text/csv',
14
+ 'application/vnd.google-apps.presentation': 'text/plain',
15
+ 'application/vnd.google-apps.drawing': 'image/png',
16
+ 'application/vnd.google-apps.script': 'application/vnd.google-apps.script+json',
17
+ };
18
+ export const EXPORT_MIME_TO_EXTENSION = {
19
+ 'text/markdown': '.md',
20
+ 'text/plain': '.txt',
21
+ 'text/csv': '.csv',
22
+ 'text/tab-separated-values': '.tsv',
23
+ 'application/pdf': '.pdf',
24
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
25
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
26
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
27
+ 'image/png': '.png',
28
+ 'image/jpeg': '.jpg',
29
+ 'application/vnd.google-apps.script+json': '.json',
30
+ };
31
+ const MAX_TEXT_EXTRACT_BYTES = 50_000;
32
+ export function isTextMimeType(mime) {
33
+ return (mime.startsWith('text/') ||
34
+ mime === 'application/json' ||
35
+ mime === 'application/vnd.google-apps.script+json');
36
+ }
37
+ function isWorkspaceMimeType(mime) {
38
+ return mime.startsWith('application/vnd.google-apps.');
39
+ }
40
+ const DownloadFileParameters = z.strictObject({
41
+ fileId: z.string().describe('The file ID from a Google Drive URL or previous tool result.'),
42
+ savePath: z
43
+ .string()
44
+ .optional()
45
+ .describe('Local file path to save the downloaded file to. Parent directories are created automatically. ' +
46
+ "If omitted, saves to the current working directory using the file's original name " +
47
+ '(with an appropriate extension for exported Google Workspace files).'),
48
+ exportMimeType: z
49
+ .string()
50
+ .optional()
51
+ .describe('For Google Workspace files only: the MIME type to export as. ' +
52
+ 'E.g. "text/markdown", "application/pdf", "text/csv". ' +
53
+ 'Defaults: Docs->text/markdown, Sheets->text/csv, Slides->text/plain, Drawings->image/png.'),
54
+ extractText: z
55
+ .boolean()
56
+ .optional()
57
+ .default(true)
58
+ .describe('If true, return text content in the response (up to 50KB) alongside saving the file. ' +
59
+ 'Works for text/*, application/json. For binary files, only the save path is returned.'),
60
+ returnAs: z
61
+ .enum(['url', 'content'])
62
+ .optional()
63
+ .default('url')
64
+ .describe('Remote mode only. ' +
65
+ '"url" (default): returns a short-lived download URL -- use curl -o to save to disk. ' +
66
+ '"content": returns file content inline (text, image, or base64 resource).'),
67
+ });
68
+ export function register(server) {
69
+ server.addTool({
70
+ name: 'downloadFile',
71
+ description: 'Downloads any file from Google Drive to a local path. ' +
72
+ 'For Google Workspace files (Docs, Sheets, Slides), exports to a specified format ' +
73
+ '(defaults to text/markdown for Docs, text/csv for Sheets). ' +
74
+ 'For regular files (PDFs, images, etc.), downloads the raw bytes. ' +
75
+ 'Optionally extracts and returns text content for text-based files.',
76
+ parameters: DownloadFileParameters,
77
+ execute: async (args, { log }) => {
78
+ const drive = await getDriveClient();
79
+ log.info(`Downloading file ${args.fileId}${args.savePath ? ` to ${args.savePath}` : ''}`);
80
+ let resolvedSavePath;
81
+ try {
82
+ // 1. Get file metadata
83
+ const metadataRes = await drive.files.get({
84
+ fileId: args.fileId,
85
+ fields: 'name,mimeType,size',
86
+ supportsAllDrives: true,
87
+ });
88
+ const fileName = path.basename(metadataRes.data.name || 'download');
89
+ const originalMimeType = metadataRes.data.mimeType || 'application/octet-stream';
90
+ const isWorkspace = isWorkspaceMimeType(originalMimeType);
91
+ // 2. Resolve export MIME type for Workspace files
92
+ let exportMime;
93
+ if (isWorkspace) {
94
+ exportMime = args.exportMimeType || WORKSPACE_EXPORT_DEFAULTS[originalMimeType];
95
+ if (!exportMime) {
96
+ const shortType = originalMimeType.replace('application/vnd.google-apps.', '');
97
+ throw new UserError(`Unsupported Google Workspace type "${shortType}". ` +
98
+ `Provide an exportMimeType parameter to specify the desired export format.`);
99
+ }
100
+ }
101
+ // ---------- Remote mode ----------
102
+ if (isRemote) {
103
+ const resolvedFileName = isWorkspace && exportMime
104
+ ? path.parse(fileName).name + (EXPORT_MIME_TO_EXTENSION[exportMime] || '')
105
+ : fileName;
106
+ const outputMime = exportMime || originalMimeType;
107
+ // --- returnAs: "url" (default) -- agent uses curl to save to disk ---
108
+ if (args.returnAs !== 'content') {
109
+ const store = requestClients.getStore();
110
+ if (!store)
111
+ throw new UserError('Request context missing.');
112
+ const token = createDownloadToken({
113
+ fileId: args.fileId,
114
+ accessToken: store.accessToken,
115
+ exportMime,
116
+ fileName: resolvedFileName,
117
+ mimeType: outputMime,
118
+ isWorkspace,
119
+ });
120
+ const result = {
121
+ downloadUrl: `${process.env.BASE_URL}/download/${token}`,
122
+ fileName: resolvedFileName,
123
+ originalMimeType,
124
+ };
125
+ if (isWorkspace && exportMime)
126
+ result.exportedAs = exportMime;
127
+ if (args.extractText !== false && isTextMimeType(outputMime)) {
128
+ try {
129
+ const textRes = isWorkspace && exportMime
130
+ ? await drive.files.export({ fileId: args.fileId, mimeType: exportMime }, { responseType: 'text' })
131
+ : await drive.files.get({ fileId: args.fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'text' });
132
+ result.textContent = textRes.data.slice(0, MAX_TEXT_EXTRACT_BYTES);
133
+ }
134
+ catch {
135
+ log.warn?.('Could not extract text content');
136
+ }
137
+ }
138
+ return JSON.stringify(result, null, 2);
139
+ }
140
+ // --- returnAs: "content" -- inline via MCP content types ---
141
+ let fileBuffer;
142
+ if (isWorkspace && exportMime) {
143
+ log.info(`Exporting Workspace file as ${exportMime}`);
144
+ const res = await drive.files.export({ fileId: args.fileId, mimeType: exportMime }, { responseType: 'arraybuffer' });
145
+ fileBuffer = Buffer.from(res.data);
146
+ }
147
+ else {
148
+ log.info('Downloading blob file into memory');
149
+ const res = await drive.files.get({ fileId: args.fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'arraybuffer' });
150
+ fileBuffer = Buffer.from(res.data);
151
+ }
152
+ const MAX_INLINE_BYTES = 100 * 1024 * 1024;
153
+ const content = [];
154
+ if (isTextMimeType(outputMime)) {
155
+ content.push({
156
+ type: 'text',
157
+ text: fileBuffer.toString('utf-8').slice(0, MAX_TEXT_EXTRACT_BYTES),
158
+ });
159
+ }
160
+ else if (outputMime.startsWith('image/') && fileBuffer.length <= MAX_INLINE_BYTES) {
161
+ content.push(await imageContent({ buffer: fileBuffer }));
162
+ }
163
+ else if (fileBuffer.length <= MAX_INLINE_BYTES) {
164
+ content.push({
165
+ type: 'resource',
166
+ resource: {
167
+ uri: `gdrive:///${args.fileId}/${resolvedFileName}`,
168
+ blob: fileBuffer.toString('base64'),
169
+ mimeType: outputMime,
170
+ },
171
+ });
172
+ }
173
+ else {
174
+ throw new UserError(`File too large for inline transfer (${(fileBuffer.length / 1024 / 1024).toFixed(1)}MB, limit 100MB).`);
175
+ }
176
+ content.push({
177
+ type: 'text',
178
+ text: JSON.stringify({
179
+ fileName: resolvedFileName,
180
+ originalMimeType,
181
+ sizeBytes: fileBuffer.length,
182
+ }),
183
+ });
184
+ return { content };
185
+ }
186
+ // ---------- Stdio mode: write to local disk ----------
187
+ resolvedSavePath = args.savePath;
188
+ if (resolvedSavePath)
189
+ resolvedSavePath = path.resolve(resolvedSavePath);
190
+ if (!resolvedSavePath) {
191
+ if (isWorkspace && exportMime) {
192
+ const baseName = path.parse(fileName).name;
193
+ const ext = EXPORT_MIME_TO_EXTENSION[exportMime] || '';
194
+ resolvedSavePath = path.join(process.cwd(), baseName + ext);
195
+ }
196
+ else {
197
+ resolvedSavePath = path.join(process.cwd(), fileName);
198
+ }
199
+ }
200
+ resolvedSavePath = path.resolve(resolvedSavePath);
201
+ // `savePath` is agent-controlled and reaches the filesystem directly, so
202
+ // confine it before creating directories or opening a write stream.
203
+ try {
204
+ resolvedSavePath = ensureWithinDownloadRoots(resolvedSavePath, parseDownloadRoots());
205
+ }
206
+ catch (boundaryError) {
207
+ throw new UserError(boundaryError?.message || String(boundaryError));
208
+ }
209
+ fs.mkdirSync(path.dirname(resolvedSavePath), { recursive: true });
210
+ let outputMime;
211
+ if (isWorkspace && exportMime) {
212
+ log.info(`Exporting Workspace file as ${exportMime}`);
213
+ const res = await drive.files.export({ fileId: args.fileId, mimeType: exportMime }, { responseType: 'stream' });
214
+ await pipeline(res.data, fs.createWriteStream(resolvedSavePath));
215
+ outputMime = exportMime;
216
+ }
217
+ else {
218
+ log.info(`Downloading blob file`);
219
+ const res = await drive.files.get({ fileId: args.fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'stream' });
220
+ await pipeline(res.data, fs.createWriteStream(resolvedSavePath));
221
+ outputMime = originalMimeType;
222
+ }
223
+ const sizeBytes = fs.statSync(resolvedSavePath).size;
224
+ let textContent;
225
+ if (args.extractText !== false && isTextMimeType(outputMime)) {
226
+ try {
227
+ const raw = fs.readFileSync(resolvedSavePath, 'utf-8');
228
+ textContent = raw.slice(0, MAX_TEXT_EXTRACT_BYTES);
229
+ }
230
+ catch {
231
+ log.warn?.(`Could not read text from ${resolvedSavePath}`);
232
+ }
233
+ }
234
+ const result = {
235
+ savedTo: resolvedSavePath,
236
+ fileName,
237
+ originalMimeType,
238
+ sizeBytes,
239
+ };
240
+ if (isWorkspace && exportMime)
241
+ result.exportedAs = exportMime;
242
+ if (textContent !== undefined)
243
+ result.textContent = textContent;
244
+ return JSON.stringify(result, null, 2);
245
+ }
246
+ catch (error) {
247
+ if (!isRemote && resolvedSavePath) {
248
+ try {
249
+ fs.unlinkSync(resolvedSavePath);
250
+ }
251
+ catch {
252
+ /* file may not exist yet */
253
+ }
254
+ }
255
+ log.error(`Error downloading file ${args.fileId}: ${error.message || error}`);
256
+ if (error instanceof UserError)
257
+ throw error;
258
+ if (error.code === 404)
259
+ throw new UserError(`File not found (ID: ${args.fileId}). Check the file ID.`);
260
+ if (error.code === 403)
261
+ throw new UserError(`Permission denied for file ${args.fileId}. Ensure the authenticated user has access.`);
262
+ throw new UserError(`Failed to download file: ${error.message || 'Unknown error'}`);
263
+ }
264
+ },
265
+ });
266
+ }
@@ -0,0 +1,48 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { getDriveClient } from '../../clients.js';
3
+ import { DocumentIdParameter } from '../../types.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'getDocumentInfo',
7
+ description: 'Gets metadata about a document including its name, owner, sharing status, and modification history.',
8
+ parameters: DocumentIdParameter,
9
+ execute: async (args, { log }) => {
10
+ const drive = await getDriveClient();
11
+ log.info(`Getting info for document: ${args.documentId}`);
12
+ try {
13
+ const response = await drive.files.get({
14
+ fileId: args.documentId,
15
+ // Note: 'permissions' and 'alternateLink' fields removed - they cause
16
+ // "Invalid field selection" errors for Google Docs files
17
+ fields: 'id,name,description,mimeType,size,createdTime,modifiedTime,webViewLink,owners(displayName,emailAddress),lastModifyingUser(displayName,emailAddress),shared,parents,version',
18
+ supportsAllDrives: true,
19
+ });
20
+ const file = response.data;
21
+ if (!file) {
22
+ throw new UserError(`Document with ID ${args.documentId} not found.`);
23
+ }
24
+ const info = {
25
+ id: file.id,
26
+ name: file.name,
27
+ mimeType: file.mimeType,
28
+ createdTime: file.createdTime,
29
+ modifiedTime: file.modifiedTime,
30
+ owner: file.owners?.[0]?.displayName || null,
31
+ lastModifyingUser: file.lastModifyingUser?.displayName || null,
32
+ shared: file.shared || false,
33
+ url: file.webViewLink,
34
+ description: file.description || null,
35
+ };
36
+ return JSON.stringify(info, null, 2);
37
+ }
38
+ catch (error) {
39
+ log.error(`Error getting document info: ${error.message || error}`);
40
+ if (error.code === 404)
41
+ throw new UserError(`Document not found (ID: ${args.documentId}).`);
42
+ if (error.code === 403)
43
+ throw new UserError('Permission denied. Make sure you have access to this document.');
44
+ throw new UserError(`Failed to get document info: ${error.message || 'Unknown error'}`);
45
+ }
46
+ },
47
+ });
48
+ }
@@ -0,0 +1,48 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'getFolderInfo',
7
+ description: 'Gets metadata about a Drive folder including its name, owner, sharing status, and parent folder.',
8
+ parameters: z.strictObject({
9
+ folderId: z.string().describe('ID of the folder to get information about.'),
10
+ }),
11
+ execute: async (args, { log }) => {
12
+ const drive = await getDriveClient();
13
+ log.info(`Getting folder info: ${args.folderId}`);
14
+ try {
15
+ const response = await drive.files.get({
16
+ fileId: args.folderId,
17
+ fields: 'id,name,description,createdTime,modifiedTime,webViewLink,owners(displayName,emailAddress),lastModifyingUser(displayName),shared,parents',
18
+ supportsAllDrives: true,
19
+ });
20
+ const file = response.data;
21
+ if (file.mimeType !== 'application/vnd.google-apps.folder') {
22
+ throw new UserError('The specified ID does not belong to a folder.');
23
+ }
24
+ const info = {
25
+ id: file.id,
26
+ name: file.name,
27
+ createdTime: file.createdTime,
28
+ modifiedTime: file.modifiedTime,
29
+ owner: file.owners?.[0]?.displayName || null,
30
+ lastModifyingUser: file.lastModifyingUser?.displayName || null,
31
+ shared: file.shared || false,
32
+ url: file.webViewLink,
33
+ description: file.description || null,
34
+ parentFolderId: file.parents?.[0] || null,
35
+ };
36
+ return JSON.stringify(info, null, 2);
37
+ }
38
+ catch (error) {
39
+ log.error(`Error getting folder info: ${error.message || error}`);
40
+ if (error.code === 404)
41
+ throw new UserError(`Folder not found (ID: ${args.folderId}).`);
42
+ if (error.code === 403)
43
+ throw new UserError('Permission denied. Make sure you have access to this folder.');
44
+ throw new UserError(`Failed to get folder info: ${error.message || 'Unknown error'}`);
45
+ }
46
+ },
47
+ });
48
+ }
@@ -0,0 +1,34 @@
1
+ import { register as listGoogleDocs } from './listGoogleDocs.js';
2
+ import { register as searchGoogleDocs } from './searchGoogleDocs.js';
3
+ import { register as listDriveFiles } from './listDriveFiles.js';
4
+ import { register as searchDriveFiles } from './searchDriveFiles.js';
5
+ import { register as getDocumentInfo } from './getDocumentInfo.js';
6
+ import { register as createFolder } from './createFolder.js';
7
+ import { register as listFolderContents } from './listFolderContents.js';
8
+ import { register as getFolderInfo } from './getFolderInfo.js';
9
+ import { register as moveFile } from './moveFile.js';
10
+ import { register as copyFile } from './copyFile.js';
11
+ import { register as renameFile } from './renameFile.js';
12
+ import { register as deleteFile } from './deleteFile.js';
13
+ import { register as createDocument } from './createDocument.js';
14
+ import { register as createFromTemplate } from './createFromTemplate.js';
15
+ import { register as downloadFile } from './downloadFile.js';
16
+ import { register as setFilePermission } from './setFilePermission.js';
17
+ export function registerDriveTools(server) {
18
+ listGoogleDocs(server);
19
+ searchGoogleDocs(server);
20
+ listDriveFiles(server);
21
+ searchDriveFiles(server);
22
+ getDocumentInfo(server);
23
+ createFolder(server);
24
+ listFolderContents(server);
25
+ getFolderInfo(server);
26
+ moveFile(server);
27
+ copyFile(server);
28
+ renameFile(server);
29
+ deleteFile(server);
30
+ createDocument(server);
31
+ createFromTemplate(server);
32
+ downloadFile(server);
33
+ setFilePermission(server);
34
+ }
@@ -0,0 +1,129 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient } from '../../clients.js';
4
+ import { escapeDriveQuery } from '../../driveQueryUtils.js';
5
+ /**
6
+ * Convenience shortcuts for common MIME types.
7
+ * Users can also pass any full MIME type string directly.
8
+ */
9
+ const MIME_TYPE_SHORTCUTS = {
10
+ document: 'application/vnd.google-apps.document',
11
+ spreadsheet: 'application/vnd.google-apps.spreadsheet',
12
+ presentation: 'application/vnd.google-apps.presentation',
13
+ folder: 'application/vnd.google-apps.folder',
14
+ form: 'application/vnd.google-apps.form',
15
+ pdf: 'application/pdf',
16
+ zip: 'application/zip',
17
+ };
18
+ export function register(server) {
19
+ server.addTool({
20
+ name: 'listDriveFiles',
21
+ description: 'Lists files across Google Drive with optional filtering by type, folder, and ownership. ' +
22
+ 'Unlike listDocuments (which only returns Google Docs), this tool works with all file types ' +
23
+ '(Sheets, PDFs, images, folders, etc.) and supports sort direction and size-based ordering. ' +
24
+ 'Use mimeType shortcuts: "document", "spreadsheet", "presentation", "folder", "form", "pdf", "zip" ' +
25
+ 'or pass any full MIME type string.',
26
+ parameters: z.strictObject({
27
+ maxResults: z
28
+ .number()
29
+ .int()
30
+ .min(1)
31
+ .max(100)
32
+ .optional()
33
+ .default(20)
34
+ .describe('Maximum number of files to return (1-100).'),
35
+ mimeType: z
36
+ .string()
37
+ .optional()
38
+ .describe('Filter by file type. Shortcuts: "document", "spreadsheet", "presentation", ' +
39
+ '"folder", "form", "pdf", "zip". Or pass a full MIME type (e.g. "image/png").'),
40
+ folderId: z
41
+ .string()
42
+ .optional()
43
+ .describe('Only return files directly inside this folder. Use "root" for the top-level Drive. ' +
44
+ 'Omit to search across all folders.'),
45
+ orderBy: z
46
+ .enum(['name', 'modifiedTime', 'createdTime', 'quotaBytesUsed'])
47
+ .optional()
48
+ .default('modifiedTime')
49
+ .describe('Field to sort results by. "quotaBytesUsed" sorts by file size (note: Google-native files report 0).'),
50
+ sortDirection: z
51
+ .enum(['asc', 'desc'])
52
+ .optional()
53
+ .default('desc')
54
+ .describe('Sort direction: "asc" for oldest/smallest first, "desc" for newest/largest first.'),
55
+ ownedByMe: z
56
+ .boolean()
57
+ .optional()
58
+ .describe('If true, only return files owned by the authenticated user.'),
59
+ sharedWithMe: z
60
+ .boolean()
61
+ .optional()
62
+ .describe('If true, only return files shared with the authenticated user (excludes files they own). ' +
63
+ 'Cannot be combined with ownedByMe.'),
64
+ modifiedAfter: z
65
+ .string()
66
+ .optional()
67
+ .describe('Only return files modified after this date (ISO 8601 format, e.g. "2024-01-01").'),
68
+ }),
69
+ execute: async (args, { log }) => {
70
+ if (args.ownedByMe && args.sharedWithMe) {
71
+ throw new UserError('ownedByMe and sharedWithMe cannot both be true.');
72
+ }
73
+ const drive = await getDriveClient();
74
+ log.info(`Listing Drive files. mimeType=${args.mimeType || 'any'}, folder=${args.folderId || 'all'}, ` +
75
+ `orderBy=${args.orderBy} ${args.sortDirection}, ownedByMe=${args.ownedByMe}, sharedWithMe=${args.sharedWithMe}`);
76
+ try {
77
+ const conditions = ['trashed=false'];
78
+ // Resolve MIME type shortcut or use value as-is
79
+ if (args.mimeType) {
80
+ const resolved = MIME_TYPE_SHORTCUTS[args.mimeType] ?? args.mimeType;
81
+ conditions.push(`mimeType='${escapeDriveQuery(resolved)}'`);
82
+ }
83
+ // Scope to a specific folder
84
+ if (args.folderId) {
85
+ conditions.push(`'${escapeDriveQuery(args.folderId)}' in parents`);
86
+ }
87
+ // Ownership filter
88
+ if (args.ownedByMe) {
89
+ conditions.push(`'me' in owners`);
90
+ }
91
+ else if (args.sharedWithMe) {
92
+ conditions.push(`sharedWithMe=true`);
93
+ }
94
+ // Date filter
95
+ if (args.modifiedAfter) {
96
+ const cutoff = new Date(args.modifiedAfter).toISOString();
97
+ conditions.push(`modifiedTime > '${escapeDriveQuery(cutoff)}'`);
98
+ }
99
+ const queryString = conditions.join(' and ');
100
+ const orderByParam = args.sortDirection === 'desc' ? `${args.orderBy} desc` : args.orderBy;
101
+ const response = await drive.files.list({
102
+ q: queryString,
103
+ pageSize: args.maxResults,
104
+ orderBy: orderByParam,
105
+ fields: 'files(id,name,mimeType,size,modifiedTime,createdTime,webViewLink,owners(displayName,emailAddress))',
106
+ supportsAllDrives: true,
107
+ includeItemsFromAllDrives: true,
108
+ });
109
+ const files = (response.data.files || []).map((file) => ({
110
+ id: file.id,
111
+ name: file.name,
112
+ mimeType: file.mimeType,
113
+ size: file.size != null ? Number(file.size) : null,
114
+ modifiedTime: file.modifiedTime,
115
+ createdTime: file.createdTime,
116
+ owner: file.owners?.[0]?.displayName || null,
117
+ url: file.webViewLink,
118
+ }));
119
+ return JSON.stringify({ files, total: files.length }, null, 2);
120
+ }
121
+ catch (error) {
122
+ log.error(`Error listing Drive files: ${error.message || error}`);
123
+ if (error.code === 403)
124
+ throw new UserError('Permission denied. Make sure you have granted Google Drive access to the application.');
125
+ throw new UserError(`Failed to list files: ${error.message || 'Unknown error'}`);
126
+ }
127
+ },
128
+ });
129
+ }
@@ -0,0 +1,83 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient } from '../../clients.js';
4
+ import { escapeDriveQuery } from '../../driveQueryUtils.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'listFolderContents',
8
+ description: "Lists files and subfolders within a Drive folder. Use folderId='root' to browse the top-level of the Drive.",
9
+ parameters: z.strictObject({
10
+ folderId: z
11
+ .string()
12
+ .describe('ID of the folder to list contents of. Use "root" for the root Drive folder.'),
13
+ includeSubfolders: z
14
+ .boolean()
15
+ .optional()
16
+ .default(true)
17
+ .describe('Whether to include subfolders in results.'),
18
+ includeFiles: z
19
+ .boolean()
20
+ .optional()
21
+ .default(true)
22
+ .describe('Whether to include files in results.'),
23
+ maxResults: z
24
+ .number()
25
+ .int()
26
+ .min(1)
27
+ .max(100)
28
+ .optional()
29
+ .default(50)
30
+ .describe('Maximum number of items to return.'),
31
+ }),
32
+ execute: async (args, { log }) => {
33
+ const drive = await getDriveClient();
34
+ log.info(`Listing contents of folder: ${args.folderId}`);
35
+ try {
36
+ let queryString = `'${escapeDriveQuery(args.folderId)}' in parents and trashed=false`;
37
+ // Filter by type if specified
38
+ if (!args.includeSubfolders && !args.includeFiles) {
39
+ throw new UserError('At least one of includeSubfolders or includeFiles must be true.');
40
+ }
41
+ if (!args.includeSubfolders) {
42
+ queryString += ` and mimeType!='application/vnd.google-apps.folder'`;
43
+ }
44
+ else if (!args.includeFiles) {
45
+ queryString += ` and mimeType='application/vnd.google-apps.folder'`;
46
+ }
47
+ const response = await drive.files.list({
48
+ q: queryString,
49
+ pageSize: args.maxResults,
50
+ orderBy: 'folder,name',
51
+ fields: 'files(id,name,mimeType,size,modifiedTime,webViewLink,owners(displayName))',
52
+ supportsAllDrives: true,
53
+ includeItemsFromAllDrives: true,
54
+ });
55
+ const items = response.data.files || [];
56
+ const folders = items
57
+ .filter((f) => f.mimeType === 'application/vnd.google-apps.folder')
58
+ .map((f) => ({
59
+ id: f.id,
60
+ name: f.name,
61
+ modifiedTime: f.modifiedTime,
62
+ }));
63
+ const files = items
64
+ .filter((f) => f.mimeType !== 'application/vnd.google-apps.folder')
65
+ .map((f) => ({
66
+ id: f.id,
67
+ name: f.name,
68
+ mimeType: f.mimeType,
69
+ modifiedTime: f.modifiedTime,
70
+ }));
71
+ return JSON.stringify({ folders, files }, null, 2);
72
+ }
73
+ catch (error) {
74
+ log.error(`Error listing folder contents: ${error.message || error}`);
75
+ if (error.code === 404)
76
+ throw new UserError('Folder not found. Check the folder ID.');
77
+ if (error.code === 403)
78
+ throw new UserError('Permission denied. Make sure you have access to this folder.');
79
+ throw new UserError(`Failed to list folder contents: ${error.message || 'Unknown error'}`);
80
+ }
81
+ },
82
+ });
83
+ }