mcp-google-multi 6.0.0-alpha.2 → 6.0.0-alpha.21
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/README.md +5 -3
- package/dist/api-probe.d.ts +18 -0
- package/dist/api-probe.js +68 -0
- package/dist/arg-normalize.d.ts +19 -0
- package/dist/arg-normalize.js +90 -0
- package/dist/auth.js +15 -72
- package/dist/client.js +3 -1
- package/dist/discover.js +35 -14
- package/dist/discovery-client.d.ts +8 -1
- package/dist/discovery-client.js +67 -15
- package/dist/doctor.d.ts +12 -0
- package/dist/doctor.js +98 -15
- package/dist/http-transport.d.ts +3 -0
- package/dist/http-transport.js +2 -1
- package/dist/index.js +4 -1
- package/dist/oauth-consent.d.ts +25 -10
- package/dist/oauth-consent.js +85 -39
- package/dist/registry.d.ts +12 -0
- package/dist/registry.js +61 -3
- package/dist/scope-catalog.d.ts +1 -0
- package/dist/scope-catalog.js +17 -1
- package/dist/services.js +3 -1
- package/dist/tools/_errors.js +69 -5
- package/dist/tools/_local-files.d.ts +3 -0
- package/dist/tools/_local-files.js +34 -0
- package/dist/tools/account-wizard.d.ts +10 -0
- package/dist/tools/account-wizard.js +52 -19
- package/dist/tools/analytics.d.ts +18 -0
- package/dist/tools/analytics.js +279 -0
- package/dist/tools/drive.d.ts +2 -1
- package/dist/tools/drive.js +75 -33
- package/dist/tools/generated/_shared.d.ts +6 -0
- package/dist/tools/generated/_shared.js +11 -1
- package/dist/tools/generated/admin.js +160 -29
- package/dist/tools/generated/analytics.d.ts +2 -0
- package/dist/tools/generated/analytics.js +981 -0
- package/dist/tools/generated/chat.js +39 -12
- package/dist/tools/generated/classroom.js +56 -14
- package/dist/tools/generated/cloudidentity.js +18 -10
- package/dist/tools/generated/cloudsearch.js +4 -3
- package/dist/tools/generated/contacts.js +13 -4
- package/dist/tools/generated/drive.js +15 -6
- package/dist/tools/generated/drivelabels.js +33 -5
- package/dist/tools/generated/forms.js +1 -1
- package/dist/tools/generated/gmail.js +39 -12
- package/dist/tools/generated/index.js +2 -0
- package/dist/tools/generated/keep.js +3 -2
- package/dist/tools/generated/licensing.js +20 -4
- package/dist/tools/generated/meet.js +1 -1
- package/dist/tools/generated/reseller.js +8 -2
- package/dist/tools/generated/script.js +13 -3
- package/dist/tools/generated/searchconsole.js +4 -2
- package/dist/tools/generated/sheets.js +2 -1
- package/dist/tools/generated/tasks.js +7 -1
- package/dist/tools/generated/vault.js +18 -9
- package/dist/tools/generated/workspaceevents.js +3 -2
- package/dist/tools/gmail.js +3 -3
- package/dist/tools/google-api.d.ts +4 -1
- package/dist/tools/google-api.js +59 -15
- package/package.json +22 -17
package/dist/tools/drive.js
CHANGED
|
@@ -4,6 +4,7 @@ import { drive as driveClient } from '@googleapis/drive';
|
|
|
4
4
|
import { accountAliasSchema, getAccountSet } from '../accounts.js';
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError } from './_errors.js';
|
|
7
|
+
import { openLocalReadStream, prepareLocalDest } from './_local-files.js';
|
|
7
8
|
import { isAllowed, writeDisabledResult } from '../write-control.js';
|
|
8
9
|
import { capText } from '../trim.js';
|
|
9
10
|
import * as fs from 'fs';
|
|
@@ -24,6 +25,52 @@ const GOOGLE_WORKSPACE_TYPES = new Set([
|
|
|
24
25
|
'application/vnd.google-apps.presentation',
|
|
25
26
|
'application/vnd.google-apps.drawing',
|
|
26
27
|
]);
|
|
28
|
+
// drive_read inlines only textual content. Beyond text/*, RFC 6839 structured-
|
|
29
|
+
// syntax suffixes (+json/+xml/...) and a few bare application/* types are text
|
|
30
|
+
// in practice — image/svg+xml was the motivating false "binary" refusal.
|
|
31
|
+
const TEXTUAL_EXACT = new Set([
|
|
32
|
+
'application/json',
|
|
33
|
+
'application/xml',
|
|
34
|
+
'application/javascript',
|
|
35
|
+
'application/x-ndjson',
|
|
36
|
+
'application/yaml',
|
|
37
|
+
'application/x-yaml',
|
|
38
|
+
'application/sql',
|
|
39
|
+
'application/x-sh',
|
|
40
|
+
'application/csv',
|
|
41
|
+
]);
|
|
42
|
+
export function isTextualMime(mimeType) {
|
|
43
|
+
const bare = mimeType.split(';')[0].trim().toLowerCase();
|
|
44
|
+
if (bare.startsWith('text/'))
|
|
45
|
+
return true;
|
|
46
|
+
if (/\+(json|xml|yaml|toml|csv)$/.test(bare))
|
|
47
|
+
return true;
|
|
48
|
+
return TEXTUAL_EXACT.has(bare);
|
|
49
|
+
}
|
|
50
|
+
const BINARY_READ_HINT = 'Binary content cannot be inlined. Use drive_download to save the file to disk, or drive_export for Google Workspace files.';
|
|
51
|
+
// Accepted alongside the full application/vnd.google-apps.* ids so the obvious
|
|
52
|
+
// short spelling ("document") works; the enum advertises both.
|
|
53
|
+
const CONVERT_SHORTHANDS = {
|
|
54
|
+
document: 'application/vnd.google-apps.document',
|
|
55
|
+
spreadsheet: 'application/vnd.google-apps.spreadsheet',
|
|
56
|
+
presentation: 'application/vnd.google-apps.presentation',
|
|
57
|
+
drawing: 'application/vnd.google-apps.drawing',
|
|
58
|
+
};
|
|
59
|
+
export function resolveConvertTarget(convertTo) {
|
|
60
|
+
if (!convertTo)
|
|
61
|
+
return undefined;
|
|
62
|
+
return CONVERT_SHORTHANDS[convertTo] ?? convertTo;
|
|
63
|
+
}
|
|
64
|
+
const CONVERT_TO_VALUES = [
|
|
65
|
+
'document',
|
|
66
|
+
'spreadsheet',
|
|
67
|
+
'presentation',
|
|
68
|
+
'drawing',
|
|
69
|
+
'application/vnd.google-apps.document',
|
|
70
|
+
'application/vnd.google-apps.spreadsheet',
|
|
71
|
+
'application/vnd.google-apps.presentation',
|
|
72
|
+
'application/vnd.google-apps.drawing',
|
|
73
|
+
];
|
|
27
74
|
// Comment/Reply fields list — Drive API requires explicit `fields` on every call.
|
|
28
75
|
const COMMENT_BASE_FIELDS = 'id,kind,content,htmlContent,createdTime,modifiedTime,resolved,anchor,author,deleted,quotedFileContent';
|
|
29
76
|
const REPLY_SUBFIELDS = 'id,content,action,createdTime,modifiedTime,author,deleted';
|
|
@@ -31,12 +78,6 @@ const COMMENT_FIELDS = `${COMMENT_BASE_FIELDS},replies(${REPLY_SUBFIELDS})`;
|
|
|
31
78
|
const COMMENT_LIST_FIELDS = `nextPageToken,comments(${COMMENT_BASE_FIELDS},replies(${REPLY_SUBFIELDS}))`;
|
|
32
79
|
const REPLY_FIELDS = `kind,htmlContent,${REPLY_SUBFIELDS}`;
|
|
33
80
|
const REPLY_LIST_FIELDS = `nextPageToken,replies(${REPLY_FIELDS})`;
|
|
34
|
-
// path.basename() is a traversal guard — a caller-supplied filename must never escape savePath.
|
|
35
|
-
export function prepareLocalDest(savePath, filename) {
|
|
36
|
-
const dest = path.join(savePath, path.basename(filename));
|
|
37
|
-
fs.mkdirSync(savePath, { recursive: true });
|
|
38
|
-
return dest;
|
|
39
|
-
}
|
|
40
81
|
export const DRIVE_QUERY_HINT = "Drive search syntax: a plain keyword is treated as a full-text search, but a " +
|
|
41
82
|
"structured query needs an operator, e.g. \"name contains 'report'\", " +
|
|
42
83
|
"\"mimeType = 'application/pdf'\", or \"'me' in owners\". " +
|
|
@@ -131,7 +172,7 @@ export function registerDriveTools(server) {
|
|
|
131
172
|
});
|
|
132
173
|
server.registerTool('drive_read', {
|
|
133
174
|
_meta: { 'anthropic/maxResultSizeChars': 100_000 },
|
|
134
|
-
description: 'Read the content of a Google Drive file (returns up to maxChars characters per call; non-Google-native files over 2MB return too_large)',
|
|
175
|
+
description: 'Read the content of a Google Drive file: Workspace docs and textual types (text/*, JSON/XML/SVG and similar) inline; other binaries return error:binary (returns up to maxChars characters per call; non-Google-native files over 2MB return too_large)',
|
|
135
176
|
inputSchema: {
|
|
136
177
|
account: accountEnum.describe('Google account alias'),
|
|
137
178
|
fileId: z.string().describe('Google Drive file ID'),
|
|
@@ -184,6 +225,7 @@ export function registerDriveTools(server) {
|
|
|
184
225
|
name,
|
|
185
226
|
mimeType,
|
|
186
227
|
error: 'binary',
|
|
228
|
+
hint: BINARY_READ_HINT,
|
|
187
229
|
webViewLink,
|
|
188
230
|
}, null, 2),
|
|
189
231
|
}],
|
|
@@ -204,7 +246,7 @@ export function registerDriveTools(server) {
|
|
|
204
246
|
}],
|
|
205
247
|
};
|
|
206
248
|
}
|
|
207
|
-
if (mimeType
|
|
249
|
+
if (mimeType && isTextualMime(mimeType)) {
|
|
208
250
|
const downloaded = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'text' });
|
|
209
251
|
return respond(String(downloaded.data));
|
|
210
252
|
}
|
|
@@ -216,6 +258,7 @@ export function registerDriveTools(server) {
|
|
|
216
258
|
name,
|
|
217
259
|
mimeType,
|
|
218
260
|
error: 'binary',
|
|
261
|
+
hint: BINARY_READ_HINT,
|
|
219
262
|
webViewLink,
|
|
220
263
|
}, null, 2),
|
|
221
264
|
}],
|
|
@@ -259,15 +302,10 @@ export function registerDriveTools(server) {
|
|
|
259
302
|
description: 'Upload a local file to Google Drive. Pass `convertTo` to import it as a native, editable Google Doc/Sheet/Slides/Drawing instead of storing the raw bytes.',
|
|
260
303
|
inputSchema: {
|
|
261
304
|
account: accountEnum.describe('Google account alias'),
|
|
262
|
-
localPath: z.string().describe('Absolute path
|
|
305
|
+
localPath: z.string().describe('Absolute path of the SOURCE file on disk to upload (on the machine running the server; this is not savePath)'),
|
|
263
306
|
filename: z.string().describe('Name as it appears in Drive'),
|
|
264
307
|
mimeType: z.string().optional().describe('Source MIME type of the local file (inferred from extension if omitted). With `convertTo`, this is the format Drive imports from.'),
|
|
265
|
-
convertTo: z.enum(
|
|
266
|
-
'application/vnd.google-apps.document',
|
|
267
|
-
'application/vnd.google-apps.spreadsheet',
|
|
268
|
-
'application/vnd.google-apps.presentation',
|
|
269
|
-
'application/vnd.google-apps.drawing',
|
|
270
|
-
]).optional().describe('Convert the upload into this native Google Workspace type on import (e.g. upload .md/.html/.docx/.txt with convertTo=...google-apps.document to get a real Google Doc). Source must be an importable format. Omit to store the file as-is.'),
|
|
308
|
+
convertTo: z.enum(CONVERT_TO_VALUES).optional().describe('Convert the upload into this native Google Workspace type on import: "document" | "spreadsheet" | "presentation" | "drawing" (full application/vnd.google-apps.* ids also accepted). E.g. upload .md/.html/.docx/.txt with convertTo=document to get a real Google Doc. Source must be an importable format. Omit to store the file as-is.'),
|
|
271
309
|
parentFolderId: z.string().optional().describe('Parent folder ID (defaults to My Drive root)'),
|
|
272
310
|
},
|
|
273
311
|
}, async ({ account, localPath, filename, mimeType: mimeTypeArg, convertTo, parentFolderId }) => {
|
|
@@ -275,13 +313,13 @@ export function registerDriveTools(server) {
|
|
|
275
313
|
const auth = await getClient(account);
|
|
276
314
|
const drive = driveClient({ version: 'v3', auth });
|
|
277
315
|
const resolvedMime = mimeTypeArg ?? (mime.lookup(localPath) || 'application/octet-stream');
|
|
278
|
-
const fileStream =
|
|
316
|
+
const fileStream = await openLocalReadStream(localPath);
|
|
279
317
|
const res = await drive.files.create({
|
|
280
318
|
requestBody: {
|
|
281
319
|
name: filename,
|
|
282
320
|
parents: parentFolderId ? [parentFolderId] : undefined,
|
|
283
321
|
// Setting a google-apps target type makes Drive convert the media on import.
|
|
284
|
-
...(convertTo ? { mimeType: convertTo } : {}),
|
|
322
|
+
...(convertTo ? { mimeType: resolveConvertTarget(convertTo) } : {}),
|
|
285
323
|
},
|
|
286
324
|
media: {
|
|
287
325
|
mimeType: resolvedMime,
|
|
@@ -303,14 +341,17 @@ export function registerDriveTools(server) {
|
|
|
303
341
|
inputSchema: {
|
|
304
342
|
account: accountEnum.describe('Google account alias'),
|
|
305
343
|
fileId: z.string().describe('Google Drive file ID'),
|
|
306
|
-
savePath: z.string().describe('Absolute
|
|
307
|
-
filename: z.string().describe('Filename to save as'),
|
|
344
|
+
savePath: z.string().describe('Absolute DIRECTORY path to save into (created if missing, on the machine running the server); the file name comes from `filename`'),
|
|
345
|
+
filename: z.string().optional().describe('Filename to save as (defaults to the file name in Drive)'),
|
|
308
346
|
},
|
|
309
347
|
}, async ({ account, fileId, savePath, filename }) => {
|
|
310
348
|
try {
|
|
311
349
|
const auth = await getClient(account);
|
|
312
350
|
const drive = driveClient({ version: 'v3', auth });
|
|
313
|
-
const
|
|
351
|
+
const name = filename
|
|
352
|
+
?? (await drive.files.get({ fileId, fields: 'name', supportsAllDrives: true })).data.name
|
|
353
|
+
?? fileId;
|
|
354
|
+
const dest = prepareLocalDest(savePath, name);
|
|
314
355
|
const res = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'stream' });
|
|
315
356
|
// pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
|
|
316
357
|
await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600 }));
|
|
@@ -329,14 +370,20 @@ export function registerDriveTools(server) {
|
|
|
329
370
|
account: accountEnum.describe('Google account alias'),
|
|
330
371
|
fileId: z.string().describe('Google Drive file ID'),
|
|
331
372
|
mimeType: z.string().describe('Target export MIME type (e.g. "application/pdf", "text/markdown", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")'),
|
|
332
|
-
savePath: z.string().describe('Absolute
|
|
333
|
-
filename: z.string().describe('Filename to save as'),
|
|
373
|
+
savePath: z.string().describe('Absolute DIRECTORY path to save into (created if missing, on the machine running the server); the file name comes from `filename`'),
|
|
374
|
+
filename: z.string().optional().describe('Filename to save as (defaults to the Drive name plus the extension implied by mimeType)'),
|
|
334
375
|
},
|
|
335
376
|
}, async ({ account, fileId, mimeType: exportMime, savePath, filename }) => {
|
|
336
377
|
try {
|
|
337
378
|
const auth = await getClient(account);
|
|
338
379
|
const drive = driveClient({ version: 'v3', auth });
|
|
339
|
-
|
|
380
|
+
let name = filename;
|
|
381
|
+
if (!name) {
|
|
382
|
+
const meta = await drive.files.get({ fileId, fields: 'name', supportsAllDrives: true });
|
|
383
|
+
const ext = mime.extension(exportMime);
|
|
384
|
+
name = `${meta.data.name ?? fileId}${ext ? `.${ext}` : ''}`;
|
|
385
|
+
}
|
|
386
|
+
const dest = prepareLocalDest(savePath, name);
|
|
340
387
|
const res = await drive.files.export({ fileId, mimeType: exportMime }, { responseType: 'stream' });
|
|
341
388
|
// pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
|
|
342
389
|
await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600 }));
|
|
@@ -384,14 +431,9 @@ export function registerDriveTools(server) {
|
|
|
384
431
|
fileId: z.string().describe('Google Drive file ID'),
|
|
385
432
|
newName: z.string().optional().describe('New filename'),
|
|
386
433
|
newParentFolderId: z.string().optional().describe('Move to this folder'),
|
|
387
|
-
localPath: z.string().optional().describe('Replace file content with this local file'),
|
|
434
|
+
localPath: z.string().optional().describe('Replace file content with this local file (path on the machine running the server)'),
|
|
388
435
|
mimeType: z.string().optional().describe('MIME type of the replacement file (required if localPath is provided)'),
|
|
389
|
-
convertTo: z.enum(
|
|
390
|
-
'application/vnd.google-apps.document',
|
|
391
|
-
'application/vnd.google-apps.spreadsheet',
|
|
392
|
-
'application/vnd.google-apps.presentation',
|
|
393
|
-
'application/vnd.google-apps.drawing',
|
|
394
|
-
]).optional().describe('When replacing content via localPath, convert the new content into this native Google Workspace type on import (e.g. replace a Google Doc body from a local .docx). Source must be an importable format.'),
|
|
436
|
+
convertTo: z.enum(CONVERT_TO_VALUES).optional().describe('When replacing content via localPath, convert the new content into this native Google Workspace type on import: "document" | "spreadsheet" | "presentation" | "drawing" (full application/vnd.google-apps.* ids also accepted).'),
|
|
395
437
|
},
|
|
396
438
|
}, async ({ account, fileId, newName, newParentFolderId, localPath: localPathArg, mimeType: mimeTypeArg, convertTo }) => {
|
|
397
439
|
try {
|
|
@@ -414,10 +456,10 @@ export function registerDriveTools(server) {
|
|
|
414
456
|
if (localPathArg) {
|
|
415
457
|
params.media = {
|
|
416
458
|
mimeType: mimeTypeArg ?? (mime.lookup(localPathArg) || 'application/octet-stream'),
|
|
417
|
-
body:
|
|
459
|
+
body: await openLocalReadStream(localPathArg),
|
|
418
460
|
};
|
|
419
461
|
if (convertTo)
|
|
420
|
-
requestBody.mimeType = convertTo;
|
|
462
|
+
requestBody.mimeType = resolveConvertTarget(convertTo);
|
|
421
463
|
}
|
|
422
464
|
const res = await drive.files.update(params);
|
|
423
465
|
return {
|
|
@@ -1440,7 +1482,7 @@ async function downloadAndUpload(sourceDrive, targetDrive, fileId, sourceMime, p
|
|
|
1440
1482
|
},
|
|
1441
1483
|
media: {
|
|
1442
1484
|
mimeType: plan.kind === 'native' ? plan.exportMime : (sourceMime ?? 'application/octet-stream'),
|
|
1443
|
-
body:
|
|
1485
|
+
body: await openLocalReadStream(tmp),
|
|
1444
1486
|
},
|
|
1445
1487
|
supportsAllDrives: true,
|
|
1446
1488
|
fields: 'id,name,mimeType,webViewLink',
|
|
@@ -13,6 +13,12 @@ export interface GeneratedToolDef {
|
|
|
13
13
|
method: ApiMethodRef;
|
|
14
14
|
params: GeneratedParam[];
|
|
15
15
|
hasBody: boolean;
|
|
16
|
+
/** Typed-body tier: these top-level args assemble into the request body
|
|
17
|
+
* (flat schemas only; deep schemas keep the single opaque `body` arg). */
|
|
18
|
+
bodyParams?: Array<{
|
|
19
|
+
field: string;
|
|
20
|
+
api: string;
|
|
21
|
+
}>;
|
|
16
22
|
shape: z.ZodRawShape;
|
|
17
23
|
}
|
|
18
24
|
export declare function accountField(): z.ZodOptional<z.ZodType<string, unknown, z.core.$ZodTypeInternals<string, unknown>>>;
|
|
@@ -25,11 +25,21 @@ export function registerGeneratedTool(registry, def, deps = {}) {
|
|
|
25
25
|
else
|
|
26
26
|
queryParams[p.api] = value;
|
|
27
27
|
}
|
|
28
|
+
let body = def.hasBody ? args.body : undefined;
|
|
29
|
+
if (def.bodyParams) {
|
|
30
|
+
const assembled = {};
|
|
31
|
+
for (const bp of def.bodyParams) {
|
|
32
|
+
const value = args[bp.field];
|
|
33
|
+
if (value !== undefined)
|
|
34
|
+
assembled[bp.api] = value;
|
|
35
|
+
}
|
|
36
|
+
body = assembled;
|
|
37
|
+
}
|
|
28
38
|
return executeApiMethod(def.method, {
|
|
29
39
|
account: args.account,
|
|
30
40
|
pathParams,
|
|
31
41
|
queryParams,
|
|
32
|
-
body
|
|
42
|
+
body,
|
|
33
43
|
}, deps);
|
|
34
44
|
});
|
|
35
45
|
}
|