mcp-google-multi 6.0.0-beta.2 → 6.0.0-beta.4

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 (181) hide show
  1. package/README.md +5 -5
  2. package/dist/accounts.d.ts +20 -4
  3. package/dist/accounts.js +68 -44
  4. package/dist/api-probe.d.ts +5 -0
  5. package/dist/api-probe.js +20 -15
  6. package/dist/arg-normalize.js +19 -38
  7. package/dist/arg-strict.d.ts +7 -0
  8. package/dist/arg-strict.js +38 -28
  9. package/dist/auth.d.ts +5 -4
  10. package/dist/auth.js +16 -23
  11. package/dist/boot-gates.d.ts +26 -0
  12. package/dist/boot-gates.js +36 -0
  13. package/dist/cimd-cache.d.ts +38 -0
  14. package/dist/cimd-cache.js +111 -0
  15. package/dist/client-config.d.ts +2 -2
  16. package/dist/client-config.js +8 -14
  17. package/dist/client.d.ts +32 -2
  18. package/dist/client.js +0 -0
  19. package/dist/compose.d.ts +18 -0
  20. package/dist/compose.js +109 -0
  21. package/dist/config-file.d.ts +13 -0
  22. package/dist/config-file.js +41 -10
  23. package/dist/discover.js +7 -10
  24. package/dist/discovery-client.d.ts +3 -0
  25. package/dist/discovery-client.js +36 -32
  26. package/dist/doctor.d.ts +9 -5
  27. package/dist/doctor.js +119 -29
  28. package/dist/env-load.js +2 -5
  29. package/dist/executor.d.ts +3 -0
  30. package/dist/executor.js +6 -6
  31. package/dist/fanout.d.ts +9 -1
  32. package/dist/fanout.js +47 -22
  33. package/dist/fs-atomic.d.ts +8 -0
  34. package/dist/fs-atomic.js +216 -29
  35. package/dist/hint-provider.d.ts +129 -0
  36. package/dist/hint-provider.js +58 -0
  37. package/dist/http-config.d.ts +2 -2
  38. package/dist/http-config.js +3 -7
  39. package/dist/http-transport.d.ts +67 -9
  40. package/dist/http-transport.js +318 -52
  41. package/dist/identity.d.ts +6 -3
  42. package/dist/identity.js +24 -8
  43. package/dist/index.js +76 -160
  44. package/dist/master-key.d.ts +3 -3
  45. package/dist/master-key.js +16 -19
  46. package/dist/mcp-token.d.ts +126 -19
  47. package/dist/mcp-token.js +557 -76
  48. package/dist/metrics-cli.d.ts +1 -1
  49. package/dist/metrics-cli.js +1 -1
  50. package/dist/metrics-tap.js +2 -5
  51. package/dist/migrate-config.d.ts +1 -2
  52. package/dist/migrate-config.js +5 -7
  53. package/dist/net-tuning.js +2 -4
  54. package/dist/oauth-as.d.ts +104 -4
  55. package/dist/oauth-as.js +368 -114
  56. package/dist/oauth-consent.d.ts +10 -0
  57. package/dist/oauth-consent.js +20 -10
  58. package/dist/outbound-allowlist.js +9 -11
  59. package/dist/reauth-hint.d.ts +1 -1
  60. package/dist/reauth-hint.js +11 -10
  61. package/dist/registry-transfer.d.ts +5 -0
  62. package/dist/registry-transfer.js +63 -11
  63. package/dist/registry.d.ts +13 -4
  64. package/dist/registry.js +100 -72
  65. package/dist/scope-catalog.d.ts +16 -0
  66. package/dist/scope-catalog.js +40 -13
  67. package/dist/scope-observability.d.ts +10 -9
  68. package/dist/scope-observability.js +34 -24
  69. package/dist/services.d.ts +18 -10
  70. package/dist/services.js +61 -22
  71. package/dist/ssrf-guard.d.ts +23 -9
  72. package/dist/ssrf-guard.js +106 -78
  73. package/dist/tenant-purge.d.ts +26 -0
  74. package/dist/tenant-purge.js +65 -0
  75. package/dist/token-store.d.ts +20 -2
  76. package/dist/token-store.js +0 -0
  77. package/dist/tools/_coerce.d.ts +4 -4
  78. package/dist/tools/_errors.d.ts +3 -0
  79. package/dist/tools/_errors.js +72 -60
  80. package/dist/tools/_local-files.d.ts +9 -0
  81. package/dist/tools/_local-files.js +22 -8
  82. package/dist/tools/account-wizard.d.ts +13 -3
  83. package/dist/tools/account-wizard.js +107 -56
  84. package/dist/tools/accounts-tool.d.ts +8 -1
  85. package/dist/tools/accounts-tool.js +35 -19
  86. package/dist/tools/admin.d.ts +10 -1
  87. package/dist/tools/admin.js +14 -11
  88. package/dist/tools/analytics.d.ts +10 -1
  89. package/dist/tools/analytics.js +14 -10
  90. package/dist/tools/calendar.d.ts +2 -1
  91. package/dist/tools/calendar.js +19 -18
  92. package/dist/tools/chat.d.ts +10 -1
  93. package/dist/tools/chat.js +12 -8
  94. package/dist/tools/contacts.d.ts +2 -1
  95. package/dist/tools/contacts.js +22 -19
  96. package/dist/tools/docs.d.ts +2 -1
  97. package/dist/tools/docs.js +35 -36
  98. package/dist/tools/drive.d.ts +2 -1
  99. package/dist/tools/drive.js +80 -74
  100. package/dist/tools/forms.d.ts +10 -1
  101. package/dist/tools/forms.js +18 -12
  102. package/dist/tools/generated/_shared.d.ts +2 -1
  103. package/dist/tools/generated/_shared.js +7 -3
  104. package/dist/tools/generated/admin.d.ts +2 -1
  105. package/dist/tools/generated/admin.js +265 -265
  106. package/dist/tools/generated/analytics.d.ts +2 -1
  107. package/dist/tools/generated/analytics.js +125 -125
  108. package/dist/tools/generated/appsmarket.d.ts +2 -1
  109. package/dist/tools/generated/appsmarket.js +5 -5
  110. package/dist/tools/generated/calendar.d.ts +2 -1
  111. package/dist/tools/generated/calendar.js +55 -55
  112. package/dist/tools/generated/chat.d.ts +2 -1
  113. package/dist/tools/generated/chat.js +95 -95
  114. package/dist/tools/generated/classroom.d.ts +2 -1
  115. package/dist/tools/generated/classroom.js +209 -209
  116. package/dist/tools/generated/cloudidentity.d.ts +2 -1
  117. package/dist/tools/generated/cloudidentity.js +133 -133
  118. package/dist/tools/generated/cloudsearch.d.ts +2 -1
  119. package/dist/tools/generated/cloudsearch.js +99 -99
  120. package/dist/tools/generated/contacts.d.ts +2 -1
  121. package/dist/tools/generated/contacts.js +29 -29
  122. package/dist/tools/generated/drive.d.ts +2 -1
  123. package/dist/tools/generated/drive.js +71 -71
  124. package/dist/tools/generated/driveactivity.d.ts +2 -1
  125. package/dist/tools/generated/driveactivity.js +3 -3
  126. package/dist/tools/generated/drivelabels.d.ts +2 -1
  127. package/dist/tools/generated/drivelabels.js +53 -53
  128. package/dist/tools/generated/forms.d.ts +2 -1
  129. package/dist/tools/generated/forms.js +7 -7
  130. package/dist/tools/generated/gmail.d.ts +2 -1
  131. package/dist/tools/generated/gmail.js +117 -117
  132. package/dist/tools/generated/groupsmigration.d.ts +2 -1
  133. package/dist/tools/generated/groupsmigration.js +3 -3
  134. package/dist/tools/generated/groupssettings.d.ts +2 -1
  135. package/dist/tools/generated/groupssettings.js +7 -7
  136. package/dist/tools/generated/index.d.ts +2 -1
  137. package/dist/tools/generated/keep.d.ts +2 -1
  138. package/dist/tools/generated/keep.js +15 -15
  139. package/dist/tools/generated/licensing.d.ts +2 -1
  140. package/dist/tools/generated/licensing.js +15 -15
  141. package/dist/tools/generated/meet.d.ts +2 -1
  142. package/dist/tools/generated/meet.js +27 -27
  143. package/dist/tools/generated/postmaster.d.ts +2 -1
  144. package/dist/tools/generated/postmaster.js +9 -9
  145. package/dist/tools/generated/reseller.d.ts +2 -1
  146. package/dist/tools/generated/reseller.js +35 -35
  147. package/dist/tools/generated/script.d.ts +2 -1
  148. package/dist/tools/generated/script.js +33 -33
  149. package/dist/tools/generated/searchconsole.d.ts +2 -1
  150. package/dist/tools/generated/searchconsole.js +3 -3
  151. package/dist/tools/generated/sheets.d.ts +2 -1
  152. package/dist/tools/generated/sheets.js +15 -15
  153. package/dist/tools/generated/tasks.d.ts +2 -1
  154. package/dist/tools/generated/tasks.js +5 -5
  155. package/dist/tools/generated/vault.d.ts +2 -1
  156. package/dist/tools/generated/vault.js +67 -67
  157. package/dist/tools/generated/workspaceevents.d.ts +2 -1
  158. package/dist/tools/generated/workspaceevents.js +31 -31
  159. package/dist/tools/gmail-mime.d.ts +1 -1
  160. package/dist/tools/gmail-mime.js +7 -12
  161. package/dist/tools/gmail.d.ts +4 -3
  162. package/dist/tools/gmail.js +67 -69
  163. package/dist/tools/google-api.d.ts +2 -3
  164. package/dist/tools/google-api.js +34 -22
  165. package/dist/tools/meet.d.ts +10 -1
  166. package/dist/tools/meet.js +12 -9
  167. package/dist/tools/searchconsole.d.ts +2 -1
  168. package/dist/tools/searchconsole.js +16 -13
  169. package/dist/tools/sheets.d.ts +2 -1
  170. package/dist/tools/sheets.js +35 -32
  171. package/dist/tools/slides.d.ts +10 -1
  172. package/dist/tools/slides.js +16 -10
  173. package/dist/tools/tasks.d.ts +2 -1
  174. package/dist/tools/tasks.js +18 -15
  175. package/dist/trim.d.ts +5 -0
  176. package/dist/trim.js +19 -0
  177. package/dist/usage-metrics.d.ts +23 -0
  178. package/dist/usage-metrics.js +67 -56
  179. package/dist/write-control.d.ts +4 -8
  180. package/dist/write-control.js +13 -6
  181. package/package.json +47 -21
@@ -1,11 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { coerceArray, coerceBoolean, coerceJson, coerceNumber } from './_coerce.js';
3
3
  import { gmail as gmailClient } from '@googleapis/gmail';
4
- import { accountAliasSchema } from '../accounts.js';
4
+ import { accountArgLive } from '../accounts.js';
5
5
  import { getClient } from '../client.js';
6
6
  import { handleGoogleApiError, invalidParams, mapGoogleError } from './_errors.js';
7
7
  import { buildReplyHeaders, composeRaw, renderMarkdown, htmlToMarkdown, HeaderInjectionError } from './gmail-mime.js';
8
- import { prepareLocalDest } from './_local-files.js';
8
+ import { hostFilesRefused, prepareLocalDest } from './_local-files.js';
9
9
  import { checkOutbound } from '../outbound-allowlist.js';
10
10
  import addressparser from 'nodemailer/lib/addressparser/index.js';
11
11
  import { lookup as lookupMime } from 'mime-types';
@@ -14,7 +14,6 @@ import { getTokenDir } from '../accounts.js';
14
14
  import { sliceClean } from '../trim.js';
15
15
  import * as path from 'path';
16
16
  import * as fs from 'fs';
17
- const accountEnum = accountAliasSchema.optional();
18
17
  function getHeader(headers, name) {
19
18
  return headers?.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ?? '';
20
19
  }
@@ -39,16 +38,15 @@ function decodeBody(payload, rawHtml = false) {
39
38
  const plain = [];
40
39
  const html = [];
41
40
  collectTextParts(payload, plain, html, true);
42
- // Concatenating every plain leaf keeps forwarded/mixed messages whole;
43
- // any plain content beats HTML because alternatives duplicate the same body.
44
- // A plain leaf is returned verbatim (byte-identical to v5).
41
+ // Concatenating every plain leaf keeps forwarded/mixed messages whole; plain
42
+ // always wins over HTML since alternatives duplicate the same body.
45
43
  if (plain.length > 0)
46
44
  return { body: plain.join('\n\n'), bodyFormat: 'plain' };
47
45
  if (html.length > 0) {
48
46
  const joined = html.join('\n\n');
49
47
  if (rawHtml)
50
48
  return { body: joined, bodyFormat: 'html' };
51
- // HTML-only -> Markdown (D6). turndown failure degrades to plain text.
49
+ // HTML-only -> Markdown. turndown failure degrades to plain text.
52
50
  const { text, ok } = htmlToMarkdown(joined);
53
51
  return { body: text, bodyFormat: ok ? 'markdown' : 'plain' };
54
52
  }
@@ -92,9 +90,8 @@ function parseAddresses(headerValue) {
92
90
  return out;
93
91
  }
94
92
  const normAddr = (a) => a.trim().toLowerCase();
95
- /** Rebuild a comma-separated address header; composeRaw/MailComposer re-encodes
96
- * names (RFC 2047) and the A4 CRLF pre-check applies, so derived recipients get
97
- * the same header-injection guard as caller-supplied ones. */
93
+ /** Rebuilds a comma-separated address header; composeRaw/MailComposer re-encodes
94
+ * names and apply the same header-injection guard as caller-supplied recipients. */
98
95
  function formatAddresses(list) {
99
96
  return list.map((a) => (a.name ? `${a.name} <${a.address}>` : a.address)).join(', ');
100
97
  }
@@ -104,12 +101,12 @@ export function deriveReplySubject(sourceSubject) {
104
101
  const s = sourceSubject.trim();
105
102
  return /^re:/i.test(s) ? s : `Re: ${s}`;
106
103
  }
107
- // The account's own-address set (primary + Gmail send-as aliases) used to
108
- // exclude the caller from reply-all. Send-as rarely changes, so memoize per
109
- // process; a stale miss only costs one extra self-copy, never a wrong send.
104
+ // Own-address set (primary + send-as aliases) for excluding the caller from
105
+ // reply-all; memoized per-process (a stale miss just costs one self-copy), keyed by alias+mailbox.
110
106
  const ownAddressCache = new Map();
111
107
  function getOwnAddresses(gmail, account, primaryEmail) {
112
- let cached = ownAddressCache.get(account);
108
+ const key = `${account} ${normAddr(primaryEmail)}`;
109
+ let cached = ownAddressCache.get(key);
113
110
  if (!cached) {
114
111
  cached = (async () => {
115
112
  const set = new Set([normAddr(primaryEmail)]);
@@ -121,17 +118,17 @@ function getOwnAddresses(gmail, account, primaryEmail) {
121
118
  }
122
119
  }
123
120
  catch {
124
- // insufficient_scope / 5xx: degrade to the primary alone, never block the send.
121
+ // insufficient_scope/5xx: degrade silently
125
122
  }
126
123
  return set;
127
124
  })();
128
- ownAddressCache.set(account, cached);
125
+ ownAddressCache.set(key, cached);
129
126
  }
130
127
  return cached;
131
128
  }
132
129
  /** Fetch the reply source once and derive threading headers plus (auto-fill)
133
130
  * to/cc/subject. On any fetch failure, degrade to the API id for threading and
134
- * report sourceFound:false so the caller can decide (per A8 error dispositions). */
131
+ * report sourceFound:false so the caller can decide. */
135
132
  export async function resolveReply(gmail, account, primaryEmail, replyToMessageId, replyAll) {
136
133
  let headers;
137
134
  try {
@@ -144,7 +141,7 @@ export async function resolveReply(gmail, account, primaryEmail, replyToMessageI
144
141
  headers = meta.data.payload?.headers;
145
142
  }
146
143
  catch {
147
- // Degrade to the API id (pre-lookup behavior) rather than blocking the send.
144
+ // Degrade to the API id rather than blocking the send.
148
145
  return { inReplyTo: replyToMessageId, references: replyToMessageId, sourceFound: false };
149
146
  }
150
147
  const threading = buildReplyHeaders(replyToMessageId, getHeader(headers, 'Message-ID'), getHeader(headers, 'References'));
@@ -180,15 +177,11 @@ export async function resolveReply(gmail, account, primaryEmail, replyToMessageI
180
177
  };
181
178
  }
182
179
  const BODY_CAP_CHARS = 50_000;
183
- // gmail_read_batch aggregate ceiling: a batch of up to 100 messages must never
184
- // blow the context window, so the summed serialized output is bounded here
185
- // (matches the tool's anthropic/maxResultSizeChars hint). Per-message bodies are
186
- // still individually capped at BODY_CAP_CHARS first.
180
+ // gmail_read_batch aggregate ceiling: bounds the summed serialized output so a
181
+ // 100-message batch can't blow the context window (per-message bodies are capped at BODY_CAP_CHARS first).
187
182
  const BATCH_MAX_RESULT_CHARS = 100_000;
188
- // Gmail's messages.send raw/JSON path rejects messages near ~25 MB (the wire
189
- // message is base64-encoded, ~+33% over the raw bytes), so cap the ESTIMATED
190
- // ENCODED total there rather than the spec's nominal 35 MB raw (spike/live
191
- // correction — a 35 MB raw payload is ~47 MB on the wire and Gmail 400s it).
183
+ // messages.send rejects near ~25MB on the wire (base64 adds ~33%), so cap the
184
+ // estimated ENCODED total here rather than the nominal 35MB raw limit, which 400s at ~47MB wire.
192
185
  const GMAIL_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
193
186
  const estimateEncoded = (rawBytes) => Math.ceil((rawBytes * 4) / 3);
194
187
  const attachmentSchema = z
@@ -202,10 +195,8 @@ const attachmentSchema = z
202
195
  /** Reads attachment files into buffers (THIS server reads them, never
203
196
  * MailComposer), enforces absolute-path + total-size guards, and derives the
204
197
  * MIME filename via basename so a caller name can't inject path separators. */
205
- // Deny reading anything inside the server's own secret dirs — mailing out
206
- // master.key / <alias>.enc / mcp-jwt.key would be a full-account-takeover
207
- // exfil channel (this same server also reads untrusted mail/drive content, so
208
- // a prompt-injected attach path is a real confused-deputy vector).
198
+ // Deny reading inside the server's own secret dirs; mailing out master.key /
199
+ // <alias>.enc / mcp-jwt.key would be account takeover (also guards against a prompt-injected attach path).
209
200
  function isInside(dir, target) {
210
201
  let d = path.resolve(dir);
211
202
  let t = path.resolve(target);
@@ -279,9 +270,8 @@ export async function readAttachments(raw, bodyBytes) {
279
270
  }
280
271
  return out;
281
272
  }
282
- /** Tagged compose-time failure; the handlers map it to an isError envelope.
283
- * `category` is the taxonomy error field (default validation_error); reply
284
- * auto-fill raises it as not_found when a source message can't be fetched. */
273
+ /** Tagged compose-time failure mapped to an isError envelope; `category`
274
+ * defaults to validation_error (reply auto-fill raises not_found instead). */
285
275
  class GmailComposeError extends Error {
286
276
  slug;
287
277
  category;
@@ -335,7 +325,7 @@ export function parseMessage(msg, bodyCap, opts) {
335
325
  };
336
326
  }
337
327
  /**
338
- * A9 gmail_read_batch core: fetch N ids with bounded parallelism, parse each,
328
+ * gmail_read_batch core: fetch N ids with bounded parallelism, parse each,
339
329
  * and bound the aggregate output. Order is preserved by input index. A per-id
340
330
  * failure becomes a `{ id, error }` entry; an account-wide auth/scope failure
341
331
  * is rethrown so the caller maps it to a whole-call isError. Returns the
@@ -382,9 +372,8 @@ export async function readBatch(gmail, account, ids, full, rawHtml) {
382
372
  }
383
373
  }
384
374
  }
385
- // Aggregate guard: walk in order, accumulating serialized size. Once the
386
- // running total would exceed the maxResultSizeChars budget, cap this and every
387
- // later success body so a big batch never blows the context window.
375
+ // Aggregate guard: walk in order accumulating serialized size; once the running
376
+ // total would exceed the budget, cap this and every later success body so the batch never blows the context window.
388
377
  let running = 0;
389
378
  let truncatedAny = false;
390
379
  let exhausted = false;
@@ -436,13 +425,19 @@ export function compactMessageRow(m) {
436
425
  snippet: snippet.length > 120 ? `${snippet.slice(0, 119)}…` : snippet,
437
426
  };
438
427
  }
439
- export function registerGmailTools(server) {
428
+ export function registerGmailTools(server, deps = {}) {
429
+ // Per-registry, live account enum + injectable client: the schema follows
430
+ // the registry's account view at parse time, not a process-global default.
431
+ const accountEnum = accountArgLive(() => server.accountAliases()).optional();
432
+ const getClientFn = deps.getClientFn ?? getClient;
433
+ const localFiles = deps.localFiles ?? true;
434
+ const registerHostFileTool = localFiles ? server.registerTool : (() => undefined);
440
435
  server.registerTool('gmail_search', {
441
436
  description: 'Search messages in a Gmail account. Returns compact rows (id, from, subject, date, snippet); ' +
442
437
  'pass full=true for threadId, to, labelIds and the untruncated snippet.',
443
438
  inputSchema: {
444
439
  account: accountEnum.describe('Google account alias'),
445
- query: z.string().describe('Gmail search syntax, e.g. "from:monaam is:unread"'),
440
+ query: z.string().describe('Gmail search syntax, e.g. "from:alice is:unread"'),
446
441
  maxResults: coerceNumber(z.number().min(1).max(100)).optional()
447
442
  .describe('Max results to return (default: 20, max: 100)'),
448
443
  full: coerceBoolean.optional().describe('Return the full row shape instead of the compact default'),
@@ -454,7 +449,7 @@ export function registerGmailTools(server) {
454
449
  if (query.trim() === '') {
455
450
  return invalidParams(account, '`query` is empty, and Gmail reads an empty query as "match everything".', 'Pass a real Gmail search term such as "is:unread" or "from:someone@example.com". Returning the whole mailbox for a blank query would look like a successful search.');
456
451
  }
457
- const auth = await getClient(account);
452
+ const auth = await getClientFn(account);
458
453
  const gmail = gmailClient({ version: 'v1', auth });
459
454
  const listRes = await gmail.users.messages.list({
460
455
  userId: 'me',
@@ -486,9 +481,8 @@ export function registerGmailTools(server) {
486
481
  });
487
482
  }
488
483
  }
489
- // Search is a pick-one-message step ~always followed by gmail_read;
490
- // the compact row is the selection signal, the rest was measured burn
491
- // (p90 14.4k chars per call). full=true restores the pre-6.0 shape.
484
+ // Search is a pick-one-message step almost always followed by gmail_read;
485
+ // the compact row is the selection signal (full=true returns the previous, heavier row).
492
486
  const rows = full === true ? results : results.map(compactMessageRow);
493
487
  return {
494
488
  content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
@@ -511,7 +505,7 @@ export function registerGmailTools(server) {
511
505
  },
512
506
  }, async ({ account, messageId, full, rawHtml }) => {
513
507
  try {
514
- const auth = await getClient(account);
508
+ const auth = await getClientFn(account);
515
509
  const gmail = gmailClient({ version: 'v1', auth });
516
510
  const res = await gmail.users.messages.get({
517
511
  userId: 'me',
@@ -542,7 +536,7 @@ export function registerGmailTools(server) {
542
536
  },
543
537
  }, async ({ account, threadId, full, rawHtml, mode }) => {
544
538
  try {
545
- const auth = await getClient(account);
539
+ const auth = await getClientFn(account);
546
540
  const gmail = gmailClient({ version: 'v1', auth });
547
541
  if (mode === 'summary') {
548
542
  const res = await gmail.users.threads.get({
@@ -592,7 +586,7 @@ export function registerGmailTools(server) {
592
586
  },
593
587
  }, async ({ account, ids, full, rawHtml }) => {
594
588
  try {
595
- const auth = await getClient(account);
589
+ const auth = await getClientFn(account);
596
590
  const gmail = gmailClient({ version: 'v1', auth });
597
591
  const result = await readBatch(gmail, account, ids ?? [], full === true, rawHtml === true);
598
592
  return {
@@ -622,13 +616,15 @@ export function registerGmailTools(server) {
622
616
  .describe('With replyToMessageId: include the source To+Cc (minus your own addresses) in cc. Default false (reply to sender only).'),
623
617
  replyToThreadId: z.string().optional()
624
618
  .describe('Thread ID to send the message in'),
625
- attachments: coerceJson(attachmentSchema),
619
+ ...(localFiles ? { attachments: coerceJson(attachmentSchema) } : {}),
626
620
  },
627
621
  }, async ({ account, to, subject, body, htmlBody, allowRawHtml, cc, replyToMessageId, replyAll, replyToThreadId, attachments }) => {
622
+ if (!localFiles && attachments?.length)
623
+ return hostFilesRefused(account, 'attachments');
628
624
  try {
629
- const auth = await getClient(account);
625
+ const auth = await getClientFn(account);
630
626
  const gmail = gmailClient({ version: 'v1', auth });
631
- const config = (await import('../accounts.js')).getAccountSet().configs[account];
627
+ const config = server.accountSet().configs[account];
632
628
  if (htmlBody !== undefined) {
633
629
  throw new GmailComposeError('E_HTMLBODY_REMOVED', 'htmlBody was removed in v6: author Markdown in `body`; for literal HTML pass `allowRawHtml: true`.');
634
630
  }
@@ -686,7 +682,7 @@ export function registerGmailTools(server) {
686
682
  return mapped ?? handleGmailError(error, account);
687
683
  }
688
684
  });
689
- server.registerTool('gmail_download_attachment', {
685
+ registerHostFileTool('gmail_download_attachment', {
690
686
  description: 'Download an email attachment to local disk. Use gmail_read first to get the attachmentId.',
691
687
  inputSchema: {
692
688
  account: accountEnum.describe('Google account alias'),
@@ -697,7 +693,7 @@ export function registerGmailTools(server) {
697
693
  },
698
694
  }, async ({ account, messageId, attachmentId, filename, savePath }) => {
699
695
  try {
700
- const auth = await getClient(account);
696
+ const auth = await getClientFn(account);
701
697
  const gmail = gmailClient({ version: 'v1', auth });
702
698
  const res = await gmail.users.messages.attachments.get({
703
699
  userId: 'me',
@@ -737,13 +733,15 @@ export function registerGmailTools(server) {
737
733
  .describe('With replyToMessageId: include the source To+Cc (minus your own addresses) in cc. Default false (reply to sender only).'),
738
734
  replyToThreadId: z.string().optional()
739
735
  .describe('Thread ID to associate the draft with'),
740
- attachments: coerceJson(attachmentSchema),
736
+ ...(localFiles ? { attachments: coerceJson(attachmentSchema) } : {}),
741
737
  },
742
738
  }, async ({ account, to, subject, body, htmlBody, allowRawHtml, cc, replyToMessageId, replyAll, replyToThreadId, attachments }) => {
739
+ if (!localFiles && attachments?.length)
740
+ return hostFilesRefused(account, 'attachments');
743
741
  try {
744
- const auth = await getClient(account);
742
+ const auth = await getClientFn(account);
745
743
  const gmail = gmailClient({ version: 'v1', auth });
746
- const config = (await import('../accounts.js')).getAccountSet().configs[account];
744
+ const config = server.accountSet().configs[account];
747
745
  if (htmlBody !== undefined) {
748
746
  throw new GmailComposeError('E_HTMLBODY_REMOVED', 'htmlBody was removed in v6: author Markdown in `body`; for literal HTML pass `allowRawHtml: true`.');
749
747
  }
@@ -811,7 +809,7 @@ export function registerGmailTools(server) {
811
809
  },
812
810
  }, async ({ account, messageId, addLabelIds, removeLabelIds }) => {
813
811
  try {
814
- const auth = await getClient(account);
812
+ const auth = await getClientFn(account);
815
813
  const gmail = gmailClient({ version: 'v1', auth });
816
814
  const res = await gmail.users.messages.modify({
817
815
  userId: 'me',
@@ -838,7 +836,7 @@ export function registerGmailTools(server) {
838
836
  },
839
837
  }, async ({ account, messageId }) => {
840
838
  try {
841
- const auth = await getClient(account);
839
+ const auth = await getClientFn(account);
842
840
  const gmail = gmailClient({ version: 'v1', auth });
843
841
  const res = await gmail.users.messages.trash({ userId: 'me', id: messageId });
844
842
  return {
@@ -858,7 +856,7 @@ export function registerGmailTools(server) {
858
856
  },
859
857
  }, async ({ account, messageId }) => {
860
858
  try {
861
- const auth = await getClient(account);
859
+ const auth = await getClientFn(account);
862
860
  const gmail = gmailClient({ version: 'v1', auth });
863
861
  await gmail.users.messages.delete({ userId: 'me', id: messageId });
864
862
  return {
@@ -880,7 +878,7 @@ export function registerGmailTools(server) {
880
878
  },
881
879
  }, async ({ account, messageIds, addLabelIds, removeLabelIds }) => {
882
880
  try {
883
- const auth = await getClient(account);
881
+ const auth = await getClientFn(account);
884
882
  const gmail = gmailClient({ version: 'v1', auth });
885
883
  await gmail.users.messages.batchModify({
886
884
  userId: 'me',
@@ -907,7 +905,7 @@ export function registerGmailTools(server) {
907
905
  },
908
906
  }, async ({ account, messageIds }) => {
909
907
  try {
910
- const auth = await getClient(account);
908
+ const auth = await getClientFn(account);
911
909
  const gmail = gmailClient({ version: 'v1', auth });
912
910
  await gmail.users.messages.batchDelete({
913
911
  userId: 'me',
@@ -932,7 +930,7 @@ export function registerGmailTools(server) {
932
930
  },
933
931
  }, async ({ account, maxResults, query }) => {
934
932
  try {
935
- const auth = await getClient(account);
933
+ const auth = await getClientFn(account);
936
934
  const gmail = gmailClient({ version: 'v1', auth });
937
935
  const res = await gmail.users.drafts.list({
938
936
  userId: 'me',
@@ -956,7 +954,7 @@ export function registerGmailTools(server) {
956
954
  },
957
955
  }, async ({ account, draftId }) => {
958
956
  try {
959
- const auth = await getClient(account);
957
+ const auth = await getClientFn(account);
960
958
  const gmail = gmailClient({ version: 'v1', auth });
961
959
  const res = await gmail.users.drafts.get({
962
960
  userId: 'me',
@@ -980,7 +978,7 @@ export function registerGmailTools(server) {
980
978
  },
981
979
  }, async ({ account, draftId }) => {
982
980
  try {
983
- const auth = await getClient(account);
981
+ const auth = await getClientFn(account);
984
982
  const gmail = gmailClient({ version: 'v1', auth });
985
983
  const res = await gmail.users.drafts.send({
986
984
  userId: 'me',
@@ -1002,7 +1000,7 @@ export function registerGmailTools(server) {
1002
1000
  },
1003
1001
  }, async ({ account }) => {
1004
1002
  try {
1005
- const auth = await getClient(account);
1003
+ const auth = await getClientFn(account);
1006
1004
  const gmail = gmailClient({ version: 'v1', auth });
1007
1005
  const res = await gmail.users.labels.list({ userId: 'me' });
1008
1006
  return {
@@ -1026,7 +1024,7 @@ export function registerGmailTools(server) {
1026
1024
  },
1027
1025
  }, async ({ account, name, messageListVisibility, labelListVisibility }) => {
1028
1026
  try {
1029
- const auth = await getClient(account);
1027
+ const auth = await getClientFn(account);
1030
1028
  const gmail = gmailClient({ version: 'v1', auth });
1031
1029
  const res = await gmail.users.labels.create({
1032
1030
  userId: 'me',
@@ -1053,7 +1051,7 @@ export function registerGmailTools(server) {
1053
1051
  },
1054
1052
  }, async ({ account, labelId }) => {
1055
1053
  try {
1056
- const auth = await getClient(account);
1054
+ const auth = await getClientFn(account);
1057
1055
  const gmail = gmailClient({ version: 'v1', auth });
1058
1056
  await gmail.users.labels.delete({ userId: 'me', id: labelId });
1059
1057
  return {
@@ -1072,7 +1070,7 @@ export function registerGmailTools(server) {
1072
1070
  },
1073
1071
  }, async ({ account }) => {
1074
1072
  try {
1075
- const auth = await getClient(account);
1073
+ const auth = await getClientFn(account);
1076
1074
  const gmail = gmailClient({ version: 'v1', auth });
1077
1075
  const res = await gmail.users.getProfile({ userId: 'me' });
1078
1076
  return {
@@ -1096,7 +1094,7 @@ export function registerGmailTools(server) {
1096
1094
  },
1097
1095
  }, async ({ account, startHistoryId, maxResults, historyTypes }) => {
1098
1096
  try {
1099
- const auth = await getClient(account);
1097
+ const auth = await getClientFn(account);
1100
1098
  const gmail = gmailClient({ version: 'v1', auth });
1101
1099
  const res = await gmail.users.history.list({
1102
1100
  userId: 'me',
@@ -1120,7 +1118,7 @@ export function registerGmailTools(server) {
1120
1118
  },
1121
1119
  }, async ({ account }) => {
1122
1120
  try {
1123
- const auth = await getClient(account);
1121
+ const auth = await getClientFn(account);
1124
1122
  const gmail = gmailClient({ version: 'v1', auth });
1125
1123
  const res = await gmail.users.settings.getVacation({ userId: 'me' });
1126
1124
  return {
@@ -1146,7 +1144,7 @@ export function registerGmailTools(server) {
1146
1144
  },
1147
1145
  }, async ({ account, enableAutoReply, responseSubject, responseBodyPlainText, startTime, endTime, restrictToContacts, restrictToDomain }) => {
1148
1146
  try {
1149
- const auth = await getClient(account);
1147
+ const auth = await getClientFn(account);
1150
1148
  const gmail = gmailClient({ version: 'v1', auth });
1151
1149
  const res = await gmail.users.settings.updateVacation({
1152
1150
  userId: 'me',
@@ -1,10 +1,9 @@
1
1
  import type { ToolRegistry } from '../registry.js';
2
2
  import { type Policy } from '../write-control.js';
3
- import { getClient } from '../client.js';
4
3
  import { type Toolsets } from '../toolsets.js';
4
+ import { type ExecuteDeps } from '../executor.js';
5
5
  import { type DiscoveryDeps, type DiscoveryMethod } from '../discovery-client.js';
6
- export interface EscapeDeps extends DiscoveryDeps {
7
- getClientFn?: typeof getClient;
6
+ export interface EscapeDeps extends DiscoveryDeps, ExecuteDeps {
8
7
  toolsets?: Toolsets;
9
8
  }
10
9
  /** Up to three closest known ids for the unknown_method did-you-mean hint;
@@ -1,13 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import { isAllowed, writeDisabledResult } from '../write-control.js';
3
- import { accountAliasSchema } from '../accounts.js';
3
+ import { accountArgLive } from '../accounts.js';
4
4
  import { getClient } from '../client.js';
5
5
  import { coerceJson } from './_coerce.js';
6
6
  import { getToolsets, toolsetEnabled } from '../toolsets.js';
7
- import { editDistance } from '../scope-catalog.js';
7
+ import { editDistance, SUGGEST_MAX_INPUT } from '../scope-catalog.js';
8
8
  import { executeApiMethod, jsonResult } from '../executor.js';
9
- import { SUPPORTED_APIS, resolveApiAliases, cudFromMethod, loadMethodIndex, searchMethods, } from '../discovery-client.js';
10
- const accountEnum = accountAliasSchema.optional();
9
+ import { SUPPORTED_APIS, resolveApiAliases, cudFromMethod, loadMethodIndex, searchMethods, MAX_SEARCH_QUERY, } from '../discovery-client.js';
10
+ import { hints } from '../hint-provider.js';
11
11
  // Policy/toolset namespace for each API alias must match the NAMED tools' service
12
12
  // names, or user deny globs and GOOGLE_TOOLSETS silently miss escape-hatch calls.
13
13
  const SERVICE_FOR_ALIAS = {
@@ -32,12 +32,21 @@ const SERVICE_FOR_ALIAS = {
32
32
  analyticsadmin: 'analytics',
33
33
  analyticsdata: 'analytics',
34
34
  };
35
+ /** Discovery ids are short dotted identifiers; the bound keeps a caller from
36
+ * sending megabytes through lookup, suggestion and the echoed message. */
37
+ const MAX_METHOD_ID = 256;
38
+ const MAX_API_ALIAS = 64;
35
39
  /** Up to three closest known ids for the unknown_method did-you-mean hint;
36
40
  * bounded distance so unrelated ids never masquerade as suggestions. */
37
41
  export function nearestMethodIds(methodId, index) {
42
+ if (methodId.length > SUGGEST_MAX_INPUT)
43
+ return [];
38
44
  const maxDist = Math.max(3, Math.floor(methodId.length / 3));
45
+ const wanted = methodId.toLowerCase();
39
46
  return index
40
- .map((m) => ({ id: m.id, d: editDistance(methodId.toLowerCase(), m.id.toLowerCase()) }))
47
+ // the distance is at least the length gap, so most ids skip the DP
48
+ .filter((m) => Math.abs(m.id.length - wanted.length) <= maxDist)
49
+ .map((m) => ({ id: m.id, d: editDistance(wanted, m.id.toLowerCase()) }))
41
50
  .filter((x) => x.d <= maxDist)
42
51
  .sort((a, b) => a.d - b.d)
43
52
  .slice(0, 3)
@@ -55,6 +64,8 @@ function describeMethod(m) {
55
64
  };
56
65
  }
57
66
  export function registerEscapeTools(registry, policy, deps = {}) {
67
+ // Live per-registry account validation, same as the curated files.
68
+ const accountEnum = accountArgLive(() => registry.accountAliases()).optional();
58
69
  const getClientFn = deps.getClientFn ?? getClient;
59
70
  const toolsets = deps.toolsets ?? getToolsets();
60
71
  const serviceForAlias = (alias) => SERVICE_FOR_ALIAS[alias] ?? alias;
@@ -63,19 +74,22 @@ export function registerEscapeTools(registry, policy, deps = {}) {
63
74
  };
64
75
  const enabledApis = Object.keys(SUPPORTED_APIS).filter(apiEnabled);
65
76
  const apiList = enabledApis.join(', ');
66
- const toolsetDisabled = (alias) => jsonResult({
67
- error: 'toolset_disabled',
68
- message: `API "${alias}" maps to service "${serviceForAlias(alias)}", which is excluded by GOOGLE_TOOLSETS.`,
69
- hint: 'Add the service to GOOGLE_TOOLSETS (or unset it) to use this API.',
70
- retriable: false,
71
- }, true);
77
+ const toolsetDisabled = (alias) => {
78
+ const rendered = hints()?.render({ kind: 'toolset_disabled', api: alias, service: serviceForAlias(alias) });
79
+ return jsonResult({
80
+ error: 'toolset_disabled',
81
+ message: rendered?.message ?? `API "${alias}" maps to service "${serviceForAlias(alias)}", which is excluded by GOOGLE_TOOLSETS.`,
82
+ hint: rendered?.hint ?? 'Add the service to GOOGLE_TOOLSETS (or unset it) to use this API.',
83
+ retriable: false,
84
+ }, true);
85
+ };
72
86
  registry.registerMeta('google_api_search', {
73
87
  description: 'Search the Google API Discovery index for any Workspace REST method, including ones with no ' +
74
88
  'dedicated tool here. Returns method ids + parameters to invoke via google_api_call. ' +
75
89
  `APIs: ${apiList}.`,
76
90
  inputSchema: {
77
- query: z.string().describe('Keywords, e.g. "slides create presentation" or "drive revisions"'),
78
- api: z.string().optional().describe('Restrict the search to one API alias'),
91
+ query: z.string().max(MAX_SEARCH_QUERY).describe('Keywords, e.g. "slides create presentation" or "drive revisions"'),
92
+ api: z.string().max(MAX_API_ALIAS).optional().describe('Restrict the search to one API alias'),
79
93
  maxResults: z.number().min(1).max(25).default(10).optional(),
80
94
  },
81
95
  annotations: { openWorldHint: true },
@@ -125,8 +139,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
125
139
  'drive.files.export) use drive_download / drive_export instead.',
126
140
  inputSchema: {
127
141
  account: accountEnum.describe('Google account alias (omit for the default account)'),
128
- api: z.string().describe(`API alias: ${apiList}`),
129
- methodId: z.string().min(1).describe('Discovery method id, e.g. "drive.revisions.list"'),
142
+ api: z.string().max(MAX_API_ALIAS).describe(`API alias: ${apiList}`),
143
+ methodId: z.string().min(1).max(MAX_METHOD_ID).describe('Discovery method id, e.g. "drive.revisions.list"'),
130
144
  pathParams: coerceJson(z.record(z.string(), z.union([z.string(), z.number()])).optional())
131
145
  .describe('Values for {placeholders} in the method path'),
132
146
  queryParams: coerceJson(z
@@ -164,9 +178,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
164
178
  }
165
179
  let method = index.find((m) => m.id === methodId);
166
180
  if (!method) {
167
- // Some discovery docs keep a legacy id prefix (the searchconsole doc's
168
- // methods are webmasters.*): when the caller prefixed with our api
169
- // alias, retry under the doc's own prefix before failing.
181
+ // Some discovery docs keep a legacy id prefix (searchconsole's methods are
182
+ // webmasters.*): retry under the doc's own prefix if the caller used our api alias.
170
183
  const docPrefix = index[0]?.id.split('.')[0];
171
184
  const [head, ...rest] = String(methodId).split('.');
172
185
  if (docPrefix && (head === apiKey || head === api) && head !== docPrefix && rest.length > 0) {
@@ -191,9 +204,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
191
204
  const cud = cudFromMethod(method);
192
205
  const policyService = serviceForAlias(apiKey);
193
206
  const lastSegment = method.id.split('.').pop() ?? method.id;
194
- // The escape hatch self-gates, so it needs the same scope signal the
195
- // registry wrapper gets; without it a privileged method would slip past
196
- // safe-writes here while its generated twin is refused.
207
+ // The escape hatch self-gates, so it needs the same scope signal as the registry
208
+ // wrapper, or a privileged method would slip past safe-writes here while its generated twin is refused.
197
209
  const toolRef = { name: `${policyService}_${lastSegment}`, service: policyService, cud, scopes: method.scopes };
198
210
  if (cud !== 'read' && !isAllowed(toolRef, policy)) {
199
211
  return writeDisabledResult(toolRef, policy, account);
@@ -203,6 +215,6 @@ export function registerEscapeTools(registry, policy, deps = {}) {
203
215
  pathParams: pathParams,
204
216
  queryParams: queryParams,
205
217
  body,
206
- }, { getClientFn });
218
+ }, { getClientFn, scopeDeps: deps.scopeDeps });
207
219
  });
208
220
  }
@@ -1,2 +1,11 @@
1
1
  import type { ToolRegistry } from '../registry.js';
2
- export declare function registerMeetTools(server: ToolRegistry): void;
2
+ import type { Account } from '../accounts.js';
3
+ import { type CuratedToolDeps } from '../client.js';
4
+ export declare function registerMeetTools(server: ToolRegistry, deps?: CuratedToolDeps): void;
5
+ export declare function handleMeetError(error: any, account: Account): {
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ isError: true;
11
+ };
@@ -1,10 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import { meet as meetClient } from '@googleapis/meet';
3
- import { accountAliasSchema } from '../accounts.js';
3
+ import { accountArgLive } from '../accounts.js';
4
4
  import { getClient } from '../client.js';
5
5
  import { handleGoogleApiError } from './_errors.js';
6
- const accountEnum = accountAliasSchema.optional();
7
- export function registerMeetTools(server) {
6
+ export function registerMeetTools(server, deps = {}) {
7
+ // Per-registry, live account enum + injectable client: the schema follows
8
+ // the registry's account view at parse time, not a process-global default.
9
+ const accountEnum = accountArgLive(() => server.accountAliases()).optional();
10
+ const getClientFn = deps.getClientFn ?? getClient;
8
11
  // ─── Conference records (past meetings) ────────────────────────────────
9
12
  server.registerTool('meet_conference_records_list', {
10
13
  description: 'List past conference records (completed Meet sessions) the user has access to',
@@ -16,7 +19,7 @@ export function registerMeetTools(server) {
16
19
  },
17
20
  }, async ({ account, pageSize, pageToken, filter }) => {
18
21
  try {
19
- const auth = await getClient(account);
22
+ const auth = await getClientFn(account);
20
23
  const meet = meetClient({ version: 'v2', auth });
21
24
  const res = await meet.conferenceRecords.list({
22
25
  pageSize: pageSize ?? 20,
@@ -39,7 +42,7 @@ export function registerMeetTools(server) {
39
42
  },
40
43
  }, async ({ account, name }) => {
41
44
  try {
42
- const auth = await getClient(account);
45
+ const auth = await getClientFn(account);
43
46
  const meet = meetClient({ version: 'v2', auth });
44
47
  const res = await meet.conferenceRecords.get({ name });
45
48
  return {
@@ -61,7 +64,7 @@ export function registerMeetTools(server) {
61
64
  },
62
65
  }, async ({ account, parent, pageSize, pageToken }) => {
63
66
  try {
64
- const auth = await getClient(account);
67
+ const auth = await getClientFn(account);
65
68
  const meet = meetClient({ version: 'v2', auth });
66
69
  const res = await meet.conferenceRecords.recordings.list({
67
70
  parent,
@@ -87,7 +90,7 @@ export function registerMeetTools(server) {
87
90
  },
88
91
  }, async ({ account, parent, pageSize, pageToken }) => {
89
92
  try {
90
- const auth = await getClient(account);
93
+ const auth = await getClientFn(account);
91
94
  const meet = meetClient({ version: 'v2', auth });
92
95
  const res = await meet.conferenceRecords.transcripts.list({
93
96
  parent,
@@ -112,7 +115,7 @@ export function registerMeetTools(server) {
112
115
  },
113
116
  }, async ({ account, parent, pageSize, pageToken }) => {
114
117
  try {
115
- const auth = await getClient(account);
118
+ const auth = await getClientFn(account);
116
119
  const meet = meetClient({ version: 'v2', auth });
117
120
  const res = await meet.conferenceRecords.transcripts.entries.list({
118
121
  parent,
@@ -128,7 +131,7 @@ export function registerMeetTools(server) {
128
131
  }
129
132
  });
130
133
  }
131
- function handleMeetError(error, account) {
134
+ export function handleMeetError(error, account) {
132
135
  return handleGoogleApiError(error, account, {
133
136
  scope: 'Meet needs the meetings.space.readonly scope and the Google Meet API enabled in Cloud Console. Confirm both for this account.',
134
137
  resource: `Meet denied this conference record to "${account}". Meet exposes records only to the organizer or a participant, so check which account hosted the meeting.`,
@@ -1,2 +1,3 @@
1
1
  import type { ToolRegistry } from '../registry.js';
2
- export declare function registerSearchConsoleTools(server: ToolRegistry): void;
2
+ import { type CuratedToolDeps } from '../client.js';
3
+ export declare function registerSearchConsoleTools(server: ToolRegistry, deps?: CuratedToolDeps): void;