mcp-google-multi 5.4.1-alpha.2 → 6.0.0-alpha.2

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 (116) hide show
  1. package/README.md +21 -16
  2. package/dist/accounts.d.ts +44 -5
  3. package/dist/accounts.js +267 -25
  4. package/dist/auth.d.ts +11 -4
  5. package/dist/auth.js +85 -78
  6. package/dist/client-config.d.ts +90 -0
  7. package/dist/client-config.js +282 -0
  8. package/dist/client.js +10 -4
  9. package/dist/config-file.d.ts +38 -0
  10. package/dist/config-file.js +117 -0
  11. package/dist/discover.js +57 -3
  12. package/dist/doctor.d.ts +86 -0
  13. package/dist/doctor.js +384 -0
  14. package/dist/env-load.d.ts +11 -0
  15. package/dist/env-load.js +52 -0
  16. package/dist/executor.d.ts +2 -0
  17. package/dist/executor.js +2 -1
  18. package/dist/fanout.js +8 -5
  19. package/dist/fs-atomic.d.ts +3 -0
  20. package/dist/fs-atomic.js +123 -0
  21. package/dist/http-config.d.ts +35 -0
  22. package/dist/http-config.js +113 -0
  23. package/dist/http-transport.d.ts +59 -0
  24. package/dist/http-transport.js +269 -0
  25. package/dist/identity.d.ts +24 -0
  26. package/dist/identity.js +19 -0
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +199 -18
  29. package/dist/master-key.d.ts +53 -0
  30. package/dist/master-key.js +242 -0
  31. package/dist/mcp-token.d.ts +81 -0
  32. package/dist/mcp-token.js +176 -0
  33. package/dist/migrate-config.d.ts +7 -0
  34. package/dist/migrate-config.js +103 -0
  35. package/dist/migrate-tokens.js +4 -3
  36. package/dist/net-tuning.d.ts +8 -0
  37. package/dist/net-tuning.js +25 -0
  38. package/dist/oauth-as.d.ts +52 -0
  39. package/dist/oauth-as.js +457 -0
  40. package/dist/oauth-consent.d.ts +28 -0
  41. package/dist/oauth-consent.js +109 -0
  42. package/dist/open-url.d.ts +1 -0
  43. package/dist/open-url.js +30 -0
  44. package/dist/reauth-hint.d.ts +2 -0
  45. package/dist/reauth-hint.js +15 -0
  46. package/dist/registry-transfer.d.ts +41 -0
  47. package/dist/registry-transfer.js +219 -0
  48. package/dist/registry.d.ts +27 -1
  49. package/dist/registry.js +117 -6
  50. package/dist/scope-catalog.d.ts +19 -0
  51. package/dist/scope-catalog.js +182 -0
  52. package/dist/scope-observability.d.ts +78 -0
  53. package/dist/scope-observability.js +192 -0
  54. package/dist/services.js +1 -1
  55. package/dist/setup-prompt.d.ts +9 -0
  56. package/dist/setup-prompt.js +59 -0
  57. package/dist/ssrf-guard.d.ts +48 -0
  58. package/dist/ssrf-guard.js +242 -0
  59. package/dist/token-store.js +23 -125
  60. package/dist/tools/_errors.d.ts +8 -2
  61. package/dist/tools/_errors.js +97 -6
  62. package/dist/tools/account-wizard.d.ts +33 -0
  63. package/dist/tools/account-wizard.js +284 -0
  64. package/dist/tools/accounts-tool.d.ts +8 -5
  65. package/dist/tools/accounts-tool.js +38 -15
  66. package/dist/tools/admin.js +2 -2
  67. package/dist/tools/calendar.js +2 -2
  68. package/dist/tools/chat.js +2 -2
  69. package/dist/tools/contacts.d.ts +40 -0
  70. package/dist/tools/contacts.js +135 -2
  71. package/dist/tools/docs.d.ts +3 -0
  72. package/dist/tools/docs.js +60 -9
  73. package/dist/tools/drive.d.ts +3 -0
  74. package/dist/tools/drive.js +60 -8
  75. package/dist/tools/forms.js +4 -2
  76. package/dist/tools/generated/_shared.d.ts +1 -3
  77. package/dist/tools/generated/_shared.js +3 -3
  78. package/dist/tools/generated/admin.js +175 -132
  79. package/dist/tools/generated/appsmarket.js +6 -2
  80. package/dist/tools/generated/calendar.js +45 -27
  81. package/dist/tools/generated/chat.js +81 -47
  82. package/dist/tools/generated/classroom.js +131 -104
  83. package/dist/tools/generated/cloudidentity.js +73 -61
  84. package/dist/tools/generated/cloudsearch.js +59 -49
  85. package/dist/tools/generated/contacts.js +22 -14
  86. package/dist/tools/generated/drive.js +51 -35
  87. package/dist/tools/generated/driveactivity.js +5 -1
  88. package/dist/tools/generated/drivelabels.js +30 -25
  89. package/dist/tools/generated/forms.js +7 -3
  90. package/dist/tools/generated/gmail.js +73 -58
  91. package/dist/tools/generated/groupsmigration.js +5 -1
  92. package/dist/tools/generated/groupssettings.js +7 -3
  93. package/dist/tools/generated/keep.js +12 -7
  94. package/dist/tools/generated/licensing.js +11 -7
  95. package/dist/tools/generated/meet.js +20 -13
  96. package/dist/tools/generated/postmaster.js +8 -4
  97. package/dist/tools/generated/reseller.js +22 -17
  98. package/dist/tools/generated/script.js +26 -16
  99. package/dist/tools/generated/sheets.js +11 -7
  100. package/dist/tools/generated/tasks.js +6 -2
  101. package/dist/tools/generated/vault.js +38 -33
  102. package/dist/tools/generated/workspaceevents.js +12 -7
  103. package/dist/tools/gmail-mime.d.ts +38 -8
  104. package/dist/tools/gmail-mime.js +75 -71
  105. package/dist/tools/gmail.d.ts +31 -0
  106. package/dist/tools/gmail.js +483 -99
  107. package/dist/tools/google-api.js +7 -4
  108. package/dist/tools/meet.js +2 -2
  109. package/dist/tools/searchconsole.js +2 -2
  110. package/dist/tools/sheets.js +6 -2
  111. package/dist/tools/slides.js +4 -2
  112. package/dist/tools/tasks.js +2 -2
  113. package/dist/types.d.ts +1 -1
  114. package/dist/write-control.d.ts +24 -1
  115. package/dist/write-control.js +26 -1
  116. package/package.json +26 -7
@@ -1,11 +1,54 @@
1
1
  import { z } from 'zod';
2
2
  import { coerceBoolean, coerceJson } from './_coerce.js';
3
3
  import { docs as docsClient } from '@googleapis/docs';
4
- import { ACCOUNTS } from '../accounts.js';
4
+ import { accountAliasSchema } from '../accounts.js';
5
5
  import { getClient } from '../client.js';
6
6
  import { handleGoogleApiError } from './_errors.js';
7
7
  import { capText } from '../trim.js';
8
- const accountEnum = z.enum(ACCOUNTS);
8
+ const accountEnum = accountAliasSchema.optional();
9
+ // A single very large insertText has been observed to apply only partially while
10
+ // the request still returns success, silently dropping the tail. Split a large
11
+ // insert into bounded pieces sent as sequential insertText requests in one atomic
12
+ // batchUpdate: small inserts keep the exact single-request path, and a large one
13
+ // can no longer be silently truncated. Docs indices are UTF-16 code units (= JS
14
+ // string length), so index math advances by `.length`; never split a surrogate pair.
15
+ export const DOCS_INSERT_CHUNK = 5000;
16
+ export function splitInsertText(text, maxLen = DOCS_INSERT_CHUNK) {
17
+ if (text.length === 0)
18
+ return [];
19
+ if (text.length <= maxLen)
20
+ return [text];
21
+ const chunks = [];
22
+ let i = 0;
23
+ while (i < text.length) {
24
+ let end = Math.min(i + maxLen, text.length);
25
+ if (end < text.length) {
26
+ const code = text.charCodeAt(end - 1);
27
+ if (code >= 0xd800 && code <= 0xdbff)
28
+ end -= 1; // keep a surrogate pair together
29
+ }
30
+ chunks.push(text.slice(i, end));
31
+ i = end;
32
+ }
33
+ return chunks;
34
+ }
35
+ // Sequential insertText requests: appends chain at end-of-segment; index inserts
36
+ // advance the cursor by each prior chunk's length so the pieces stay contiguous.
37
+ export function buildInsertRequests(text, index) {
38
+ const chunks = splitInsertText(text);
39
+ const requests = [];
40
+ let cursor = index;
41
+ for (const chunk of chunks) {
42
+ if (cursor === undefined) {
43
+ requests.push({ insertText: { text: chunk, endOfSegmentLocation: { segmentId: '' } } });
44
+ }
45
+ else {
46
+ requests.push({ insertText: { text: chunk, location: { index: cursor } } });
47
+ cursor += chunk.length;
48
+ }
49
+ }
50
+ return requests;
51
+ }
9
52
  function extractPlainText(body) {
10
53
  return extractPlainTextInRange(body, 0, Infinity);
11
54
  }
@@ -304,21 +347,23 @@ export function registerDocsTools(server) {
304
347
  try {
305
348
  const auth = await getClient(account);
306
349
  const docs = docsClient({ version: 'v1', auth });
307
- const request = { insertText: { text } };
308
- if (index !== undefined) {
309
- request.insertText.location = { index };
310
- }
311
- else {
312
- request.insertText.endOfSegmentLocation = { segmentId: '' };
350
+ const requests = buildInsertRequests(text, index);
351
+ if (requests.length === 0) {
352
+ return {
353
+ content: [{ type: 'text', text: JSON.stringify({
354
+ documentId, insertedAt: index ?? 'end', inserted: 0,
355
+ }, null, 2) }],
356
+ };
313
357
  }
314
358
  const res = await docs.documents.batchUpdate({
315
359
  documentId,
316
- requestBody: { requests: [request] },
360
+ requestBody: { requests },
317
361
  });
318
362
  return {
319
363
  content: [{ type: 'text', text: JSON.stringify({
320
364
  documentId: res.data.documentId,
321
365
  insertedAt: index ?? 'end',
366
+ inserted: text.length,
322
367
  }, null, 2) }],
323
368
  };
324
369
  }
@@ -327,6 +372,8 @@ export function registerDocsTools(server) {
327
372
  }
328
373
  });
329
374
  server.registerTool('docs_replace_text', {
375
+ // Insert/append-capable or non-convergent: a retry duplicates content.
376
+ annotations: { idempotentHint: false },
330
377
  description: 'Find and replace all occurrences of text in a document',
331
378
  inputSchema: {
332
379
  account: accountEnum.describe('Google account alias'),
@@ -978,6 +1025,8 @@ export function registerDocsTools(server) {
978
1025
  });
979
1026
  // ─── Table mutations (one tool with operation discriminator) ───────────
980
1027
  server.registerTool('docs_modify_table', {
1028
+ // Insert/append-capable or non-convergent: a retry duplicates content.
1029
+ annotations: { idempotentHint: false },
981
1030
  description: 'Mutate a table by operation: insertRow, insertColumn, deleteRow, deleteColumn, mergeCells, unmergeCells. Locate the cell with tableStartIndex (the table\'s start index in the doc) plus rowIndex/columnIndex (0-based).',
982
1031
  inputSchema: {
983
1032
  account: accountEnum.describe('Google account alias'),
@@ -1156,6 +1205,8 @@ export function registerDocsTools(server) {
1156
1205
  });
1157
1206
  // ─── Batch update escape hatch ─────────────────────────────────────────
1158
1207
  server.registerTool('docs_batch_update', {
1208
+ // Insert/append-capable or non-convergent: a retry duplicates content.
1209
+ annotations: { idempotentHint: false },
1159
1210
  description: 'Generic documents.batchUpdate pass-through. Accepts the full Request union (40 types). See https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request',
1160
1211
  inputSchema: {
1161
1212
  account: accountEnum.describe('Google account alias'),
@@ -1,5 +1,8 @@
1
1
  import type { ToolRegistry } from '../registry.js';
2
2
  export declare function prepareLocalDest(savePath: string, filename: string): string;
3
+ export declare const DRIVE_QUERY_HINT: string;
4
+ export declare function normalizeDriveQuery(raw: string): string;
5
+ export declare function isDriveInvalidQuery(error: any): boolean;
3
6
  export declare function resolveShareNotification(opts: {
4
7
  type: string;
5
8
  role: string;
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { coerceArray, coerceBoolean } from './_coerce.js';
3
3
  import { drive as driveClient } from '@googleapis/drive';
4
- import { ACCOUNTS, ACCOUNT_CONFIG } from '../accounts.js';
4
+ import { accountAliasSchema, getAccountSet } from '../accounts.js';
5
5
  import { getClient } from '../client.js';
6
6
  import { handleGoogleApiError } from './_errors.js';
7
7
  import { isAllowed, writeDisabledResult } from '../write-control.js';
@@ -12,7 +12,10 @@ import * as os from 'os';
12
12
  import * as crypto from 'crypto';
13
13
  import { pipeline } from 'node:stream/promises';
14
14
  import mime from 'mime-types';
15
- const accountEnum = z.enum(ACCOUNTS);
15
+ const accountEnum = accountAliasSchema.optional();
16
+ // drive_transfer's two-account form is the sanctioned schema exception and
17
+ // stays REQUIRED: omission must fail at the schema, not as a runtime riddle.
18
+ const requiredAccountEnum = accountAliasSchema;
16
19
  const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
17
20
  const FALLBACK_MAX_BYTES = 1024 * 1024 * 1024; // 1GB
18
21
  const GOOGLE_WORKSPACE_TYPES = new Set([
@@ -34,6 +37,41 @@ export function prepareLocalDest(savePath, filename) {
34
37
  fs.mkdirSync(savePath, { recursive: true });
35
38
  return dest;
36
39
  }
40
+ export const DRIVE_QUERY_HINT = "Drive search syntax: a plain keyword is treated as a full-text search, but a " +
41
+ "structured query needs an operator, e.g. \"name contains 'report'\", " +
42
+ "\"mimeType = 'application/pdf'\", or \"'me' in owners\". " +
43
+ 'Reference: https://developers.google.com/drive/api/guides/search-files';
44
+ // A bare keyword is not valid Drive `q` syntax (Google 400s "Invalid Value"), yet
45
+ // it is the natural thing to pass to a "search" tool, so wrap it as a full-text
46
+ // match. A query already carrying a comparison operator or a quoted literal is a
47
+ // structured query and is passed through untouched.
48
+ export function normalizeDriveQuery(raw) {
49
+ const q = raw.trim();
50
+ if (q === '')
51
+ return q;
52
+ // Structured iff it carries a comparison operator or the membership form
53
+ // "<value> in (parents|owners|writers|readers)". A bare apostrophe does NOT
54
+ // count (names like O'Brien are keywords, not query syntax).
55
+ const hasComparison = /(\bcontains\b|!=|<=|>=|[=<>])/i.test(q);
56
+ const hasMembership = /\bin\s+(parents|owners|writers|readers)\b/i.test(q);
57
+ if (hasComparison || hasMembership)
58
+ return q;
59
+ const escaped = q.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
60
+ return `fullText contains '${escaped}'`;
61
+ }
62
+ // Drive rejects a malformed query with HTTP 400 / reason "invalid" and the opaque
63
+ // message "Invalid Value"; detect that so drive_search returns query-syntax help
64
+ // instead of a generic upstream_error.
65
+ export function isDriveInvalidQuery(error) {
66
+ const raw = error?.code ?? error?.status ?? error?.response?.status;
67
+ const status = typeof raw === 'string' ? Number(raw) : raw;
68
+ if (status !== 400)
69
+ return false;
70
+ const reason = error?.errors?.[0]?.reason ??
71
+ error?.response?.data?.error?.errors?.[0]?.reason;
72
+ const message = error?.response?.data?.error?.message ?? error?.message ?? '';
73
+ return reason === 'invalid' || reason === 'invalidQuery' || /invalid value/i.test(String(message));
74
+ }
37
75
  // sendNotificationEmail is only valid for user/group permissions, and Google forbids
38
76
  // disabling it on an ownership transfer. Returns undefined to omit the param entirely.
39
77
  export function resolveShareNotification(opts) {
@@ -46,10 +84,10 @@ export function resolveShareNotification(opts) {
46
84
  export function registerDriveTools(server) {
47
85
  // ─── Read / search / list ──────────────────────────────────────────────
48
86
  server.registerTool('drive_search', {
49
- description: 'Search files in a Google Drive account',
87
+ description: 'Search files in a Google Drive account. A plain keyword is treated as a full-text search; pass Drive query syntax for a structured search.',
50
88
  inputSchema: {
51
89
  account: accountEnum.describe('Google account alias'),
52
- query: z.string().describe('Drive search syntax, e.g. "name contains \'MoU\'"'),
90
+ query: z.string().describe('A plain keyword (full-text search) or Drive query syntax, e.g. "name contains \'MoU\'"'),
53
91
  maxResults: z.number().min(1).max(100).default(20).optional()
54
92
  .describe('Max results to return (default: 20)'),
55
93
  driveId: z.string().optional().describe('Optional shared drive ID'),
@@ -59,7 +97,7 @@ export function registerDriveTools(server) {
59
97
  const auth = await getClient(account);
60
98
  const drive = driveClient({ version: 'v3', auth });
61
99
  const params = {
62
- q: query,
100
+ q: normalizeDriveQuery(query),
63
101
  pageSize: maxResults ?? 20,
64
102
  fields: 'files(id,name,mimeType,modifiedTime,webViewLink,size,parents,driveId)',
65
103
  supportsAllDrives: true,
@@ -75,10 +113,24 @@ export function registerDriveTools(server) {
75
113
  };
76
114
  }
77
115
  catch (error) {
116
+ if (isDriveInvalidQuery(error)) {
117
+ const message = error?.response?.data?.error?.message ?? error?.message ?? 'Invalid Value';
118
+ return {
119
+ content: [{ type: 'text', text: JSON.stringify({
120
+ error: 'invalid_query',
121
+ message,
122
+ hint: DRIVE_QUERY_HINT,
123
+ retriable: false,
124
+ account,
125
+ }) }],
126
+ isError: true,
127
+ };
128
+ }
78
129
  return handleDriveError(error, account);
79
130
  }
80
131
  });
81
132
  server.registerTool('drive_read', {
133
+ _meta: { 'anthropic/maxResultSizeChars': 100_000 },
82
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)',
83
135
  inputSchema: {
84
136
  account: accountEnum.describe('Google account alias'),
@@ -1095,8 +1147,8 @@ export function registerDriveTools(server) {
1095
1147
  'revision history, and permissions do not transfer. move=true trashes the source after a ' +
1096
1148
  'successful copy and requires deletes to be allowed by write-control.',
1097
1149
  inputSchema: {
1098
- fromAccount: accountEnum.describe('Source account alias'),
1099
- toAccount: accountEnum.describe('Target account alias'),
1150
+ fromAccount: requiredAccountEnum.describe('Source account alias'),
1151
+ toAccount: requiredAccountEnum.describe('Target account alias'),
1100
1152
  fileId: z.string().describe('File ID in the source account (folders are not supported)'),
1101
1153
  parentFolderId: z.string().optional().describe('Target folder ID (default: target My Drive root)'),
1102
1154
  newName: z.string().optional().describe('Rename the copy (default: keep the source name)'),
@@ -1150,7 +1202,7 @@ export function registerDriveTools(server) {
1150
1202
  };
1151
1203
  }
1152
1204
  const intendedName = newName ?? meta.data.name ?? 'transferred-file';
1153
- const targetEmail = ACCOUNT_CONFIG[toAccount].email;
1205
+ const targetEmail = getAccountSet().configs[toAccount].email;
1154
1206
  const finish = async (data, strategy, flags) => {
1155
1207
  let moveFailed = false;
1156
1208
  let moveSkipped = false;
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { forms as formsClient } from '@googleapis/forms';
3
- import { ACCOUNTS } from '../accounts.js';
3
+ import { accountAliasSchema } from '../accounts.js';
4
4
  import { getClient } from '../client.js';
5
5
  import { handleGoogleApiError } from './_errors.js';
6
6
  import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
7
- const accountEnum = z.enum(ACCOUNTS);
7
+ const accountEnum = accountAliasSchema.optional();
8
8
  export function registerFormsTools(server) {
9
9
  server.registerTool('forms_get', {
10
10
  description: 'Get a Google Form (definition: questions, sections, settings). Requires forms.body scope.',
@@ -119,6 +119,8 @@ export function registerFormsTools(server) {
119
119
  }
120
120
  });
121
121
  server.registerTool('forms_batch_update', {
122
+ // Insert/append-capable or non-convergent: a retry duplicates content.
123
+ annotations: { idempotentHint: false },
122
124
  description: 'Generic forms.batchUpdate pass-through: add/edit/delete questions, update form info and settings. See https://developers.google.com/workspace/forms/api/reference/rest/v1/forms/request',
123
125
  inputSchema: {
124
126
  account: accountEnum.describe('Google account alias'),
@@ -15,7 +15,5 @@ export interface GeneratedToolDef {
15
15
  hasBody: boolean;
16
16
  shape: z.ZodRawShape;
17
17
  }
18
- export declare function accountField(): z.ZodEnum<{
19
- [x: string]: string;
20
- }>;
18
+ export declare function accountField(): z.ZodOptional<z.ZodType<string, unknown, z.core.$ZodTypeInternals<string, unknown>>>;
21
19
  export declare function registerGeneratedTool(registry: ToolRegistry, def: GeneratedToolDef, deps?: ExecuteDeps): void;
@@ -1,8 +1,7 @@
1
- import { z } from 'zod';
2
- import { ACCOUNTS } from '../../accounts.js';
1
+ import { accountAliasSchema } from '../../accounts.js';
3
2
  import { executeApiMethod } from '../../executor.js';
4
3
  export function accountField() {
5
- return z.enum(ACCOUNTS).describe('Google account alias');
4
+ return accountAliasSchema.optional().describe('Google account alias (omit for the default account)');
6
5
  }
7
6
  export function registerGeneratedTool(registry, def, deps = {}) {
8
7
  // Widened locally: `cud` is a registry extension the SDK config type doesn't
@@ -13,6 +12,7 @@ export function registerGeneratedTool(registry, def, deps = {}) {
13
12
  inputSchema: def.shape,
14
13
  cud: def.cud,
15
14
  annotations: { openWorldHint: true },
15
+ requiredScopes: def.method.scopes,
16
16
  }, async (args) => {
17
17
  const pathParams = {};
18
18
  const queryParams = {};