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
@@ -1,13 +1,17 @@
1
1
  import { z } from 'zod';
2
2
  import { slides as slidesClient } from '@googleapis/slides';
3
- import { accountAliasSchema } from '../accounts.js';
3
+ import { accountArgLive } from '../accounts.js';
4
4
  import { getClient } from '../client.js';
5
5
  import { handleGoogleApiError } from './_errors.js';
6
6
  import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
7
7
  import { sliceClean } from '../trim.js';
8
- const accountEnum = accountAliasSchema.optional();
9
8
  const SLIDE_TEXT_DIGEST_CHARS = 200;
10
- export function registerSlidesTools(server) {
9
+ export function registerSlidesTools(server, deps = {}) {
10
+ // Per-registry, LIVE account enum + injectable client (S1.10): the
11
+ // schema follows the registry's account view at parse time, and the
12
+ // custody path is the context's, not the process global.
13
+ const accountEnum = accountArgLive(() => server.accountAliases()).optional();
14
+ const getClientFn = deps.getClientFn ?? getClient;
11
15
  server.registerTool('slides_create', {
12
16
  description: 'Create a new Google Slides presentation (starts with one blank slide)',
13
17
  inputSchema: {
@@ -16,7 +20,7 @@ export function registerSlidesTools(server) {
16
20
  },
17
21
  }, async ({ account, title }) => {
18
22
  try {
19
- const auth = await getClient(account);
23
+ const auth = await getClientFn(account);
20
24
  const slides = slidesClient({ version: 'v1', auth });
21
25
  const res = await slides.presentations.create({ requestBody: { title } });
22
26
  return {
@@ -41,7 +45,7 @@ export function registerSlidesTools(server) {
41
45
  },
42
46
  }, async ({ account, presentationId, full }) => {
43
47
  try {
44
- const auth = await getClient(account);
48
+ const auth = await getClientFn(account);
45
49
  const slides = slidesClient({ version: 'v1', auth });
46
50
  const res = await slides.presentations.get({ presentationId });
47
51
  const payload = full ? res.data : summarizePresentation(res.data);
@@ -62,7 +66,7 @@ export function registerSlidesTools(server) {
62
66
  },
63
67
  }, async ({ account, presentationId, pageObjectId }) => {
64
68
  try {
65
- const auth = await getClient(account);
69
+ const auth = await getClientFn(account);
66
70
  const slides = slidesClient({ version: 'v1', auth });
67
71
  const res = await slides.presentations.pages.get({ presentationId, pageObjectId });
68
72
  return {
@@ -85,7 +89,7 @@ export function registerSlidesTools(server) {
85
89
  },
86
90
  }, async ({ account, presentationId, pageObjectId, mimeType, thumbnailSize }) => {
87
91
  try {
88
- const auth = await getClient(account);
92
+ const auth = await getClientFn(account);
89
93
  const slides = slidesClient({ version: 'v1', auth });
90
94
  const res = await slides.presentations.pages.getThumbnail({
91
95
  presentationId,
@@ -116,7 +120,7 @@ export function registerSlidesTools(server) {
116
120
  },
117
121
  }, async ({ account, presentationId, requests, writeControl }) => {
118
122
  try {
119
- const auth = await getClient(account);
123
+ const auth = await getClientFn(account);
120
124
  const slides = slidesClient({ version: 'v1', auth });
121
125
  const res = await slides.presentations.batchUpdate({
122
126
  presentationId,
@@ -180,6 +184,9 @@ export function summarizePresentation(p) {
180
184
  hint: 'Pass full:true for the raw Presentation JSON, or slides_page_get for one slide.',
181
185
  };
182
186
  }
183
- function handleSlidesError(error, account) {
184
- return handleGoogleApiError(error, account, "Slides tools require the optional \"slides\" scope bundle. Add GOOGLE_OPTIONAL_SCOPES=slides (or include \"slides\" in the list) and re-auth.");
187
+ export function handleSlidesError(error, account) {
188
+ return handleGoogleApiError(error, account, {
189
+ scope: "Slides tools require the optional \"slides\" scope bundle. Add GOOGLE_OPTIONAL_SCOPES=slides (or include \"slides\" in the list) and re-auth.",
190
+ bundle: 'slides',
191
+ });
185
192
  }
@@ -1,2 +1,3 @@
1
1
  import type { ToolRegistry } from '../registry.js';
2
- export declare function registerTasksTools(server: ToolRegistry): void;
2
+ import { type CuratedToolDeps } from '../client.js';
3
+ export declare function registerTasksTools(server: ToolRegistry, deps?: CuratedToolDeps): void;
@@ -1,11 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  import { coerceBoolean } from './_coerce.js';
3
3
  import { tasks as tasksClient } from '@googleapis/tasks';
4
- import { accountAliasSchema } from '../accounts.js';
4
+ import { accountArgLive } from '../accounts.js';
5
5
  import { getClient } from '../client.js';
6
6
  import { handleGoogleApiError, invalidParams } from './_errors.js';
7
- const accountEnum = accountAliasSchema.optional();
8
- export function registerTasksTools(server) {
7
+ export function registerTasksTools(server, deps = {}) {
8
+ // Per-registry, LIVE account enum + injectable client (S1.10): the
9
+ // schema follows the registry's account view at parse time, and the
10
+ // custody path is the context's, not the process global.
11
+ const accountEnum = accountArgLive(() => server.accountAliases()).optional();
12
+ const getClientFn = deps.getClientFn ?? getClient;
9
13
  // ─── Tasklists ─────────────────────────────────────────────────────────
10
14
  server.registerTool('tasks_lists_list', {
11
15
  description: 'List all tasklists (the user\'s top-level task containers)',
@@ -16,7 +20,7 @@ export function registerTasksTools(server) {
16
20
  },
17
21
  }, async ({ account, maxResults, pageToken }) => {
18
22
  try {
19
- const auth = await getClient(account);
23
+ const auth = await getClientFn(account);
20
24
  const tasks = tasksClient({ version: 'v1', auth });
21
25
  const res = await tasks.tasklists.list({
22
26
  maxResults: maxResults ?? 100,
@@ -38,7 +42,7 @@ export function registerTasksTools(server) {
38
42
  },
39
43
  }, async ({ account, tasklistId }) => {
40
44
  try {
41
- const auth = await getClient(account);
45
+ const auth = await getClientFn(account);
42
46
  const tasks = tasksClient({ version: 'v1', auth });
43
47
  const res = await tasks.tasklists.get({ tasklist: tasklistId });
44
48
  return {
@@ -57,7 +61,7 @@ export function registerTasksTools(server) {
57
61
  },
58
62
  }, async ({ account, title }) => {
59
63
  try {
60
- const auth = await getClient(account);
64
+ const auth = await getClientFn(account);
61
65
  const tasks = tasksClient({ version: 'v1', auth });
62
66
  const res = await tasks.tasklists.insert({
63
67
  requestBody: { title },
@@ -79,7 +83,7 @@ export function registerTasksTools(server) {
79
83
  },
80
84
  }, async ({ account, tasklistId, title }) => {
81
85
  try {
82
- const auth = await getClient(account);
86
+ const auth = await getClientFn(account);
83
87
  const tasks = tasksClient({ version: 'v1', auth });
84
88
  const res = await tasks.tasklists.patch({
85
89
  tasklist: tasklistId,
@@ -101,7 +105,7 @@ export function registerTasksTools(server) {
101
105
  },
102
106
  }, async ({ account, tasklistId }) => {
103
107
  try {
104
- const auth = await getClient(account);
108
+ const auth = await getClientFn(account);
105
109
  const tasks = tasksClient({ version: 'v1', auth });
106
110
  await tasks.tasklists.delete({ tasklist: tasklistId });
107
111
  return {
@@ -132,7 +136,7 @@ export function registerTasksTools(server) {
132
136
  },
133
137
  }, async ({ account, tasklistId, ...params }) => {
134
138
  try {
135
- const auth = await getClient(account);
139
+ const auth = await getClientFn(account);
136
140
  const tasks = tasksClient({ version: 'v1', auth });
137
141
  const res = await tasks.tasks.list({
138
142
  tasklist: tasklistId,
@@ -155,7 +159,7 @@ export function registerTasksTools(server) {
155
159
  },
156
160
  }, async ({ account, tasklistId, taskId }) => {
157
161
  try {
158
- const auth = await getClient(account);
162
+ const auth = await getClientFn(account);
159
163
  const tasks = tasksClient({ version: 'v1', auth });
160
164
  const res = await tasks.tasks.get({ tasklist: tasklistId, task: taskId });
161
165
  return {
@@ -180,7 +184,7 @@ export function registerTasksTools(server) {
180
184
  },
181
185
  }, async ({ account, tasklistId, title, notes, due, status, parent, previous }) => {
182
186
  try {
183
- const auth = await getClient(account);
187
+ const auth = await getClientFn(account);
184
188
  const tasks = tasksClient({ version: 'v1', auth });
185
189
  const requestBody = { title };
186
190
  if (notes !== undefined)
@@ -216,7 +220,7 @@ export function registerTasksTools(server) {
216
220
  },
217
221
  }, async ({ account, tasklistId, taskId, title, notes, due, status }) => {
218
222
  try {
219
- const auth = await getClient(account);
223
+ const auth = await getClientFn(account);
220
224
  const tasks = tasksClient({ version: 'v1', auth });
221
225
  const requestBody = {};
222
226
  if (title !== undefined)
@@ -252,7 +256,7 @@ export function registerTasksTools(server) {
252
256
  },
253
257
  }, async ({ account, tasklistId, taskId }) => {
254
258
  try {
255
- const auth = await getClient(account);
259
+ const auth = await getClientFn(account);
256
260
  const tasks = tasksClient({ version: 'v1', auth });
257
261
  await tasks.tasks.delete({ tasklist: tasklistId, task: taskId });
258
262
  return {
@@ -275,7 +279,7 @@ export function registerTasksTools(server) {
275
279
  },
276
280
  }, async ({ account, tasklistId, taskId, parent, previous, destinationTasklist }) => {
277
281
  try {
278
- const auth = await getClient(account);
282
+ const auth = await getClientFn(account);
279
283
  const tasks = tasksClient({ version: 'v1', auth });
280
284
  const res = await tasks.tasks.move({
281
285
  tasklist: tasklistId,
@@ -300,7 +304,7 @@ export function registerTasksTools(server) {
300
304
  },
301
305
  }, async ({ account, tasklistId }) => {
302
306
  try {
303
- const auth = await getClient(account);
307
+ const auth = await getClientFn(account);
304
308
  const tasks = tasksClient({ version: 'v1', auth });
305
309
  await tasks.tasks.clear({ tasklist: tasklistId });
306
310
  return {
package/dist/trim.d.ts CHANGED
@@ -6,6 +6,11 @@ export interface CappedText {
6
6
  }
7
7
  export declare function capText(text: string, maxChars: number, offset?: number): CappedText;
8
8
  export declare function sliceClean(text: string, max: number): string;
9
+ /** An untrusted value made fit for one log line: never throws, at most `max`
10
+ * characters of it (then `...(len=N)`), each unsafe character as a literal
11
+ * `\uXXXX`. Backslash stays as is, so a value already through here comes back
12
+ * unchanged and a clean short value keeps its exact text. */
13
+ export declare function logSafe(value: unknown, max: number): string;
9
14
  /** Largest prefix whose JSON encoding (the wrapping quotes included) fits
10
15
  * `maxEncoded`. Slicing raw characters overshoots whenever the text contains
11
16
  * quotes or backslashes, which JSON.stringify doubles: the escape hatch's
package/dist/trim.js CHANGED
@@ -11,6 +11,26 @@ export function sliceClean(text, max) {
11
11
  const sliced = text.slice(0, max);
12
12
  return /[\uD800-\uDBFF]$/.test(sliced) ? sliced.slice(0, -1) : sliced;
13
13
  }
14
+ // C0, DEL and C1 (NEL, 8-bit CSI and OSC) can end a line or drive a terminal;
15
+ // the line and paragraph separators end a line for many readers; the bidi and
16
+ // invisible format controls make a line read differently from its bytes.
17
+ const LOG_UNSAFE = /[\p{Cc}\u061c\u200e\u200f\u202a-\u202e\u2028\u2029\u2066-\u2069]/gu;
18
+ /** An untrusted value made fit for one log line: never throws, at most `max`
19
+ * characters of it (then `...(len=N)`), each unsafe character as a literal
20
+ * `\uXXXX`. Backslash stays as is, so a value already through here comes back
21
+ * unchanged and a clean short value keeps its exact text. */
22
+ export function logSafe(value, max) {
23
+ let text;
24
+ try {
25
+ text = String(value);
26
+ }
27
+ catch {
28
+ return '<unprintable>';
29
+ }
30
+ const cut = text.length > max ? sliceClean(text, max) : text;
31
+ const escaped = cut.replace(LOG_UNSAFE, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
32
+ return cut === text ? escaped : `${escaped}...(len=${text.length})`;
33
+ }
14
34
  /** Largest prefix whose JSON encoding (the wrapping quotes included) fits
15
35
  * `maxEncoded`. Slicing raw characters overshoots whenever the text contains
16
36
  * quotes or backslashes, which JSON.stringify doubles: the escape hatch's
@@ -86,6 +86,26 @@ export interface InitOptions {
86
86
  now?: () => number;
87
87
  monotonic?: () => number;
88
88
  log?: (line: string) => void;
89
+ /** Default true. Three chains correlate CONSECUTIVE calls, which is only
90
+ * meaningful when one caller owns the whole process: the retry and bigram
91
+ * chains, and pendingMid (the escape hatch's resolved methodId, attached to
92
+ * the next google_api_call event's `mid`). A host serving several callers
93
+ * sets false so none of the three ever cross-link a call between tenants.
94
+ * DayAgg shape is unchanged (retries/bigrams stay empty maps);
95
+ * escape.methods still counts every resolved call either way. */
96
+ sequencingEnabled?: boolean;
97
+ /** Default true. When false, no events.jsonl / events.1.jsonl is ever
98
+ * created or appended to; only agg/ exists. A host serving several callers
99
+ * sets false: each event line carries a minute timestamp, the boot id and a
100
+ * per-process sequence number, which can be joined against a per-caller log
101
+ * such as an audit trail; the day aggregate keeps the same counters without
102
+ * order. */
103
+ eventLog?: boolean;
104
+ /** Default none. A host's own closed slug set, unioned with
105
+ * KNOWN_ERROR_SLUGS for this instance only; each entry must match
106
+ * /^[A-Za-z][A-Za-z0-9_]{0,63}$/ or it is dropped silently. Absent,
107
+ * behavior is byte-identical to today. */
108
+ extraErrorSlugs?: readonly string[];
89
109
  }
90
110
  /** Deep-add b into a for the DayAgg shape (numbers add, maps union). */
91
111
  export declare function mergeDay(a: DayAgg, b: DayAgg): DayAgg;
@@ -108,6 +128,9 @@ export declare class Metrics {
108
128
  private dirty;
109
129
  private failures;
110
130
  private disabled;
131
+ private readonly sequencing;
132
+ private readonly eventLog;
133
+ private readonly errorSlugs;
111
134
  constructor(opts: InitOptions);
112
135
  private day;
113
136
  private initStorage;
@@ -33,10 +33,11 @@ const TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
33
33
  // Declared argument keys are camelCase identifiers, some dotted
34
34
  // (groupKey.id, debugOptions.enableDebugging).
35
35
  const ARG_KEY_RE = /^[A-Za-z][A-Za-z0-9_.]{0,63}$/;
36
+ const EXTRA_ERROR_SLUG_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
36
37
  /** Union of every `error:` slug literal emitted anywhere in src/ (kept honest
37
38
  * by a set-equality grep test). Anything outside buckets to `other`. */
38
39
  export const KNOWN_ERROR_SLUGS = new Set([
39
- 'E_ALIAS_EXISTS', 'E_CIMD_INVALID', 'E_ENV_ACCOUNTS_MODE', 'E_MCP_TOKEN_INVALID',
40
+ 'E_ALIAS_EXISTS', 'E_CIMD_INVALID', 'E_CIMD_SSRF_BLOCKED', 'E_ENV_ACCOUNTS_MODE', 'E_MCP_TOKEN_INVALID',
40
41
  'E_NO_DEFAULT_ACCOUNT', 'E_UNKNOWN_BUNDLE', 'E_VALIDATION', 'ambiguous',
41
42
  'api_not_enabled', 'auth_required', 'bad_request', 'binary', 'binary_unsupported',
42
43
  'confirmation_declined', 'diagnose_failed', 'discovery_unavailable', 'dispatch_timeout',
@@ -44,7 +45,7 @@ export const KNOWN_ERROR_SLUGS = new Set([
44
45
  'insufficient_scope', 'internal', 'invalid_client', 'invalid_client_metadata',
45
46
  'invalid_grant', 'invalid_params', 'invalid_query', 'invalid_request',
46
47
  'invalid_scope', 'network_error', 'not_found', 'rate_limited',
47
- 'reauth_required', 'recipient_not_allowed', 'too_large', 'toolset_disabled', 'unknown_api',
48
+ 'reauth_required', 'recipient_not_allowed', 'temporarily_unavailable', 'tenant_not_found', 'too_large', 'toolset_disabled', 'unknown_api',
48
49
  'unknown_argument', 'unknown_method', 'unsupported_grant_type', 'unsupported_type',
49
50
  'untrusted_host', 'upstream_error', 'validation_error', 'write_disabled',
50
51
  ]);
@@ -221,7 +222,14 @@ export class Metrics {
221
222
  dirty = false;
222
223
  failures = 0;
223
224
  disabled = false;
225
+ sequencing;
226
+ eventLog;
227
+ errorSlugs;
224
228
  constructor(opts) {
229
+ this.sequencing = opts.sequencingEnabled !== false;
230
+ this.eventLog = opts.eventLog !== false;
231
+ const extras = (opts.extraErrorSlugs ?? []).filter((s) => EXTRA_ERROR_SLUG_RE.test(s));
232
+ this.errorSlugs = extras.length ? new Set([...KNOWN_ERROR_SLUGS, ...extras]) : KNOWN_ERROR_SLUGS;
225
233
  this.dir = opts.dir ?? path.join(stateDir(opts.env), METRICS_DIR_NAME);
226
234
  this.aggDir = path.join(this.dir, 'agg');
227
235
  this.eventsFile = path.join(this.dir, 'events.jsonl');
@@ -250,7 +258,8 @@ export class Metrics {
250
258
  }
251
259
  }
252
260
  this.pruneAggs();
253
- this.pruneEventAge(this.eventsFile);
261
+ if (this.eventLog)
262
+ this.pruneEventAge(this.eventsFile);
254
263
  }
255
264
  catch (e) {
256
265
  this.log(`local usage metrics: init failed (${e.message}); disabled for this process\n`);
@@ -318,7 +327,7 @@ export class Metrics {
318
327
  if (typeof first === 'string' && first.length < 65_536) {
319
328
  const env = JSON.parse(first);
320
329
  if (typeof env.error === 'string')
321
- slug = KNOWN_ERROR_SLUGS.has(env.error) ? env.error : 'other';
330
+ slug = this.errorSlugs.has(env.error) ? env.error : 'other';
322
331
  hinted = typeof env.hint === 'string' && env.hint.length > 0;
323
332
  }
324
333
  }
@@ -357,45 +366,51 @@ export class Metrics {
357
366
  f.calls += 1;
358
367
  addInto(f, fan);
359
368
  }
360
- // retry self-correction chain (spec section 1): in-memory only.
369
+ // retry self-correction chain (spec section 1): in-memory only. Both
370
+ // chains link consecutive calls, so they only run when one caller owns
371
+ // the process (sequencingEnabled).
361
372
  const nowM = this.mono();
362
- const prev = this.lastError.get(entry.name);
363
- if (prev && nowM - prev.at <= RETRY_WINDOW_MS) {
364
- const rr = (this.deltas.retries[prev.slug] ??= { hintedOk: 0, hintedFail: 0, unhintedOk: 0, unhintedFail: 0 });
365
- const key = `${prev.hinted ? 'hinted' : 'unhinted'}${ok ? 'Ok' : 'Fail'}`;
366
- rr[key] += 1;
367
- this.lastError.delete(entry.name);
368
- }
369
- if (!ok && slug)
370
- this.lastError.set(entry.name, { slug, hinted: hinted === true, at: nowM });
371
- // bigrams: ordered pairs within the process, 5-minute gap breaks the chain.
372
- if (this.lastTool && nowM - this.lastTool.at <= BIGRAM_GAP_MS) {
373
- addInto(this.deltas.bigrams, `${this.lastTool.name}>${entry.name}`);
373
+ if (this.sequencing) {
374
+ const prev = this.lastError.get(entry.name);
375
+ if (prev && nowM - prev.at <= RETRY_WINDOW_MS) {
376
+ const rr = (this.deltas.retries[prev.slug] ??= { hintedOk: 0, hintedFail: 0, unhintedOk: 0, unhintedFail: 0 });
377
+ const key = `${prev.hinted ? 'hinted' : 'unhinted'}${ok ? 'Ok' : 'Fail'}`;
378
+ rr[key] += 1;
379
+ this.lastError.delete(entry.name);
380
+ }
381
+ if (!ok && slug)
382
+ this.lastError.set(entry.name, { slug, hinted: hinted === true, at: nowM });
383
+ // bigrams: ordered pairs within the process, 5-minute gap breaks the chain.
384
+ if (this.lastTool && nowM - this.lastTool.at <= BIGRAM_GAP_MS) {
385
+ addInto(this.deltas.bigrams, `${this.lastTool.name}>${entry.name}`);
386
+ }
387
+ this.lastTool = { name: entry.name, at: nowM };
374
388
  }
375
- this.lastTool = { name: entry.name, at: nowM };
376
- const event = {
377
- ts: new Date(this.now()).toISOString().slice(0, 16) + ':00Z',
378
- boot: this.bootId,
379
- seq: this.seq++,
380
- tool: entry.name,
381
- ok,
382
- ms: Math.round(ms),
383
- chars: charsBucket(chars),
384
- src: 'handler',
385
- };
386
- if (slug)
387
- event.err = slug;
388
- if (hinted !== undefined)
389
- event.hint = hinted;
390
- if (fan)
391
- event.fan = fan;
392
- // mid: set mid-dispatch by recordEscapeMethod, attached to the escape
393
- // call's own event line (per-event field, never a synthetic event).
394
- if (entry.name === 'google_api_call' && this.pendingMid) {
395
- event.mid = this.pendingMid;
396
- this.pendingMid = undefined;
389
+ if (this.eventLog) {
390
+ const event = {
391
+ ts: new Date(this.now()).toISOString().slice(0, 16) + ':00Z',
392
+ boot: this.bootId,
393
+ seq: this.seq++,
394
+ tool: entry.name,
395
+ ok,
396
+ ms: Math.round(ms),
397
+ chars: charsBucket(chars),
398
+ src: 'handler',
399
+ };
400
+ if (slug)
401
+ event.err = slug;
402
+ if (hinted !== undefined)
403
+ event.hint = hinted;
404
+ if (fan)
405
+ event.fan = fan;
406
+ // mid: set mid-dispatch by recordEscapeMethod, attached to the escape
407
+ // call's own event line (per-event field, never a synthetic event).
408
+ if (this.sequencing && entry.name === 'google_api_call' && this.pendingMid) {
409
+ event.mid = this.pendingMid;
410
+ this.pendingMid = undefined;
411
+ }
412
+ this.events.push(JSON.stringify(event));
397
413
  }
398
- this.events.push(JSON.stringify(event));
399
414
  this.dirty = true;
400
415
  }
401
416
  /** Escape hatch: only the RESOLVED methodId (double-gated) or a miss. */
@@ -407,7 +422,8 @@ export class Metrics {
407
422
  }
408
423
  else if (idOrNull.length <= METHOD_ID_MAX && METHOD_ID_RE.test(idOrNull)) {
409
424
  addInto(this.deltas.escape.methods, idOrNull);
410
- this.pendingMid = idOrNull;
425
+ if (this.sequencing)
426
+ this.pendingMid = idOrNull;
411
427
  }
412
428
  this.dirty = true;
413
429
  }
@@ -475,12 +491,14 @@ export class Metrics {
475
491
  else {
476
492
  addInto(this.deltas.rpc, RPC_CODES.has(kind) ? `rpc_error_${kind}` : 'rpc_error_other');
477
493
  }
478
- this.events.push(JSON.stringify({
479
- ts: new Date(this.now()).toISOString().slice(0, 16) + ':00Z',
480
- boot: this.bootId, seq: this.seq++,
481
- tool: tool && TOOL_NAME_RE.test(tool) ? tool : 'other',
482
- ok: false, ms: 0, chars: 'le64', src: 'rpc',
483
- }));
494
+ if (this.eventLog) {
495
+ this.events.push(JSON.stringify({
496
+ ts: new Date(this.now()).toISOString().slice(0, 16) + ':00Z',
497
+ boot: this.bootId, seq: this.seq++,
498
+ tool: tool && TOOL_NAME_RE.test(tool) ? tool : 'other',
499
+ ok: false, ms: 0, chars: 'le64', src: 'rpc',
500
+ }));
501
+ }
484
502
  this.dirty = true;
485
503
  }
486
504
  catch { /* never throws outward */ }
@@ -1,3 +1,4 @@
1
+ import { hints } from './hint-provider.js';
1
2
  const PROFILES = ['read-only', 'safe-writes', 'full-writes'];
2
3
  /** The irreversible set (frozen, cc-write-control): real sends leave the
3
4
  * user's (or org's) identity unrecallably; permanent deletes bypass Trash.
@@ -155,10 +156,17 @@ export function writeDisabledResult(tool, policy, account) {
155
156
  // Recomputed rather than passed in: every call site has already run
156
157
  // isAllowed, and a hint that disagrees with the verdict is worse than none.
157
158
  const reason = denyReason(tool, policy) ?? { rule: 'profile', profile: policy.profile };
159
+ const rendered = hints()?.render({
160
+ kind: 'write_denied',
161
+ tool: { name: tool.name, service: tool.service, cud: tool.cud },
162
+ reason,
163
+ policy,
164
+ });
158
165
  const envelope = {
159
166
  error: 'write_disabled',
160
- message: `"${tool.name}" (${tool.cud}) is disabled by the current write-control policy (profile: ${policy.profile}${policy.readOnly ? ', GOOGLE_READ_ONLY=true' : ''}).`,
161
- hint: denyHint(tool, reason),
167
+ message: rendered?.message ??
168
+ `"${tool.name}" (${tool.cud}) is disabled by the current write-control policy (profile: ${policy.profile}${policy.readOnly ? ', GOOGLE_READ_ONLY=true' : ''}).`,
169
+ hint: rendered?.hint ?? denyHint(tool, reason),
162
170
  retriable: false,
163
171
  ...(account ? { account } : {}),
164
172
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-google-multi",
3
- "version": "6.0.0-beta.2",
3
+ "version": "6.0.0-beta.3",
4
4
  "mcpName": "io.github.bakissation/mcp-google-multi",
5
5
  "description": "Local MCP server for Google Workspace (Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Search Console, +Forms/Chat/Admin) across multiple accounts — OAuth-only, encrypted token storage, deny-by-default writes.",
6
6
  "type": "module",
@@ -9,6 +9,32 @@
9
9
  "mcp-google-multi": "dist/index.js"
10
10
  },
11
11
  "main": "dist/index.js",
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./identity": "./dist/identity.js",
15
+ "./compose": "./dist/compose.js",
16
+ "./registry": "./dist/registry.js",
17
+ "./oauth-as": "./dist/oauth-as.js",
18
+ "./accounts": "./dist/accounts.js",
19
+ "./token-store": "./dist/token-store.js",
20
+ "./http-transport": "./dist/http-transport.js",
21
+ "./http-config": "./dist/http-config.js",
22
+ "./client": "./dist/client.js",
23
+ "./config-file": "./dist/config-file.js",
24
+ "./fs-atomic": "./dist/fs-atomic.js",
25
+ "./master-key": "./dist/master-key.js",
26
+ "./mcp-token": "./dist/mcp-token.js",
27
+ "./boot-gates": "./dist/boot-gates.js",
28
+ "./tenant-purge": "./dist/tenant-purge.js",
29
+ "./doctor": "./dist/doctor.js",
30
+ "./write-control": "./dist/write-control.js",
31
+ "./scope-catalog": "./dist/scope-catalog.js",
32
+ "./auth": "./dist/auth.js",
33
+ "./usage-metrics": "./dist/usage-metrics.js",
34
+ "./metrics-tap": "./dist/metrics-tap.js",
35
+ "./hint-provider": "./dist/hint-provider.js",
36
+ "./package.json": "./package.json"
37
+ },
12
38
  "files": [
13
39
  "dist"
14
40
  ],