google-tools-mcp 1.2.12 → 2.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 (97) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +537 -276
  3. package/dist/cachedToolsList.js +52 -52
  4. package/dist/googleDocsApiHelpers.js +76 -12
  5. package/dist/helpers.js +572 -252
  6. package/dist/httpAuth.js +296 -0
  7. package/dist/index.js +162 -11
  8. package/dist/logger.js +87 -87
  9. package/dist/markdown-transformer/docsToMarkdown.js +128 -0
  10. package/dist/markdown-transformer/index.js +17 -7
  11. package/dist/markdown-transformer/markdownToDocs.js +185 -27
  12. package/dist/readTracker.js +136 -17
  13. package/dist/sessionContext.js +37 -0
  14. package/dist/setup.js +187 -9
  15. package/dist/tools/calendar/getBusy.js +64 -64
  16. package/dist/tools/calendar/getEvents.js +141 -141
  17. package/dist/tools/calendar/getFree.js +225 -225
  18. package/dist/tools/calendar/index.js +19 -19
  19. package/dist/tools/calendar/listCalendars.js +38 -38
  20. package/dist/tools/calendar/listRecurringInstances.js +83 -83
  21. package/dist/tools/calendar/manageCalendar.js +121 -121
  22. package/dist/tools/calendar/manageEvent.js +258 -258
  23. package/dist/tools/calendar/moveEvent.js +66 -66
  24. package/dist/tools/docs/addTab.js +10 -12
  25. package/dist/tools/docs/appendToGoogleDoc.js +9 -4
  26. package/dist/tools/docs/deleteRange.js +9 -4
  27. package/dist/tools/docs/findAndReplace.js +9 -4
  28. package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
  29. package/dist/tools/docs/formatting/getFormatting.js +171 -171
  30. package/dist/tools/docs/insertImage.js +15 -2
  31. package/dist/tools/docs/insertPageBreak.js +4 -1
  32. package/dist/tools/docs/insertTable.js +4 -1
  33. package/dist/tools/docs/insertTableWithData.js +4 -1
  34. package/dist/tools/docs/modifyText.js +14 -4
  35. package/dist/tools/docs/modifyText.test.js +84 -84
  36. package/dist/tools/docs/readGoogleDoc.js +71 -11
  37. package/dist/tools/docs/renameTab.js +12 -14
  38. package/dist/tools/drafts.js +165 -165
  39. package/dist/tools/drive/createDocument.js +12 -0
  40. package/dist/tools/drive/downloadFile.js +268 -268
  41. package/dist/tools/drive/getFileInfo.js +48 -48
  42. package/dist/tools/drive/getFilePath.js +50 -50
  43. package/dist/tools/drive/listDriveFiles.js +112 -112
  44. package/dist/tools/drive/listSharedDrives.js +52 -52
  45. package/dist/tools/drive/listSharedWithMe.js +97 -97
  46. package/dist/tools/drive/uploadFile.js +111 -111
  47. package/dist/tools/extras/index.js +9 -9
  48. package/dist/tools/extras/readDriveFile.js +197 -196
  49. package/dist/tools/extras/readFile.js +84 -84
  50. package/dist/tools/extras/searchFileContents.js +81 -81
  51. package/dist/tools/forms/batchUpdateForm.js +81 -81
  52. package/dist/tools/forms/createForm.js +54 -54
  53. package/dist/tools/forms/getForm.js +118 -118
  54. package/dist/tools/forms/getFormResponse.js +45 -45
  55. package/dist/tools/forms/index.js +15 -15
  56. package/dist/tools/forms/listFormResponses.js +56 -56
  57. package/dist/tools/forms/setPublishSettings.js +59 -59
  58. package/dist/tools/gmail/drafts.js +165 -165
  59. package/dist/tools/gmail/labels.js +54 -83
  60. package/dist/tools/gmail/messages.js +444 -441
  61. package/dist/tools/gmail/settings.js +358 -528
  62. package/dist/tools/gmail/threads.js +286 -164
  63. package/dist/tools/index.js +511 -473
  64. package/dist/tools/labels.js +103 -103
  65. package/dist/tools/legacyAliases.js +426 -0
  66. package/dist/tools/maps/directions.js +72 -0
  67. package/dist/tools/maps/geocode.js +16 -0
  68. package/dist/tools/maps/index.js +15 -0
  69. package/dist/tools/maps/mapsClient.js +83 -0
  70. package/dist/tools/maps/placeDetails.js +16 -0
  71. package/dist/tools/maps/reverseGeocode.js +19 -0
  72. package/dist/tools/maps/searchNearby.js +54 -0
  73. package/dist/tools/maps/searchPlaces.js +24 -0
  74. package/dist/tools/messages.js +448 -448
  75. package/dist/tools/settings.js +528 -528
  76. package/dist/tools/slides/createPresentation.js +104 -104
  77. package/dist/tools/slides/createShape.js +92 -92
  78. package/dist/tools/slides/createTextBox.js +82 -82
  79. package/dist/tools/slides/deleteSlide.js +30 -30
  80. package/dist/tools/slides/duplicateSlide.js +37 -37
  81. package/dist/tools/slides/exportThumbnail.js +42 -42
  82. package/dist/tools/slides/formatParagraph.js +72 -72
  83. package/dist/tools/slides/formatText.js +84 -84
  84. package/dist/tools/slides/getPresentation.js +87 -87
  85. package/dist/tools/slides/index.js +33 -33
  86. package/dist/tools/slides/reorderSlides.js +41 -41
  87. package/dist/tools/slides/replaceAllText.js +46 -46
  88. package/dist/tools/slides/setBackground.js +58 -58
  89. package/dist/tools/slides/speakerNotes.js +102 -102
  90. package/dist/tools/slides/styleShape.js +111 -111
  91. package/dist/tools/slides/updatePresentation.js +128 -128
  92. package/dist/tools/threads.js +145 -145
  93. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +30 -6
  94. package/dist/tools/utils/replaceDocumentWithMarkdown.js +71 -8
  95. package/dist/updateCheck.js +202 -0
  96. package/dist/workspace.js +122 -0
  97. package/package.json +82 -81
@@ -1,111 +1,111 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { UserError } from 'fastmcp';
4
- import { z } from 'zod';
5
- import { getDriveClient } from '../../clients.js';
6
-
7
- // Common MIME type mappings for file extensions
8
- const MIME_TYPES = {
9
- '.pdf': 'application/pdf',
10
- '.doc': 'application/msword',
11
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
12
- '.xls': 'application/vnd.ms-excel',
13
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
14
- '.ppt': 'application/vnd.ms-powerpoint',
15
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
16
- '.txt': 'text/plain',
17
- '.csv': 'text/csv',
18
- '.json': 'application/json',
19
- '.xml': 'application/xml',
20
- '.html': 'text/html',
21
- '.htm': 'text/html',
22
- '.md': 'text/markdown',
23
- '.png': 'image/png',
24
- '.jpg': 'image/jpeg',
25
- '.jpeg': 'image/jpeg',
26
- '.gif': 'image/gif',
27
- '.svg': 'image/svg+xml',
28
- '.webp': 'image/webp',
29
- '.mp3': 'audio/mpeg',
30
- '.mp4': 'video/mp4',
31
- '.zip': 'application/zip',
32
- '.gz': 'application/gzip',
33
- '.tar': 'application/x-tar',
34
- };
35
-
36
- export function register(server) {
37
- server.addTool({
38
- name: 'uploadFile',
39
- description:
40
- 'Uploads a local file from the filesystem to Google Drive. ' +
41
- 'Auto-detects MIME type from file extension. ' +
42
- 'Returns the uploaded file\'s Drive ID and URL.',
43
- parameters: z.object({
44
- localPath: z.string().describe('Absolute path to the local file to upload.'),
45
- name: z
46
- .string()
47
- .optional()
48
- .describe('Custom name for the file in Drive. If not provided, uses the local filename.'),
49
- parentFolderId: z
50
- .string()
51
- .optional()
52
- .describe('ID of the Drive folder to upload into. If not provided, uploads to the root of My Drive.'),
53
- mimeType: z
54
- .string()
55
- .optional()
56
- .describe('MIME type override. If not provided, auto-detected from file extension.'),
57
- }),
58
- execute: async (args, { log }) => {
59
- const drive = await getDriveClient();
60
-
61
- // Validate file exists
62
- if (!fs.existsSync(args.localPath)) {
63
- throw new UserError(`File not found: ${args.localPath}`);
64
- }
65
-
66
- const stats = fs.statSync(args.localPath);
67
- if (!stats.isFile()) {
68
- throw new UserError(`Path is not a file: ${args.localPath}`);
69
- }
70
-
71
- const fileName = args.name || path.basename(args.localPath);
72
- const ext = path.extname(args.localPath).toLowerCase();
73
- const mimeType = args.mimeType || MIME_TYPES[ext] || 'application/octet-stream';
74
-
75
- log.info(`Uploading "${args.localPath}" as "${fileName}" (${mimeType}, ${stats.size} bytes)`);
76
-
77
- try {
78
- const fileMetadata = { name: fileName };
79
- if (args.parentFolderId) {
80
- fileMetadata.parents = [args.parentFolderId];
81
- }
82
-
83
- const response = await drive.files.create({
84
- requestBody: fileMetadata,
85
- media: {
86
- mimeType,
87
- body: fs.createReadStream(args.localPath),
88
- },
89
- fields: 'id,name,webViewLink,mimeType,size',
90
- supportsAllDrives: true,
91
- });
92
-
93
- const file = response.data;
94
- return JSON.stringify({
95
- id: file.id,
96
- name: file.name,
97
- url: file.webViewLink,
98
- mimeType: file.mimeType,
99
- size: file.size,
100
- }, null, 2);
101
- } catch (error) {
102
- log.error(`Error uploading file: ${error.message || error}`);
103
- if (error.code === 404)
104
- throw new UserError('Destination folder not found. Check the parentFolderId.');
105
- if (error.code === 403)
106
- throw new UserError('Permission denied. Make sure you have write access to the destination folder.');
107
- throw new UserError(`Failed to upload file: ${error.message || 'Unknown error'}`);
108
- }
109
- },
110
- });
111
- }
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { UserError } from 'fastmcp';
4
+ import { z } from 'zod';
5
+ import { getDriveClient } from '../../clients.js';
6
+
7
+ // Common MIME type mappings for file extensions
8
+ const MIME_TYPES = {
9
+ '.pdf': 'application/pdf',
10
+ '.doc': 'application/msword',
11
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
12
+ '.xls': 'application/vnd.ms-excel',
13
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
14
+ '.ppt': 'application/vnd.ms-powerpoint',
15
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
16
+ '.txt': 'text/plain',
17
+ '.csv': 'text/csv',
18
+ '.json': 'application/json',
19
+ '.xml': 'application/xml',
20
+ '.html': 'text/html',
21
+ '.htm': 'text/html',
22
+ '.md': 'text/markdown',
23
+ '.png': 'image/png',
24
+ '.jpg': 'image/jpeg',
25
+ '.jpeg': 'image/jpeg',
26
+ '.gif': 'image/gif',
27
+ '.svg': 'image/svg+xml',
28
+ '.webp': 'image/webp',
29
+ '.mp3': 'audio/mpeg',
30
+ '.mp4': 'video/mp4',
31
+ '.zip': 'application/zip',
32
+ '.gz': 'application/gzip',
33
+ '.tar': 'application/x-tar',
34
+ };
35
+
36
+ export function register(server) {
37
+ server.addTool({
38
+ name: 'uploadFile',
39
+ description:
40
+ 'Uploads a local file from the filesystem to Google Drive. ' +
41
+ 'Auto-detects MIME type from file extension. ' +
42
+ 'Returns the uploaded file\'s Drive ID and URL.',
43
+ parameters: z.object({
44
+ localPath: z.string().describe('Absolute path to the local file to upload.'),
45
+ name: z
46
+ .string()
47
+ .optional()
48
+ .describe('Custom name for the file in Drive. If not provided, uses the local filename.'),
49
+ parentFolderId: z
50
+ .string()
51
+ .optional()
52
+ .describe('ID of the Drive folder to upload into. If not provided, uploads to the root of My Drive.'),
53
+ mimeType: z
54
+ .string()
55
+ .optional()
56
+ .describe('MIME type override. If not provided, auto-detected from file extension.'),
57
+ }),
58
+ execute: async (args, { log }) => {
59
+ const drive = await getDriveClient();
60
+
61
+ // Validate file exists
62
+ if (!fs.existsSync(args.localPath)) {
63
+ throw new UserError(`File not found: ${args.localPath}`);
64
+ }
65
+
66
+ const stats = fs.statSync(args.localPath);
67
+ if (!stats.isFile()) {
68
+ throw new UserError(`Path is not a file: ${args.localPath}`);
69
+ }
70
+
71
+ const fileName = args.name || path.basename(args.localPath);
72
+ const ext = path.extname(args.localPath).toLowerCase();
73
+ const mimeType = args.mimeType || MIME_TYPES[ext] || 'application/octet-stream';
74
+
75
+ log.info(`Uploading "${args.localPath}" as "${fileName}" (${mimeType}, ${stats.size} bytes)`);
76
+
77
+ try {
78
+ const fileMetadata = { name: fileName };
79
+ if (args.parentFolderId) {
80
+ fileMetadata.parents = [args.parentFolderId];
81
+ }
82
+
83
+ const response = await drive.files.create({
84
+ requestBody: fileMetadata,
85
+ media: {
86
+ mimeType,
87
+ body: fs.createReadStream(args.localPath),
88
+ },
89
+ fields: 'id,name,webViewLink,mimeType,size',
90
+ supportsAllDrives: true,
91
+ });
92
+
93
+ const file = response.data;
94
+ return JSON.stringify({
95
+ id: file.id,
96
+ name: file.name,
97
+ url: file.webViewLink,
98
+ mimeType: file.mimeType,
99
+ size: file.size,
100
+ }, null, 2);
101
+ } catch (error) {
102
+ log.error(`Error uploading file: ${error.message || error}`);
103
+ if (error.code === 404)
104
+ throw new UserError('Destination folder not found. Check the parentFolderId.');
105
+ if (error.code === 403)
106
+ throw new UserError('Permission denied. Make sure you have write access to the destination folder.');
107
+ throw new UserError(`Failed to upload file: ${error.message || 'Unknown error'}`);
108
+ }
109
+ },
110
+ });
111
+ }
@@ -1,9 +1,9 @@
1
- import { register as readFile } from './readFile.js';
2
- import { register as searchFileContents } from './searchFileContents.js';
3
- import { register as readDriveFile } from './readDriveFile.js';
4
-
5
- export function registerExtrasTools(server) {
6
- readFile(server);
7
- searchFileContents(server);
8
- readDriveFile(server);
9
- }
1
+ import { register as readFile } from './readFile.js';
2
+ import { register as searchFileContents } from './searchFileContents.js';
3
+ import { register as readDriveFile } from './readDriveFile.js';
4
+
5
+ export function registerExtrasTools(server) {
6
+ readFile(server);
7
+ searchFileContents(server);
8
+ readDriveFile(server);
9
+ }
@@ -1,54 +1,54 @@
1
- import { UserError } from 'fastmcp';
2
- import { z } from 'zod';
3
- import { getDriveClient, getDocsClient, getSheetsClient } from '../../clients.js';
4
- import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
5
- import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
- import { extractText, downloadBuffer } from './readFile.js';
7
- import { trackRead } from '../../readTracker.js';
8
-
9
- // Google-native MIME types
10
- const GOOGLE_DOC = 'application/vnd.google-apps.document';
11
- const GOOGLE_SHEET = 'application/vnd.google-apps.spreadsheet';
12
- const GOOGLE_SLIDES = 'application/vnd.google-apps.presentation';
13
-
14
- // Capabilities by file type category
15
- const CAPABILITIES = {
16
- googleDoc: {
17
- fileType: 'Google Doc',
18
- capabilities: ['read', 'readDocument', 'modifyText', 'replaceDocumentWithMarkdown', 'appendMarkdown', 'appendText', 'findAndReplace', 'deleteRange'],
19
- },
20
- googleSheet: {
21
- fileType: 'Google Sheet',
22
- capabilities: ['read', 'readSpreadsheet', 'writeSpreadsheet', 'appendRows', 'formatCells'],
23
- },
24
- docx: {
25
- fileType: 'Word Document (.docx)',
26
- capabilities: ['read'],
27
- note: 'This is a Word document. Editing tools (modifyText, appendMarkdown, etc.) are not available unless converted to a Google Doc via copyFile or Drive UI.',
28
- },
29
- pdf: {
30
- fileType: 'PDF',
31
- capabilities: ['read'],
32
- note: 'This is a PDF file. Editing tools are not available. Convert to a Google Doc first if editing is needed.',
33
- },
34
- other: {
35
- fileType: 'Unknown',
36
- capabilities: ['download'],
37
- note: 'This file type cannot be read as text. Use downloadFile to download it.',
38
- },
39
- };
40
-
41
- export function register(server) {
42
- server.addTool({
43
- name: 'readDriveFile',
44
- description:
45
- 'Unified read tool: accepts any Google Drive file ID, auto-detects the file type (Google Doc, Sheet, Word .docx, PDF), ' +
46
- 'and routes to the appropriate reader. Returns content along with metadata about available capabilities. ' +
47
- 'Use this when you have a file ID but don\'t know the file type, or when readDocument fails with a 400 error.',
48
- parameters: z.object({
49
- fileId: z
50
- .string()
51
- .describe('The Google Drive file ID (the long string between /d/ and /edit in a Drive URL).'),
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDriveClient, getDocsClient, getSheetsClient } from '../../clients.js';
4
+ import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ import { extractText, downloadBuffer } from './readFile.js';
7
+ import { trackRead } from '../../readTracker.js';
8
+
9
+ // Google-native MIME types
10
+ const GOOGLE_DOC = 'application/vnd.google-apps.document';
11
+ const GOOGLE_SHEET = 'application/vnd.google-apps.spreadsheet';
12
+ const GOOGLE_SLIDES = 'application/vnd.google-apps.presentation';
13
+
14
+ // Capabilities by file type category
15
+ const CAPABILITIES = {
16
+ googleDoc: {
17
+ fileType: 'Google Doc',
18
+ capabilities: ['read', 'readDocument', 'modifyText', 'replaceDocumentWithMarkdown', 'appendMarkdown', 'appendText', 'findAndReplace', 'deleteRange'],
19
+ },
20
+ googleSheet: {
21
+ fileType: 'Google Sheet',
22
+ capabilities: ['read', 'readSpreadsheet', 'writeSpreadsheet', 'appendRows', 'formatCells'],
23
+ },
24
+ docx: {
25
+ fileType: 'Word Document (.docx)',
26
+ capabilities: ['read'],
27
+ note: 'This is a Word document. Editing tools (modifyText, appendMarkdown, etc.) are not available unless converted to a Google Doc via copyFile or Drive UI.',
28
+ },
29
+ pdf: {
30
+ fileType: 'PDF',
31
+ capabilities: ['read'],
32
+ note: 'This is a PDF file. Editing tools are not available. Convert to a Google Doc first if editing is needed.',
33
+ },
34
+ other: {
35
+ fileType: 'Unknown',
36
+ capabilities: ['download'],
37
+ note: 'This file type cannot be read as text. Use downloadFile to download it.',
38
+ },
39
+ };
40
+
41
+ export function register(server) {
42
+ server.addTool({
43
+ name: 'readDriveFile',
44
+ description:
45
+ 'Unified read tool: accepts any Google Drive file ID, auto-detects the file type (Google Doc, Sheet, Word .docx, PDF), ' +
46
+ 'and routes to the appropriate reader. Returns content along with metadata about available capabilities. ' +
47
+ 'Use this when you have a file ID but don\'t know the file type, or when readDocument fails with a 400 error.',
48
+ parameters: z.object({
49
+ fileId: z
50
+ .string()
51
+ .describe('The Google Drive file ID (the long string between /d/ and /edit in a Drive URL).'),
52
52
  format: z
53
53
  .enum(['text', 'json', 'markdown'])
54
54
  .optional()
@@ -62,150 +62,151 @@ export function register(server) {
62
62
  range: z
63
63
  .string()
64
64
  .optional()
65
- .describe("A1 notation range for Google Sheets (e.g., 'Sheet1!A1:C10'). Required for spreadsheets."),
66
- }),
67
- execute: async (args, { log }) => {
68
- const drive = await getDriveClient();
69
-
70
- // 1. Get file metadata to determine type
71
- let fileMeta;
72
- try {
73
- const res = await drive.files.get({
74
- fileId: args.fileId,
75
- fields: 'id,name,mimeType,modifiedTime,size',
76
- supportsAllDrives: true,
77
- });
78
- fileMeta = res.data;
79
- } catch (error) {
80
- if (error.code === 404) throw new UserError(`File not found: ${args.fileId}`);
81
- if (error.code === 403) throw new UserError('Permission denied. Check that the file is shared with this account.');
82
- throw new UserError(`Failed to get file info: ${error.message}`);
83
- }
84
-
85
- log.info(`File "${fileMeta.name}" is ${fileMeta.mimeType}`);
86
- trackRead(args.fileId, fileMeta.modifiedTime);
87
-
88
- // 2. Route based on MIME type
89
- if (fileMeta.mimeType === GOOGLE_DOC) {
90
- return await readGoogleDoc(args, fileMeta, log);
91
- }
92
-
93
- if (fileMeta.mimeType === GOOGLE_SHEET) {
94
- return await readGoogleSheet(args, fileMeta, log);
95
- }
96
-
97
- // Try reading as docx/pdf
98
- const text = await tryReadBinaryFile(drive, args.fileId, fileMeta, log);
99
- if (text !== null) {
100
- return text;
101
- }
102
-
103
- // Unsupported type
104
- const capInfo = CAPABILITIES.other;
105
- capInfo.fileType = fileMeta.mimeType;
106
- return JSON.stringify({
107
- file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
108
- ...capInfo,
109
- content: null,
110
- }, null, 2);
111
- },
112
- });
113
- }
114
-
115
- async function readGoogleDoc(args, fileMeta, log) {
116
- const docs = await getDocsClient();
117
- const res = await docs.documents.get({
118
- documentId: args.fileId,
119
- fields: args.format === 'text' ? 'body(content(paragraph(elements(textRun(content)))))' : '*',
120
- });
121
-
122
- let content;
123
- if (args.format === 'json') {
124
- content = JSON.stringify(res.data, null, 2);
65
+ .describe("A1 notation range for Google Sheets (e.g., 'Sheet1!A1:C10'). Required for spreadsheets."),
66
+ }),
67
+ execute: async (args, { log }) => {
68
+ const drive = await getDriveClient();
69
+
70
+ // 1. Get file metadata to determine type
71
+ let fileMeta;
72
+ try {
73
+ const res = await drive.files.get({
74
+ fileId: args.fileId,
75
+ fields: 'id,name,mimeType,modifiedTime,size',
76
+ supportsAllDrives: true,
77
+ });
78
+ fileMeta = res.data;
79
+ } catch (error) {
80
+ if (error.code === 404) throw new UserError(`File not found: ${args.fileId}`);
81
+ if (error.code === 403) throw new UserError('Permission denied. Check that the file is shared with this account.');
82
+ throw new UserError(`Failed to get file info: ${error.message}`);
83
+ }
84
+
85
+ log.info(`File "${fileMeta.name}" is ${fileMeta.mimeType}`);
86
+ trackRead(args.fileId, fileMeta.modifiedTime);
87
+
88
+ // 2. Route based on MIME type
89
+ if (fileMeta.mimeType === GOOGLE_DOC) {
90
+ return await readGoogleDoc(args, fileMeta, log);
91
+ }
92
+
93
+ if (fileMeta.mimeType === GOOGLE_SHEET) {
94
+ return await readGoogleSheet(args, fileMeta, log);
95
+ }
96
+
97
+ // Try reading as docx/pdf
98
+ const text = await tryReadBinaryFile(drive, args.fileId, fileMeta, log);
99
+ if (text !== null) {
100
+ return text;
101
+ }
102
+
103
+ // Unsupported type
104
+ const capInfo = CAPABILITIES.other;
105
+ capInfo.fileType = fileMeta.mimeType;
106
+ return JSON.stringify({
107
+ file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
108
+ ...capInfo,
109
+ content: null,
110
+ }, null, 2);
111
+ },
112
+ });
113
+ }
114
+
115
+ async function readGoogleDoc(args, fileMeta, log) {
116
+ const docs = await getDocsClient();
117
+ const res = await docs.documents.get({
118
+ documentId: args.fileId,
119
+ fields: args.format === 'text' ? 'revisionId,body(content(paragraph(elements(textRun(content)))))' : '*',
120
+ });
121
+
122
+ let content;
123
+ if (args.format === 'json') {
124
+ content = JSON.stringify(res.data, null, 2);
125
125
  } else if (args.format === 'markdown') {
126
126
  content = docsJsonToMarkdown(res.data, { plainMarkdown: args.plainMarkdown });
127
- } else {
128
- // text
129
- let textContent = '';
130
- res.data.body?.content?.forEach((element) => {
131
- if (element.paragraph?.elements) {
132
- element.paragraph.elements.forEach((pe) => {
133
- if (pe.textRun?.content) textContent += pe.textRun.content;
134
- });
135
- }
136
- if (element.table?.tableRows) {
137
- element.table.tableRows.forEach((row) => {
138
- row.tableCells?.forEach((cell) => {
139
- cell.content?.forEach((cellElement) => {
140
- cellElement.paragraph?.elements?.forEach((pe) => {
141
- if (pe.textRun?.content) textContent += pe.textRun.content;
142
- });
143
- });
144
- });
145
- });
146
- }
147
- });
148
- content = textContent || '(empty document)';
149
- }
150
-
151
- return JSON.stringify({
152
- file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
153
- ...CAPABILITIES.googleDoc,
154
- content,
155
- }, null, 2);
156
- }
157
-
158
- async function readGoogleSheet(args, fileMeta, log) {
159
- if (!args.range) {
160
- // Return sheet info without data
161
- const sheets = await getSheetsClient();
162
- const info = await sheets.spreadsheets.get({
163
- spreadsheetId: args.fileId,
164
- fields: 'sheets(properties(sheetId,title,gridProperties))',
165
- });
166
- const sheetList = info.data.sheets?.map(s => ({
167
- title: s.properties.title,
168
- sheetId: s.properties.sheetId,
169
- rows: s.properties.gridProperties?.rowCount,
170
- cols: s.properties.gridProperties?.columnCount,
171
- }));
172
- return JSON.stringify({
173
- file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
174
- ...CAPABILITIES.googleSheet,
175
- note: 'Provide a range parameter (e.g., "Sheet1!A1:C10") to read data. Available sheets listed below.',
176
- sheets: sheetList,
177
- }, null, 2);
178
- }
179
-
180
- const sheets = await getSheetsClient();
181
- const response = await sheets.spreadsheets.values.get({
182
- spreadsheetId: args.fileId,
183
- range: args.range,
184
- });
185
- return JSON.stringify({
186
- file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
187
- ...CAPABILITIES.googleSheet,
188
- range: args.range,
189
- values: response.data.values || [],
190
- }, null, 2);
191
- }
192
-
193
- async function tryReadBinaryFile(drive, fileId, fileMeta, log) {
194
- const { mimeType, name } = fileMeta;
195
- const isDocxFile = mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || name?.endsWith('.docx');
196
- const isPdfFile = mimeType === 'application/pdf' || name?.endsWith('.pdf');
197
-
198
- if (!isDocxFile && !isPdfFile) return null;
199
-
200
- const buffer = await downloadBuffer(drive, fileId);
201
- const text = await extractText(buffer, mimeType, name);
202
-
203
- if (text === null) return null;
204
-
205
- const capInfo = isDocxFile ? CAPABILITIES.docx : CAPABILITIES.pdf;
206
- return JSON.stringify({
207
- file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
208
- ...capInfo,
209
- content: text,
210
- }, null, 2);
211
- }
127
+ } else {
128
+ // text
129
+ let textContent = '';
130
+ res.data.body?.content?.forEach((element) => {
131
+ if (element.paragraph?.elements) {
132
+ element.paragraph.elements.forEach((pe) => {
133
+ if (pe.textRun?.content) textContent += pe.textRun.content;
134
+ });
135
+ }
136
+ if (element.table?.tableRows) {
137
+ element.table.tableRows.forEach((row) => {
138
+ row.tableCells?.forEach((cell) => {
139
+ cell.content?.forEach((cellElement) => {
140
+ cellElement.paragraph?.elements?.forEach((pe) => {
141
+ if (pe.textRun?.content) textContent += pe.textRun.content;
142
+ });
143
+ });
144
+ });
145
+ });
146
+ }
147
+ });
148
+ content = textContent || '(empty document)';
149
+ }
150
+
151
+ trackRead(args.fileId, fileMeta.modifiedTime, args.format === 'markdown' ? content : undefined, res.data.revisionId);
152
+ return JSON.stringify({
153
+ file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
154
+ ...CAPABILITIES.googleDoc,
155
+ content,
156
+ }, null, 2);
157
+ }
158
+
159
+ async function readGoogleSheet(args, fileMeta, log) {
160
+ if (!args.range) {
161
+ // Return sheet info without data
162
+ const sheets = await getSheetsClient();
163
+ const info = await sheets.spreadsheets.get({
164
+ spreadsheetId: args.fileId,
165
+ fields: 'sheets(properties(sheetId,title,gridProperties))',
166
+ });
167
+ const sheetList = info.data.sheets?.map(s => ({
168
+ title: s.properties.title,
169
+ sheetId: s.properties.sheetId,
170
+ rows: s.properties.gridProperties?.rowCount,
171
+ cols: s.properties.gridProperties?.columnCount,
172
+ }));
173
+ return JSON.stringify({
174
+ file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
175
+ ...CAPABILITIES.googleSheet,
176
+ note: 'Provide a range parameter (e.g., "Sheet1!A1:C10") to read data. Available sheets listed below.',
177
+ sheets: sheetList,
178
+ }, null, 2);
179
+ }
180
+
181
+ const sheets = await getSheetsClient();
182
+ const response = await sheets.spreadsheets.values.get({
183
+ spreadsheetId: args.fileId,
184
+ range: args.range,
185
+ });
186
+ return JSON.stringify({
187
+ file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
188
+ ...CAPABILITIES.googleSheet,
189
+ range: args.range,
190
+ values: response.data.values || [],
191
+ }, null, 2);
192
+ }
193
+
194
+ async function tryReadBinaryFile(drive, fileId, fileMeta, log) {
195
+ const { mimeType, name } = fileMeta;
196
+ const isDocxFile = mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || name?.endsWith('.docx');
197
+ const isPdfFile = mimeType === 'application/pdf' || name?.endsWith('.pdf');
198
+
199
+ if (!isDocxFile && !isPdfFile) return null;
200
+
201
+ const buffer = await downloadBuffer(drive, fileId);
202
+ const text = await extractText(buffer, mimeType, name);
203
+
204
+ if (text === null) return null;
205
+
206
+ const capInfo = isDocxFile ? CAPABILITIES.docx : CAPABILITIES.pdf;
207
+ return JSON.stringify({
208
+ file: { id: fileMeta.id, name: fileMeta.name, mimeType: fileMeta.mimeType },
209
+ ...capInfo,
210
+ content: text,
211
+ }, null, 2);
212
+ }