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

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 (165) hide show
  1. package/dist/accounts.d.ts +17 -1
  2. package/dist/accounts.js +52 -15
  3. package/dist/api-probe.d.ts +5 -0
  4. package/dist/api-probe.js +20 -15
  5. package/dist/arg-normalize.js +6 -5
  6. package/dist/arg-strict.d.ts +7 -0
  7. package/dist/arg-strict.js +34 -4
  8. package/dist/auth.d.ts +4 -3
  9. package/dist/auth.js +11 -13
  10. package/dist/boot-gates.d.ts +26 -0
  11. package/dist/boot-gates.js +38 -0
  12. package/dist/cimd-cache.d.ts +38 -0
  13. package/dist/cimd-cache.js +117 -0
  14. package/dist/client.d.ts +32 -2
  15. package/dist/client.js +0 -0
  16. package/dist/compose.d.ts +11 -0
  17. package/dist/compose.js +109 -0
  18. package/dist/config-file.d.ts +13 -0
  19. package/dist/config-file.js +37 -1
  20. package/dist/discover.js +3 -1
  21. package/dist/discovery-client.d.ts +3 -0
  22. package/dist/discovery-client.js +25 -2
  23. package/dist/doctor.d.ts +6 -2
  24. package/dist/doctor.js +119 -23
  25. package/dist/executor.d.ts +3 -0
  26. package/dist/executor.js +4 -2
  27. package/dist/fanout.d.ts +9 -1
  28. package/dist/fanout.js +44 -10
  29. package/dist/fs-atomic.d.ts +11 -0
  30. package/dist/fs-atomic.js +219 -28
  31. package/dist/hint-provider.d.ts +129 -0
  32. package/dist/hint-provider.js +64 -0
  33. package/dist/http-transport.d.ts +62 -4
  34. package/dist/http-transport.js +343 -24
  35. package/dist/identity.d.ts +4 -1
  36. package/dist/identity.js +25 -6
  37. package/dist/index.js +66 -138
  38. package/dist/master-key.js +13 -5
  39. package/dist/mcp-token.d.ts +136 -19
  40. package/dist/mcp-token.js +569 -72
  41. package/dist/migrate-config.js +2 -2
  42. package/dist/oauth-as.d.ts +103 -3
  43. package/dist/oauth-as.js +374 -79
  44. package/dist/oauth-consent.d.ts +10 -0
  45. package/dist/oauth-consent.js +16 -0
  46. package/dist/outbound-allowlist.js +5 -2
  47. package/dist/reauth-hint.d.ts +1 -1
  48. package/dist/reauth-hint.js +12 -8
  49. package/dist/registry-transfer.d.ts +5 -0
  50. package/dist/registry-transfer.js +65 -5
  51. package/dist/registry.d.ts +18 -1
  52. package/dist/registry.js +71 -15
  53. package/dist/scope-catalog.d.ts +11 -0
  54. package/dist/scope-catalog.js +23 -6
  55. package/dist/scope-observability.d.ts +8 -6
  56. package/dist/scope-observability.js +29 -13
  57. package/dist/services.d.ts +15 -10
  58. package/dist/services.js +59 -15
  59. package/dist/ssrf-guard.d.ts +23 -9
  60. package/dist/ssrf-guard.js +101 -62
  61. package/dist/tenant-purge.d.ts +26 -0
  62. package/dist/tenant-purge.js +67 -0
  63. package/dist/token-store.d.ts +20 -2
  64. package/dist/token-store.js +0 -0
  65. package/dist/tools/_errors.d.ts +3 -0
  66. package/dist/tools/_errors.js +53 -24
  67. package/dist/tools/_local-files.d.ts +9 -0
  68. package/dist/tools/_local-files.js +18 -0
  69. package/dist/tools/account-wizard.d.ts +10 -1
  70. package/dist/tools/account-wizard.js +74 -17
  71. package/dist/tools/accounts-tool.d.ts +7 -0
  72. package/dist/tools/accounts-tool.js +34 -18
  73. package/dist/tools/admin.d.ts +10 -1
  74. package/dist/tools/admin.js +15 -11
  75. package/dist/tools/analytics.d.ts +10 -1
  76. package/dist/tools/analytics.js +13 -8
  77. package/dist/tools/calendar.d.ts +2 -1
  78. package/dist/tools/calendar.js +18 -14
  79. package/dist/tools/chat.d.ts +10 -1
  80. package/dist/tools/chat.js +13 -8
  81. package/dist/tools/contacts.d.ts +2 -1
  82. package/dist/tools/contacts.js +17 -13
  83. package/dist/tools/docs.d.ts +2 -1
  84. package/dist/tools/docs.js +34 -30
  85. package/dist/tools/drive.d.ts +2 -1
  86. package/dist/tools/drive.js +65 -50
  87. package/dist/tools/forms.d.ts +10 -1
  88. package/dist/tools/forms.js +19 -12
  89. package/dist/tools/generated/_shared.d.ts +2 -1
  90. package/dist/tools/generated/_shared.js +7 -3
  91. package/dist/tools/generated/admin.d.ts +2 -1
  92. package/dist/tools/generated/admin.js +265 -265
  93. package/dist/tools/generated/analytics.d.ts +2 -1
  94. package/dist/tools/generated/analytics.js +125 -125
  95. package/dist/tools/generated/appsmarket.d.ts +2 -1
  96. package/dist/tools/generated/appsmarket.js +5 -5
  97. package/dist/tools/generated/calendar.d.ts +2 -1
  98. package/dist/tools/generated/calendar.js +55 -55
  99. package/dist/tools/generated/chat.d.ts +2 -1
  100. package/dist/tools/generated/chat.js +95 -95
  101. package/dist/tools/generated/classroom.d.ts +2 -1
  102. package/dist/tools/generated/classroom.js +209 -209
  103. package/dist/tools/generated/cloudidentity.d.ts +2 -1
  104. package/dist/tools/generated/cloudidentity.js +133 -133
  105. package/dist/tools/generated/cloudsearch.d.ts +2 -1
  106. package/dist/tools/generated/cloudsearch.js +99 -99
  107. package/dist/tools/generated/contacts.d.ts +2 -1
  108. package/dist/tools/generated/contacts.js +29 -29
  109. package/dist/tools/generated/drive.d.ts +2 -1
  110. package/dist/tools/generated/drive.js +71 -71
  111. package/dist/tools/generated/driveactivity.d.ts +2 -1
  112. package/dist/tools/generated/driveactivity.js +3 -3
  113. package/dist/tools/generated/drivelabels.d.ts +2 -1
  114. package/dist/tools/generated/drivelabels.js +53 -53
  115. package/dist/tools/generated/forms.d.ts +2 -1
  116. package/dist/tools/generated/forms.js +7 -7
  117. package/dist/tools/generated/gmail.d.ts +2 -1
  118. package/dist/tools/generated/gmail.js +117 -117
  119. package/dist/tools/generated/groupsmigration.d.ts +2 -1
  120. package/dist/tools/generated/groupsmigration.js +3 -3
  121. package/dist/tools/generated/groupssettings.d.ts +2 -1
  122. package/dist/tools/generated/groupssettings.js +7 -7
  123. package/dist/tools/generated/index.d.ts +2 -1
  124. package/dist/tools/generated/keep.d.ts +2 -1
  125. package/dist/tools/generated/keep.js +15 -15
  126. package/dist/tools/generated/licensing.d.ts +2 -1
  127. package/dist/tools/generated/licensing.js +15 -15
  128. package/dist/tools/generated/meet.d.ts +2 -1
  129. package/dist/tools/generated/meet.js +27 -27
  130. package/dist/tools/generated/postmaster.d.ts +2 -1
  131. package/dist/tools/generated/postmaster.js +9 -9
  132. package/dist/tools/generated/reseller.d.ts +2 -1
  133. package/dist/tools/generated/reseller.js +35 -35
  134. package/dist/tools/generated/script.d.ts +2 -1
  135. package/dist/tools/generated/script.js +33 -33
  136. package/dist/tools/generated/searchconsole.d.ts +2 -1
  137. package/dist/tools/generated/searchconsole.js +3 -3
  138. package/dist/tools/generated/sheets.d.ts +2 -1
  139. package/dist/tools/generated/sheets.js +15 -15
  140. package/dist/tools/generated/tasks.d.ts +2 -1
  141. package/dist/tools/generated/tasks.js +5 -5
  142. package/dist/tools/generated/vault.d.ts +2 -1
  143. package/dist/tools/generated/vault.js +67 -67
  144. package/dist/tools/generated/workspaceevents.d.ts +2 -1
  145. package/dist/tools/generated/workspaceevents.js +31 -31
  146. package/dist/tools/gmail.d.ts +2 -1
  147. package/dist/tools/gmail.js +45 -33
  148. package/dist/tools/google-api.d.ts +2 -3
  149. package/dist/tools/google-api.js +30 -16
  150. package/dist/tools/meet.d.ts +10 -1
  151. package/dist/tools/meet.js +13 -9
  152. package/dist/tools/searchconsole.d.ts +2 -1
  153. package/dist/tools/searchconsole.js +17 -13
  154. package/dist/tools/sheets.d.ts +2 -1
  155. package/dist/tools/sheets.js +36 -32
  156. package/dist/tools/slides.d.ts +10 -1
  157. package/dist/tools/slides.js +17 -10
  158. package/dist/tools/tasks.d.ts +2 -1
  159. package/dist/tools/tasks.js +19 -15
  160. package/dist/trim.d.ts +5 -0
  161. package/dist/trim.js +20 -0
  162. package/dist/usage-metrics.d.ts +23 -0
  163. package/dist/usage-metrics.js +65 -47
  164. package/dist/write-control.js +10 -2
  165. package/package.json +27 -1
package/dist/doctor.js CHANGED
@@ -2,16 +2,19 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as readline from 'node:readline';
4
4
  import { getAccountSet } from './accounts.js';
5
- import { deriveAccountHealth } from './tools/accounts-tool.js';
5
+ import { accountHealthDepsFor, deriveAccountHealth } from './tools/accounts-tool.js';
6
+ import { isOwnerContext } from './identity.js';
6
7
  import { peekMasterKeyProvenance, deleteMasterKeyMaterial } from './master-key.js';
7
8
  import { hasToken } from './token-store.js';
8
9
  import { configDir, loadConfigFile } from './config-file.js';
9
10
  import { envValueSource } from './env-load.js';
11
+ import { reauthHint } from './reauth-hint.js';
10
12
  import { describeMetricsDir, resolveUsageMetrics, sourceLabel } from './usage-metrics.js';
11
- import { probeApiEnablement } from './api-probe.js';
13
+ import { apiProbeDepsFor, probeApiEnablement } from './api-probe.js';
12
14
  import { safeMessage, stringifyEnvelope } from './tools/_errors.js';
13
15
  import { resolveHttpConfig, HttpConfigError } from './http-config.js';
14
16
  import { parseOwnerEmails } from './http-transport.js';
17
+ import { hints } from './hint-provider.js';
15
18
  const MIN_NODE_MAJOR = 22;
16
19
  const DEFAULT_DEPS = {
17
20
  nodeVersion: process.versions.node,
@@ -68,9 +71,22 @@ export function apiEnableLink(api) {
68
71
  return `https://console.cloud.google.com/apis/library/${api}.googleapis.com`;
69
72
  }
70
73
  const LEGACY_ENV_KEYS = ['GOOGLE_ACCOUNTS', 'GOOGLE_OPTIONAL_SCOPES', 'GOOGLE_ADMIN_ACCOUNTS'];
71
- function sectionRuntime(deps) {
74
+ function sectionRuntime(deps, tenantScope = false) {
72
75
  const major = Number.parseInt(deps.nodeVersion.split('.')[0] ?? '0', 10);
73
76
  if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
77
+ if (tenantScope) {
78
+ // The host version is operator infrastructure, not the tenant's (E7
79
+ // residue): no version string, and the fix is the operator's to apply.
80
+ const rendered = hints()?.render({ kind: 'operator_only', slug: 'E_NODE_TOO_OLD' });
81
+ return {
82
+ id: 1,
83
+ title: 'Runtime',
84
+ verdict: 'fail',
85
+ slug: 'E_NODE_TOO_OLD',
86
+ lines: ['Runtime below the supported version'],
87
+ hint: rendered?.hint ?? 'Tell the server operator the code E_NODE_TOO_OLD.',
88
+ };
89
+ }
74
90
  return {
75
91
  id: 1,
76
92
  title: 'Runtime',
@@ -80,23 +96,36 @@ function sectionRuntime(deps) {
80
96
  hint: `Upgrade to Node ${MIN_NODE_MAJOR} LTS or newer.`,
81
97
  };
82
98
  }
83
- return { id: 1, title: 'Runtime', verdict: 'ok', lines: [`Node.js ${deps.nodeVersion} (>= ${MIN_NODE_MAJOR})`] };
99
+ return {
100
+ id: 1,
101
+ title: 'Runtime',
102
+ verdict: 'ok',
103
+ lines: [tenantScope ? 'Runtime ok' : `Node.js ${deps.nodeVersion} (>= ${MIN_NODE_MAJOR})`],
104
+ };
84
105
  }
85
- function sectionConfig(deps, set) {
106
+ function sectionConfig(deps, set, tenantScope = false) {
86
107
  const lines = [];
87
108
  let verdict = 'ok';
88
109
  let hint;
89
110
  let slug;
90
111
  if (!set || set.aliases.length === 0) {
112
+ const tenantHint = tenantScope ? (hints()?.render({ kind: 'no_accounts' })?.hint ?? '') : undefined;
91
113
  return {
92
114
  id: 2,
93
115
  title: 'Config',
94
116
  verdict: 'fail',
95
117
  slug: 'E_NO_ACCOUNTS_CONFIGURED',
96
118
  lines: ['No accounts configured.'],
97
- hint: 'Add an account: run `npx mcp-google-multi migrate-config` or set GOOGLE_ACCOUNTS.',
119
+ ...(tenantScope
120
+ ? (tenantHint ? { hint: tenantHint } : {})
121
+ : { hint: 'Add an account: run `npx mcp-google-multi migrate-config` or set GOOGLE_ACCOUNTS.' }),
98
122
  };
99
123
  }
124
+ // The config file, process env, working directory and metrics store below
125
+ // are the operator's host, not the context's: a tenant sees its own count.
126
+ if (tenantScope) {
127
+ return { id: 2, title: 'Config', verdict: 'ok', lines: [`${set.aliases.length} account(s) configured`] };
128
+ }
100
129
  const cfgPath = path.join(configDir(), 'config.json');
101
130
  lines.push(deps.fileExists(cfgPath) ? `config.json present (${set.aliases.length} account(s))` : `config.json absent — accounts sourced from env (${set.aliases.length})`);
102
131
  const legacyEnv = LEGACY_ENV_KEYS.filter((k) => deps.env[k]);
@@ -181,7 +210,8 @@ function sectionsTokensAndScopes(deps, aliases) {
181
210
  if (scopeVerdict === 'ok')
182
211
  scopeVerdict = 'warn';
183
212
  scopeLines.push(`${alias}: ${r.callable.length} callable, ${r.requestable.length} requested-not-granted`);
184
- scopeHint = `Re-auth to grant missing scopes: \`npx mcp-google-multi auth --account ${alias}\`.`;
213
+ const rendered = hints()?.render({ kind: 'scopes_missing', account: alias });
214
+ scopeHint = rendered?.hint ?? `Re-auth to grant missing scopes. ${reauthHint(alias)}`;
185
215
  }
186
216
  else {
187
217
  scopeLines.push(`${alias}: ${r.callable.length} callable, all profile scopes granted`);
@@ -192,7 +222,7 @@ function sectionsTokensAndScopes(deps, aliases) {
192
222
  { id: 5, title: 'Scopes', verdict: scopeVerdict, lines: scopeLines, ...(scopeHint ? { hint: scopeHint } : {}), ...(scopeVerdict === 'warn' ? { slug: 'E_SCOPE_NOT_GRANTED' } : {}) },
193
223
  ];
194
224
  }
195
- async function sectionApiEnablement(deps, aliases) {
225
+ async function sectionApiEnablement(deps, aliases, tenantScope = false) {
196
226
  if (!deps.probeApi) {
197
227
  return { id: 6, title: 'API enablement', verdict: 'unknown', lines: ['Probe not run (no live account probe configured).'] };
198
228
  }
@@ -209,8 +239,15 @@ async function sectionApiEnablement(deps, aliases) {
209
239
  results = await deps.probeApi(healthy);
210
240
  }
211
241
  catch (e) {
212
- // Network / transient: WARN with the target, never crash the report.
213
- return { id: 6, title: 'API enablement', verdict: 'warn', lines: [`Probe could not complete: ${e?.message ?? e}`] };
242
+ // Network / transient: WARN with the target, never crash the report. The
243
+ // raw text can name an egress or proxy host (E7 residue): tenant scope
244
+ // drops it.
245
+ return {
246
+ id: 6,
247
+ title: 'API enablement',
248
+ verdict: 'warn',
249
+ lines: [tenantScope ? 'Probe could not complete.' : `Probe could not complete: ${e?.message ?? e}`],
250
+ };
214
251
  }
215
252
  if (results.length === 0) {
216
253
  return { id: 6, title: 'API enablement', verdict: 'unknown', lines: [`No probeable service scopes granted on "${healthy}".`] };
@@ -218,13 +255,14 @@ async function sectionApiEnablement(deps, aliases) {
218
255
  const disabled = results.filter((r) => r.notEnabled);
219
256
  const lines = results.map((r) => `${r.service}: ${r.ok ? 'enabled' : r.notEnabled ? 'NOT ENABLED' : `unknown (${r.message ?? 'error'})`}`);
220
257
  if (disabled.length > 0) {
258
+ const rendered = hints()?.render({ kind: 'apis_disabled', services: disabled.map((r) => ({ service: r.service, api: r.api })) });
221
259
  return {
222
260
  id: 6,
223
261
  title: 'API enablement',
224
262
  verdict: 'fail',
225
263
  slug: 'E_API_NOT_ENABLED',
226
264
  lines,
227
- hint: disabled.map((r) => `Enable ${r.service}: ${apiEnableLink(r.api)}`).join('\n'),
265
+ hint: rendered?.hint ?? disabled.map((r) => `Enable ${r.service}: ${apiEnableLink(r.api)}`).join('\n'),
228
266
  };
229
267
  }
230
268
  return { id: 6, title: 'API enablement', verdict: 'ok', lines: lines.length ? lines : ['(probed account, all enabled)'] };
@@ -299,21 +337,29 @@ export function overallVerdict(sections) {
299
337
  }
300
338
  return worst;
301
339
  }
302
- export async function runDiagnostics(deps = DEFAULT_DEPS) {
340
+ export async function runDiagnostics(deps = DEFAULT_DEPS, opts = {}) {
341
+ // Tenant scope omits the OPERATOR sections: §3 (key provenance = box
342
+ // infrastructure) and §7 (its owner-gate lines print MCP_OWNER_EMAILS
343
+ // values — other people's addresses from a tenant's seat), and trims §2 to
344
+ // the context's own account count.
345
+ const tenantScope = opts.scope === 'tenant';
303
346
  const sections = [];
304
- sections.push(sectionRuntime(deps));
347
+ sections.push(sectionRuntime(deps, tenantScope));
305
348
  const set = deps.accountSet();
306
- sections.push(sectionConfig(deps, set));
349
+ sections.push(sectionConfig(deps, set, tenantScope));
307
350
  const aliases = set?.aliases ?? [];
308
- sections.push(sectionKeys(deps, aliases));
351
+ if (!tenantScope)
352
+ sections.push(sectionKeys(deps, aliases));
309
353
  if (aliases.length > 0) {
310
354
  const [tokens, scopes] = sectionsTokensAndScopes(deps, aliases);
311
355
  sections.push(tokens, scopes);
312
- sections.push(await sectionApiEnablement(deps, aliases));
356
+ sections.push(await sectionApiEnablement(deps, aliases, tenantScope));
357
+ }
358
+ if (!tenantScope) {
359
+ const http = await sectionHttp(deps, aliases);
360
+ if (http)
361
+ sections.push(http);
313
362
  }
314
- const http = await sectionHttp(deps, aliases);
315
- if (http)
316
- sections.push(http);
317
363
  return { verdict: overallVerdict(sections), sections };
318
364
  }
319
365
  // --- skins -----------------------------------------------------------------
@@ -333,9 +379,35 @@ export function renderDoctorText(report) {
333
379
  out.push(`Overall: ${report.verdict.toUpperCase()}`);
334
380
  return out.join('\n');
335
381
  }
336
- /** Mask the local-part of an email so a report never carries a full address (BR8). */
382
+ const EMAIL_LOCAL_CHAR = /[A-Za-z0-9._%+-]/;
383
+ const EMAIL_DOMAIN_CHAR = /[A-Za-z0-9.-]/;
384
+ /** Mask the local-part of an email so a report never carries a full address
385
+ * (BR8). A linear scan, not a regex: the `local* @domain` pattern backtracks
386
+ * polynomially on long local-charset runs without an @ (CodeQL
387
+ * js/polynomial-redos), and report text can embed arbitrary error strings. */
337
388
  function maskEmails(text) {
338
- return text.replace(/([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*(@[A-Za-z0-9.-]+)/g, '$1***$2');
389
+ let out = '';
390
+ let i = 0;
391
+ while (i < text.length) {
392
+ if (text[i] === '@') {
393
+ let j = i + 1;
394
+ while (j < text.length && EMAIL_DOMAIN_CHAR.test(text[j]))
395
+ j += 1;
396
+ let k = out.length;
397
+ while (k > 0 && EMAIL_LOCAL_CHAR.test(out[k - 1]))
398
+ k -= 1;
399
+ if (j > i + 1 && k < out.length) {
400
+ // keep the first local char, mask the rest ('*' is outside the local
401
+ // charset, so a masked run can never be re-masked by a later @)
402
+ out = `${out.slice(0, k + 1)}***${text.slice(i, j)}`;
403
+ i = j;
404
+ continue;
405
+ }
406
+ }
407
+ out += text[i];
408
+ i += 1;
409
+ }
410
+ return out;
339
411
  }
340
412
  /** Redacted, paste-ready bug report (BR8): verdicts + provenance labels + token
341
413
  * statuses + slugs; NO token values, secrets, keys, or full email addresses. */
@@ -356,6 +428,26 @@ export function exitCodeFor(report, strict) {
356
428
  return 1;
357
429
  return 0;
358
430
  }
431
+ /** The engine's account, token and probe reads bound to one context, so a
432
+ * report never shows (or probes with) another context's accounts. */
433
+ function diagnosticsDepsFor(ctx) {
434
+ const health = accountHealthDepsFor(ctx.tokenStore, () => ctx.accounts);
435
+ const probe = apiProbeDepsFor(ctx.tokenStore.readToken, ctx.getClient);
436
+ return {
437
+ ...DEFAULT_DEPS,
438
+ accountSet: () => {
439
+ try {
440
+ return ctx.accounts;
441
+ }
442
+ catch {
443
+ return null;
444
+ }
445
+ },
446
+ accountHealth: (alias) => deriveAccountHealth(alias, health),
447
+ anyTokensExist: (aliases) => aliases.some((a) => ctx.tokenStore.hasToken(a)),
448
+ probeApi: (alias) => probeApiEnablement(alias, probe),
449
+ };
450
+ }
359
451
  /** Agent-callable structured health report (read-only). Mirrors `doctor`'s
360
452
  * engine. Registered as a META tool, like `account_list`: it introspects this
361
453
  * server rather than Google data, and as a normal tool it became a service of
@@ -363,14 +455,18 @@ export function exitCodeFor(report, strict) {
363
455
  * from already-registered services, and this runs after that), so it was
364
456
  * advertised only once something else expanded the surface. The README sends
365
457
  * people here when they are stuck, so it has to be findable. */
366
- export function registerDiagnoseTool(registry) {
458
+ export function registerDiagnoseTool(registry, ctx) {
459
+ // A non-owner context gets the tenant-scoped report; only the owner context
460
+ // (or the context-free CLI surface) keeps the full operator report.
461
+ const scope = ctx && !isOwnerContext(ctx) ? 'tenant' : 'operator';
462
+ const deps = ctx ? diagnosticsDepsFor(ctx) : DEFAULT_DEPS;
367
463
  registry.registerMeta('diagnose', {
368
464
  annotations: { readOnlyHint: true, openWorldHint: true },
369
465
  description: 'Health report for this server: runtime, config, keys, per-account token status, scope grants, and API enablement. Read-only; returns copy-pasteable fixes for anything wrong. Call this to diagnose auth/config failures.',
370
466
  inputSchema: {},
371
467
  }, async () => {
372
468
  try {
373
- const result = await runDiagnostics();
469
+ const result = await runDiagnostics(deps, { scope });
374
470
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
375
471
  }
376
472
  catch (e) {
@@ -1,4 +1,5 @@
1
1
  import { getClient } from './client.js';
2
+ import { type ScopeSetDeps } from './scope-observability.js';
2
3
  export declare const MAX_RESPONSE_CHARS = 100000;
3
4
  export type QueryValue = string | number | boolean;
4
5
  export type QueryParams = Record<string, QueryValue | QueryValue[]>;
@@ -13,6 +14,8 @@ export interface ApiMethodRef {
13
14
  }
14
15
  export interface ExecuteDeps {
15
16
  getClientFn?: typeof getClient;
17
+ /** Token + profile view behind a 403 scope hint; absent = the single owner. */
18
+ scopeDeps?: ScopeSetDeps;
16
19
  }
17
20
  export interface ExecuteArgs {
18
21
  account: string;
package/dist/executor.js CHANGED
@@ -4,6 +4,7 @@ import { expandPath, isGoogleApiUrl } from './discovery-client.js';
4
4
  import { handleGoogleApiError } from './tools/_errors.js';
5
5
  import { scopeHintForMethod } from './scope-observability.js';
6
6
  import { checkOutboundForMethod } from './outbound-allowlist.js';
7
+ import { hints } from './hint-provider.js';
7
8
  export const MAX_RESPONSE_CHARS = 100_000;
8
9
  export function jsonResult(payload, isError = false) {
9
10
  const base = { content: [{ type: 'text', text: JSON.stringify(payload) }] };
@@ -70,10 +71,11 @@ export async function executeApiMethod(method, args, deps = {}) {
70
71
  }, true);
71
72
  }
72
73
  if (!isGoogleApiUrl(url)) {
74
+ const rendered = hints()?.render({ kind: 'untrusted_host' });
73
75
  return jsonResult({
74
76
  error: 'untrusted_host',
75
77
  message: `Refusing to send credentials to a non-googleapis.com host for "${method.id}".`,
76
- hint: 'The discovery cache may be corrupt; delete DISCOVERY_CACHE_PATH and retry.',
78
+ hint: rendered?.hint ?? 'The discovery cache may be corrupt; delete DISCOVERY_CACHE_PATH and retry.',
77
79
  retriable: false,
78
80
  account: args.account,
79
81
  }, true);
@@ -103,6 +105,6 @@ export async function executeApiMethod(method, args, deps = {}) {
103
105
  return { content: [{ type: 'text', text }] };
104
106
  }
105
107
  catch (error) {
106
- return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account) : null);
108
+ return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account, deps.scopeDeps) : null);
107
109
  }
108
110
  }
package/dist/fanout.d.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  export declare const CSV_RE: RegExp;
3
- export declare function fanoutAccountField(description: string): z.ZodType;
3
+ export declare function fanoutAccountField(description: string, aliases: () => readonly string[]): z.ZodType;
4
+ /** '*' over an empty account set would fan out to nothing and report success. */
5
+ export declare function noAccountsResult(): {
6
+ content: {
7
+ type: "text";
8
+ text: string;
9
+ }[];
10
+ isError: true;
11
+ };
4
12
  export type AccountSelector = {
5
13
  ok: true;
6
14
  fanout: boolean;
package/dist/fanout.js CHANGED
@@ -1,22 +1,48 @@
1
1
  import { z } from 'zod';
2
- import { ACCOUNTS, getAccountSet, unknownAliasMessage } from './accounts.js';
2
+ import { getAccountSet, liveAccountCheck, unknownAliasMessage } from './accounts.js';
3
+ import { echoName, echoNames, MAX_SUGGESTED_KEYS } from './arg-strict.js';
4
+ import { hints } from './hint-provider.js';
3
5
  export const CSV_RE = /^[a-zA-Z0-9_-]+(\s*,\s*[a-zA-Z0-9_-]+)+$/;
4
6
  const FANOUT_CONCURRENCY = 5;
5
- export function fanoutAccountField(description) {
6
- const csvExample = ACCOUNTS.length > 1 ? `; or a CSV subset like "${ACCOUNTS.slice(0, 2).join(',')}"` : '';
7
+ // `aliases` is required (no global default): the field is baked into a
8
+ // registry's inputSchema, so it must read THAT registry's account set or a
9
+ // '*' would advertise and accept another registry's aliases. It is a getter:
10
+ // the set is read at parse time, so an alias added mid-session (account_add,
11
+ // a tenant link) is valid on tools registered before it existed.
12
+ export function fanoutAccountField(description, aliases) {
13
+ const boot = aliases();
14
+ const csvExample = boot.length > 1 ? `; or a CSV subset like "${boot.slice(0, 2).join(',')}"` : '';
7
15
  // The union's OWN error only fires when every branch aborts at the type
8
16
  // check (a non-string). For a string, zod surfaces the single non-aborted
9
17
  // branch verbatim, which is the CSV regex, so a mistyped alias used to read
10
18
  // as "Invalid string: must match pattern /^[a-zA-Z0-9_-]+(,...)/" and never
11
19
  // named an alias. Hence the same message on BOTH.
12
- const bad = () => unknownAliasMessage(ACCOUNTS, true);
20
+ const bad = () => unknownAliasMessage(aliases(), true);
13
21
  return z
14
- // '*' first so the tuple is statically non-empty even when ACCOUNTS is empty
15
- // (a fresh install): z.enum requires [string, ...string[]].
16
- .union([z.enum(['*', ...ACCOUNTS]), z.string().regex(CSV_RE, { error: bad })], { error: bad })
22
+ // The selector branch stays first: tools/list injects the live
23
+ // '*' + alias enum into it (registry withLiveAccountEnum).
24
+ .union([z.string().check(liveAccountCheck(aliases, true)), z.string().regex(CSV_RE, { error: bad })], { error: bad })
17
25
  .optional()
18
26
  .describe(`${description}; "*" = all accounts${csvExample}; omit for the default account`);
19
27
  }
28
+ /** '*' over an empty account set would fan out to nothing and report success. */
29
+ export function noAccountsResult() {
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: JSON.stringify({
35
+ error: 'validation_error',
36
+ message: 'No Google account is configured, so "*" selects nothing.',
37
+ hint: unknownAliasMessage([]),
38
+ retriable: false,
39
+ account: '*',
40
+ }),
41
+ },
42
+ ],
43
+ isError: true,
44
+ };
45
+ }
20
46
  export function parseAccountSelector(value, accounts = getAccountSet().aliases) {
21
47
  if (value === '*')
22
48
  return { ok: true, fanout: true, aliases: [...accounts] };
@@ -53,10 +79,11 @@ export function invalidAccountsResult(invalid, accounts = getAccountSet().aliase
53
79
  type: 'text',
54
80
  text: JSON.stringify({
55
81
  error: 'validation_error',
56
- message: `Unknown account alias(es): ${invalid.join(', ')}.`,
82
+ // The caller's CSV can hold any number of tokens: echo a prefix.
83
+ message: `Unknown account alias(es): ${echoNames(invalid)}.`,
57
84
  hint: unknownAliasMessage(accounts, true),
58
85
  retriable: false,
59
- account: invalid.join(','),
86
+ account: invalid.slice(0, MAX_SUGGESTED_KEYS).map(echoName).join(','),
60
87
  }),
61
88
  },
62
89
  ],
@@ -87,10 +114,17 @@ export async function runFanout(handler, args, aliases) {
87
114
  results[i] = res?.isError ? { account, ok: false, error: payload } : { account, ok: true, data: payload };
88
115
  }
89
116
  catch (err) {
117
+ const rendered = hints()?.render({ kind: 'internal', account });
90
118
  results[i] = {
91
119
  account,
92
120
  ok: false,
93
- error: { error: 'internal', message: err.message, retriable: false, account },
121
+ error: {
122
+ error: 'internal',
123
+ message: rendered?.message ?? err.message,
124
+ hint: rendered?.hint,
125
+ retriable: false,
126
+ account,
127
+ },
94
128
  };
95
129
  }
96
130
  }
@@ -1,3 +1,14 @@
1
+ /** Which process instance holds a lock: PID namespace id, start time in
2
+ * clock ticks since boot, and a boot id prefix. A part this process cannot
3
+ * read is `host` or `0` (all three without /proc). */
4
+ interface LockIdentity {
5
+ ns: string;
6
+ start: string;
7
+ boot: string;
8
+ }
9
+ /** Test hook: act as a process with this identity (undefined restores the real one). */
10
+ export declare function __setLockIdentityForTest(identity?: LockIdentity): void;
1
11
  export declare function withFileLock<T>(absPath: string, fn: () => T, label?: string): T;
2
12
  export declare function atomicWriteFileSync(absPath: string, contents: string, mode?: number): void;
3
13
  export declare function atomicWriteWithLock(absPath: string, contents: string, mode?: number): void;
14
+ export {};