mcp-google-multi 6.0.0-beta.1 → 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 (182) hide show
  1. package/README.md +5 -3
  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 +61 -4
  7. package/dist/arg-normalize.js +229 -4
  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/cimd-cache.d.ts +38 -0
  15. package/dist/cimd-cache.js +117 -0
  16. package/dist/client.d.ts +32 -2
  17. package/dist/client.js +0 -0
  18. package/dist/compose.d.ts +11 -0
  19. package/dist/compose.js +109 -0
  20. package/dist/config-file.d.ts +14 -0
  21. package/dist/config-file.js +41 -1
  22. package/dist/discover.js +38 -15
  23. package/dist/discovery-client.d.ts +11 -1
  24. package/dist/discovery-client.js +120 -18
  25. package/dist/doctor.d.ts +15 -3
  26. package/dist/doctor.js +151 -27
  27. package/dist/env-load.d.ts +11 -0
  28. package/dist/env-load.js +23 -1
  29. package/dist/executor.d.ts +3 -0
  30. package/dist/executor.js +16 -3
  31. package/dist/fanout.d.ts +9 -1
  32. package/dist/fanout.js +83 -14
  33. package/dist/fs-atomic.d.ts +11 -0
  34. package/dist/fs-atomic.js +219 -28
  35. package/dist/hint-provider.d.ts +129 -0
  36. package/dist/hint-provider.js +64 -0
  37. package/dist/http-transport.d.ts +73 -6
  38. package/dist/http-transport.js +350 -26
  39. package/dist/identity.d.ts +4 -1
  40. package/dist/identity.js +25 -6
  41. package/dist/index.js +142 -114
  42. package/dist/master-key.js +13 -5
  43. package/dist/mcp-token.d.ts +136 -19
  44. package/dist/mcp-token.js +569 -72
  45. package/dist/metrics-cli.d.ts +21 -0
  46. package/dist/metrics-cli.js +206 -0
  47. package/dist/metrics-tap.d.ts +12 -0
  48. package/dist/metrics-tap.js +95 -0
  49. package/dist/migrate-config.js +2 -2
  50. package/dist/oauth-as.d.ts +103 -3
  51. package/dist/oauth-as.js +374 -79
  52. package/dist/oauth-consent.d.ts +35 -10
  53. package/dist/oauth-consent.js +101 -39
  54. package/dist/outbound-allowlist.d.ts +34 -0
  55. package/dist/outbound-allowlist.js +111 -0
  56. package/dist/reauth-hint.d.ts +1 -1
  57. package/dist/reauth-hint.js +12 -8
  58. package/dist/registry-transfer.d.ts +5 -0
  59. package/dist/registry-transfer.js +65 -5
  60. package/dist/registry.d.ts +66 -2
  61. package/dist/registry.js +214 -18
  62. package/dist/scope-catalog.d.ts +11 -0
  63. package/dist/scope-catalog.js +39 -6
  64. package/dist/scope-observability.d.ts +8 -6
  65. package/dist/scope-observability.js +29 -13
  66. package/dist/services.d.ts +17 -4
  67. package/dist/services.js +77 -6
  68. package/dist/setup-prompt.d.ts +1 -1
  69. package/dist/setup-prompt.js +1 -1
  70. package/dist/ssrf-guard.d.ts +23 -9
  71. package/dist/ssrf-guard.js +101 -62
  72. package/dist/tenant-purge.d.ts +26 -0
  73. package/dist/tenant-purge.js +67 -0
  74. package/dist/token-store.d.ts +20 -2
  75. package/dist/token-store.js +0 -0
  76. package/dist/tools/_coerce.d.ts +5 -0
  77. package/dist/tools/_coerce.js +11 -0
  78. package/dist/tools/_errors.d.ts +34 -3
  79. package/dist/tools/_errors.js +346 -53
  80. package/dist/tools/_local-files.d.ts +9 -0
  81. package/dist/tools/_local-files.js +25 -2
  82. package/dist/tools/account-wizard.d.ts +34 -2
  83. package/dist/tools/account-wizard.js +223 -65
  84. package/dist/tools/accounts-tool.d.ts +7 -0
  85. package/dist/tools/accounts-tool.js +34 -18
  86. package/dist/tools/admin.d.ts +10 -1
  87. package/dist/tools/admin.js +21 -16
  88. package/dist/tools/analytics.d.ts +27 -0
  89. package/dist/tools/analytics.js +289 -0
  90. package/dist/tools/calendar.d.ts +9 -1
  91. package/dist/tools/calendar.js +93 -29
  92. package/dist/tools/chat.d.ts +10 -1
  93. package/dist/tools/chat.js +20 -12
  94. package/dist/tools/contacts.d.ts +2 -1
  95. package/dist/tools/contacts.js +74 -43
  96. package/dist/tools/docs.d.ts +2 -1
  97. package/dist/tools/docs.js +90 -84
  98. package/dist/tools/drive.d.ts +2 -1
  99. package/dist/tools/drive.js +197 -120
  100. package/dist/tools/forms.d.ts +10 -1
  101. package/dist/tools/forms.js +26 -19
  102. package/dist/tools/generated/_shared.d.ts +8 -1
  103. package/dist/tools/generated/_shared.js +18 -4
  104. package/dist/tools/generated/admin.d.ts +2 -1
  105. package/dist/tools/generated/admin.js +610 -479
  106. package/dist/tools/generated/analytics.d.ts +3 -0
  107. package/dist/tools/generated/analytics.js +981 -0
  108. package/dist/tools/generated/appsmarket.d.ts +2 -1
  109. package/dist/tools/generated/appsmarket.js +9 -9
  110. package/dist/tools/generated/calendar.d.ts +2 -1
  111. package/dist/tools/generated/calendar.js +83 -83
  112. package/dist/tools/generated/chat.d.ts +2 -1
  113. package/dist/tools/generated/chat.js +178 -151
  114. package/dist/tools/generated/classroom.d.ts +2 -1
  115. package/dist/tools/generated/classroom.js +467 -425
  116. package/dist/tools/generated/cloudidentity.d.ts +2 -1
  117. package/dist/tools/generated/cloudidentity.js +209 -201
  118. package/dist/tools/generated/cloudsearch.d.ts +2 -1
  119. package/dist/tools/generated/cloudsearch.js +139 -138
  120. package/dist/tools/generated/contacts.d.ts +2 -1
  121. package/dist/tools/generated/contacts.js +49 -40
  122. package/dist/tools/generated/drive.d.ts +2 -1
  123. package/dist/tools/generated/drive.js +133 -124
  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 +113 -85
  128. package/dist/tools/generated/forms.d.ts +2 -1
  129. package/dist/tools/generated/forms.js +14 -14
  130. package/dist/tools/generated/gmail.d.ts +2 -1
  131. package/dist/tools/generated/gmail.js +253 -226
  132. package/dist/tools/generated/groupsmigration.d.ts +2 -1
  133. package/dist/tools/generated/groupsmigration.js +4 -4
  134. package/dist/tools/generated/groupssettings.d.ts +2 -1
  135. package/dist/tools/generated/groupssettings.js +10 -10
  136. package/dist/tools/generated/index.d.ts +2 -1
  137. package/dist/tools/generated/index.js +2 -0
  138. package/dist/tools/generated/keep.d.ts +2 -1
  139. package/dist/tools/generated/keep.js +23 -22
  140. package/dist/tools/generated/licensing.d.ts +2 -1
  141. package/dist/tools/generated/licensing.js +54 -38
  142. package/dist/tools/generated/meet.d.ts +2 -1
  143. package/dist/tools/generated/meet.js +41 -41
  144. package/dist/tools/generated/method-map.d.ts +2 -0
  145. package/dist/tools/generated/method-map.js +894 -0
  146. package/dist/tools/generated/postmaster.d.ts +2 -1
  147. package/dist/tools/generated/postmaster.js +12 -12
  148. package/dist/tools/generated/reseller.d.ts +2 -1
  149. package/dist/tools/generated/reseller.js +69 -63
  150. package/dist/tools/generated/script.d.ts +2 -1
  151. package/dist/tools/generated/script.js +62 -52
  152. package/dist/tools/generated/searchconsole.d.ts +2 -1
  153. package/dist/tools/generated/searchconsole.js +8 -6
  154. package/dist/tools/generated/sheets.d.ts +2 -1
  155. package/dist/tools/generated/sheets.js +25 -24
  156. package/dist/tools/generated/tasks.d.ts +2 -1
  157. package/dist/tools/generated/tasks.js +15 -9
  158. package/dist/tools/generated/vault.d.ts +2 -1
  159. package/dist/tools/generated/vault.js +132 -123
  160. package/dist/tools/generated/workspaceevents.d.ts +2 -1
  161. package/dist/tools/generated/workspaceevents.js +48 -47
  162. package/dist/tools/gmail.d.ts +12 -2
  163. package/dist/tools/gmail.js +95 -51
  164. package/dist/tools/google-api.d.ts +2 -3
  165. package/dist/tools/google-api.js +76 -33
  166. package/dist/tools/meet.d.ts +10 -1
  167. package/dist/tools/meet.js +18 -11
  168. package/dist/tools/searchconsole.d.ts +2 -1
  169. package/dist/tools/searchconsole.js +27 -23
  170. package/dist/tools/sheets.d.ts +2 -1
  171. package/dist/tools/sheets.js +72 -80
  172. package/dist/tools/slides.d.ts +10 -1
  173. package/dist/tools/slides.js +23 -16
  174. package/dist/tools/tasks.d.ts +2 -1
  175. package/dist/tools/tasks.js +35 -31
  176. package/dist/trim.d.ts +26 -0
  177. package/dist/trim.js +67 -0
  178. package/dist/usage-metrics.d.ts +178 -0
  179. package/dist/usage-metrics.js +621 -0
  180. package/dist/write-control.d.ts +27 -1
  181. package/dist/write-control.js +99 -12
  182. package/package.json +34 -3
@@ -1,6 +1,24 @@
1
+ import { echoName, echoNames, screenArguments, unknownArgEnvelope, } from './arg-strict.js';
2
+ import { logSafe, sliceClean } from './trim.js';
3
+ // The SDK synthesizes input-validation failures as isError RESULTS, skipping
4
+ // the handler entirely: no handler of ours runs, so no envelope exists and the
5
+ // client gets free text. The text is prose in both SDK eras (v1 prefixed it
6
+ // with "MCP error -32602: ", v2 dropped that prefix), while handler envelopes
7
+ // are always JSON starting with "{".
8
+ const SCHEMA_VALIDATION_TEXT = /^(MCP error -32602: )?Input validation error:/;
9
+ export function isSchemaValidationText(text) {
10
+ return SCHEMA_VALIDATION_TEXT.test(text);
11
+ }
12
+ /** Capture form of the v2 text. The tool-name class excludes ':' and ' ', so
13
+ * the first ": " after the name is unambiguously the delimiter. */
14
+ const VALIDATION_PROSE = /^(?:MCP error -32602: )?Input validation error: Invalid arguments for tool ([A-Za-z0-9_.-]+): ([\s\S]+)$/;
15
+ /** The SDK's OTHER synthesized prose on the same catch. Both prefixes are
16
+ * SDK-authored; a HANDLER's free text is not, and must survive untouched
17
+ * (the account wizard answers in prose today). */
18
+ const SDK_OUTPUT_PROSE = /^(?:MCP error -32602: )?Output validation error: /;
1
19
  // Wire-level tools/call argument normalization. Clients (LLMs) recurringly
2
20
  // snake_case a camelCase parameter (thread_id for threadId) and burn a retry
3
- // on the -32602. A schema-level fix is off the table: SDK 1.x advertises an
21
+ // on the -32602. A schema-level fix is off the table: the SDK advertises an
4
22
  // EMPTY input schema for any non-object wrapper (pipe/preprocess), so the
5
23
  // only seam that keeps tools/list intact is the JSON-RPC message itself —
6
24
  // which is versioned MCP spec, stabler than any SDK internal. The rename is
@@ -36,7 +54,7 @@ export function normalizeCallArguments(shape, args) {
36
54
  }
37
55
  return { args: out ?? args, renamed };
38
56
  }
39
- export function normalizeMessage(msg, shapeFor, log = (l) => process.stderr.write(`${l}\n`)) {
57
+ export function normalizeMessage(msg, shapeFor, log = (l) => process.stderr.write(`${l}\n`), onRename) {
40
58
  const m = msg;
41
59
  if (m.method !== 'tools/call' || typeof m.params?.name !== 'string')
42
60
  return msg;
@@ -51,16 +69,85 @@ export function normalizeMessage(msg, shapeFor, log = (l) => process.stderr.writ
51
69
  return msg;
52
70
  // Key names only — argument VALUES never reach the log.
53
71
  log(`[args] ${m.params.name}: ${renamed.map(([f, t]) => `${f} -> ${t}`).join(', ')}`);
72
+ try {
73
+ onRename?.(m.params.name, renamed.length);
74
+ }
75
+ catch { /* observers never break dispatch */ }
54
76
  return {
55
77
  ...msg,
56
78
  params: { ...m.params, arguments: normalized },
57
79
  };
58
80
  }
81
+ /**
82
+ * Screen a normalized tools/call for undeclared keys. `warn` forwards exactly
83
+ * as before and only reports; `reject` answers with the taxonomy envelope and
84
+ * never reaches the handler, so nothing is sent to Google on a guess.
85
+ */
86
+ export function screenMessage(msg, opts, log = (l) => process.stderr.write(`${l}\n`)) {
87
+ if (opts.mode === 'off')
88
+ return { action: 'forward', msg };
89
+ const m = msg;
90
+ if (m.method !== 'tools/call' || typeof m.params?.name !== 'string')
91
+ return { action: 'forward', msg };
92
+ const args = m.params.arguments;
93
+ if (!args || typeof args !== 'object' || Array.isArray(args))
94
+ return { action: 'forward', msg };
95
+ const tool = m.params.name;
96
+ const declared = opts.declaredFor(tool);
97
+ // Unregistered tool. The spec keeps this a PROTOCOL error (unknown tool is
98
+ // not something the model can fix by adjusting arguments), so the channel
99
+ // stays a JSON-RPC error; only the message gets the did-you-mean and the
100
+ // gated-service next step. Without `unknownTool`, or on a notification,
101
+ // the SDK's own "not found" still answers.
102
+ if (!declared) {
103
+ if (!opts.unknownTool || m.id === undefined)
104
+ return { action: 'forward', msg };
105
+ return {
106
+ action: 'reject',
107
+ response: {
108
+ jsonrpc: '2.0',
109
+ id: m.id,
110
+ error: { code: -32602, message: opts.unknownTool(tool) },
111
+ },
112
+ };
113
+ }
114
+ const screened = screenArguments(tool, args, declared);
115
+ if (screened.unknown.length === 0 && screened.redundant.length === 0)
116
+ return { action: 'forward', msg };
117
+ const all = [...screened.unknown.map((u) => u.sent), ...screened.redundant];
118
+ // Key names only; argument VALUES never reach the log. The names are the
119
+ // caller's, so they are escaped here and only here: the envelope is JSON.
120
+ log(`[args] ${tool}: undeclared ${logSafe(echoNames(all), 600)}${opts.mode === 'warn' ? ' (dropped)' : ' (rejected)'}`);
121
+ try {
122
+ // Only ever a DECLARED key or the literal placeholder, so the metrics
123
+ // closed-vocabulary rule holds: the caller's key is never persisted.
124
+ opts.onDrop?.(tool, screened.unknown.map((u) => u.suggestions[0] ?? '_unmatched'));
125
+ }
126
+ catch { /* observers never break dispatch */ }
127
+ if (opts.mode === 'warn' || screened.unknown.length === 0)
128
+ return { action: 'forward', msg };
129
+ // Nothing to answer (a malformed notification): dispatch as before.
130
+ if (m.id === undefined)
131
+ return { action: 'forward', msg };
132
+ const account = args.account;
133
+ const siblings = screened.unknown.some((u) => u.suggestions.length > 0)
134
+ ? []
135
+ : (opts.siblingsFor?.(tool, screened.unknown.map((u) => u.sent)) ?? []);
136
+ const envelope = unknownArgEnvelope(tool, screened.unknown, declared, typeof account === 'string' ? echoName(account) : undefined, siblings);
137
+ return {
138
+ action: 'reject',
139
+ response: {
140
+ jsonrpc: '2.0',
141
+ id: m.id,
142
+ result: { content: [{ type: 'text', text: JSON.stringify(envelope) }], isError: true },
143
+ },
144
+ };
145
+ }
59
146
  /** Wrap a server-side transport so tools/call argument keys are normalized
60
147
  * before the SDK validates them. The Protocol assigns `onmessage` during
61
148
  * connect(); the interceptor lives in that setter, so the wrapper works
62
149
  * identically for stdio and (per-request, stateless) HTTP transports. */
63
- export function withArgNormalization(transport, shapeFor, log) {
150
+ export function withArgNormalization(transport, shapeFor, log, onRename, strict) {
64
151
  const wrapper = {
65
152
  start: () => transport.start(),
66
153
  send: (message, options) => transport.send(message, options),
@@ -70,7 +157,145 @@ export function withArgNormalization(transport, shapeFor, log) {
70
157
  get: () => transport.onmessage,
71
158
  set: (handler) => {
72
159
  transport.onmessage = handler
73
- ? (message, extra) => handler(normalizeMessage(message, shapeFor, log), extra)
160
+ ? (message, extra) => {
161
+ // Rename FIRST: a snake_case twin of a declared key is a fix, not
162
+ // an unknown argument, so it must never reach the screen.
163
+ const normalized = normalizeMessage(message, shapeFor, log, onRename);
164
+ if (!strict)
165
+ return handler(normalized, extra);
166
+ const outcome = screenMessage(normalized, strict, log);
167
+ if (outcome.action === 'forward')
168
+ return handler(outcome.msg, extra);
169
+ // Last-resort fallback: dispatch as before rather than hang the
170
+ // caller. The stderr line and the counter already fired above.
171
+ // `send` is async, so a rejected promise needs catching too: a
172
+ // bare `void` left the client waiting for a frame that never came.
173
+ const fallback = () => handler(normalized, extra);
174
+ try {
175
+ // Answer on the INNER transport so the metrics tap still sees the
176
+ // frame and clears its pending id.
177
+ void Promise.resolve(transport.send(outcome.response)).catch(fallback);
178
+ }
179
+ catch {
180
+ fallback();
181
+ }
182
+ }
183
+ : undefined;
184
+ },
185
+ });
186
+ for (const prop of ['onclose', 'onerror']) {
187
+ Object.defineProperty(wrapper, prop, {
188
+ get: () => transport[prop],
189
+ set: (v) => {
190
+ transport[prop] = v;
191
+ },
192
+ });
193
+ }
194
+ Object.defineProperty(wrapper, 'sessionId', { get: () => transport.sessionId });
195
+ if (transport.setProtocolVersion) {
196
+ wrapper.setProtocolVersion = (v) => transport.setProtocolVersion(v);
197
+ }
198
+ // v2-only, called by Protocol.connect() before start(). A no-op today (both
199
+ // sides default to the same exported constant), but a proxy that silently
200
+ // eats a member the SDK calls is a bug waiting for the first caller that
201
+ // passes supportedProtocolVersions explicitly.
202
+ if (transport.setSupportedProtocolVersions) {
203
+ wrapper.setSupportedProtocolVersions = (v) => transport.setSupportedProtocolVersions(v);
204
+ }
205
+ return wrapper;
206
+ }
207
+ const MAX_ACCOUNT_CHARS = 64;
208
+ const MAX_ISSUE_CHARS = 800;
209
+ const MAX_PROSE_CHARS = 500;
210
+ const ENVELOPE_PENDING_CAP = 1_000;
211
+ export function validationEnvelope(tool, issues, account) {
212
+ return {
213
+ error: 'validation_error',
214
+ message: `${tool} arguments failed validation: ${sliceClean(issues, MAX_ISSUE_CHARS)}`,
215
+ hint: `Fix the argument named in the message and call again. This tool's inputSchema in tools/list lists every accepted argument, its type and its allowed values. Nothing was sent to Google.`,
216
+ retriable: false,
217
+ account: account ?? '',
218
+ };
219
+ }
220
+ /** Floor for the OTHER bare prose the same SDK catch produces: an output-schema
221
+ * failure. Nothing is known about the cause, so it says exactly that. */
222
+ export function outputFailureEnvelope(tool, text, account) {
223
+ return {
224
+ error: 'internal',
225
+ message: `${tool} returned a result the protocol rejected: ${sliceClean(text, MAX_PROSE_CHARS)}`,
226
+ hint: 'A server-side failure, not a rejected argument. Retry once; if it repeats, the message above is the signal to report.',
227
+ retriable: false,
228
+ account: account ?? '',
229
+ };
230
+ }
231
+ export function rewriteValidationFrame(message, pending, opts) {
232
+ const m = message;
233
+ // Responses only. A server-initiated REQUEST (elicitation, sampling) also
234
+ // carries an id, from a different id space, and evicting on it would drop a
235
+ // real pending call.
236
+ if (m.id === undefined || m.method !== undefined)
237
+ return message;
238
+ const call = pending.get(m.id);
239
+ pending.delete(m.id);
240
+ if (m.result?.isError !== true)
241
+ return message;
242
+ const content = m.result.content;
243
+ if (!content || content.length !== 1 || content[0]?.type !== 'text')
244
+ return message;
245
+ const text = content[0].text;
246
+ if (typeof text !== 'string' || text.startsWith('{'))
247
+ return message;
248
+ const parsed = isSchemaValidationText(text) ? VALIDATION_PROSE.exec(text) : null;
249
+ // Anything the SDK did not author is a handler's own answer. Leave it alone.
250
+ if (!parsed && !SDK_OUTPUT_PROSE.test(text))
251
+ return message;
252
+ const tool = parsed?.[1] ?? call?.tool;
253
+ if (!tool || (opts.isKnownTool && !opts.isKnownTool(tool)))
254
+ return message;
255
+ const account = call?.account ?? opts.defaultAccount?.();
256
+ const envelope = parsed
257
+ ? validationEnvelope(tool, parsed[2], account)
258
+ : outputFailureEnvelope(tool, text, account);
259
+ return {
260
+ ...message,
261
+ result: { ...m.result, content: [{ type: 'text', text: JSON.stringify(envelope) }] },
262
+ };
263
+ }
264
+ export function withValidationEnvelope(transport, opts = {}) {
265
+ const pending = new Map();
266
+ const wrapper = {
267
+ start: () => transport.start(),
268
+ send: (message, options) => {
269
+ let out = message;
270
+ try {
271
+ out = rewriteValidationFrame(message, pending, opts);
272
+ }
273
+ catch { /* the seam may never break the wire */ }
274
+ return transport.send(out, options);
275
+ },
276
+ close: () => transport.close(),
277
+ };
278
+ Object.defineProperty(wrapper, 'onmessage', {
279
+ get: () => transport.onmessage,
280
+ set: (handler) => {
281
+ transport.onmessage = handler
282
+ ? (message, extra) => {
283
+ try {
284
+ const m = message;
285
+ if (m.method === 'tools/call' && m.id !== undefined && typeof m.params?.name === 'string') {
286
+ if (pending.size >= ENVELOPE_PENDING_CAP)
287
+ pending.clear();
288
+ const args = m.params.arguments;
289
+ const raw = args && typeof args === 'object' ? args.account : undefined;
290
+ pending.set(m.id, {
291
+ tool: m.params.name,
292
+ account: typeof raw === 'string' ? sliceClean(raw, MAX_ACCOUNT_CHARS) : undefined,
293
+ });
294
+ }
295
+ }
296
+ catch { /* never break dispatch */ }
297
+ handler(message, extra);
298
+ }
74
299
  : undefined;
75
300
  },
76
301
  });
@@ -0,0 +1,61 @@
1
+ export type UnknownArgMode = 'reject' | 'warn' | 'off';
2
+ /**
3
+ * `GOOGLE_ARG_UNKNOWN`: reject | warn | off. A typo in the VALUE still falls
4
+ * back to `warn`, not to the default: an operator who misspells the setting
5
+ * has said nothing about which behavior they want, and guessing the strict
6
+ * one would turn a config typo into failing tool calls.
7
+ */
8
+ export declare function unknownArgMode(env?: NodeJS.ProcessEnv): UnknownArgMode;
9
+ /** Tools that legitimately accept open-ended top-level keys. Empty at 6.0.0:
10
+ * the escape hatch is NOT one, because its open-endedness lives in the VALUES
11
+ * of queryParams/body, never in its six fixed top-level keys. */
12
+ export declare const STRICT_EXEMPT_TOOLS: ReadonlySet<string>;
13
+ /** Metadata a client may legitimately attach. Measured against all registered
14
+ * tools: no declared key starts with `_` or contains `/`, so neither rule can
15
+ * shadow a real parameter. */
16
+ export declare function isExemptKey(key: string): boolean;
17
+ /** A call can carry thousands of unknown keys, or one of megabytes: only the
18
+ * first few earn a did-you-mean, and the log line and the envelope echo a
19
+ * bounded prefix of each name. The reject decision itself is never capped. */
20
+ export declare const MAX_SUGGESTED_KEYS = 8;
21
+ export declare function echoName(name: string): string;
22
+ /** The first MAX_SUGGESTED_KEYS names, each capped, then "and N more". */
23
+ export declare function echoNames(names: readonly string[], quote?: boolean): string;
24
+ /**
25
+ * Rank declared keys for an unknown key. Tiers, best non-empty tier wins:
26
+ * 0 same key modulo case and separators
27
+ * 1 every token of the unknown key appears in the declared key
28
+ * 2 substring containment of the flattened forms
29
+ * 3 a genuine typo by edit distance
30
+ * Plain edit distance alone cannot carry this: parentid -> parentfolderid is
31
+ * distance 6, well past any sane threshold, which is why tier 1 exists.
32
+ */
33
+ export declare function suggestKeys(unknown: string, declared: readonly string[], limit?: number): string[];
34
+ export interface ScreenedKey {
35
+ /** the key the caller sent; for stderr and the agent-facing hint only */
36
+ sent: string;
37
+ /** best suggestions, always declared keys of the tool being called */
38
+ suggestions: string[];
39
+ }
40
+ export interface ScreenResult {
41
+ unknown: ScreenedKey[];
42
+ /** dropped exactly as before: the caller also sent the declared key */
43
+ redundant: string[];
44
+ }
45
+ /**
46
+ * Split a call's post-rename keys into declared, redundant-duplicate and
47
+ * genuinely unknown. `declared` is the tool's full declared key list.
48
+ */
49
+ export declare function screenArguments(tool: string, args: Record<string, unknown>, declared: readonly string[]): ScreenResult;
50
+ export interface SiblingSpelling {
51
+ key: string;
52
+ tools: string[];
53
+ }
54
+ /** The agent-facing envelope. Values are never included, only key names. */
55
+ export declare function unknownArgEnvelope(tool: string, unknown: ScreenedKey[], declared: readonly string[], account: string | undefined, siblings?: SiblingSpelling[]): {
56
+ error: string;
57
+ message: string;
58
+ hint: string;
59
+ retriable: boolean;
60
+ account?: string;
61
+ };
@@ -0,0 +1,193 @@
1
+ // Unknown-argument screening (backlog item 14). zod strips undeclared keys
2
+ // before a handler runs, so a misremembered argument name used to produce a
3
+ // SUCCESS response with wrong behavior: `parentId` on drive_create_folder was
4
+ // dropped and the folder landed in My Drive root. Field-reported, then
5
+ // reproduced: the typo, the correct name and a wholly invented key all
6
+ // returned byte-identical responses.
7
+ //
8
+ // This module is pure and holds no registry reference. It never rewrites a key
9
+ // and never touches a VALUE: it only decides that a key is undeclared, and
10
+ // suggests what the caller probably meant. Rewriting would risk sending a real
11
+ // value to Google on a guess, which is strictly worse than refusing.
12
+ import { editDistance, SUGGEST_MAX_INPUT } from './scope-catalog.js';
13
+ import { sliceClean } from './trim.js';
14
+ /**
15
+ * `GOOGLE_ARG_UNKNOWN`: reject | warn | off. A typo in the VALUE still falls
16
+ * back to `warn`, not to the default: an operator who misspells the setting
17
+ * has said nothing about which behavior they want, and guessing the strict
18
+ * one would turn a config typo into failing tool calls.
19
+ */
20
+ export function unknownArgMode(env = process.env) {
21
+ const raw = (env.GOOGLE_ARG_UNKNOWN ?? '').trim().toLowerCase();
22
+ if (raw === '')
23
+ return DEFAULT_MODE;
24
+ if (raw === 'reject' || raw === 'warn' || raw === 'off')
25
+ return raw;
26
+ process.stderr.write(`GOOGLE_ARG_UNKNOWN="${raw}" is not valid (reject | warn | off); using warn\n`);
27
+ return 'warn';
28
+ }
29
+ // The staged rollout is over: `warn` shipped first as pure observability, and
30
+ // this is the flip it was staging for. An undeclared argument is dropped by
31
+ // zod before the handler runs, so `warn` means the CLIENT sees a confident
32
+ // success for a call the server did not perform: drive_list with a misspelled
33
+ // folder key returned the My Drive root, byte-identical to no argument at all.
34
+ // A wrong answer that reads as right is the failure mode agents recover from
35
+ // worst, so the default refuses the call and says what it probably meant.
36
+ const DEFAULT_MODE = 'reject';
37
+ /** Tools that legitimately accept open-ended top-level keys. Empty at 6.0.0:
38
+ * the escape hatch is NOT one, because its open-endedness lives in the VALUES
39
+ * of queryParams/body, never in its six fixed top-level keys. */
40
+ export const STRICT_EXEMPT_TOOLS = new Set();
41
+ /** Keys a CLIENT adds on its own, not keys the model chose. `random_string`
42
+ * is the long-standing probe some clients send to a tool they read as taking
43
+ * no arguments; the others are call-plumbing that bridges and proxies have
44
+ * been observed to fold into `arguments` instead of `params._meta`, where the
45
+ * spec puts them. Rejecting these would fail a call the model got right.
46
+ * Measured against the full 978-tool surface: none is a declared key. */
47
+ const CLIENT_ARTIFACT_KEYS = new Set([
48
+ 'random_string',
49
+ 'toolCallId',
50
+ 'tool_call_id',
51
+ 'tool_call_description',
52
+ ]);
53
+ /** Metadata a client may legitimately attach. Measured against all registered
54
+ * tools: no declared key starts with `_` or contains `/`, so neither rule can
55
+ * shadow a real parameter. */
56
+ export function isExemptKey(key) {
57
+ return key.startsWith('_') || key.includes('/') || CLIENT_ARTIFACT_KEYS.has(key);
58
+ }
59
+ /** Generic words that must never be offered as a suggestion on containment
60
+ * alone; they match far too much to be useful. */
61
+ const GENERIC = new Set(['id', 'name', 'title', 'body', 'parent', 'text', 'content', 'type', 'value', 'key', 'data']);
62
+ const flatten = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
63
+ /** A call can carry thousands of unknown keys, or one of megabytes: only the
64
+ * first few earn a did-you-mean, and the log line and the envelope echo a
65
+ * bounded prefix of each name. The reject decision itself is never capped. */
66
+ export const MAX_SUGGESTED_KEYS = 8;
67
+ const MAX_ECHO_CHARS = 64;
68
+ export function echoName(name) {
69
+ return name.length > MAX_ECHO_CHARS ? `${sliceClean(name, MAX_ECHO_CHARS)}...` : name;
70
+ }
71
+ /** The first MAX_SUGGESTED_KEYS names, each capped, then "and N more". */
72
+ export function echoNames(names, quote = false) {
73
+ const shown = names.slice(0, MAX_SUGGESTED_KEYS).map((n) => (quote ? `"${echoName(n)}"` : echoName(n)));
74
+ const rest = names.length - shown.length;
75
+ return rest > 0 ? `${shown.join(', ')} and ${rest} more` : shown.join(', ');
76
+ }
77
+ function tokens(s) {
78
+ return s
79
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
80
+ .split(/[\s_\-.]+/)
81
+ .map((t) => t.toLowerCase())
82
+ .filter(Boolean);
83
+ }
84
+ /**
85
+ * Rank declared keys for an unknown key. Tiers, best non-empty tier wins:
86
+ * 0 same key modulo case and separators
87
+ * 1 every token of the unknown key appears in the declared key
88
+ * 2 substring containment of the flattened forms
89
+ * 3 a genuine typo by edit distance
90
+ * Plain edit distance alone cannot carry this: parentid -> parentfolderid is
91
+ * distance 6, well past any sane threshold, which is why tier 1 exists.
92
+ */
93
+ export function suggestKeys(unknown, declared, limit = 2) {
94
+ if (unknown.length > SUGGEST_MAX_INPUT)
95
+ return [];
96
+ const uFlat = flatten(unknown);
97
+ const uTok = tokens(unknown);
98
+ const tiers = [[], [], [], []];
99
+ for (const key of declared) {
100
+ const kFlat = flatten(key);
101
+ const kTok = tokens(key);
102
+ if (kFlat === uFlat) {
103
+ tiers[0].push({ key, rank: 0 });
104
+ continue;
105
+ }
106
+ if (uTok.length >= 2 && uTok.every((t) => kTok.includes(t))) {
107
+ tiers[1].push({ key, rank: Math.abs(kTok.length - uTok.length) });
108
+ continue;
109
+ }
110
+ if (uFlat.length >= 4 && !GENERIC.has(kFlat) && (kFlat.includes(uFlat) || uFlat.includes(kFlat))) {
111
+ tiers[2].push({ key, rank: Math.abs(kFlat.length - uFlat.length) });
112
+ continue;
113
+ }
114
+ const maxDist = Math.max(1, Math.floor(uFlat.length / 4));
115
+ // The distance is at least the length gap: skip the DP when that alone fails.
116
+ if (Math.abs(uFlat.length - kFlat.length) > maxDist)
117
+ continue;
118
+ const d = editDistance(uFlat, kFlat);
119
+ if (d <= maxDist)
120
+ tiers[3].push({ key, rank: d });
121
+ }
122
+ const tier = tiers.find((t) => t.length > 0);
123
+ if (!tier)
124
+ return [];
125
+ // Stable: declaration order breaks rank ties.
126
+ return tier
127
+ .map((e, i) => ({ ...e, i }))
128
+ .sort((a, b) => a.rank - b.rank || a.i - b.i)
129
+ .slice(0, limit)
130
+ .map((e) => e.key);
131
+ }
132
+ /**
133
+ * Split a call's post-rename keys into declared, redundant-duplicate and
134
+ * genuinely unknown. `declared` is the tool's full declared key list.
135
+ */
136
+ export function screenArguments(tool, args, declared) {
137
+ const out = { unknown: [], redundant: [] };
138
+ if (STRICT_EXEMPT_TOOLS.has(tool) || declared.length === 0)
139
+ return out;
140
+ const declaredSet = new Set(declared);
141
+ // Tier 0 (same key modulo case and separators) is a map lookup, so every key
142
+ // gets it and a redundant twin is recognised past the ranking budget too.
143
+ // A near-miss duplicate (a typo sent next to its declared key) is recognised
144
+ // only within the budget: a call needs 9 of them before one is refused.
145
+ const byFlat = new Map();
146
+ for (const k of declared)
147
+ byFlat.set(flatten(k), [...(byFlat.get(flatten(k)) ?? []), k]);
148
+ let ranked = 0;
149
+ for (const key of Object.keys(args)) {
150
+ if (declaredSet.has(key) || isExemptKey(key))
151
+ continue;
152
+ const twins = key.length <= SUGGEST_MAX_INPUT ? byFlat.get(flatten(key)) : undefined;
153
+ const suggestions = twins ? twins.slice(0, 2) : ranked++ < MAX_SUGGESTED_KEYS ? suggestKeys(key, declared) : [];
154
+ // The caller sent both spellings, so the declared one already won and the
155
+ // outcome is what it has always been. Never fail a call that works today.
156
+ if (suggestions.some((s) => s in args)) {
157
+ out.redundant.push(key);
158
+ continue;
159
+ }
160
+ out.unknown.push({ sent: key, suggestions });
161
+ }
162
+ return out;
163
+ }
164
+ /** The agent-facing envelope. Values are never included, only key names. */
165
+ export function unknownArgEnvelope(tool, unknown, declared, account, siblings = []) {
166
+ const names = echoNames(unknown.map((u) => u.sent), true);
167
+ const accepts = `This tool accepts: ${declared.join(', ')}.`;
168
+ const suggested = unknown.flatMap((u) => u.suggestions);
169
+ let hint;
170
+ if (suggested.length > 0) {
171
+ const did = unknown
172
+ .filter((u) => u.suggestions.length > 0)
173
+ .map((u) => u.suggestions.map((s) => `"${s}"`).join(' or '))
174
+ .join(', ');
175
+ hint = `Did you mean ${did}? ${accepts}`;
176
+ }
177
+ else if (siblings.length > 0) {
178
+ const spelled = siblings
179
+ .map((s) => `"${s.key}" (${s.tools.slice(0, 3).join(', ')})`)
180
+ .join(' and ');
181
+ hint = `${accepts} Other tools in this service spell a similar argument ${spelled}.`;
182
+ }
183
+ else {
184
+ hint = accepts;
185
+ }
186
+ return {
187
+ error: 'unknown_argument',
188
+ message: `${tool} does not accept ${names}. Nothing was sent to Google.`,
189
+ hint,
190
+ retriable: false,
191
+ ...(account !== undefined ? { account } : {}),
192
+ };
193
+ }
package/dist/auth.d.ts CHANGED
@@ -1,17 +1,18 @@
1
+ import type { AccountSet } from './accounts.js';
1
2
  import { ADMIN_SCOPES } from './scope-catalog.js';
2
3
  export declare const BASE_SCOPES: string[];
3
4
  export declare const OPTIONAL_SCOPE_BUNDLES: Record<string, string[]>;
4
5
  export { ADMIN_SCOPES };
5
6
  /** Union of every account's resolved bundles: a service registers if ANY
6
7
  * account can authorize it; per-account authz happens at call time (BR2). */
7
- export declare function getOptionalBundles(): string[];
8
+ export declare function getOptionalBundles(set?: AccountSet): string[];
8
9
  /** Aliases granted ADMIN_SCOPES: per-account admin flag (env
9
10
  * GOOGLE_ADMIN_ACCOUNTS overrides config.json at resolve) OR the account's
10
11
  * scope profile carrying admin (boolean or "admin" bundle) — equivalent forms. */
11
- export declare function getAdminAccounts(): string[];
12
+ export declare function getAdminAccounts(set?: AccountSet): string[];
12
13
  /** Scopes are fixed at consent time: changing an account's profile (or the
13
14
  * legacy env) changes its consent set and requires re-running auth. Evaluated
14
15
  * per account: `work` can carry admin + gmail_settings while `personal` is
15
16
  * never asked for them. */
16
- export declare function resolveScopesForAccount(alias: string): string[];
17
+ export declare function resolveScopesForAccount(alias: string, set?: AccountSet): string[];
17
18
  export declare function runAuthFlow(args: string[]): Promise<void>;