google-tools-mcp 1.0.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.
Files changed (95) hide show
  1. package/dist/auth.js +275 -0
  2. package/dist/cachedToolsList.js +52 -0
  3. package/dist/clients.js +97 -0
  4. package/dist/googleDocsApiHelpers.js +883 -0
  5. package/dist/googleSheetsApiHelpers.js +808 -0
  6. package/dist/helpers.js +210 -0
  7. package/dist/index.js +55 -0
  8. package/dist/logger.js +45 -0
  9. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  10. package/dist/markdown-transformer/index.js +126 -0
  11. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  12. package/dist/tools/docs/addTab.js +92 -0
  13. package/dist/tools/docs/appendToGoogleDoc.js +81 -0
  14. package/dist/tools/docs/comments/addComment.js +83 -0
  15. package/dist/tools/docs/comments/deleteComment.js +30 -0
  16. package/dist/tools/docs/comments/getComment.js +45 -0
  17. package/dist/tools/docs/comments/index.js +14 -0
  18. package/dist/tools/docs/comments/listComments.js +43 -0
  19. package/dist/tools/docs/comments/replyToComment.js +35 -0
  20. package/dist/tools/docs/comments/resolveComment.js +55 -0
  21. package/dist/tools/docs/deleteRange.js +72 -0
  22. package/dist/tools/docs/findAndReplace.js +54 -0
  23. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  24. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  25. package/dist/tools/docs/formatting/index.js +6 -0
  26. package/dist/tools/docs/index.js +38 -0
  27. package/dist/tools/docs/insertImage.js +122 -0
  28. package/dist/tools/docs/insertPageBreak.js +58 -0
  29. package/dist/tools/docs/insertTable.js +53 -0
  30. package/dist/tools/docs/insertTableWithData.js +135 -0
  31. package/dist/tools/docs/insertText.js +61 -0
  32. package/dist/tools/docs/listDocumentTabs.js +60 -0
  33. package/dist/tools/docs/modifyText.js +158 -0
  34. package/dist/tools/docs/readGoogleDoc.js +165 -0
  35. package/dist/tools/docs/renameTab.js +61 -0
  36. package/dist/tools/drive/copyFile.js +63 -0
  37. package/dist/tools/drive/createDocument.js +89 -0
  38. package/dist/tools/drive/createFolder.js +48 -0
  39. package/dist/tools/drive/createFromTemplate.js +82 -0
  40. package/dist/tools/drive/deleteFile.js +72 -0
  41. package/dist/tools/drive/getDocumentInfo.js +48 -0
  42. package/dist/tools/drive/getFolderInfo.js +48 -0
  43. package/dist/tools/drive/index.js +30 -0
  44. package/dist/tools/drive/listFolderContents.js +82 -0
  45. package/dist/tools/drive/listGoogleDocs.js +67 -0
  46. package/dist/tools/drive/listSharedDrives.js +52 -0
  47. package/dist/tools/drive/listSharedWithMe.js +97 -0
  48. package/dist/tools/drive/moveFile.js +54 -0
  49. package/dist/tools/drive/renameFile.js +39 -0
  50. package/dist/tools/drive/searchGoogleDocs.js +98 -0
  51. package/dist/tools/extras/index.js +7 -0
  52. package/dist/tools/extras/readFile.js +82 -0
  53. package/dist/tools/extras/searchFileContents.js +81 -0
  54. package/dist/tools/gmail/drafts.js +165 -0
  55. package/dist/tools/gmail/labels.js +103 -0
  56. package/dist/tools/gmail/messages.js +448 -0
  57. package/dist/tools/gmail/settings.js +528 -0
  58. package/dist/tools/gmail/threads.js +145 -0
  59. package/dist/tools/index.js +163 -0
  60. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  61. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  62. package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
  63. package/dist/tools/sheets/appendTableRows.js +50 -0
  64. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  65. package/dist/tools/sheets/batchWrite.js +59 -0
  66. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  67. package/dist/tools/sheets/copyFormatting.js +59 -0
  68. package/dist/tools/sheets/createSpreadsheet.js +71 -0
  69. package/dist/tools/sheets/createTable.js +120 -0
  70. package/dist/tools/sheets/deleteChart.js +41 -0
  71. package/dist/tools/sheets/deleteSheet.js +43 -0
  72. package/dist/tools/sheets/deleteTable.js +56 -0
  73. package/dist/tools/sheets/duplicateSheet.js +53 -0
  74. package/dist/tools/sheets/formatCells.js +106 -0
  75. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  76. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  77. package/dist/tools/sheets/getTable.js +48 -0
  78. package/dist/tools/sheets/groupRows.js +62 -0
  79. package/dist/tools/sheets/index.js +66 -0
  80. package/dist/tools/sheets/insertChart.js +225 -0
  81. package/dist/tools/sheets/listGoogleSheets.js +62 -0
  82. package/dist/tools/sheets/listTables.js +55 -0
  83. package/dist/tools/sheets/readCellFormat.js +143 -0
  84. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  85. package/dist/tools/sheets/renameSheet.js +48 -0
  86. package/dist/tools/sheets/setColumnWidths.js +43 -0
  87. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  88. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  89. package/dist/tools/sheets/updateTableRange.js +51 -0
  90. package/dist/tools/sheets/writeSpreadsheet.js +43 -0
  91. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
  92. package/dist/tools/utils/index.js +6 -0
  93. package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
  94. package/dist/types.js +186 -0
  95. package/package.json +30 -0
@@ -0,0 +1,82 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient, getDocsClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'createDocumentFromTemplate',
7
+ description: 'Creates a new document by copying an existing template and optionally replacing placeholder text. Provide key-value pairs in the replacements parameter to substitute template variables.',
8
+ parameters: z.object({
9
+ templateId: z.string().describe('ID of the template document to copy from.'),
10
+ newTitle: z.string().min(1).describe('Title for the new document.'),
11
+ parentFolderId: z
12
+ .string()
13
+ .optional()
14
+ .describe('ID of folder where document should be created. If not provided, creates in Drive root.'),
15
+ replacements: z
16
+ .record(z.string())
17
+ .optional()
18
+ .describe('Key-value pairs for text replacements in the template (e.g., {"{{NAME}}": "John Doe", "{{DATE}}": "2024-01-01"}).'),
19
+ }),
20
+ execute: async (args, { log }) => {
21
+ const drive = await getDriveClient();
22
+ log.info(`Creating document from template ${args.templateId} with title "${args.newTitle}"`);
23
+ try {
24
+ // First copy the template
25
+ const copyMetadata = {
26
+ name: args.newTitle,
27
+ };
28
+ if (args.parentFolderId) {
29
+ copyMetadata.parents = [args.parentFolderId];
30
+ }
31
+ const response = await drive.files.copy({
32
+ fileId: args.templateId,
33
+ requestBody: copyMetadata,
34
+ fields: 'id,name,webViewLink',
35
+ supportsAllDrives: true,
36
+ });
37
+ const document = response.data;
38
+ let result = `Successfully created document "${document.name}" from template (ID: ${document.id})\nView Link: ${document.webViewLink}`;
39
+ // Apply text replacements if provided
40
+ if (args.replacements && Object.keys(args.replacements).length > 0) {
41
+ try {
42
+ const docs = await getDocsClient();
43
+ const requests = [];
44
+ // Create replace requests for each replacement
45
+ for (const [searchText, replaceText] of Object.entries(args.replacements)) {
46
+ requests.push({
47
+ replaceAllText: {
48
+ containsText: {
49
+ text: searchText,
50
+ matchCase: false,
51
+ },
52
+ replaceText: replaceText,
53
+ },
54
+ });
55
+ }
56
+ if (requests.length > 0) {
57
+ await docs.documents.batchUpdate({
58
+ documentId: document.id,
59
+ requestBody: { requests },
60
+ });
61
+ const replacementCount = Object.keys(args.replacements).length;
62
+ result += `\n\nApplied ${replacementCount} text replacement${replacementCount !== 1 ? 's' : ''} to the document.`;
63
+ }
64
+ }
65
+ catch (replacementError) {
66
+ log.warn(`Document created but failed to apply replacements: ${replacementError.message}`);
67
+ result += `\n\nDocument created but failed to apply text replacements. You can make changes manually.`;
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ catch (error) {
73
+ log.error(`Error creating document from template: ${error.message || error}`);
74
+ if (error.code === 404)
75
+ throw new UserError('Template document or parent folder not found. Check the IDs.');
76
+ if (error.code === 403)
77
+ throw new UserError('Permission denied. Make sure you have read access to the template and write access to the destination folder.');
78
+ throw new UserError(`Failed to create document from template: ${error.message || 'Unknown error'}`);
79
+ }
80
+ },
81
+ });
82
+ }
@@ -0,0 +1,72 @@
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: 'deleteFile',
7
+ description: 'Moves a file or folder to the trash, or permanently deletes it. Set permanent=true for irreversible deletion.',
8
+ parameters: z.object({
9
+ fileId: z
10
+ .string()
11
+ .describe('The file or folder ID from a Google Drive URL or a previous tool result.'),
12
+ permanent: z
13
+ .boolean()
14
+ .optional()
15
+ .default(false)
16
+ .describe('If true, permanently deletes the file instead of moving it to trash.'),
17
+ }),
18
+ execute: async (args, { log }) => {
19
+ const drive = await getDriveClient();
20
+ log.info(`Deleting file ${args.fileId} ${args.permanent ? '(permanent)' : '(to trash)'}`);
21
+ try {
22
+ // Get file info before deletion
23
+ const fileInfo = await drive.files.get({
24
+ fileId: args.fileId,
25
+ fields: 'name,mimeType',
26
+ supportsAllDrives: true,
27
+ });
28
+ const fileName = fileInfo.data.name;
29
+ const isFolder = fileInfo.data.mimeType === 'application/vnd.google-apps.folder';
30
+ if (args.permanent) {
31
+ await drive.files.delete({
32
+ fileId: args.fileId,
33
+ supportsAllDrives: true,
34
+ });
35
+ return JSON.stringify({
36
+ success: true,
37
+ action: 'permanently_deleted',
38
+ fileId: args.fileId,
39
+ fileName,
40
+ type: isFolder ? 'folder' : 'file',
41
+ message: `Permanently deleted ${isFolder ? 'folder' : 'file'} "${fileName}".`,
42
+ }, null, 2);
43
+ }
44
+ else {
45
+ await drive.files.update({
46
+ fileId: args.fileId,
47
+ requestBody: {
48
+ trashed: true,
49
+ },
50
+ supportsAllDrives: true,
51
+ });
52
+ return JSON.stringify({
53
+ success: true,
54
+ action: 'trashed',
55
+ fileId: args.fileId,
56
+ fileName,
57
+ type: isFolder ? 'folder' : 'file',
58
+ message: `Moved ${isFolder ? 'folder' : 'file'} "${fileName}" to trash. It can be restored from the trash.`,
59
+ }, null, 2);
60
+ }
61
+ }
62
+ catch (error) {
63
+ log.error(`Error deleting file: ${error.message || error}`);
64
+ if (error.code === 404)
65
+ throw new UserError('File not found. Check the file ID.');
66
+ if (error.code === 403)
67
+ throw new UserError('Permission denied. Make sure you have delete access to this file.');
68
+ throw new UserError(`Failed to delete file: ${error.message || 'Unknown error'}`);
69
+ }
70
+ },
71
+ });
72
+ }
@@ -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.object({
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,30 @@
1
+ import { register as listGoogleDocs } from './listGoogleDocs.js';
2
+ import { register as searchGoogleDocs } from './searchGoogleDocs.js';
3
+ import { register as getDocumentInfo } from './getDocumentInfo.js';
4
+ import { register as createFolder } from './createFolder.js';
5
+ import { register as listFolderContents } from './listFolderContents.js';
6
+ import { register as getFolderInfo } from './getFolderInfo.js';
7
+ import { register as moveFile } from './moveFile.js';
8
+ import { register as copyFile } from './copyFile.js';
9
+ import { register as renameFile } from './renameFile.js';
10
+ import { register as deleteFile } from './deleteFile.js';
11
+ import { register as createDocument } from './createDocument.js';
12
+ import { register as createFromTemplate } from './createFromTemplate.js';
13
+ import { register as listSharedDrives } from './listSharedDrives.js';
14
+ import { register as listSharedWithMe } from './listSharedWithMe.js';
15
+ export function registerDriveTools(server) {
16
+ listGoogleDocs(server);
17
+ searchGoogleDocs(server);
18
+ getDocumentInfo(server);
19
+ createFolder(server);
20
+ listFolderContents(server);
21
+ getFolderInfo(server);
22
+ moveFile(server);
23
+ copyFile(server);
24
+ renameFile(server);
25
+ deleteFile(server);
26
+ createDocument(server);
27
+ createFromTemplate(server);
28
+ listSharedDrives(server);
29
+ listSharedWithMe(server);
30
+ }
@@ -0,0 +1,82 @@
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: 'listFolderContents',
7
+ description: "Lists files and subfolders within a Drive folder. Use folderId='root' to browse the top-level of the Drive.",
8
+ parameters: z.object({
9
+ folderId: z
10
+ .string()
11
+ .describe('ID of the folder to list contents of. Use "root" for the root Drive folder.'),
12
+ includeSubfolders: z
13
+ .boolean()
14
+ .optional()
15
+ .default(true)
16
+ .describe('Whether to include subfolders in results.'),
17
+ includeFiles: z
18
+ .boolean()
19
+ .optional()
20
+ .default(true)
21
+ .describe('Whether to include files in results.'),
22
+ maxResults: z
23
+ .number()
24
+ .int()
25
+ .min(1)
26
+ .max(100)
27
+ .optional()
28
+ .default(50)
29
+ .describe('Maximum number of items to return.'),
30
+ }),
31
+ execute: async (args, { log }) => {
32
+ const drive = await getDriveClient();
33
+ log.info(`Listing contents of folder: ${args.folderId}`);
34
+ try {
35
+ let queryString = `'${args.folderId}' in parents and trashed=false`;
36
+ // Filter by type if specified
37
+ if (!args.includeSubfolders && !args.includeFiles) {
38
+ throw new UserError('At least one of includeSubfolders or includeFiles must be true.');
39
+ }
40
+ if (!args.includeSubfolders) {
41
+ queryString += ` and mimeType!='application/vnd.google-apps.folder'`;
42
+ }
43
+ else if (!args.includeFiles) {
44
+ queryString += ` and mimeType='application/vnd.google-apps.folder'`;
45
+ }
46
+ const response = await drive.files.list({
47
+ q: queryString,
48
+ pageSize: args.maxResults,
49
+ orderBy: 'folder,name',
50
+ fields: 'files(id,name,mimeType,size,modifiedTime,webViewLink,owners(displayName))',
51
+ supportsAllDrives: true,
52
+ includeItemsFromAllDrives: true,
53
+ });
54
+ const items = response.data.files || [];
55
+ const folders = items
56
+ .filter((f) => f.mimeType === 'application/vnd.google-apps.folder')
57
+ .map((f) => ({
58
+ id: f.id,
59
+ name: f.name,
60
+ modifiedTime: f.modifiedTime,
61
+ }));
62
+ const files = items
63
+ .filter((f) => f.mimeType !== 'application/vnd.google-apps.folder')
64
+ .map((f) => ({
65
+ id: f.id,
66
+ name: f.name,
67
+ mimeType: f.mimeType,
68
+ modifiedTime: f.modifiedTime,
69
+ }));
70
+ return JSON.stringify({ folders, files }, null, 2);
71
+ }
72
+ catch (error) {
73
+ log.error(`Error listing folder contents: ${error.message || error}`);
74
+ if (error.code === 404)
75
+ throw new UserError('Folder not found. Check the folder ID.');
76
+ if (error.code === 403)
77
+ throw new UserError('Permission denied. Make sure you have access to this folder.');
78
+ throw new UserError(`Failed to list folder contents: ${error.message || 'Unknown error'}`);
79
+ }
80
+ },
81
+ });
82
+ }
@@ -0,0 +1,67 @@
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: 'listDocuments',
7
+ description: 'Lists Google Documents in your Drive, optionally filtered by name or content. Use modifiedAfter to find recently changed documents.',
8
+ parameters: z.object({
9
+ maxResults: z
10
+ .number()
11
+ .int()
12
+ .min(1)
13
+ .max(100)
14
+ .optional()
15
+ .default(20)
16
+ .describe('Maximum number of documents to return (1-100).'),
17
+ query: z.string().optional().describe('Search query to filter documents by name or content.'),
18
+ orderBy: z
19
+ .enum(['name', 'modifiedTime', 'createdTime'])
20
+ .optional()
21
+ .default('modifiedTime')
22
+ .describe('Sort order for results.'),
23
+ modifiedAfter: z
24
+ .string()
25
+ .optional()
26
+ .describe('Only return documents modified after this date (ISO 8601 format, e.g., "2024-01-01").'),
27
+ }),
28
+ execute: async (args, { log }) => {
29
+ const drive = await getDriveClient();
30
+ log.info(`Listing Google Docs. Query: ${args.query || 'none'}, Max: ${args.maxResults}, Order: ${args.orderBy}`);
31
+ try {
32
+ // Build the query string for Google Drive API
33
+ let queryString = "mimeType='application/vnd.google-apps.document' and trashed=false";
34
+ if (args.query) {
35
+ queryString += ` and (name contains '${args.query}' or fullText contains '${args.query}')`;
36
+ }
37
+ if (args.modifiedAfter) {
38
+ const cutoffDate = new Date(args.modifiedAfter).toISOString();
39
+ queryString += ` and modifiedTime > '${cutoffDate}'`;
40
+ }
41
+ const response = await drive.files.list({
42
+ q: queryString,
43
+ pageSize: args.maxResults,
44
+ orderBy: args.orderBy === 'name' ? 'name' : args.orderBy,
45
+ fields: 'files(id,name,modifiedTime,createdTime,size,webViewLink,owners(displayName,emailAddress))',
46
+ supportsAllDrives: true,
47
+ includeItemsFromAllDrives: true,
48
+ });
49
+ const files = response.data.files || [];
50
+ const documents = files.map((file) => ({
51
+ id: file.id,
52
+ name: file.name,
53
+ modifiedTime: file.modifiedTime,
54
+ owner: file.owners?.[0]?.displayName || null,
55
+ url: file.webViewLink,
56
+ }));
57
+ return JSON.stringify({ documents }, null, 2);
58
+ }
59
+ catch (error) {
60
+ log.error(`Error listing Google Docs: ${error.message || error}`);
61
+ if (error.code === 403)
62
+ throw new UserError('Permission denied. Make sure you have granted Google Drive access to the application.');
63
+ throw new UserError(`Failed to list documents: ${error.message || 'Unknown error'}`);
64
+ }
65
+ },
66
+ });
67
+ }
@@ -0,0 +1,52 @@
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: 'listSharedDrives',
7
+ description: 'Lists all Shared Drives (Team Drives) the user has access to. Use the returned drive ID with listFolderContents to browse its contents.',
8
+ parameters: z.object({
9
+ maxResults: z
10
+ .number()
11
+ .int()
12
+ .min(1)
13
+ .max(100)
14
+ .optional()
15
+ .default(50)
16
+ .describe('Maximum number of shared drives to return.'),
17
+ pageToken: z
18
+ .string()
19
+ .optional()
20
+ .describe('Token for fetching the next page of results.'),
21
+ }),
22
+ execute: async (args, { log }) => {
23
+ const drive = await getDriveClient();
24
+ log.info('Listing shared drives');
25
+ try {
26
+ const response = await drive.drives.list({
27
+ pageSize: args.maxResults,
28
+ pageToken: args.pageToken || undefined,
29
+ fields: 'nextPageToken,drives(id,name,createdTime,hidden)',
30
+ });
31
+ const drives = (response.data.drives || []).map((d) => ({
32
+ id: d.id,
33
+ name: d.name,
34
+ createdTime: d.createdTime,
35
+ hidden: d.hidden || false,
36
+ }));
37
+ const result = {
38
+ drives,
39
+ nextPageToken: response.data.nextPageToken || null,
40
+ totalCount: drives.length,
41
+ };
42
+ return JSON.stringify(result, null, 2);
43
+ }
44
+ catch (error) {
45
+ log.error(`Error listing shared drives: ${error.message || error}`);
46
+ if (error.code === 403)
47
+ throw new UserError('Permission denied. Make sure the Drive API scope includes shared drives.');
48
+ throw new UserError(`Failed to list shared drives: ${error.message || 'Unknown error'}`);
49
+ }
50
+ },
51
+ });
52
+ }
@@ -0,0 +1,97 @@
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: 'listSharedWithMe',
7
+ description: 'Lists files and folders that have been shared with the user ("Shared with me"). Returns items shared by others that are not in the user\'s own Drive. Use folder IDs with listFolderContents to navigate into shared folders.',
8
+ parameters: z.object({
9
+ mimeType: z
10
+ .string()
11
+ .optional()
12
+ .describe('Filter by MIME type. Use "application/vnd.google-apps.folder" for folders only, "application/vnd.google-apps.document" for Docs, "application/vnd.google-apps.spreadsheet" for Sheets, etc.'),
13
+ query: z
14
+ .string()
15
+ .optional()
16
+ .describe('Search query to filter shared items by name (uses "contains" matching).'),
17
+ maxResults: z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .max(100)
22
+ .optional()
23
+ .default(50)
24
+ .describe('Maximum number of items to return.'),
25
+ pageToken: z
26
+ .string()
27
+ .optional()
28
+ .describe('Token for fetching the next page of results.'),
29
+ orderBy: z
30
+ .enum(['name', 'modifiedTime', 'sharedWithMeTime'])
31
+ .optional()
32
+ .default('sharedWithMeTime')
33
+ .describe('How to sort results. Defaults to most recently shared first.'),
34
+ }),
35
+ execute: async (args, { log }) => {
36
+ const drive = await getDriveClient();
37
+ log.info('Listing shared with me');
38
+ try {
39
+ let queryParts = ['sharedWithMe=true', 'trashed=false'];
40
+ if (args.mimeType) {
41
+ queryParts.push(`mimeType='${args.mimeType}'`);
42
+ }
43
+ if (args.query) {
44
+ queryParts.push(`name contains '${args.query.replace(/'/g, "\\'")}'`);
45
+ }
46
+ const orderBy = args.orderBy === 'sharedWithMeTime'
47
+ ? 'sharedWithMeTime desc'
48
+ : args.orderBy === 'modifiedTime'
49
+ ? 'modifiedTime desc'
50
+ : 'name';
51
+ const response = await drive.files.list({
52
+ q: queryParts.join(' and '),
53
+ pageSize: args.maxResults,
54
+ pageToken: args.pageToken || undefined,
55
+ orderBy,
56
+ fields: 'nextPageToken,files(id,name,mimeType,modifiedTime,sharedWithMeTime,sharingUser(displayName,emailAddress),webViewLink,owners(displayName))',
57
+ supportsAllDrives: true,
58
+ includeItemsFromAllDrives: true,
59
+ });
60
+ const items = response.data.files || [];
61
+ const folders = items
62
+ .filter((f) => f.mimeType === 'application/vnd.google-apps.folder')
63
+ .map((f) => ({
64
+ id: f.id,
65
+ name: f.name,
66
+ sharedWithMeTime: f.sharedWithMeTime,
67
+ sharedBy: f.sharingUser?.displayName || f.owners?.[0]?.displayName || null,
68
+ sharedByEmail: f.sharingUser?.emailAddress || null,
69
+ }));
70
+ const files = items
71
+ .filter((f) => f.mimeType !== 'application/vnd.google-apps.folder')
72
+ .map((f) => ({
73
+ id: f.id,
74
+ name: f.name,
75
+ mimeType: f.mimeType,
76
+ modifiedTime: f.modifiedTime,
77
+ sharedWithMeTime: f.sharedWithMeTime,
78
+ sharedBy: f.sharingUser?.displayName || f.owners?.[0]?.displayName || null,
79
+ sharedByEmail: f.sharingUser?.emailAddress || null,
80
+ }));
81
+ const result = {
82
+ folders,
83
+ files,
84
+ nextPageToken: response.data.nextPageToken || null,
85
+ totalCount: items.length,
86
+ };
87
+ return JSON.stringify(result, null, 2);
88
+ }
89
+ catch (error) {
90
+ log.error(`Error listing shared items: ${error.message || error}`);
91
+ if (error.code === 403)
92
+ throw new UserError('Permission denied. Make sure you have the correct Drive API scopes.');
93
+ throw new UserError(`Failed to list shared items: ${error.message || 'Unknown error'}`);
94
+ }
95
+ },
96
+ });
97
+ }
@@ -0,0 +1,54 @@
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: 'moveFile',
7
+ description: 'Moves a file or folder to a different Drive folder. By default adds the new parent while keeping existing parents; set removeFromAllParents=true for a true move.',
8
+ parameters: z.object({
9
+ fileId: z
10
+ .string()
11
+ .describe('The file or folder ID from a Google Drive URL or a previous tool result.'),
12
+ newParentId: z.string().describe('ID of the destination folder. Use "root" for Drive root.'),
13
+ removeFromAllParents: z
14
+ .boolean()
15
+ .optional()
16
+ .default(false)
17
+ .describe('If true, removes from all current parents. If false, adds to new parent while keeping existing parents.'),
18
+ }),
19
+ execute: async (args, { log }) => {
20
+ const drive = await getDriveClient();
21
+ log.info(`Moving file ${args.fileId} to folder ${args.newParentId}`);
22
+ try {
23
+ // First get the current parents
24
+ const fileInfo = await drive.files.get({
25
+ fileId: args.fileId,
26
+ fields: 'name,parents',
27
+ supportsAllDrives: true,
28
+ });
29
+ const fileName = fileInfo.data.name;
30
+ const currentParents = fileInfo.data.parents || [];
31
+ let updateParams = {
32
+ fileId: args.fileId,
33
+ addParents: args.newParentId,
34
+ fields: 'id,name,parents',
35
+ supportsAllDrives: true,
36
+ };
37
+ if (args.removeFromAllParents && currentParents.length > 0) {
38
+ updateParams.removeParents = currentParents.join(',');
39
+ }
40
+ const response = await drive.files.update(updateParams);
41
+ const action = args.removeFromAllParents ? 'moved' : 'copied';
42
+ return `Successfully ${action} "${fileName}" to new location.\nFile ID: ${response.data.id}`;
43
+ }
44
+ catch (error) {
45
+ log.error(`Error moving file: ${error.message || error}`);
46
+ if (error.code === 404)
47
+ throw new UserError('File or destination folder not found. Check the IDs.');
48
+ if (error.code === 403)
49
+ throw new UserError('Permission denied. Make sure you have write access to both source and destination.');
50
+ throw new UserError(`Failed to move file: ${error.message || 'Unknown error'}`);
51
+ }
52
+ },
53
+ });
54
+ }