mcp-google-multi 6.0.0-alpha.6 → 6.0.0-alpha.60

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 (176) hide show
  1. package/README.md +6 -4
  2. package/dist/accounts.d.ts +22 -1
  3. package/dist/accounts.js +64 -16
  4. package/dist/api-probe.d.ts +5 -0
  5. package/dist/api-probe.js +23 -15
  6. package/dist/arg-normalize.d.ts +76 -0
  7. package/dist/arg-normalize.js +314 -0
  8. package/dist/arg-strict.d.ts +61 -0
  9. package/dist/arg-strict.js +193 -0
  10. package/dist/auth.d.ts +4 -3
  11. package/dist/auth.js +26 -85
  12. package/dist/boot-gates.d.ts +26 -0
  13. package/dist/boot-gates.js +38 -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 +14 -0
  19. package/dist/config-file.js +41 -1
  20. package/dist/discover.js +35 -14
  21. package/dist/discovery-client.d.ts +11 -1
  22. package/dist/discovery-client.js +102 -18
  23. package/dist/doctor.d.ts +27 -3
  24. package/dist/doctor.js +204 -33
  25. package/dist/env-load.d.ts +11 -0
  26. package/dist/env-load.js +23 -1
  27. package/dist/executor.d.ts +3 -0
  28. package/dist/executor.js +13 -2
  29. package/dist/fanout.d.ts +9 -1
  30. package/dist/fanout.js +74 -13
  31. package/dist/fs-atomic.d.ts +11 -0
  32. package/dist/fs-atomic.js +219 -28
  33. package/dist/http-transport.d.ts +36 -3
  34. package/dist/http-transport.js +71 -12
  35. package/dist/identity.d.ts +4 -1
  36. package/dist/identity.js +25 -6
  37. package/dist/index.js +142 -111
  38. package/dist/master-key.js +13 -5
  39. package/dist/mcp-token.d.ts +42 -6
  40. package/dist/mcp-token.js +65 -8
  41. package/dist/metrics-cli.d.ts +21 -0
  42. package/dist/metrics-cli.js +206 -0
  43. package/dist/metrics-tap.d.ts +12 -0
  44. package/dist/metrics-tap.js +95 -0
  45. package/dist/migrate-config.js +2 -2
  46. package/dist/oauth-as.d.ts +87 -3
  47. package/dist/oauth-as.js +196 -22
  48. package/dist/oauth-consent.d.ts +35 -10
  49. package/dist/oauth-consent.js +101 -39
  50. package/dist/outbound-allowlist.d.ts +34 -0
  51. package/dist/outbound-allowlist.js +108 -0
  52. package/dist/reauth-hint.d.ts +1 -1
  53. package/dist/reauth-hint.js +8 -8
  54. package/dist/registry-transfer.d.ts +5 -0
  55. package/dist/registry-transfer.js +65 -5
  56. package/dist/registry.d.ts +71 -2
  57. package/dist/registry.js +258 -21
  58. package/dist/scope-catalog.d.ts +11 -0
  59. package/dist/scope-catalog.js +39 -6
  60. package/dist/scope-observability.d.ts +8 -6
  61. package/dist/scope-observability.js +2 -1
  62. package/dist/services.d.ts +17 -4
  63. package/dist/services.js +62 -6
  64. package/dist/setup-prompt.d.ts +1 -1
  65. package/dist/setup-prompt.js +1 -1
  66. package/dist/tenant-purge.d.ts +26 -0
  67. package/dist/tenant-purge.js +67 -0
  68. package/dist/token-store.d.ts +20 -2
  69. package/dist/token-store.js +0 -0
  70. package/dist/tools/_coerce.d.ts +5 -0
  71. package/dist/tools/_coerce.js +11 -0
  72. package/dist/tools/_errors.d.ts +31 -3
  73. package/dist/tools/_errors.js +307 -43
  74. package/dist/tools/_local-files.d.ts +9 -0
  75. package/dist/tools/_local-files.js +25 -2
  76. package/dist/tools/account-wizard.d.ts +34 -2
  77. package/dist/tools/account-wizard.js +223 -65
  78. package/dist/tools/accounts-tool.d.ts +7 -0
  79. package/dist/tools/accounts-tool.js +24 -14
  80. package/dist/tools/admin.d.ts +2 -1
  81. package/dist/tools/admin.js +20 -15
  82. package/dist/tools/analytics.d.ts +19 -0
  83. package/dist/tools/analytics.js +288 -0
  84. package/dist/tools/calendar.d.ts +9 -1
  85. package/dist/tools/calendar.js +93 -29
  86. package/dist/tools/chat.d.ts +2 -1
  87. package/dist/tools/chat.js +18 -11
  88. package/dist/tools/contacts.d.ts +2 -1
  89. package/dist/tools/contacts.js +74 -43
  90. package/dist/tools/docs.d.ts +2 -1
  91. package/dist/tools/docs.js +90 -84
  92. package/dist/tools/drive.d.ts +2 -1
  93. package/dist/tools/drive.js +197 -120
  94. package/dist/tools/forms.d.ts +2 -1
  95. package/dist/tools/forms.js +21 -17
  96. package/dist/tools/generated/_shared.d.ts +8 -1
  97. package/dist/tools/generated/_shared.js +18 -4
  98. package/dist/tools/generated/admin.d.ts +2 -1
  99. package/dist/tools/generated/admin.js +610 -479
  100. package/dist/tools/generated/analytics.d.ts +3 -0
  101. package/dist/tools/generated/analytics.js +981 -0
  102. package/dist/tools/generated/appsmarket.d.ts +2 -1
  103. package/dist/tools/generated/appsmarket.js +9 -9
  104. package/dist/tools/generated/calendar.d.ts +2 -1
  105. package/dist/tools/generated/calendar.js +83 -83
  106. package/dist/tools/generated/chat.d.ts +2 -1
  107. package/dist/tools/generated/chat.js +178 -151
  108. package/dist/tools/generated/classroom.d.ts +2 -1
  109. package/dist/tools/generated/classroom.js +467 -425
  110. package/dist/tools/generated/cloudidentity.d.ts +2 -1
  111. package/dist/tools/generated/cloudidentity.js +209 -201
  112. package/dist/tools/generated/cloudsearch.d.ts +2 -1
  113. package/dist/tools/generated/cloudsearch.js +139 -138
  114. package/dist/tools/generated/contacts.d.ts +2 -1
  115. package/dist/tools/generated/contacts.js +49 -40
  116. package/dist/tools/generated/drive.d.ts +2 -1
  117. package/dist/tools/generated/drive.js +133 -124
  118. package/dist/tools/generated/driveactivity.d.ts +2 -1
  119. package/dist/tools/generated/driveactivity.js +3 -3
  120. package/dist/tools/generated/drivelabels.d.ts +2 -1
  121. package/dist/tools/generated/drivelabels.js +113 -85
  122. package/dist/tools/generated/forms.d.ts +2 -1
  123. package/dist/tools/generated/forms.js +14 -14
  124. package/dist/tools/generated/gmail.d.ts +2 -1
  125. package/dist/tools/generated/gmail.js +253 -226
  126. package/dist/tools/generated/groupsmigration.d.ts +2 -1
  127. package/dist/tools/generated/groupsmigration.js +4 -4
  128. package/dist/tools/generated/groupssettings.d.ts +2 -1
  129. package/dist/tools/generated/groupssettings.js +10 -10
  130. package/dist/tools/generated/index.d.ts +2 -1
  131. package/dist/tools/generated/index.js +2 -0
  132. package/dist/tools/generated/keep.d.ts +2 -1
  133. package/dist/tools/generated/keep.js +23 -22
  134. package/dist/tools/generated/licensing.d.ts +2 -1
  135. package/dist/tools/generated/licensing.js +54 -38
  136. package/dist/tools/generated/meet.d.ts +2 -1
  137. package/dist/tools/generated/meet.js +41 -41
  138. package/dist/tools/generated/method-map.d.ts +2 -0
  139. package/dist/tools/generated/method-map.js +894 -0
  140. package/dist/tools/generated/postmaster.d.ts +2 -1
  141. package/dist/tools/generated/postmaster.js +12 -12
  142. package/dist/tools/generated/reseller.d.ts +2 -1
  143. package/dist/tools/generated/reseller.js +69 -63
  144. package/dist/tools/generated/script.d.ts +2 -1
  145. package/dist/tools/generated/script.js +62 -52
  146. package/dist/tools/generated/searchconsole.d.ts +2 -1
  147. package/dist/tools/generated/searchconsole.js +8 -6
  148. package/dist/tools/generated/sheets.d.ts +2 -1
  149. package/dist/tools/generated/sheets.js +25 -24
  150. package/dist/tools/generated/tasks.d.ts +2 -1
  151. package/dist/tools/generated/tasks.js +15 -9
  152. package/dist/tools/generated/vault.d.ts +2 -1
  153. package/dist/tools/generated/vault.js +132 -123
  154. package/dist/tools/generated/workspaceevents.d.ts +2 -1
  155. package/dist/tools/generated/workspaceevents.js +48 -47
  156. package/dist/tools/gmail.d.ts +12 -2
  157. package/dist/tools/gmail.js +95 -51
  158. package/dist/tools/google-api.d.ts +2 -3
  159. package/dist/tools/google-api.js +66 -27
  160. package/dist/tools/meet.d.ts +2 -1
  161. package/dist/tools/meet.js +17 -10
  162. package/dist/tools/searchconsole.d.ts +2 -1
  163. package/dist/tools/searchconsole.js +27 -23
  164. package/dist/tools/sheets.d.ts +2 -1
  165. package/dist/tools/sheets.js +72 -80
  166. package/dist/tools/slides.d.ts +2 -1
  167. package/dist/tools/slides.js +18 -14
  168. package/dist/tools/tasks.d.ts +2 -1
  169. package/dist/tools/tasks.js +35 -31
  170. package/dist/trim.d.ts +21 -0
  171. package/dist/trim.js +47 -0
  172. package/dist/usage-metrics.d.ts +161 -0
  173. package/dist/usage-metrics.js +609 -0
  174. package/dist/write-control.d.ts +27 -1
  175. package/dist/write-control.js +90 -11
  176. package/package.json +50 -19
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # mcp-google-multi
2
2
 
3
- The most complete **local Google Workspace MCP server**: Gmail, Drive, Calendar, Sheets, Docs, Slides, Forms, Contacts, Tasks, Chat, Meet, Classroom, Vault, Admin and more — **every OAuth-reachable Workspace API method** as a tool, across **multiple Google accounts** at once, from Claude Code or any MCP client.
3
+ The most complete **local Google Workspace MCP server**: Gmail, Drive, Calendar, Sheets, Docs, Slides, Forms, Contacts, Tasks, Chat, Meet, Analytics (GA4), Search Console, Classroom, Vault, Admin and more — **every OAuth-reachable API method** as a tool, across **multiple Google accounts** at once, from Claude Code or any MCP client.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/mcp-google-multi?label=npm&color=cb3837)](https://www.npmjs.com/package/mcp-google-multi)
6
6
 
7
- - 🧰 **Exhaustive** — 874 tools across 28 services + an escape hatch for anything else → [COVERAGE.md](./COVERAGE.md)
7
+ - 🧰 **Exhaustive** — 940 tools across 29 services, now including Google Analytics (GA4), + an escape hatch for anything else → [COVERAGE.md](./COVERAGE.md)
8
8
  - 🔑 **Multi-account** — drive any number of Google accounts by alias, or fan one call out across all of them
9
9
  - 🔒 **Private by design** — your own OAuth app, tokens encrypted at rest (AES-256-GCM), writes deny-by-default, no telemetry, no metering — it talks only to Google
10
- - 🌐 **Local or remote** — runs locally over stdio, or self-hosted over HTTP with its own built-in OAuth 2.1 server (Claude Code's `/mcp` login and the claude.ai connector, zero custom UI) → [remote setup](./docs/http-setup.md)
10
+ - 🌐 **Local or remote** — runs locally over stdio, or self-hosted over HTTP with its own built-in OAuth 2.1 server (Claude Code's `/mcp` login and the claude.ai connector, zero custom UI). Pull-and-up Docker Compose with optional automatic HTTPS → [remote setup](./docs/http-setup.md)
11
11
  - ✉️ **Built for real work** — send and read email in Markdown with attachments and one-call replies, an interactive setup wizard with a `doctor` self-check, and per-account scope profiles → [features tour](./docs/features.md)
12
12
 
13
13
  ## Quick setup
@@ -20,6 +20,8 @@ New to all this? It's written for someone who just installed Claude Code and has
20
20
  npm install -g mcp-google-multi
21
21
  ```
22
22
 
23
+ Claude Desktop user? You can skip npm entirely: download the `mcp-google-multi.mcpb` bundle from the [latest release](https://github.com/bakissation/mcp-google-multi/releases/latest), double-click it (or drag it into Claude Desktop → Settings → Extensions), and fill in the values from step 2 when prompted.
24
+
23
25
  2. **Make your Google key** (the one manual part, a few minutes, because Google has no way to script it). Follow the step-by-step [Google Cloud setup](./docs/google-cloud-setup.md), or just ask Claude Code: *"walk me through creating a Google OAuth Desktop client for mcp-google-multi."* You finish with two values, a **Client ID** and a **Client Secret**. It's free and private to you.
24
26
 
25
27
  3. **Put them in a file.** In the folder you'll run from, make a file named `.env` and paste this, filling in your values:
@@ -51,7 +53,7 @@ New to all this? It's written for someone who just installed Claude Code and has
51
53
 
52
54
  *On a server or from claude.ai?* Advanced path: [Remote / HTTP setup](./docs/http-setup.md). *Coming from v5?* [v6 migration guide](./MIGRATION-v6.md).
53
55
 
54
- **Go deeper:** [Configuration reference](./docs/configuration.md) · [What's covered](./COVERAGE.md) · [Features tour](./docs/features.md) · [Remote / HTTP setup](./docs/http-setup.md) · [Secrets in a vault](./docs/secrets.md) · [Migrating to v6](./MIGRATION-v6.md) · [Security policy](./SECURITY.md) · [Roadmap](https://github.com/bakissation/mcp-google-multi/milestones)
56
+ **Go deeper:** [Configuration reference](./docs/configuration.md) · [What's covered](./COVERAGE.md) · [Features tour](./docs/features.md) · [Remote / HTTP setup](./docs/http-setup.md) · [Secrets in a vault](./docs/secrets.md) · [Local usage metrics](./docs/usage-metrics.md) · [Migrating to v6](./MIGRATION-v6.md) · [Security policy](./SECURITY.md) · [Roadmap](https://github.com/bakissation/mcp-google-multi/milestones)
55
57
 
56
58
  ## Maintainer & credits
57
59
 
@@ -18,7 +18,11 @@ export interface AccountSet {
18
18
  defaultAccount?: string;
19
19
  defaultAccountSource?: 'env' | 'config';
20
20
  }
21
- export declare function resolveAccounts(env?: NodeJS.ProcessEnv, filePath?: string, onInvalid?: 'exit' | 'throw'): AccountSet;
21
+ export declare function resolveAccounts(env?: NodeJS.ProcessEnv, filePath?: string, onInvalid?: 'exit' | 'throw', opts?: {
22
+ tokenDir?: string;
23
+ emptyOk?: boolean;
24
+ }): AccountSet;
25
+ export declare function fileStamp(filePath: string, version: number): string;
22
26
  /** Live accessor: dispatch-time readers use this, never a captured snapshot. */
23
27
  export declare function getAccountSet(): AccountSet;
24
28
  /** Re-resolve after a config.json mutation and swap the live set. */
@@ -30,6 +34,11 @@ export declare function isAccountSetStale(): boolean;
30
34
  * Snapshot from the initial load; enums widen only when the registry is
31
35
  * rebuilt after a mutation (account_add, later slice). */
32
36
  export declare const ACCOUNTS: string[];
37
+ /** One message for a mistyped alias, shared by every account schema. `aliases`
38
+ * is the SCHEMA's own snapshot, not the live registry: those are the values the
39
+ * enum actually accepts, and naming a runtime-added alias as valid while
40
+ * rejecting it would be a lie. */
41
+ export declare function unknownAliasMessage(aliases: readonly string[], selectors?: boolean): string;
33
42
  /**
34
43
  * The `account` param schema, empty-registry-safe. A `z.enum` requires at least
35
44
  * one value, so a fresh install (zero aliases) would throw at schema-build time
@@ -40,6 +49,18 @@ export declare const ACCOUNTS: string[];
40
49
  export declare function accountAliasSchemaFor(aliases: readonly string[]): z.ZodType<string>;
41
50
  /** Shared, load-time snapshot used by every tool's `account` field. */
42
51
  export declare const accountAliasSchema: z.ZodType<string>;
52
+ /** Live account argument: validates at PARSE time against the accessor's
53
+ * CURRENT aliases, so an alias added mid-session (account_add, or a tenant
54
+ * alias link) is immediately valid on already-registered tools — a baked
55
+ * z.enum freezes the boot-time set until restart. An empty set stays
56
+ * permissive (same empty-safe rule as above); dispatch still resolves the
57
+ * alias against the live registry. */
58
+ export declare function accountArgLive(aliases: () => readonly string[]): z.ZodType<string>;
59
+ /** The live membership check behind accountArgLive and the fan-out field;
60
+ * `selectors` also admits "*". An empty set stays permissive. */
61
+ export declare function liveAccountCheck(aliases: () => readonly string[], selectors: boolean): z.core.$ZodCheck<string>;
62
+ /** True for a field built on liveAccountCheck, optional or not. */
63
+ export declare function isLiveAccountField(field: unknown): boolean;
43
64
  /**
44
65
  * BR-4: the stdio/http SERVER never boots with an empty registry — a fresh user
45
66
  * bootstraps via env / `migrate-config` / `account import` / `auth` first. The
package/dist/accounts.js CHANGED
@@ -2,8 +2,8 @@ import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import { z } from 'zod';
4
4
  import { loadEnvFiles } from './env-load.js';
5
- import { CONFIG_VERSION, configDir, configFilePath, failStartup, loadConfigFile, mutateConfigFile } from './config-file.js';
6
- import { BUNDLE_CATALOG, closestBundle, resolveBundleAliases } from './scope-catalog.js';
5
+ import { ALIAS_RE, CONFIG_VERSION, configDir, configFilePath, failStartup, isReservedAlias, loadConfigFile, mutateConfigFile } from './config-file.js';
6
+ import { closestBundle, isKnownBundle, resolveBundleAliases } from './scope-catalog.js';
7
7
  const envLoad = loadEnvFiles();
8
8
  const defaultTokenDir = path.join(configDir(), 'tokens');
9
9
  const tokenDir = process.env.TOKEN_STORE_PATH
@@ -15,14 +15,14 @@ export function getTokenDir() {
15
15
  function parseCsv(value) {
16
16
  return (value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
17
17
  }
18
- function accountPaths(alias) {
18
+ function accountPaths(alias, dir = tokenDir) {
19
19
  return {
20
- tokenPath: path.join(tokenDir, alias, 'token.json'),
21
- encPath: path.join(tokenDir, `${alias}.enc`),
20
+ tokenPath: path.join(dir, alias, 'token.json'),
21
+ encPath: path.join(dir, `${alias}.enc`),
22
22
  };
23
23
  }
24
24
  /** v5 env parser, guards verbatim. Format: GOOGLE_ACCOUNTS="alias1:email1,alias2:email2". */
25
- function parseEnvAccounts(raw, adminAliases) {
25
+ function parseEnvAccounts(raw, adminAliases, dir = tokenDir) {
26
26
  const configs = {};
27
27
  const aliases = [];
28
28
  for (const entry of raw.split(',')) {
@@ -40,7 +40,7 @@ function parseEnvAccounts(raw, adminAliases) {
40
40
  }
41
41
  // Restrict alias to a safe charset so it can't escape `tokenDir` via path traversal
42
42
  // (e.g. "../../etc/passwd:foo@bar.com" in .env).
43
- if (!/^[a-zA-Z0-9_-]+$/.test(alias) || ['__proto__', 'constructor', 'prototype'].includes(alias)) {
43
+ if (!ALIAS_RE.test(alias) || isReservedAlias(alias)) {
44
44
  throw new Error(`Invalid alias "${alias}". Allowed characters: letters, digits, underscore, hyphen.`);
45
45
  }
46
46
  if (aliases.includes(alias)) {
@@ -49,7 +49,7 @@ function parseEnvAccounts(raw, adminAliases) {
49
49
  aliases.push(alias);
50
50
  configs[alias] = {
51
51
  email,
52
- ...accountPaths(alias),
52
+ ...accountPaths(alias, dir),
53
53
  admin: adminAliases.includes(alias) || undefined,
54
54
  source: 'env',
55
55
  };
@@ -74,7 +74,11 @@ function noAccountsMessage() {
74
74
  * empty behaves as unset, mirroring GOOGLE_ACCOUNTS semantics.
75
75
  */
76
76
  let warnedLegacy = false;
77
- export function resolveAccounts(env = process.env, filePath = configFilePath(), onInvalid = 'exit') {
77
+ export function resolveAccounts(env = process.env, filePath = configFilePath(), onInvalid = 'exit',
78
+ // emptyOk: zero accounts is a valid set, not an error, even with 'throw'
79
+ // (a context that has linked nothing yet). An invalid file still fails.
80
+ opts = {}) {
81
+ const dir = opts.tokenDir ?? tokenDir;
78
82
  const adminEnv = parseCsv(env.GOOGLE_ADMIN_ACCOUNTS);
79
83
  const rawEnv = env.GOOGLE_ACCOUNTS;
80
84
  const fail = (slug, message) => {
@@ -91,7 +95,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
91
95
  if (bundle === 'admin') {
92
96
  fail('E_UNKNOWN_BUNDLE', '"admin" is not a global bundle: grant it per account via GOOGLE_ADMIN_ACCOUNTS or an "admin: true" scope profile.');
93
97
  }
94
- if (!(bundle in BUNDLE_CATALOG)) {
98
+ if (!isKnownBundle(bundle)) {
95
99
  const hint = closestBundle(bundle);
96
100
  fail('E_UNKNOWN_BUNDLE', `unknown bundle "${bundle}" in GOOGLE_OPTIONAL_SCOPES${hint ? ` — did you mean "${hint}"?` : ''}`);
97
101
  }
@@ -102,7 +106,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
102
106
  }
103
107
  }
104
108
  if (rawEnv && rawEnv.trim() !== '') {
105
- const { aliases, configs } = parseEnvAccounts(rawEnv, adminEnv);
109
+ const { aliases, configs } = parseEnvAccounts(rawEnv, adminEnv, dir);
106
110
  materializeFirstRun(aliases, configs, filePath);
107
111
  const def = resolveDefaultAccount(env, null, aliases, fail);
108
112
  return {
@@ -129,7 +133,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
129
133
  // empty (BR-4), enforced by assertServerAccountsConfigured() in index.ts.
130
134
  // The dispatch-path reload ('throw') still throws so a mid-session emptied
131
135
  // config.json keeps the last-good registry (BR-7) instead of dropping tools.
132
- if (onInvalid === 'throw') {
136
+ if (onInvalid === 'throw' && !opts.emptyOk) {
133
137
  throw new Error(`E_NO_ACCOUNTS_CONFIGURED: ${noAccountsMessage()}`);
134
138
  }
135
139
  return {
@@ -148,7 +152,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
148
152
  for (const [name, profile] of Object.entries(config?.scopeProfiles ?? {})) {
149
153
  const bundles = resolveBundleAliases(profile.bundles);
150
154
  for (const bundle of bundles) {
151
- if (!(bundle in BUNDLE_CATALOG)) {
155
+ if (!isKnownBundle(bundle)) {
152
156
  const hint = closestBundle(bundle);
153
157
  fail('E_UNKNOWN_BUNDLE', `unknown bundle "${bundle}" in scope profile "${name}"${hint ? ` — did you mean "${hint}"?` : ''}`);
154
158
  }
@@ -167,7 +171,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
167
171
  aliases.push(alias);
168
172
  configs[alias] = {
169
173
  email: entry.email,
170
- ...accountPaths(alias),
174
+ ...accountPaths(alias, dir),
171
175
  scopeProfile: entry.scopeProfile,
172
176
  admin: adminEnv.length > 0 ? adminEnv.includes(alias) : entry.admin,
173
177
  source: 'config',
@@ -196,7 +200,7 @@ function resolveDefaultAccount(env, fromConfig, aliases, fail) {
196
200
  }
197
201
  return { defaultAccount: value, defaultAccountSource: fromEnv ? 'env' : 'config' };
198
202
  }
199
- function fileStamp(filePath, version) {
203
+ export function fileStamp(filePath, version) {
200
204
  try {
201
205
  return `${version}:${fs.statSync(filePath).mtimeMs}`;
202
206
  }
@@ -278,6 +282,17 @@ export function isAccountSetStale() {
278
282
  * Snapshot from the initial load; enums widen only when the registry is
279
283
  * rebuilt after a mutation (account_add, later slice). */
280
284
  export const ACCOUNTS = current.aliases;
285
+ /** One message for a mistyped alias, shared by every account schema. `aliases`
286
+ * is the SCHEMA's own snapshot, not the live registry: those are the values the
287
+ * enum actually accepts, and naming a runtime-added alias as valid while
288
+ * rejecting it would be a lie. */
289
+ export function unknownAliasMessage(aliases, selectors = false) {
290
+ if (aliases.length === 0)
291
+ return 'No Google account is configured yet. Add one with account_add.';
292
+ const all = selectors ? '; "*" for all accounts' : '';
293
+ const csv = selectors && aliases.length > 1 ? `; or a CSV subset like "${aliases.slice(0, 2).join(',')}"` : '';
294
+ return `Unknown account alias. Valid: ${aliases.join(', ')}${all}${csv}. Run account_list if accounts changed since this server started.`;
295
+ }
281
296
  /**
282
297
  * The `account` param schema, empty-registry-safe. A `z.enum` requires at least
283
298
  * one value, so a fresh install (zero aliases) would throw at schema-build time
@@ -286,10 +301,43 @@ export const ACCOUNTS = current.aliases;
286
301
  * are no aliases: no alias exists to enumerate, the server refuses to boot empty
287
302
  * anyway, and dispatch validates the account against the live set. */
288
303
  export function accountAliasSchemaFor(aliases) {
289
- return aliases.length > 0 ? z.enum(aliases) : z.string();
304
+ return aliases.length > 0
305
+ ? z.enum(aliases, { error: () => unknownAliasMessage(aliases) })
306
+ : z.string();
290
307
  }
291
308
  /** Shared, load-time snapshot used by every tool's `account` field. */
292
309
  export const accountAliasSchema = accountAliasSchemaFor(ACCOUNTS);
310
+ /** Live account argument: validates at PARSE time against the accessor's
311
+ * CURRENT aliases, so an alias added mid-session (account_add, or a tenant
312
+ * alias link) is immediately valid on already-registered tools — a baked
313
+ * z.enum freezes the boot-time set until restart. An empty set stays
314
+ * permissive (same empty-safe rule as above); dispatch still resolves the
315
+ * alias against the live registry. */
316
+ export function accountArgLive(aliases) {
317
+ return z.string().check(liveAccountCheck(aliases, false));
318
+ }
319
+ // The check instance survives the clones .describe() and .optional() make,
320
+ // so it is what marks a field as an account selector for the registry.
321
+ const LIVE_ACCOUNT_CHECKS = new WeakSet();
322
+ /** The live membership check behind accountArgLive and the fan-out field;
323
+ * `selectors` also admits "*". An empty set stays permissive. */
324
+ export function liveAccountCheck(aliases, selectors) {
325
+ const check = z.superRefine((value, ctx) => {
326
+ const current = aliases();
327
+ if (current.length === 0 || current.includes(value) || (selectors && value === '*'))
328
+ return;
329
+ ctx.addIssue({ code: 'custom', message: unknownAliasMessage(current, selectors) });
330
+ });
331
+ LIVE_ACCOUNT_CHECKS.add(check);
332
+ return check;
333
+ }
334
+ /** True for a field built on liveAccountCheck, optional or not. */
335
+ export function isLiveAccountField(field) {
336
+ let def = field?._zod?.def;
337
+ if (def?.type === 'optional')
338
+ def = def.innerType?._zod?.def;
339
+ return def?.checks?.some((c) => LIVE_ACCOUNT_CHECKS.has(c)) ?? false;
340
+ }
293
341
  /**
294
342
  * BR-4: the stdio/http SERVER never boots with an empty registry — a fresh user
295
343
  * bootstraps via env / `migrate-config` / `account import` / `auth` first. The
@@ -1,3 +1,5 @@
1
+ import { getClient } from './client.js';
2
+ import { readToken } from './token-store.js';
1
3
  import type { ApiProbeResult } from './doctor.js';
2
4
  export interface ApiProbeSpec {
3
5
  service: string;
@@ -15,4 +17,7 @@ export interface ApiProbeDeps {
15
17
  grantedScopes: (alias: string) => string[];
16
18
  request: (alias: string, url: string) => Promise<void>;
17
19
  }
20
+ /** Probe deps over one context's token reads and client; the module-level
21
+ * pair is the single owner's. */
22
+ export declare function apiProbeDepsFor(readTokenFn: typeof readToken, getClientFn: typeof getClient): ApiProbeDeps;
18
23
  export declare function probeApiEnablement(alias: string, deps?: ApiProbeDeps): Promise<ApiProbeResult[]>;
package/dist/api-probe.js CHANGED
@@ -17,25 +17,33 @@ export const API_PROBES = [
17
17
  { service: 'chat', api: 'chat', url: 'https://chat.googleapis.com/v1/spaces?pageSize=1', scopePrefixes: [`${P}chat.`] },
18
18
  { service: 'meet', api: 'meet', url: 'https://meet.googleapis.com/v2/conferenceRecords?pageSize=1', scopePrefixes: [`${P}meetings.`] },
19
19
  { service: 'forms', api: 'forms', url: `https://forms.googleapis.com/v1/forms/${BOGUS_ID}`, scopePrefixes: [`${P}forms.`], notFoundMeansEnabled: true },
20
+ // Probes the Admin API only: the Data API has no no-arg read (every call
21
+ // needs a property id), so its enablement surfaces on first report instead.
22
+ { service: 'analytics', api: 'analyticsadmin', url: 'https://analyticsadmin.googleapis.com/v1beta/accountSummaries?pageSize=1', scopePrefixes: [`${P}analytics`] },
20
23
  ];
21
24
  export function planProbes(granted, probes = API_PROBES) {
22
25
  return probes.filter((p) => granted.some((s) => p.scopePrefixes.some((prefix) => s.startsWith(prefix))));
23
26
  }
24
- const DEFAULT_DEPS = {
25
- grantedScopes: (alias) => {
26
- try {
27
- const scope = readToken(alias)?.scope;
28
- return typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
29
- }
30
- catch {
31
- return [];
32
- }
33
- },
34
- request: async (alias, url) => {
35
- const auth = await getClient(alias);
36
- await auth.request({ url, timeout: 10_000 });
37
- },
38
- };
27
+ /** Probe deps over one context's token reads and client; the module-level
28
+ * pair is the single owner's. */
29
+ export function apiProbeDepsFor(readTokenFn, getClientFn) {
30
+ return {
31
+ grantedScopes: (alias) => {
32
+ try {
33
+ const scope = readTokenFn(alias)?.scope;
34
+ return typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ },
40
+ request: async (alias, url) => {
41
+ const auth = await getClientFn(alias);
42
+ await auth.request({ url, timeout: 10_000 });
43
+ },
44
+ };
45
+ }
46
+ const DEFAULT_DEPS = apiProbeDepsFor(readToken, getClient);
39
47
  export async function probeApiEnablement(alias, deps = DEFAULT_DEPS) {
40
48
  const results = [];
41
49
  for (const spec of planProbes(deps.grantedScopes(alias))) {
@@ -0,0 +1,76 @@
1
+ import type { Transport, JSONRPCMessage } from "@modelcontextprotocol/server";
2
+ import { type SiblingSpelling, type UnknownArgMode } from './arg-strict.js';
3
+ export declare function isSchemaValidationText(text: string): boolean;
4
+ export declare function argNormalizationEnabled(env?: NodeJS.ProcessEnv): boolean;
5
+ /** Declared scalar kind per schema key; drives value coercion on RENAMED keys
6
+ * only. Clients string-encode values for keys absent from the advertised
7
+ * schema, so a renamed key almost always arrives as a string — without
8
+ * coercion the rename would just move the -32602 from the key to the value. */
9
+ export type ArgKind = 'number' | 'boolean' | 'other';
10
+ export type ArgShape = ReadonlyMap<string, ArgKind>;
11
+ export declare function normalizeCallArguments(shape: ArgShape, args: Record<string, unknown>): {
12
+ args: Record<string, unknown>;
13
+ renamed: [string, string][];
14
+ };
15
+ export declare function normalizeMessage(msg: JSONRPCMessage, shapeFor: (tool: string) => ArgShape | undefined, log?: (line: string) => void, onRename?: (tool: string, renames: number) => void): JSONRPCMessage;
16
+ export interface StrictArgOptions {
17
+ mode: UnknownArgMode;
18
+ /** full declared key list for a tool, in declaration order */
19
+ declaredFor: (tool: string) => readonly string[] | undefined;
20
+ /** sibling spellings of a concept elsewhere in the same service */
21
+ siblingsFor?: (tool: string, keys: string[]) => SiblingSpelling[];
22
+ /** Replacement text for the SDK's bare "Tool X not found". Still answered as
23
+ * a JSON-RPC error, which is what the spec prescribes for an unknown tool;
24
+ * only the message improves. */
25
+ unknownTool?: (tool: string) => string;
26
+ onDrop?: (tool: string, resolvedKeys: string[]) => void;
27
+ }
28
+ export type ScreenOutcome = {
29
+ action: 'forward';
30
+ msg: JSONRPCMessage;
31
+ } | {
32
+ action: 'reject';
33
+ response: JSONRPCMessage;
34
+ };
35
+ /**
36
+ * Screen a normalized tools/call for undeclared keys. `warn` forwards exactly
37
+ * as before and only reports; `reject` answers with the taxonomy envelope and
38
+ * never reaches the handler, so nothing is sent to Google on a guess.
39
+ */
40
+ export declare function screenMessage(msg: JSONRPCMessage, opts: StrictArgOptions, log?: (line: string) => void): ScreenOutcome;
41
+ /** Wrap a server-side transport so tools/call argument keys are normalized
42
+ * before the SDK validates them. The Protocol assigns `onmessage` during
43
+ * connect(); the interceptor lives in that setter, so the wrapper works
44
+ * identically for stdio and (per-request, stateless) HTTP transports. */
45
+ export declare function withArgNormalization(transport: Transport, shapeFor: (tool: string) => ArgShape | undefined, log?: (line: string) => void, onRename?: (tool: string, renames: number) => void, strict?: StrictArgOptions): Transport;
46
+ export interface ValidationEnvelopeOptions {
47
+ /** Membership against the REGISTERED tool set: the name on the frame is
48
+ * ultimately client-supplied, and it must never enter an envelope unchecked. */
49
+ isKnownTool?: (name: string) => boolean;
50
+ /** The account the call WOULD have run as. The registry injects the default
51
+ * AFTER validation, so a validation failure never got that far. */
52
+ defaultAccount?: () => string | undefined;
53
+ }
54
+ interface PendingCall {
55
+ tool: string;
56
+ account?: string;
57
+ }
58
+ export declare function validationEnvelope(tool: string, issues: string, account: string | undefined): {
59
+ error: string;
60
+ message: string;
61
+ hint: string;
62
+ retriable: boolean;
63
+ account: string;
64
+ };
65
+ /** Floor for the OTHER bare prose the same SDK catch produces: an output-schema
66
+ * failure. Nothing is known about the cause, so it says exactly that. */
67
+ export declare function outputFailureEnvelope(tool: string, text: string, account: string | undefined): {
68
+ error: string;
69
+ message: string;
70
+ hint: string;
71
+ retriable: boolean;
72
+ account: string;
73
+ };
74
+ export declare function rewriteValidationFrame(message: JSONRPCMessage, pending: Map<string | number, PendingCall>, opts: ValidationEnvelopeOptions): JSONRPCMessage;
75
+ export declare function withValidationEnvelope(transport: Transport, opts?: ValidationEnvelopeOptions): Transport;
76
+ export {};