google-tools-mcp 1.0.4 → 1.0.6
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/dist/tools/drive/downloadFile.js +268 -0
- package/dist/tools/drive/index.js +2 -0
- package/dist/tools/drive/listDriveFiles.js +3 -2
- package/package.json +1 -1
- package/dist/tools/docs/formatting/applyTextStyle.js +0 -49
- package/dist/tools/docs/insertText.js +0 -61
- package/dist/tools/drive/getDocumentInfo.js +0 -48
- package/dist/tools/drive/listGoogleDocs.js +0 -67
- package/dist/tools/sheets/listGoogleSheets.js +0 -62
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { Readable } from 'stream';
|
|
7
|
+
import { pipeline } from 'stream/promises';
|
|
8
|
+
import { createWriteStream } from 'fs';
|
|
9
|
+
|
|
10
|
+
// Google Workspace MIME types and their export formats
|
|
11
|
+
const EXPORT_FORMATS = {
|
|
12
|
+
'application/vnd.google-apps.document': {
|
|
13
|
+
label: 'Google Doc',
|
|
14
|
+
formats: {
|
|
15
|
+
pdf: 'application/pdf',
|
|
16
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
17
|
+
md: 'text/markdown',
|
|
18
|
+
txt: 'text/plain',
|
|
19
|
+
html: 'text/html',
|
|
20
|
+
rtf: 'application/rtf',
|
|
21
|
+
odt: 'application/vnd.oasis.opendocument.text',
|
|
22
|
+
epub: 'application/epub+zip',
|
|
23
|
+
},
|
|
24
|
+
defaultFormat: 'pdf',
|
|
25
|
+
extensions: {
|
|
26
|
+
pdf: '.pdf',
|
|
27
|
+
docx: '.docx',
|
|
28
|
+
md: '.md',
|
|
29
|
+
txt: '.txt',
|
|
30
|
+
html: '.html',
|
|
31
|
+
rtf: '.rtf',
|
|
32
|
+
odt: '.odt',
|
|
33
|
+
epub: '.epub',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
'application/vnd.google-apps.spreadsheet': {
|
|
37
|
+
label: 'Google Sheet',
|
|
38
|
+
formats: {
|
|
39
|
+
pdf: 'application/pdf',
|
|
40
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
41
|
+
csv: 'text/csv',
|
|
42
|
+
tsv: 'text/tab-separated-values',
|
|
43
|
+
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
44
|
+
},
|
|
45
|
+
defaultFormat: 'xlsx',
|
|
46
|
+
extensions: {
|
|
47
|
+
pdf: '.pdf',
|
|
48
|
+
xlsx: '.xlsx',
|
|
49
|
+
csv: '.csv',
|
|
50
|
+
tsv: '.tsv',
|
|
51
|
+
ods: '.ods',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
'application/vnd.google-apps.presentation': {
|
|
55
|
+
label: 'Google Slides',
|
|
56
|
+
formats: {
|
|
57
|
+
pdf: 'application/pdf',
|
|
58
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
59
|
+
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
60
|
+
txt: 'text/plain',
|
|
61
|
+
},
|
|
62
|
+
defaultFormat: 'pdf',
|
|
63
|
+
extensions: {
|
|
64
|
+
pdf: '.pdf',
|
|
65
|
+
pptx: '.pptx',
|
|
66
|
+
odp: '.odp',
|
|
67
|
+
txt: '.txt',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
'application/vnd.google-apps.drawing': {
|
|
71
|
+
label: 'Google Drawing',
|
|
72
|
+
formats: {
|
|
73
|
+
pdf: 'application/pdf',
|
|
74
|
+
png: 'image/png',
|
|
75
|
+
jpg: 'image/jpeg',
|
|
76
|
+
svg: 'image/svg+xml',
|
|
77
|
+
},
|
|
78
|
+
defaultFormat: 'pdf',
|
|
79
|
+
extensions: {
|
|
80
|
+
pdf: '.pdf',
|
|
81
|
+
png: '.png',
|
|
82
|
+
jpg: '.jpg',
|
|
83
|
+
svg: '.svg',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// All supported export format keys
|
|
89
|
+
const ALL_FORMAT_KEYS = [
|
|
90
|
+
...new Set(
|
|
91
|
+
Object.values(EXPORT_FORMATS).flatMap((t) => Object.keys(t.formats))
|
|
92
|
+
),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export function register(server) {
|
|
96
|
+
server.addTool({
|
|
97
|
+
name: 'downloadFile',
|
|
98
|
+
description:
|
|
99
|
+
'Downloads a file from Google Drive to the local filesystem. ' +
|
|
100
|
+
'For Google Workspace files (Docs, Sheets, Slides, Drawings), specify an export format ' +
|
|
101
|
+
'(e.g. pdf, docx, md, xlsx, csv, pptx). For regular files (images, PDFs, etc.), ' +
|
|
102
|
+
'downloads the original file directly.',
|
|
103
|
+
parameters: z.object({
|
|
104
|
+
fileId: z.string().describe('The ID of the file to download.'),
|
|
105
|
+
localPath: z
|
|
106
|
+
.string()
|
|
107
|
+
.describe(
|
|
108
|
+
'The local file path to save the downloaded file to. ' +
|
|
109
|
+
'Can be an absolute path or relative to the current working directory. ' +
|
|
110
|
+
'If a directory is provided, the file will be saved with its Drive name.'
|
|
111
|
+
),
|
|
112
|
+
exportFormat: z
|
|
113
|
+
.enum(ALL_FORMAT_KEYS)
|
|
114
|
+
.optional()
|
|
115
|
+
.describe(
|
|
116
|
+
'Export format for Google Workspace files. ' +
|
|
117
|
+
'Docs: pdf, docx, md, txt, html, rtf, odt, epub. ' +
|
|
118
|
+
'Sheets: pdf, xlsx, csv, tsv, ods. ' +
|
|
119
|
+
'Slides: pdf, pptx, odp, txt. ' +
|
|
120
|
+
'Drawings: pdf, png, jpg, svg. ' +
|
|
121
|
+
'Ignored for non-Google files (they download as-is).'
|
|
122
|
+
),
|
|
123
|
+
}),
|
|
124
|
+
execute: async (args, { log }) => {
|
|
125
|
+
const drive = await getDriveClient();
|
|
126
|
+
log.info(`Downloading file: ${args.fileId}`);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Get file metadata first
|
|
130
|
+
const metaResponse = await drive.files.get({
|
|
131
|
+
fileId: args.fileId,
|
|
132
|
+
fields: 'id,name,mimeType,size',
|
|
133
|
+
supportsAllDrives: true,
|
|
134
|
+
});
|
|
135
|
+
const file = metaResponse.data;
|
|
136
|
+
if (!file) {
|
|
137
|
+
throw new UserError(
|
|
138
|
+
`File with ID ${args.fileId} not found.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const isGoogleWorkspace =
|
|
143
|
+
file.mimeType in EXPORT_FORMATS;
|
|
144
|
+
|
|
145
|
+
// Determine export MIME type and file extension
|
|
146
|
+
let exportMimeType = null;
|
|
147
|
+
let fileExtension = '';
|
|
148
|
+
|
|
149
|
+
if (isGoogleWorkspace) {
|
|
150
|
+
const typeInfo = EXPORT_FORMATS[file.mimeType];
|
|
151
|
+
const format =
|
|
152
|
+
args.exportFormat || typeInfo.defaultFormat;
|
|
153
|
+
|
|
154
|
+
if (!typeInfo.formats[format]) {
|
|
155
|
+
const available = Object.keys(
|
|
156
|
+
typeInfo.formats
|
|
157
|
+
).join(', ');
|
|
158
|
+
throw new UserError(
|
|
159
|
+
`Format '${format}' is not supported for ${typeInfo.label}. ` +
|
|
160
|
+
`Available formats: ${available}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
exportMimeType = typeInfo.formats[format];
|
|
165
|
+
fileExtension = typeInfo.extensions[format];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Resolve the local path
|
|
169
|
+
let destPath = path.resolve(args.localPath);
|
|
170
|
+
|
|
171
|
+
// Check if destPath is a directory
|
|
172
|
+
let isDir = false;
|
|
173
|
+
try {
|
|
174
|
+
const stat = await fs.stat(destPath);
|
|
175
|
+
isDir = stat.isDirectory();
|
|
176
|
+
} catch {
|
|
177
|
+
// Path doesn't exist yet — treat as a file path
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isDir) {
|
|
181
|
+
// Build filename from Drive name + appropriate extension
|
|
182
|
+
let fileName = file.name;
|
|
183
|
+
if (isGoogleWorkspace && fileExtension) {
|
|
184
|
+
// Google Workspace files don't have extensions in their name
|
|
185
|
+
fileName = file.name + fileExtension;
|
|
186
|
+
}
|
|
187
|
+
destPath = path.join(destPath, fileName);
|
|
188
|
+
} else if (
|
|
189
|
+
isGoogleWorkspace &&
|
|
190
|
+
fileExtension &&
|
|
191
|
+
!destPath.endsWith(fileExtension)
|
|
192
|
+
) {
|
|
193
|
+
// Append extension if not already present
|
|
194
|
+
destPath = destPath + fileExtension;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Ensure parent directory exists
|
|
198
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
199
|
+
|
|
200
|
+
// Download the file
|
|
201
|
+
if (isGoogleWorkspace) {
|
|
202
|
+
log.info(
|
|
203
|
+
`Exporting ${EXPORT_FORMATS[file.mimeType].label} as ${args.exportFormat || EXPORT_FORMATS[file.mimeType].defaultFormat}`
|
|
204
|
+
);
|
|
205
|
+
const exportResponse = await drive.files.export(
|
|
206
|
+
{ fileId: args.fileId, mimeType: exportMimeType },
|
|
207
|
+
{ responseType: 'stream' }
|
|
208
|
+
);
|
|
209
|
+
await pipeline(
|
|
210
|
+
exportResponse.data,
|
|
211
|
+
createWriteStream(destPath)
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
log.info(`Downloading binary file: ${file.name}`);
|
|
215
|
+
const downloadResponse = await drive.files.get(
|
|
216
|
+
{
|
|
217
|
+
fileId: args.fileId,
|
|
218
|
+
alt: 'media',
|
|
219
|
+
supportsAllDrives: true,
|
|
220
|
+
},
|
|
221
|
+
{ responseType: 'stream' }
|
|
222
|
+
);
|
|
223
|
+
await pipeline(
|
|
224
|
+
downloadResponse.data,
|
|
225
|
+
createWriteStream(destPath)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Get the final file size
|
|
230
|
+
const stat = await fs.stat(destPath);
|
|
231
|
+
|
|
232
|
+
const result = {
|
|
233
|
+
success: true,
|
|
234
|
+
fileName: file.name,
|
|
235
|
+
savedTo: destPath,
|
|
236
|
+
fileSize: stat.size,
|
|
237
|
+
mimeType: isGoogleWorkspace
|
|
238
|
+
? exportMimeType
|
|
239
|
+
: file.mimeType,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (isGoogleWorkspace) {
|
|
243
|
+
result.exportedAs =
|
|
244
|
+
args.exportFormat ||
|
|
245
|
+
EXPORT_FORMATS[file.mimeType].defaultFormat;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return JSON.stringify(result, null, 2);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error instanceof UserError) throw error;
|
|
251
|
+
log.error(
|
|
252
|
+
`Error downloading file: ${error.message || error}`
|
|
253
|
+
);
|
|
254
|
+
if (error.code === 404)
|
|
255
|
+
throw new UserError(
|
|
256
|
+
`File not found (ID: ${args.fileId}).`
|
|
257
|
+
);
|
|
258
|
+
if (error.code === 403)
|
|
259
|
+
throw new UserError(
|
|
260
|
+
'Permission denied. Make sure you have access to this file.'
|
|
261
|
+
);
|
|
262
|
+
throw new UserError(
|
|
263
|
+
`Failed to download file: ${error.message || 'Unknown error'}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
@@ -12,6 +12,7 @@ import { register as createDocument } from './createDocument.js';
|
|
|
12
12
|
import { register as createFromTemplate } from './createFromTemplate.js';
|
|
13
13
|
import { register as listSharedDrives } from './listSharedDrives.js';
|
|
14
14
|
import { register as listSharedWithMe } from './listSharedWithMe.js';
|
|
15
|
+
import { register as downloadFile } from './downloadFile.js';
|
|
15
16
|
export function registerDriveTools(server) {
|
|
16
17
|
listDriveFiles(server);
|
|
17
18
|
searchGoogleDocs(server);
|
|
@@ -27,4 +28,5 @@ export function registerDriveTools(server) {
|
|
|
27
28
|
createFromTemplate(server);
|
|
28
29
|
listSharedDrives(server);
|
|
29
30
|
listSharedWithMe(server);
|
|
31
|
+
downloadFile(server);
|
|
30
32
|
}
|
|
@@ -56,8 +56,9 @@ export function register(server) {
|
|
|
56
56
|
// Filter by mime type
|
|
57
57
|
if (args.fileType !== 'all') {
|
|
58
58
|
queryParts.push(`mimeType='${MIME_TYPES[args.fileType]}'`);
|
|
59
|
-
} else {
|
|
60
|
-
// Exclude folders from "all" results
|
|
59
|
+
} else if (!args.query) {
|
|
60
|
+
// Exclude folders from "all" results only when not doing fullText search
|
|
61
|
+
// (Drive API doesn't allow fullText contains with negated mimeType filters)
|
|
61
62
|
queryParts.push("mimeType!='application/vnd.google-apps.folder'");
|
|
62
63
|
}
|
|
63
64
|
|
package/package.json
CHANGED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { UserError } from 'fastmcp';
|
|
2
|
-
import { getDocsClient } from '../../../clients.js';
|
|
3
|
-
import { ApplyTextStyleToolParameters, NotImplementedError, } from '../../../types.js';
|
|
4
|
-
import * as GDocsHelpers from '../../../googleDocsApiHelpers.js';
|
|
5
|
-
export function register(server) {
|
|
6
|
-
server.addTool({
|
|
7
|
-
name: 'applyTextStyle',
|
|
8
|
-
description: 'Applies character-level formatting (bold, italic, color, font, etc.) to text identified by a character range or by searching for a text string. This is the primary tool for styling text in a document.',
|
|
9
|
-
parameters: ApplyTextStyleToolParameters,
|
|
10
|
-
execute: async (args, { log }) => {
|
|
11
|
-
const docs = await getDocsClient();
|
|
12
|
-
let { startIndex, endIndex } = args.target; // Will be updated if target is text
|
|
13
|
-
log.info(`Applying text style in doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}. Target: ${JSON.stringify(args.target)}, Style: ${JSON.stringify(args.style)}`);
|
|
14
|
-
try {
|
|
15
|
-
// Determine target range
|
|
16
|
-
if ('textToFind' in args.target) {
|
|
17
|
-
const range = await GDocsHelpers.findTextRange(docs, args.documentId, args.target.textToFind, args.target.matchInstance, args.tabId);
|
|
18
|
-
if (!range) {
|
|
19
|
-
throw new UserError(`Could not find instance ${args.target.matchInstance} of text "${args.target.textToFind}"${args.tabId ? ` in tab ${args.tabId}` : ''}.`);
|
|
20
|
-
}
|
|
21
|
-
startIndex = range.startIndex;
|
|
22
|
-
endIndex = range.endIndex;
|
|
23
|
-
log.info(`Found text "${args.target.textToFind}" (instance ${args.target.matchInstance}) at range ${startIndex}-${endIndex}`);
|
|
24
|
-
}
|
|
25
|
-
if (startIndex === undefined || endIndex === undefined) {
|
|
26
|
-
throw new UserError('Target range could not be determined.');
|
|
27
|
-
}
|
|
28
|
-
if (endIndex <= startIndex) {
|
|
29
|
-
throw new UserError('End index must be greater than start index for styling.');
|
|
30
|
-
}
|
|
31
|
-
// Build the request
|
|
32
|
-
const requestInfo = GDocsHelpers.buildUpdateTextStyleRequest(startIndex, endIndex, args.style, args.tabId);
|
|
33
|
-
if (!requestInfo) {
|
|
34
|
-
return 'No valid text styling options were provided.';
|
|
35
|
-
}
|
|
36
|
-
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [requestInfo.request]);
|
|
37
|
-
return `Successfully applied text style (${requestInfo.fields.join(', ')}) to range ${startIndex}-${endIndex}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
log.error(`Error applying text style in doc ${args.documentId}: ${error.message || error}`);
|
|
41
|
-
if (error instanceof UserError)
|
|
42
|
-
throw error;
|
|
43
|
-
if (error instanceof NotImplementedError)
|
|
44
|
-
throw error; // Should not happen here
|
|
45
|
-
throw new UserError(`Failed to apply text style: ${error.message || 'Unknown error'}`);
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { UserError } from 'fastmcp';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { getDocsClient } from '../../clients.js';
|
|
4
|
-
import { DocumentIdParameter } from '../../types.js';
|
|
5
|
-
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
-
export function register(server) {
|
|
7
|
-
server.addTool({
|
|
8
|
-
name: 'insertText',
|
|
9
|
-
description: "Inserts text at a specific character index within a document. Use readDocument with format='json' to determine the correct index.",
|
|
10
|
-
parameters: DocumentIdParameter.extend({
|
|
11
|
-
text: z.string().min(1).describe('The text to insert.'),
|
|
12
|
-
index: z
|
|
13
|
-
.number()
|
|
14
|
-
.int()
|
|
15
|
-
.min(1)
|
|
16
|
-
.describe("1-based character index within the document body. Use readDocument with format='json' to inspect indices."),
|
|
17
|
-
tabId: z
|
|
18
|
-
.string()
|
|
19
|
-
.optional()
|
|
20
|
-
.describe('The ID of the specific tab to insert into. If not specified, inserts into the first tab (or legacy document.body for documents without tabs).'),
|
|
21
|
-
}),
|
|
22
|
-
execute: async (args, { log }) => {
|
|
23
|
-
const docs = await getDocsClient();
|
|
24
|
-
log.info(`Inserting text in doc ${args.documentId} at index ${args.index}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
|
|
25
|
-
try {
|
|
26
|
-
if (args.tabId) {
|
|
27
|
-
// For tab-specific inserts, we need to verify the tab exists first
|
|
28
|
-
const docInfo = await docs.documents.get({
|
|
29
|
-
documentId: args.documentId,
|
|
30
|
-
includeTabsContent: true,
|
|
31
|
-
fields: 'tabs(tabProperties,documentTab)',
|
|
32
|
-
});
|
|
33
|
-
const targetTab = GDocsHelpers.findTabById(docInfo.data, args.tabId);
|
|
34
|
-
if (!targetTab) {
|
|
35
|
-
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
36
|
-
}
|
|
37
|
-
if (!targetTab.documentTab) {
|
|
38
|
-
throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
|
|
39
|
-
}
|
|
40
|
-
// Insert with tabId
|
|
41
|
-
const location = { index: args.index, tabId: args.tabId };
|
|
42
|
-
const request = {
|
|
43
|
-
insertText: { location, text: args.text },
|
|
44
|
-
};
|
|
45
|
-
await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [request]);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
// Use existing helper for backward compatibility
|
|
49
|
-
await GDocsHelpers.insertText(docs, args.documentId, args.text, args.index);
|
|
50
|
-
}
|
|
51
|
-
return `Successfully inserted text at index ${args.index}${args.tabId ? ` in tab ${args.tabId}` : ''}.`;
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
log.error(`Error inserting text in doc ${args.documentId}: ${error.message || error}`);
|
|
55
|
-
if (error instanceof UserError)
|
|
56
|
-
throw error;
|
|
57
|
-
throw new UserError(`Failed to insert text: ${error.message || 'Unknown error'}`);
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
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: 'listSpreadsheets',
|
|
7
|
-
description: 'Lists spreadsheets in your Drive, optionally filtered by name or content.',
|
|
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 spreadsheets to return (1-100).'),
|
|
17
|
-
query: z
|
|
18
|
-
.string()
|
|
19
|
-
.optional()
|
|
20
|
-
.describe('Search query to filter spreadsheets by name or content.'),
|
|
21
|
-
orderBy: z
|
|
22
|
-
.enum(['name', 'modifiedTime', 'createdTime'])
|
|
23
|
-
.optional()
|
|
24
|
-
.default('modifiedTime')
|
|
25
|
-
.describe('Sort order for results.'),
|
|
26
|
-
}),
|
|
27
|
-
execute: async (args, { log }) => {
|
|
28
|
-
const drive = await getDriveClient();
|
|
29
|
-
log.info(`Listing Google Sheets. Query: ${args.query || 'none'}, Max: ${args.maxResults}, Order: ${args.orderBy}`);
|
|
30
|
-
try {
|
|
31
|
-
// Build the query string for Google Drive API
|
|
32
|
-
let queryString = "mimeType='application/vnd.google-apps.spreadsheet' and trashed=false";
|
|
33
|
-
if (args.query) {
|
|
34
|
-
queryString += ` and (name contains '${args.query}' or fullText contains '${args.query}')`;
|
|
35
|
-
}
|
|
36
|
-
const response = await drive.files.list({
|
|
37
|
-
q: queryString,
|
|
38
|
-
pageSize: args.maxResults,
|
|
39
|
-
orderBy: args.orderBy === 'name' ? 'name' : args.orderBy,
|
|
40
|
-
fields: 'files(id,name,modifiedTime,createdTime,size,webViewLink,owners(displayName,emailAddress))',
|
|
41
|
-
supportsAllDrives: true,
|
|
42
|
-
includeItemsFromAllDrives: true,
|
|
43
|
-
});
|
|
44
|
-
const files = response.data.files || [];
|
|
45
|
-
const spreadsheets = files.map((file) => ({
|
|
46
|
-
id: file.id,
|
|
47
|
-
name: file.name,
|
|
48
|
-
modifiedTime: file.modifiedTime,
|
|
49
|
-
owner: file.owners?.[0]?.displayName || null,
|
|
50
|
-
url: file.webViewLink,
|
|
51
|
-
}));
|
|
52
|
-
return JSON.stringify({ spreadsheets }, null, 2);
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
log.error(`Error listing Google Sheets: ${error.message || error}`);
|
|
56
|
-
if (error.code === 403)
|
|
57
|
-
throw new UserError('Permission denied. Make sure you have granted Google Drive access to the application.');
|
|
58
|
-
throw new UserError(`Failed to list spreadsheets: ${error.message || 'Unknown error'}`);
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
}
|