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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +5 -5
  2. package/dist/accounts.d.ts +20 -4
  3. package/dist/accounts.js +68 -44
  4. package/dist/api-probe.d.ts +5 -0
  5. package/dist/api-probe.js +20 -15
  6. package/dist/arg-normalize.js +19 -38
  7. package/dist/arg-strict.d.ts +7 -0
  8. package/dist/arg-strict.js +38 -28
  9. package/dist/auth.d.ts +5 -4
  10. package/dist/auth.js +16 -23
  11. package/dist/boot-gates.d.ts +26 -0
  12. package/dist/boot-gates.js +36 -0
  13. package/dist/cimd-cache.d.ts +38 -0
  14. package/dist/cimd-cache.js +111 -0
  15. package/dist/client-config.d.ts +2 -2
  16. package/dist/client-config.js +8 -14
  17. package/dist/client.d.ts +32 -2
  18. package/dist/client.js +0 -0
  19. package/dist/compose.d.ts +18 -0
  20. package/dist/compose.js +109 -0
  21. package/dist/config-file.d.ts +13 -0
  22. package/dist/config-file.js +41 -10
  23. package/dist/discover.js +7 -10
  24. package/dist/discovery-client.d.ts +3 -0
  25. package/dist/discovery-client.js +36 -32
  26. package/dist/doctor.d.ts +9 -5
  27. package/dist/doctor.js +119 -29
  28. package/dist/env-load.js +2 -5
  29. package/dist/executor.d.ts +3 -0
  30. package/dist/executor.js +6 -6
  31. package/dist/fanout.d.ts +9 -1
  32. package/dist/fanout.js +47 -22
  33. package/dist/fs-atomic.d.ts +8 -0
  34. package/dist/fs-atomic.js +216 -29
  35. package/dist/hint-provider.d.ts +129 -0
  36. package/dist/hint-provider.js +58 -0
  37. package/dist/http-config.d.ts +2 -2
  38. package/dist/http-config.js +3 -7
  39. package/dist/http-transport.d.ts +67 -9
  40. package/dist/http-transport.js +318 -52
  41. package/dist/identity.d.ts +6 -3
  42. package/dist/identity.js +24 -8
  43. package/dist/index.js +76 -160
  44. package/dist/master-key.d.ts +3 -3
  45. package/dist/master-key.js +16 -19
  46. package/dist/mcp-token.d.ts +126 -19
  47. package/dist/mcp-token.js +557 -76
  48. package/dist/metrics-cli.d.ts +1 -1
  49. package/dist/metrics-cli.js +1 -1
  50. package/dist/metrics-tap.js +2 -5
  51. package/dist/migrate-config.d.ts +1 -2
  52. package/dist/migrate-config.js +5 -7
  53. package/dist/net-tuning.js +2 -4
  54. package/dist/oauth-as.d.ts +104 -4
  55. package/dist/oauth-as.js +368 -114
  56. package/dist/oauth-consent.d.ts +10 -0
  57. package/dist/oauth-consent.js +20 -10
  58. package/dist/outbound-allowlist.js +9 -11
  59. package/dist/reauth-hint.d.ts +1 -1
  60. package/dist/reauth-hint.js +11 -10
  61. package/dist/registry-transfer.d.ts +5 -0
  62. package/dist/registry-transfer.js +63 -11
  63. package/dist/registry.d.ts +13 -4
  64. package/dist/registry.js +100 -72
  65. package/dist/scope-catalog.d.ts +16 -0
  66. package/dist/scope-catalog.js +40 -13
  67. package/dist/scope-observability.d.ts +10 -9
  68. package/dist/scope-observability.js +34 -24
  69. package/dist/services.d.ts +18 -10
  70. package/dist/services.js +61 -22
  71. package/dist/ssrf-guard.d.ts +23 -9
  72. package/dist/ssrf-guard.js +106 -78
  73. package/dist/tenant-purge.d.ts +26 -0
  74. package/dist/tenant-purge.js +65 -0
  75. package/dist/token-store.d.ts +20 -2
  76. package/dist/token-store.js +0 -0
  77. package/dist/tools/_coerce.d.ts +4 -4
  78. package/dist/tools/_errors.d.ts +3 -0
  79. package/dist/tools/_errors.js +72 -60
  80. package/dist/tools/_local-files.d.ts +9 -0
  81. package/dist/tools/_local-files.js +22 -8
  82. package/dist/tools/account-wizard.d.ts +13 -3
  83. package/dist/tools/account-wizard.js +107 -56
  84. package/dist/tools/accounts-tool.d.ts +8 -1
  85. package/dist/tools/accounts-tool.js +35 -19
  86. package/dist/tools/admin.d.ts +10 -1
  87. package/dist/tools/admin.js +14 -11
  88. package/dist/tools/analytics.d.ts +10 -1
  89. package/dist/tools/analytics.js +14 -10
  90. package/dist/tools/calendar.d.ts +2 -1
  91. package/dist/tools/calendar.js +19 -18
  92. package/dist/tools/chat.d.ts +10 -1
  93. package/dist/tools/chat.js +12 -8
  94. package/dist/tools/contacts.d.ts +2 -1
  95. package/dist/tools/contacts.js +22 -19
  96. package/dist/tools/docs.d.ts +2 -1
  97. package/dist/tools/docs.js +35 -36
  98. package/dist/tools/drive.d.ts +2 -1
  99. package/dist/tools/drive.js +80 -74
  100. package/dist/tools/forms.d.ts +10 -1
  101. package/dist/tools/forms.js +18 -12
  102. package/dist/tools/generated/_shared.d.ts +2 -1
  103. package/dist/tools/generated/_shared.js +7 -3
  104. package/dist/tools/generated/admin.d.ts +2 -1
  105. package/dist/tools/generated/admin.js +265 -265
  106. package/dist/tools/generated/analytics.d.ts +2 -1
  107. package/dist/tools/generated/analytics.js +125 -125
  108. package/dist/tools/generated/appsmarket.d.ts +2 -1
  109. package/dist/tools/generated/appsmarket.js +5 -5
  110. package/dist/tools/generated/calendar.d.ts +2 -1
  111. package/dist/tools/generated/calendar.js +55 -55
  112. package/dist/tools/generated/chat.d.ts +2 -1
  113. package/dist/tools/generated/chat.js +95 -95
  114. package/dist/tools/generated/classroom.d.ts +2 -1
  115. package/dist/tools/generated/classroom.js +209 -209
  116. package/dist/tools/generated/cloudidentity.d.ts +2 -1
  117. package/dist/tools/generated/cloudidentity.js +133 -133
  118. package/dist/tools/generated/cloudsearch.d.ts +2 -1
  119. package/dist/tools/generated/cloudsearch.js +99 -99
  120. package/dist/tools/generated/contacts.d.ts +2 -1
  121. package/dist/tools/generated/contacts.js +29 -29
  122. package/dist/tools/generated/drive.d.ts +2 -1
  123. package/dist/tools/generated/drive.js +71 -71
  124. package/dist/tools/generated/driveactivity.d.ts +2 -1
  125. package/dist/tools/generated/driveactivity.js +3 -3
  126. package/dist/tools/generated/drivelabels.d.ts +2 -1
  127. package/dist/tools/generated/drivelabels.js +53 -53
  128. package/dist/tools/generated/forms.d.ts +2 -1
  129. package/dist/tools/generated/forms.js +7 -7
  130. package/dist/tools/generated/gmail.d.ts +2 -1
  131. package/dist/tools/generated/gmail.js +117 -117
  132. package/dist/tools/generated/groupsmigration.d.ts +2 -1
  133. package/dist/tools/generated/groupsmigration.js +3 -3
  134. package/dist/tools/generated/groupssettings.d.ts +2 -1
  135. package/dist/tools/generated/groupssettings.js +7 -7
  136. package/dist/tools/generated/index.d.ts +2 -1
  137. package/dist/tools/generated/keep.d.ts +2 -1
  138. package/dist/tools/generated/keep.js +15 -15
  139. package/dist/tools/generated/licensing.d.ts +2 -1
  140. package/dist/tools/generated/licensing.js +15 -15
  141. package/dist/tools/generated/meet.d.ts +2 -1
  142. package/dist/tools/generated/meet.js +27 -27
  143. package/dist/tools/generated/postmaster.d.ts +2 -1
  144. package/dist/tools/generated/postmaster.js +9 -9
  145. package/dist/tools/generated/reseller.d.ts +2 -1
  146. package/dist/tools/generated/reseller.js +35 -35
  147. package/dist/tools/generated/script.d.ts +2 -1
  148. package/dist/tools/generated/script.js +33 -33
  149. package/dist/tools/generated/searchconsole.d.ts +2 -1
  150. package/dist/tools/generated/searchconsole.js +3 -3
  151. package/dist/tools/generated/sheets.d.ts +2 -1
  152. package/dist/tools/generated/sheets.js +15 -15
  153. package/dist/tools/generated/tasks.d.ts +2 -1
  154. package/dist/tools/generated/tasks.js +5 -5
  155. package/dist/tools/generated/vault.d.ts +2 -1
  156. package/dist/tools/generated/vault.js +67 -67
  157. package/dist/tools/generated/workspaceevents.d.ts +2 -1
  158. package/dist/tools/generated/workspaceevents.js +31 -31
  159. package/dist/tools/gmail-mime.d.ts +1 -1
  160. package/dist/tools/gmail-mime.js +7 -12
  161. package/dist/tools/gmail.d.ts +4 -3
  162. package/dist/tools/gmail.js +67 -69
  163. package/dist/tools/google-api.d.ts +2 -3
  164. package/dist/tools/google-api.js +34 -22
  165. package/dist/tools/meet.d.ts +10 -1
  166. package/dist/tools/meet.js +12 -9
  167. package/dist/tools/searchconsole.d.ts +2 -1
  168. package/dist/tools/searchconsole.js +16 -13
  169. package/dist/tools/sheets.d.ts +2 -1
  170. package/dist/tools/sheets.js +35 -32
  171. package/dist/tools/slides.d.ts +10 -1
  172. package/dist/tools/slides.js +16 -10
  173. package/dist/tools/tasks.d.ts +2 -1
  174. package/dist/tools/tasks.js +18 -15
  175. package/dist/trim.d.ts +5 -0
  176. package/dist/trim.js +19 -0
  177. package/dist/usage-metrics.d.ts +23 -0
  178. package/dist/usage-metrics.js +67 -56
  179. package/dist/write-control.d.ts +4 -8
  180. package/dist/write-control.js +13 -6
  181. package/package.json +47 -21
package/dist/fs-atomic.js CHANGED
@@ -5,23 +5,212 @@ const LOCK_TIMEOUT_MS = 5_000;
5
5
  const LOCK_RETRY_MS = 10;
6
6
  const RENAME_ATTEMPTS = 5;
7
7
  const RENAME_RETRY_MS = 20;
8
+ // A holder this process cannot identify (another PID namespace, another boot,
9
+ // or no /proc) is dead only once its lock is this old.
10
+ const LOCK_LEASE_MS = 60_000;
11
+ // Breaking a lock takes microseconds, so an older breaker file was left by a crash.
12
+ const BREAKER_STALE_MS = 2_000;
13
+ const PROCESS_START_MS = performance.timeOrigin;
14
+ const OWNER_NAME = /^(\d+)\.(\d+|host)\.(\d+)\.([0-9a-f]{8}|0)\.[0-9a-f]{12}\.owner$/;
15
+ function startOf(pid) {
16
+ try {
17
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
18
+ const start = stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19];
19
+ return /^\d+$/.test(start ?? '') ? start : undefined;
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ function selfIdentity() {
26
+ const identity = { ns: 'host', start: '0', boot: '0' };
27
+ try {
28
+ identity.ns = /^pid:\[(\d+)\]$/.exec(fs.readlinkSync('/proc/self/ns/pid'))?.[1] ?? 'host';
29
+ }
30
+ catch {
31
+ // no /proc
32
+ }
33
+ try {
34
+ const boot = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').replace(/[^0-9a-f]/g, '').slice(0, 8);
35
+ if (boot.length === 8)
36
+ identity.boot = boot;
37
+ }
38
+ catch {
39
+ // no /proc
40
+ }
41
+ try {
42
+ // A /proc mounted for another PID namespace describes other processes,
43
+ // so no pid can be looked up in it.
44
+ if (fs.readlinkSync('/proc/self') === String(process.pid))
45
+ identity.start = startOf('self') ?? '0';
46
+ }
47
+ catch {
48
+ // no /proc
49
+ }
50
+ return identity;
51
+ }
52
+ let SELF = selfIdentity();
53
+ /** Test hook: act as a process with this identity (undefined restores the real one). */
54
+ export function __setLockIdentityForTest(identity) {
55
+ SELF = identity ?? selfIdentity();
56
+ }
8
57
  function sleep(ms) {
9
58
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
10
59
  }
11
- // Hardlink-based advisory lock, generalized path-keyed from the token store's
12
- // alias-keyed original (same mechanics: dead-owner recovery via signal-0 probe,
13
- // EPERM = alive, corrupt lock content recovered as dead, 5s timeout).
60
+ function sameFile(a, b) {
61
+ return a.ino === b.ino && a.dev === b.dev;
62
+ }
63
+ function isEnoent(error) {
64
+ return error.code === 'ENOENT';
65
+ }
66
+ // Undefined when the lock changed hands while it was being read.
67
+ function readHolder(lock) {
68
+ const before = fs.lstatSync(lock, { bigint: true });
69
+ const body = before.isFile() ? fs.readFileSync(lock, 'utf8') : '';
70
+ let owner;
71
+ if (before.isFile()) {
72
+ const dir = path.dirname(lock);
73
+ const prefix = `${path.basename(lock)}.`;
74
+ for (const name of fs.readdirSync(dir)) {
75
+ if (!name.startsWith(prefix))
76
+ continue;
77
+ const m = OWNER_NAME.exec(name.slice(prefix.length));
78
+ if (!m || m[1] !== body)
79
+ continue;
80
+ const ownerPath = path.join(dir, name);
81
+ try {
82
+ if (sameFile(fs.lstatSync(ownerPath, { bigint: true }), before)) {
83
+ owner = { path: ownerPath, ns: m[2], start: m[3], boot: m[4] };
84
+ break;
85
+ }
86
+ }
87
+ catch (error) {
88
+ if (!isEnoent(error))
89
+ throw error;
90
+ }
91
+ }
92
+ }
93
+ const after = fs.lstatSync(lock, { bigint: true });
94
+ if (!sameFile(before, after))
95
+ return undefined;
96
+ return { body, ino: after.ino, dev: after.dev, mtimeNs: after.mtimeNs, mtimeMs: Number(after.mtimeNs) / 1e6, owner };
97
+ }
98
+ // The owner file's random name tells two holders apart even when the second
99
+ // lock reuses the first one's freed inode.
100
+ function sameHolder(a, b) {
101
+ return sameFile(a, b) && a.body === b.body && a.mtimeNs === b.mtimeNs && a.owner?.path === b.owner?.path;
102
+ }
103
+ function probeDead(pid) {
104
+ try {
105
+ process.kill(pid, 0);
106
+ return false;
107
+ }
108
+ catch (error) {
109
+ const code = error.code;
110
+ // EPERM: PID exists but is not signalable (another user's process).
111
+ if (code === 'ESRCH')
112
+ return true;
113
+ if (code !== 'EPERM')
114
+ throw error;
115
+ return false;
116
+ }
117
+ }
118
+ function holderDead(holder, now) {
119
+ const pid = Number(holder.body);
120
+ if (!(Number.isSafeInteger(pid) && pid > 0))
121
+ return true;
122
+ const leaseOver = now - holder.mtimeMs >= LOCK_LEASE_MS;
123
+ const owner = holder.owner;
124
+ // No owner file: an older writer, whose namespace is unknown.
125
+ if (!owner)
126
+ return probeDead(pid) || leaseOver;
127
+ if (owner.ns !== SELF.ns || owner.boot !== SELF.boot)
128
+ return leaseOver;
129
+ // Two unreadable parts compare equal without naming the same namespace or
130
+ // boot, so only a fully known identity is looked up by start time.
131
+ if (SELF.ns !== 'host' && SELF.boot !== '0' && SELF.start !== '0' && owner.start !== '0') {
132
+ // A live namespace id is never shared, so the recorded pid is ours to look
133
+ // up: a restarted container reuses both the id and the pid, but not the start time.
134
+ const start = startOf(pid);
135
+ if (start !== undefined)
136
+ return start !== owner.start;
137
+ return probeDead(pid) || leaseOver;
138
+ }
139
+ if (pid === process.pid)
140
+ return holder.mtimeMs < PROCESS_START_MS;
141
+ return probeDead(pid) || leaseOver;
142
+ }
143
+ // One waiter at a time breaks a dead lock, or the second could unlink the
144
+ // first one's new lock; false when another waiter is already breaking it.
145
+ function breakLock(lock, holder) {
146
+ const breaker = `${lock}.break`;
147
+ // A token, not the inode: a breaker created after ours was removed can
148
+ // get our freed inode.
149
+ const token = `${process.pid}.${randomBytes(6).toString('hex')}`;
150
+ try {
151
+ fs.writeFileSync(breaker, token, { mode: 0o600, flag: 'wx' });
152
+ }
153
+ catch (error) {
154
+ if (error.code !== 'EEXIST')
155
+ throw error;
156
+ clearStaleBreaker(breaker);
157
+ return false;
158
+ }
159
+ try {
160
+ const current = readHolder(lock);
161
+ if (current && sameHolder(current, holder)) {
162
+ fs.rmSync(lock, { force: true });
163
+ if (holder.owner)
164
+ fs.rmSync(holder.owner.path, { force: true });
165
+ }
166
+ }
167
+ catch (error) {
168
+ if (!isEnoent(error))
169
+ throw error;
170
+ }
171
+ finally {
172
+ releaseBreaker(breaker, token);
173
+ }
174
+ return true;
175
+ }
176
+ // A symlink, or a breaker dated the other side of a clock step, would
177
+ // otherwise block every break.
178
+ function clearStaleBreaker(breaker) {
179
+ try {
180
+ const st = fs.lstatSync(breaker);
181
+ if (!st.isFile() || Math.abs(Date.now() - st.mtimeMs) >= BREAKER_STALE_MS)
182
+ fs.rmSync(breaker, { force: true });
183
+ }
184
+ catch (error) {
185
+ if (!isEnoent(error))
186
+ throw error;
187
+ }
188
+ }
189
+ function releaseBreaker(breaker, token) {
190
+ try {
191
+ if (fs.readFileSync(breaker, 'utf8') === token)
192
+ fs.rmSync(breaker, { force: true });
193
+ }
194
+ catch (error) {
195
+ if (!isEnoent(error))
196
+ throw error;
197
+ }
198
+ }
199
+ // Hardlink-based advisory lock; the lock body is the bare pid, as older
200
+ // readers expect. See docs/internals.md.
14
201
  export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
15
202
  const dir = path.dirname(absPath);
16
203
  const lock = path.join(dir, `.${path.basename(absPath)}.lock`);
17
- const ownerFile = `${lock}.${process.pid}.${randomBytes(6).toString('hex')}.owner`;
204
+ const ownerFile = `${lock}.${process.pid}.${SELF.ns}.${SELF.start}.${SELF.boot}.${randomBytes(6).toString('hex')}.owner`;
18
205
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
19
206
  fs.writeFileSync(ownerFile, String(process.pid), { mode: 0o600, flag: 'wx' });
20
207
  const deadline = Date.now() + LOCK_TIMEOUT_MS;
208
+ let linked = false;
21
209
  try {
22
210
  while (true) {
23
211
  try {
24
212
  fs.linkSync(ownerFile, lock);
213
+ linked = true;
25
214
  break;
26
215
  }
27
216
  catch (error) {
@@ -29,33 +218,13 @@ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
29
218
  if (err.code !== 'EEXIST')
30
219
  throw error;
31
220
  try {
32
- const observedOwner = fs.readFileSync(lock, 'utf8');
33
- const owner = Number(observedOwner);
34
- let ownerDead = !(Number.isSafeInteger(owner) && owner > 0);
35
- if (!ownerDead) {
36
- try {
37
- process.kill(owner, 0);
38
- }
39
- catch (ownerError) {
40
- const code = ownerError.code;
41
- // EPERM: PID exists but is not signalable (recycled by another
42
- // user); treat as alive, never break a lock we cannot verify.
43
- if (code === 'ESRCH')
44
- ownerDead = true;
45
- else if (code !== 'EPERM')
46
- throw ownerError;
47
- }
48
- }
49
- if (ownerDead) {
50
- if (fs.readFileSync(lock, 'utf8') === observedOwner)
51
- fs.rmSync(lock, { force: true });
221
+ const holder = readHolder(lock);
222
+ if (holder && holderDead(holder, Date.now()) && breakLock(lock, holder))
52
223
  continue;
53
- }
54
224
  }
55
225
  catch (readError) {
56
- if (readError.code === 'ENOENT') {
226
+ if (isEnoent(readError))
57
227
  continue;
58
- }
59
228
  throw readError;
60
229
  }
61
230
  if (Date.now() >= deadline) {
@@ -66,14 +235,32 @@ export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
66
235
  }
67
236
  }
68
237
  finally {
69
- fs.rmSync(ownerFile, { force: true });
238
+ if (!linked)
239
+ fs.rmSync(ownerFile, { force: true });
70
240
  }
241
+ let bound = false;
71
242
  try {
243
+ // Some network and FUSE filesystems give each name of one file its own
244
+ // inode number; there the owner file cannot vouch for the lock.
245
+ bound = sameFile(fs.lstatSync(ownerFile, { bigint: true }), fs.lstatSync(lock, { bigint: true }));
72
246
  return fn();
73
247
  }
74
248
  finally {
75
- fs.rmSync(lock, { force: true });
249
+ releaseLock(lock, ownerFile, bound);
250
+ }
251
+ }
252
+ // A holder whose lock was broken (its owner file removed with it) must not
253
+ // remove its successor's lock.
254
+ function releaseLock(lock, ownerFile, bound) {
255
+ try {
256
+ if (!bound || sameFile(fs.lstatSync(ownerFile, { bigint: true }), fs.lstatSync(lock, { bigint: true })))
257
+ fs.rmSync(lock, { force: true });
258
+ }
259
+ catch (error) {
260
+ if (!isEnoent(error))
261
+ throw error;
76
262
  }
263
+ fs.rmSync(ownerFile, { force: true });
77
264
  }
78
265
  export function atomicWriteFileSync(absPath, contents, mode = 0o600) {
79
266
  const dir = path.dirname(absPath);
@@ -0,0 +1,129 @@
1
+ import type { DenyReason, Policy } from './write-control.js';
2
+ export type HintCase = {
3
+ kind: 'reauth';
4
+ account: string;
5
+ } | {
6
+ kind: 'legacy_plaintext_token';
7
+ account: string;
8
+ } | {
9
+ kind: 'dead_refresh_token';
10
+ account: string;
11
+ } | {
12
+ kind: 'token_unreadable';
13
+ account: string;
14
+ } | {
15
+ kind: 'scopes_missing';
16
+ account: string;
17
+ } | {
18
+ kind: 'scope_not_granted';
19
+ account: string;
20
+ scope: string;
21
+ } | {
22
+ kind: 'scope_needs_bundle';
23
+ account: string;
24
+ scope: string;
25
+ bundle: string;
26
+ } | {
27
+ kind: 'scope_base_excluded';
28
+ account: string;
29
+ scope: string;
30
+ } | {
31
+ kind: 'scope_account_type';
32
+ account: string;
33
+ scope: string;
34
+ } | {
35
+ kind: 'scope_unknown_scope';
36
+ scope: string;
37
+ } | {
38
+ kind: 'scope_unknown';
39
+ account: string;
40
+ serviceText?: string;
41
+ bundle?: string;
42
+ } | {
43
+ kind: 'write_denied';
44
+ tool: {
45
+ name: string;
46
+ service: string;
47
+ cud: string;
48
+ };
49
+ reason: DenyReason;
50
+ policy: Policy;
51
+ } | {
52
+ kind: 'policy_summary';
53
+ policy: Policy;
54
+ } | {
55
+ kind: 'outbound_denied';
56
+ what: string;
57
+ blocked: string[];
58
+ entries: string[];
59
+ } | {
60
+ kind: 'no_default_account';
61
+ aliases: string[];
62
+ } | {
63
+ kind: 'no_accounts';
64
+ } | {
65
+ kind: 'api_not_enabled';
66
+ api?: string;
67
+ service?: string;
68
+ project?: string;
69
+ googleMessage: string;
70
+ } | {
71
+ kind: 'apis_disabled';
72
+ services: {
73
+ service: string;
74
+ api: string;
75
+ }[];
76
+ } | {
77
+ kind: 'daily_quota';
78
+ } | {
79
+ kind: 'project_suspended';
80
+ account: string;
81
+ } | {
82
+ kind: 'invalid_scope';
83
+ account: string;
84
+ } | {
85
+ kind: 'no_oauth_client';
86
+ } | {
87
+ kind: 'network_error';
88
+ code: string;
89
+ } | {
90
+ kind: 'local_path';
91
+ code: string;
92
+ path?: string;
93
+ } | {
94
+ kind: 'internal';
95
+ account?: string;
96
+ } | {
97
+ kind: 'untrusted_host';
98
+ } | {
99
+ kind: 'toolset_disabled';
100
+ api: string;
101
+ service: string;
102
+ } | {
103
+ kind: 'service_not_enabled';
104
+ tool: string;
105
+ service: string;
106
+ bundle?: string;
107
+ aliases: string[];
108
+ admin: boolean;
109
+ toolsetOff: boolean;
110
+ awaitingRestart: boolean;
111
+ } | {
112
+ kind: 'operator_only';
113
+ slug: string;
114
+ };
115
+ export declare const HINT_KINDS: readonly ["reauth", "legacy_plaintext_token", "dead_refresh_token", "token_unreadable", "scopes_missing", "scope_not_granted", "scope_needs_bundle", "scope_base_excluded", "scope_account_type", "scope_unknown_scope", "scope_unknown", "write_denied", "policy_summary", "outbound_denied", "no_default_account", "no_accounts", "api_not_enabled", "apis_disabled", "daily_quota", "project_suspended", "invalid_scope", "no_oauth_client", "network_error", "local_path", "internal", "untrusted_host", "toolset_disabled", "service_not_enabled", "operator_only"];
116
+ /** `message` only for the kinds whose message leaks today (write_denied,
117
+ * no_oauth_client, local_path, internal, toolset_disabled); every other call
118
+ * site ignores it. `retriable` only where it may legitimately differ from
119
+ * the owner's default (scope_not_granted). */
120
+ export interface Rendered {
121
+ hint: string;
122
+ message?: string;
123
+ retriable?: boolean;
124
+ }
125
+ export interface HintProvider {
126
+ render(c: HintCase): Rendered;
127
+ }
128
+ export declare function setHintProvider(p: HintProvider | null): void;
129
+ export declare function hints(): HintProvider | null;
@@ -0,0 +1,58 @@
1
+ // A plain array literal, not derived from the union, so a missing member is
2
+ // caught by the completeness check below rather than silently compiling.
3
+ export const HINT_KINDS = [
4
+ 'reauth',
5
+ 'legacy_plaintext_token',
6
+ 'dead_refresh_token',
7
+ 'token_unreadable',
8
+ 'scopes_missing',
9
+ 'scope_not_granted',
10
+ 'scope_needs_bundle',
11
+ 'scope_base_excluded',
12
+ 'scope_account_type',
13
+ 'scope_unknown_scope',
14
+ 'scope_unknown',
15
+ 'write_denied',
16
+ 'policy_summary',
17
+ 'outbound_denied',
18
+ 'no_default_account',
19
+ 'no_accounts',
20
+ 'api_not_enabled',
21
+ 'apis_disabled',
22
+ 'daily_quota',
23
+ 'project_suspended',
24
+ 'invalid_scope',
25
+ 'no_oauth_client',
26
+ 'network_error',
27
+ 'local_path',
28
+ 'internal',
29
+ 'untrusted_host',
30
+ 'toolset_disabled',
31
+ 'service_not_enabled',
32
+ 'operator_only',
33
+ ];
34
+ const _hintKindsComplete = true;
35
+ void _hintKindsComplete;
36
+ // Process-global, like the HTTP reauth linker: one process is either the
37
+ // single owner or one multi-tenant host, never both.
38
+ let installed = null;
39
+ export function setHintProvider(p) {
40
+ installed = p;
41
+ }
42
+ // A throwing provider must never take down the call site: it falls back to
43
+ // the same literal a missing provider would, never leaking its own error text.
44
+ export function hints() {
45
+ if (!installed)
46
+ return null;
47
+ const provider = installed;
48
+ return {
49
+ render(c) {
50
+ try {
51
+ return provider.render(c);
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ },
57
+ };
58
+ }
@@ -11,7 +11,7 @@ export interface HttpConfig {
11
11
  transport: Transport;
12
12
  host: string;
13
13
  port: number;
14
- /** Canonical public base URL (BR4): lowercase scheme+host, no default port,
14
+ /** Canonical public base URL: lowercase scheme+host, no default port,
15
15
  * no trailing slash, no fragment/query. The issuer and every advertised
16
16
  * endpoint derive from this. */
17
17
  publicUrl: string;
@@ -27,7 +27,7 @@ export interface HttpConfig {
27
27
  export declare function transportIncludesHttp(t: Transport): boolean;
28
28
  export declare function resolveTransport(env?: NodeJS.ProcessEnv): Transport;
29
29
  /**
30
- * Canonicalize a public base URL per BR4. WHATWG `URL` already lowercases the
30
+ * Canonicalize a public base URL. WHATWG `URL` already lowercases the
31
31
  * scheme+host and omits a default port from `.host`; this adds the HTTPS-only
32
32
  * scheme check, strips fragment/query/userinfo, and removes a trailing slash.
33
33
  */
@@ -1,9 +1,5 @@
1
- // Transport selection + public-base-URL / Origin / host config plumbing (B11).
2
- // Pure, resolved once at boot; the HTTP host that consumes this lands in B12
3
- // (cc-transport-hosting) and the AS metadata in B13 (oauth-authorization-server).
4
- // Owns the canonicalization rule (cc-transport-hosting BR4): a single mismatch
5
- // between the advertised resource URI and the client's `resource`/`aud` = a
6
- // perpetual 401 loop (the #1 interop bug), so it is normalized exactly once.
1
+ // Transport + public-base-URL / Origin / host config, pure and resolved once
2
+ // at boot. A mismatch between the advertised URI and the client's `aud` is a perpetual 401 loop.
7
3
  export const DEFAULT_HTTP_HOST = '127.0.0.1';
8
4
  export const DEFAULT_HTTP_PORT = 4243;
9
5
  export const CLAUDE_AI_ORIGIN = 'https://claude.ai';
@@ -30,7 +26,7 @@ export function resolveTransport(env = process.env) {
30
26
  throw new HttpConfigError('E_INVALID_TRANSPORT', `MCP_TRANSPORT must be one of stdio|http|both (got "${env.MCP_TRANSPORT}")`);
31
27
  }
32
28
  /**
33
- * Canonicalize a public base URL per BR4. WHATWG `URL` already lowercases the
29
+ * Canonicalize a public base URL. WHATWG `URL` already lowercases the
34
30
  * scheme+host and omits a default port from `.host`; this adds the HTTPS-only
35
31
  * scheme check, strips fragment/query/userinfo, and removes a trailing slash.
36
32
  */
@@ -2,26 +2,58 @@ import type { McpServer, Transport } from "@modelcontextprotocol/server";
2
2
  import { type IncomingMessage, type ServerResponse } from 'node:http';
3
3
  import type { HttpConfig } from './http-config.js';
4
4
  import { type ArgShape, type StrictArgOptions, type ValidationEnvelopeOptions } from './arg-normalize.js';
5
+ export { logSafe } from './trim.js';
5
6
  export type AuthOutcome = {
6
7
  ok: true;
8
+ sub?: string;
7
9
  } | {
8
10
  ok: false;
9
11
  status: number;
10
12
  body: string;
11
13
  headers?: Record<string, string>;
12
14
  };
13
- /** Bearer / owner check for POST /mcp. B12 default = loopback-owner; B13 swaps in JWT verify. */
15
+ /** Bearer / owner check for POST /mcp. Default = loopback-owner; the AS swaps in JWT verify. */
14
16
  export type Authenticator = (req: IncomingMessage) => AuthOutcome | Promise<AuthOutcome>;
15
- /** A mounted extra route (the AS endpoints, B13). Return true if it wrote a response. */
17
+ /** A mounted extra route (the AS endpoints). Return true if it wrote a response. */
16
18
  export type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: URL) => boolean | Promise<boolean>;
19
+ /** What a verified subject resolves to: ITS server plus the per-request hooks
20
+ * bound to that server's registry (tool shapes, declared keys, the default
21
+ * account a validation envelope names). The metrics observers (metricsTap,
22
+ * onArgRename) stay host-wide and must not be bound to any one registry. */
23
+ export interface ServerTarget {
24
+ server: McpServer;
25
+ argShapeFor?: (tool: string) => ArgShape | undefined;
26
+ strictArgs?: StrictArgOptions;
27
+ validationEnvelope?: ValidationEnvelopeOptions;
28
+ }
17
29
  export interface HttpHostOptions {
18
- /** The ONE McpServer + registry built at boot (P1 / BV gap #4: never per request). */
30
+ /** The ONE McpServer + registry built at boot (never per request). */
19
31
  server: McpServer;
20
32
  config: HttpConfig;
21
33
  version: string;
22
34
  ownerConfigured: boolean;
23
35
  authenticate: Authenticator;
24
- /** Extra routes keyed by exact pathname (AS endpoints mount here in B13). */
36
+ /** Tenant-resolution seam: map the verified subject to ITS server. Absent
37
+ * (free core) = every request dispatches to the one boot `server`. Return
38
+ * null for an unknown subject -> 403 tenant_not_found. Each distinct
39
+ * resolved server gets its own dispatch lane. With a resolver, the
40
+ * per-request hooks come from the resolved target ONLY: the host-level
41
+ * argShapeFor / strictArgs / validationEnvelope describe the boot server's
42
+ * registry, never another subject's. */
43
+ resolveServer?: (claims: {
44
+ sub: string;
45
+ }) => Promise<ServerTarget | null | {
46
+ refused: {
47
+ error: string;
48
+ message: string;
49
+ };
50
+ }> | ServerTarget | null | {
51
+ refused: {
52
+ error: string;
53
+ message: string;
54
+ };
55
+ };
56
+ /** Extra routes keyed by exact pathname (AS endpoints mount here). */
25
57
  routes?: Record<string, RouteHandler>;
26
58
  log?: (line: string) => void;
27
59
  /** Max /mcp JSON body bytes (DoS guard). */
@@ -40,24 +72,49 @@ export interface HttpHostOptions {
40
72
  onArgRename?: (tool: string, renames: number) => void;
41
73
  /** Unknown-argument screening (arg-strict.ts); absent = off. */
42
74
  strictArgs?: StrictArgOptions;
75
+ /** Per-server cap on /mcp requests reading their body, queued or running;
76
+ * the next answers 429 lane_busy before its body is read. Unset = no cap. */
77
+ maxQueuedPerLane?: number;
78
+ /** Max JSON-RPC messages in one /mcp body (default 16). */
79
+ maxBatchFrames?: number;
43
80
  }
44
81
  export declare function parseOwnerEmails(env?: NodeJS.ProcessEnv): string[];
45
82
  /** Front guard: an Origin, if present, must be allowlisted; a Host must be
46
83
  * allowlisted. Absent Origin passes (native/CLI/backend clients — the claude.ai
47
- * connector calls /mcp server-to-server with no Origin, BV-4). */
84
+ * connector calls /mcp server-to-server with no Origin). */
48
85
  export declare function originAllowed(origin: string | undefined, allowed: string[]): boolean;
49
86
  export declare function hostAllowed(host: string | undefined, allowed: string[]): boolean;
50
- /** JSON-RPC method name(s) for an observability log line — no params, no PII. */
87
+ /** JSON-RPC method name(s) for an observability log line: no params, no PII.
88
+ * The method is the caller's: anything not shaped like a method name, which
89
+ * includes a value whose String() throws, is logged as `<invalid>`. */
51
90
  export declare function jsonRpcMethod(body: unknown): string;
52
91
  export declare class HttpTransportHost {
53
92
  private readonly opts;
54
93
  private httpServer?;
55
- private lock;
94
+ private readonly locks;
95
+ private readonly depth;
96
+ private readonly sockets;
97
+ private readonly pending;
98
+ private readonly served;
99
+ private readonly handling;
100
+ private closing;
101
+ private graceful?;
56
102
  constructor(opts: HttpHostOptions);
57
- private serialize;
103
+ private serializeFor;
58
104
  private log;
59
105
  start(): Promise<void>;
60
- close(): Promise<void>;
106
+ /** Without graceMs, every connection is cut at once, even during a graceful
107
+ * close. With it, the host stops accepting, lets in-flight requests finish
108
+ * and their responses finish writing, then resolves once each client has
109
+ * closed its side; whatever is still open after graceMs is cut. A repeat
110
+ * call with graceMs during a graceful close joins it, keeping the first
111
+ * grace. graceMs must be a number from 0 to 2^31-1 ms (about 24.8 days, the
112
+ * longest delay a timer holds); anything else, Infinity included, rejects
113
+ * with a TypeError and leaves the host as it was. */
114
+ close(opts?: {
115
+ graceMs?: number;
116
+ }): Promise<void>;
117
+ private closeGracefully;
61
118
  /** The bound port (useful when listening on port 0 in tests). */
62
119
  address(): {
63
120
  port: number;
@@ -65,6 +122,7 @@ export declare class HttpTransportHost {
65
122
  private handle;
66
123
  private frontGuard;
67
124
  private mcp;
125
+ private admitted;
68
126
  private health;
69
127
  private readJson;
70
128
  private fail;