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.
- package/LICENSE +21 -21
- package/README.md +537 -276
- package/dist/cachedToolsList.js +52 -52
- package/dist/googleDocsApiHelpers.js +76 -12
- package/dist/helpers.js +572 -252
- package/dist/httpAuth.js +296 -0
- package/dist/index.js +162 -11
- package/dist/logger.js +87 -87
- package/dist/markdown-transformer/docsToMarkdown.js +128 -0
- package/dist/markdown-transformer/index.js +17 -7
- package/dist/markdown-transformer/markdownToDocs.js +185 -27
- package/dist/readTracker.js +136 -17
- package/dist/sessionContext.js +37 -0
- package/dist/setup.js +187 -9
- package/dist/tools/calendar/getBusy.js +64 -64
- package/dist/tools/calendar/getEvents.js +141 -141
- package/dist/tools/calendar/getFree.js +225 -225
- package/dist/tools/calendar/index.js +19 -19
- package/dist/tools/calendar/listCalendars.js +38 -38
- package/dist/tools/calendar/listRecurringInstances.js +83 -83
- package/dist/tools/calendar/manageCalendar.js +121 -121
- package/dist/tools/calendar/manageEvent.js +258 -258
- package/dist/tools/calendar/moveEvent.js +66 -66
- package/dist/tools/docs/addTab.js +10 -12
- package/dist/tools/docs/appendToGoogleDoc.js +9 -4
- package/dist/tools/docs/deleteRange.js +9 -4
- package/dist/tools/docs/findAndReplace.js +9 -4
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
- package/dist/tools/docs/formatting/getFormatting.js +171 -171
- package/dist/tools/docs/insertImage.js +15 -2
- package/dist/tools/docs/insertPageBreak.js +4 -1
- package/dist/tools/docs/insertTable.js +4 -1
- package/dist/tools/docs/insertTableWithData.js +4 -1
- package/dist/tools/docs/modifyText.js +14 -4
- package/dist/tools/docs/modifyText.test.js +84 -84
- package/dist/tools/docs/readGoogleDoc.js +71 -11
- package/dist/tools/docs/renameTab.js +12 -14
- package/dist/tools/drafts.js +165 -165
- package/dist/tools/drive/createDocument.js +12 -0
- package/dist/tools/drive/downloadFile.js +268 -268
- package/dist/tools/drive/getFileInfo.js +48 -48
- package/dist/tools/drive/getFilePath.js +50 -50
- package/dist/tools/drive/listDriveFiles.js +112 -112
- package/dist/tools/drive/listSharedDrives.js +52 -52
- package/dist/tools/drive/listSharedWithMe.js +97 -97
- package/dist/tools/drive/uploadFile.js +111 -111
- package/dist/tools/extras/index.js +9 -9
- package/dist/tools/extras/readDriveFile.js +197 -196
- package/dist/tools/extras/readFile.js +84 -84
- package/dist/tools/extras/searchFileContents.js +81 -81
- package/dist/tools/forms/batchUpdateForm.js +81 -81
- package/dist/tools/forms/createForm.js +54 -54
- package/dist/tools/forms/getForm.js +118 -118
- package/dist/tools/forms/getFormResponse.js +45 -45
- package/dist/tools/forms/index.js +15 -15
- package/dist/tools/forms/listFormResponses.js +56 -56
- package/dist/tools/forms/setPublishSettings.js +59 -59
- package/dist/tools/gmail/drafts.js +165 -165
- package/dist/tools/gmail/labels.js +54 -83
- package/dist/tools/gmail/messages.js +444 -441
- package/dist/tools/gmail/settings.js +358 -528
- package/dist/tools/gmail/threads.js +286 -164
- package/dist/tools/index.js +511 -473
- package/dist/tools/labels.js +103 -103
- package/dist/tools/legacyAliases.js +426 -0
- package/dist/tools/maps/directions.js +72 -0
- package/dist/tools/maps/geocode.js +16 -0
- package/dist/tools/maps/index.js +15 -0
- package/dist/tools/maps/mapsClient.js +83 -0
- package/dist/tools/maps/placeDetails.js +16 -0
- package/dist/tools/maps/reverseGeocode.js +19 -0
- package/dist/tools/maps/searchNearby.js +54 -0
- package/dist/tools/maps/searchPlaces.js +24 -0
- package/dist/tools/messages.js +448 -448
- package/dist/tools/settings.js +528 -528
- package/dist/tools/slides/createPresentation.js +104 -104
- package/dist/tools/slides/createShape.js +92 -92
- package/dist/tools/slides/createTextBox.js +82 -82
- package/dist/tools/slides/deleteSlide.js +30 -30
- package/dist/tools/slides/duplicateSlide.js +37 -37
- package/dist/tools/slides/exportThumbnail.js +42 -42
- package/dist/tools/slides/formatParagraph.js +72 -72
- package/dist/tools/slides/formatText.js +84 -84
- package/dist/tools/slides/getPresentation.js +87 -87
- package/dist/tools/slides/index.js +33 -33
- package/dist/tools/slides/reorderSlides.js +41 -41
- package/dist/tools/slides/replaceAllText.js +46 -46
- package/dist/tools/slides/setBackground.js +58 -58
- package/dist/tools/slides/speakerNotes.js +102 -102
- package/dist/tools/slides/styleShape.js +111 -111
- package/dist/tools/slides/updatePresentation.js +128 -128
- package/dist/tools/threads.js +145 -145
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +30 -6
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +71 -8
- package/dist/updateCheck.js +202 -0
- package/dist/workspace.js +122 -0
- 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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
}
|