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.
Files changed (60) hide show
  1. package/README.md +5 -3
  2. package/dist/api-probe.d.ts +18 -0
  3. package/dist/api-probe.js +68 -0
  4. package/dist/arg-normalize.d.ts +19 -0
  5. package/dist/arg-normalize.js +90 -0
  6. package/dist/auth.js +15 -72
  7. package/dist/client.js +3 -1
  8. package/dist/discover.js +35 -14
  9. package/dist/discovery-client.d.ts +8 -1
  10. package/dist/discovery-client.js +67 -15
  11. package/dist/doctor.d.ts +12 -0
  12. package/dist/doctor.js +98 -15
  13. package/dist/http-transport.d.ts +3 -0
  14. package/dist/http-transport.js +2 -1
  15. package/dist/index.js +4 -1
  16. package/dist/oauth-consent.d.ts +25 -10
  17. package/dist/oauth-consent.js +85 -39
  18. package/dist/registry.d.ts +12 -0
  19. package/dist/registry.js +61 -3
  20. package/dist/scope-catalog.d.ts +1 -0
  21. package/dist/scope-catalog.js +17 -1
  22. package/dist/services.js +3 -1
  23. package/dist/tools/_errors.js +69 -5
  24. package/dist/tools/_local-files.d.ts +3 -0
  25. package/dist/tools/_local-files.js +34 -0
  26. package/dist/tools/account-wizard.d.ts +10 -0
  27. package/dist/tools/account-wizard.js +52 -19
  28. package/dist/tools/analytics.d.ts +18 -0
  29. package/dist/tools/analytics.js +279 -0
  30. package/dist/tools/drive.d.ts +2 -1
  31. package/dist/tools/drive.js +75 -33
  32. package/dist/tools/generated/_shared.d.ts +6 -0
  33. package/dist/tools/generated/_shared.js +11 -1
  34. package/dist/tools/generated/admin.js +160 -29
  35. package/dist/tools/generated/analytics.d.ts +2 -0
  36. package/dist/tools/generated/analytics.js +981 -0
  37. package/dist/tools/generated/chat.js +39 -12
  38. package/dist/tools/generated/classroom.js +56 -14
  39. package/dist/tools/generated/cloudidentity.js +18 -10
  40. package/dist/tools/generated/cloudsearch.js +4 -3
  41. package/dist/tools/generated/contacts.js +13 -4
  42. package/dist/tools/generated/drive.js +15 -6
  43. package/dist/tools/generated/drivelabels.js +33 -5
  44. package/dist/tools/generated/forms.js +1 -1
  45. package/dist/tools/generated/gmail.js +39 -12
  46. package/dist/tools/generated/index.js +2 -0
  47. package/dist/tools/generated/keep.js +3 -2
  48. package/dist/tools/generated/licensing.js +20 -4
  49. package/dist/tools/generated/meet.js +1 -1
  50. package/dist/tools/generated/reseller.js +8 -2
  51. package/dist/tools/generated/script.js +13 -3
  52. package/dist/tools/generated/searchconsole.js +4 -2
  53. package/dist/tools/generated/sheets.js +2 -1
  54. package/dist/tools/generated/tasks.js +7 -1
  55. package/dist/tools/generated/vault.js +18 -9
  56. package/dist/tools/generated/workspaceevents.js +3 -2
  57. package/dist/tools/gmail.js +3 -3
  58. package/dist/tools/google-api.d.ts +4 -1
  59. package/dist/tools/google-api.js +59 -15
  60. package/package.json +22 -17
@@ -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?.startsWith('text/')) {
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 to file on disk'),
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 = fs.createReadStream(localPath);
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 directory path to save into'),
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 dest = prepareLocalDest(savePath, filename);
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 directory path to save into'),
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
- const dest = prepareLocalDest(savePath, filename);
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: fs.createReadStream(localPathArg),
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: fs.createReadStream(tmp),
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: def.hasBody ? args.body : undefined,
42
+ body,
33
43
  }, deps);
34
44
  });
35
45
  }