mcp-google-multi 6.0.0-beta.3 → 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 (91) hide show
  1. package/README.md +5 -5
  2. package/dist/accounts.d.ts +3 -3
  3. package/dist/accounts.js +18 -31
  4. package/dist/arg-normalize.js +15 -35
  5. package/dist/arg-strict.js +8 -28
  6. package/dist/auth.d.ts +1 -1
  7. package/dist/auth.js +5 -10
  8. package/dist/boot-gates.d.ts +1 -1
  9. package/dist/boot-gates.js +3 -5
  10. package/dist/cimd-cache.js +5 -11
  11. package/dist/client-config.d.ts +2 -2
  12. package/dist/client-config.js +8 -14
  13. package/dist/client.js +0 -0
  14. package/dist/compose.d.ts +8 -1
  15. package/dist/compose.js +11 -11
  16. package/dist/config-file.js +7 -12
  17. package/dist/discover.js +4 -9
  18. package/dist/discovery-client.js +14 -33
  19. package/dist/doctor.d.ts +3 -3
  20. package/dist/doctor.js +12 -18
  21. package/dist/env-load.js +2 -5
  22. package/dist/executor.js +2 -4
  23. package/dist/fanout.js +8 -17
  24. package/dist/fs-atomic.d.ts +0 -3
  25. package/dist/fs-atomic.js +6 -10
  26. package/dist/hint-provider.js +4 -10
  27. package/dist/http-config.d.ts +2 -2
  28. package/dist/http-config.js +3 -7
  29. package/dist/http-transport.d.ts +5 -5
  30. package/dist/http-transport.js +39 -92
  31. package/dist/identity.d.ts +2 -2
  32. package/dist/identity.js +6 -9
  33. package/dist/index.js +19 -31
  34. package/dist/master-key.d.ts +3 -3
  35. package/dist/master-key.js +11 -22
  36. package/dist/mcp-token.d.ts +3 -13
  37. package/dist/mcp-token.js +16 -32
  38. package/dist/metrics-cli.d.ts +1 -1
  39. package/dist/metrics-cli.js +1 -1
  40. package/dist/metrics-tap.js +2 -5
  41. package/dist/migrate-config.d.ts +1 -2
  42. package/dist/migrate-config.js +3 -5
  43. package/dist/net-tuning.js +2 -4
  44. package/dist/oauth-as.d.ts +2 -2
  45. package/dist/oauth-as.js +32 -73
  46. package/dist/oauth-consent.js +4 -10
  47. package/dist/outbound-allowlist.js +4 -9
  48. package/dist/reauth-hint.js +2 -5
  49. package/dist/registry-transfer.d.ts +1 -1
  50. package/dist/registry-transfer.js +5 -13
  51. package/dist/registry.d.ts +1 -9
  52. package/dist/registry.js +38 -66
  53. package/dist/scope-catalog.d.ts +5 -0
  54. package/dist/scope-catalog.js +17 -7
  55. package/dist/scope-observability.d.ts +2 -3
  56. package/dist/scope-observability.js +8 -14
  57. package/dist/services.d.ts +3 -0
  58. package/dist/services.js +5 -10
  59. package/dist/ssrf-guard.js +11 -22
  60. package/dist/tenant-purge.js +2 -4
  61. package/dist/token-store.js +0 -0
  62. package/dist/tools/_coerce.d.ts +4 -4
  63. package/dist/tools/_errors.js +22 -39
  64. package/dist/tools/_local-files.js +4 -8
  65. package/dist/tools/account-wizard.d.ts +4 -3
  66. package/dist/tools/account-wizard.js +48 -54
  67. package/dist/tools/accounts-tool.d.ts +1 -1
  68. package/dist/tools/accounts-tool.js +1 -1
  69. package/dist/tools/admin.js +2 -3
  70. package/dist/tools/analytics.js +4 -5
  71. package/dist/tools/calendar.js +4 -7
  72. package/dist/tools/chat.js +2 -3
  73. package/dist/tools/contacts.js +8 -9
  74. package/dist/tools/docs.js +4 -9
  75. package/dist/tools/drive.js +18 -27
  76. package/dist/tools/forms.js +2 -3
  77. package/dist/tools/gmail-mime.d.ts +1 -1
  78. package/dist/tools/gmail-mime.js +7 -12
  79. package/dist/tools/gmail.d.ts +2 -2
  80. package/dist/tools/gmail.js +26 -40
  81. package/dist/tools/google-api.js +5 -7
  82. package/dist/tools/meet.js +2 -3
  83. package/dist/tools/searchconsole.js +2 -3
  84. package/dist/tools/sheets.js +2 -3
  85. package/dist/tools/slides.js +2 -3
  86. package/dist/tools/tasks.js +2 -3
  87. package/dist/trim.js +2 -3
  88. package/dist/usage-metrics.js +5 -12
  89. package/dist/write-control.d.ts +4 -8
  90. package/dist/write-control.js +3 -4
  91. package/package.json +21 -21
@@ -13,9 +13,8 @@ export function configFilePath(env = process.env) {
13
13
  export function tenantsDir(env = process.env) {
14
14
  return path.join(configDir(env), 'tenants');
15
15
  }
16
- // A tenant id becomes a filesystem path segment exactly like an account
17
- // alias, so it rides the same traversal guard. 'owner' is the single-owner
18
- // context's subject and client lane, so no tenant may take it.
16
+ // A tenant id becomes a filesystem path segment like an account alias (same
17
+ // traversal guard); 'owner' is the single-owner subject, so no tenant may take it.
19
18
  function assertTenantId(tenantId) {
20
19
  if (!ALIAS_RE.test(tenantId) || isReservedAlias(tenantId) || tenantId === 'owner') {
21
20
  throw new Error(`E_TENANT_ID_INVALID: invalid tenant id "${tenantId}".`);
@@ -52,9 +51,7 @@ const accountEntrySchema = z.strictObject({
52
51
  admin: z.boolean().optional(),
53
52
  });
54
53
  // .strict() everywhere: any unknown or secret-shaped key (clientSecret,
55
- // masterKey, jwtKey, clientId) fails validation, structurally enforcing
56
- // secrets-env-only. scopeProfiles/defaultAccount/discovery/toolsets are part
57
- // of the frozen envelope; their consumers land in later slices.
54
+ // masterKey, jwtKey, clientId) fails validation, structurally enforcing secrets-env-only.
58
55
  export const RESERVED_ALIASES = ['__proto__', 'constructor', 'prototype'];
59
56
  export function isReservedAlias(name) {
60
57
  return RESERVED_ALIASES.includes(name);
@@ -72,9 +69,8 @@ const configSchema = z.strictObject({
72
69
  defaultAccount: z.string().optional(),
73
70
  discovery: z.enum(['lazy', 'curated', 'eager']).optional(),
74
71
  toolsets: z.string().optional(),
75
- // Local usage metrics (metrics-feature-spec): absent = off. Downgrade rule:
76
- // a pre-6.0 build reading a config carrying this key fails E_CONFIG_INVALID
77
- // (strictObject); remove the key first.
72
+ // Absent = off. A pre-6.0 build reading a config carrying this key fails
73
+ // E_CONFIG_INVALID (strictObject); remove the key first to downgrade.
78
74
  usageMetrics: z.boolean().optional(),
79
75
  });
80
76
  export function failStartup(slug, message) {
@@ -125,9 +121,8 @@ export function loadConfigFile(filePath = configFilePath(), onInvalid = 'exit')
125
121
  const issue = result.error.issues[0];
126
122
  fail('E_CONFIG_INVALID', `${filePath}: ${issue.path.join('.') || '(root)'}: ${issue.message}. Secrets never belong in config.json.`);
127
123
  }
128
- // Post-schema because zod's key regex passes these; a __proto__ record key
129
- // is a prototype-pollution foothold, and Object.prototype member names break
130
- // plain-object lookups downstream.
124
+ // Post-schema: zod's key regex passes these, but a __proto__ record key is
125
+ // a prototype-pollution foothold downstream.
131
126
  const data = result.data;
132
127
  for (const [section, keys] of [
133
128
  ['accounts', Object.keys(data.accounts ?? {})],
package/dist/discover.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { describePolicy } from './write-control.js';
3
3
  import { hints } from './hint-provider.js';
4
- // The description budget is the whole point of lazy mode: a full op list per
5
- // service put lazy tools/list at ~11.5k tokens (RQ2), so descriptions carry a
6
- // CAPPED vocabulary and the complete catalog stays in this tool's RESULT.
4
+ // The description budget is the whole point of lazy mode: descriptions carry
5
+ // a CAPPED vocabulary, and the complete catalog stays in this tool's RESULT.
7
6
  const CURATED_OPS_CAP = 10;
8
7
  const GENERATED_GROUPS_CAP = 6;
9
8
  function opVocabulary(registry, service) {
@@ -15,8 +14,7 @@ function opVocabulary(registry, service) {
15
14
  parts.push(`${shown.join(', ')}${more > 0 ? ` +${more} more` : ''}`);
16
15
  }
17
16
  if (generated.length > 0) {
18
- // The generated long tail compresses to its resource groups: denser and
19
- // more selective than any truncated name list.
17
+ // Compresses to resource groups: denser than any truncated name list.
20
18
  const counts = new Map();
21
19
  for (const op of generated) {
22
20
  const group = op.split('_')[0];
@@ -31,10 +29,7 @@ function opVocabulary(registry, service) {
31
29
  }
32
30
  export function registerDiscoverTools(registry, policy) {
33
31
  const registerMeta = registry.registerMeta;
34
- // Agent-controllable runtime expansion (D3 refinement): reveal the whole
35
- // curated quality layer for heavy Google work, collapse to reclaim the
36
- // name/schema context budget after. stdio-only semantics — over stateless
37
- // HTTP the mode is forced curated and these are no-ops.
32
+ // stdio-only semantics: over stateless HTTP the mode is forced curated and these are no-ops.
38
33
  registerMeta('discover_all', {
39
34
  description: 'Reveal ALL curated Google tools at once (instead of per-service discovery). Use when ' +
40
35
  'starting substantial Google work; prefer these over google_api_call. Pair with discover_reset.',
@@ -35,10 +35,8 @@ export const SUPPORTED_APIS = {
35
35
  vault: { id: 'vault', version: 'v1' },
36
36
  workspaceevents: { id: 'workspaceevents', version: 'v1' },
37
37
  };
38
- // Names agents actually type for an API, mapped to real SUPPORTED_APIS keys.
39
- // Motivated by observed escape-hatch misses ("analytics" is two Discovery
40
- // APIs, the Admin SDK is three); keep entries plural-target only when the
41
- // split is real.
38
+ // Names agents actually type for an API, mapped to real SUPPORTED_APIS keys;
39
+ // plural-target entries only where the split ("analytics" is two APIs) is real.
42
40
  export const API_ALIASES = {
43
41
  analytics: ['analyticsadmin', 'analyticsdata'],
44
42
  ga4: ['analyticsadmin', 'analyticsdata'],
@@ -74,9 +72,8 @@ export function discoveryCacheDir(env = process.env) {
74
72
  return (env.DISCOVERY_CACHE_PATH ??
75
73
  path.join(env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'mcp-google-multi', 'discovery'));
76
74
  }
77
- // Default cache path only (an operator's DISCOVERY_CACHE_PATH may be shared,
78
- // even /tmp, and keeps its mode), only when this process owns it, and best
79
- // effort. Clears group/other bits and keeps setuid/setgid/sticky.
75
+ // Default cache path only (an operator's DISCOVERY_CACHE_PATH may be shared
76
+ // and keeps its mode), only when this process owns it; clears group/other bits.
80
77
  function tightenOwnDir(dir) {
81
78
  if (process.platform === 'win32')
82
79
  return;
@@ -110,9 +107,8 @@ export function buildMethodIndex(doc, api) {
110
107
  if (!method.id || !method.httpMethod || !method.path)
111
108
  continue;
112
109
  const params = method.parameters ?? {};
113
- // Google serves parameters{} with unstable key order between fetches;
114
- // parameterOrder is the canonical sequence, so sort by it (alphabetical
115
- // tie-break) to keep gen-tools output deterministic.
110
+ // parameters{} has unstable key order between fetches; sort by the
111
+ // canonical parameterOrder (alphabetical tie-break) for deterministic output.
116
112
  const order = method.parameterOrder ?? [];
117
113
  const pos = (n) => {
118
114
  const i = order.indexOf(n);
@@ -174,9 +170,8 @@ export async function loadMethodIndex(api, deps = {}) {
174
170
  if (cacheFresh())
175
171
  doc = readCache();
176
172
  if (!doc) {
177
- // Newer APIs (analyticsadmin/analyticsdata) are absent from the central
178
- // discovery directory (404); each service's own $discovery endpoint is
179
- // authoritative, so try both — same fallback as scripts/fetch-discovery.
173
+ // Newer APIs are absent from the central discovery directory (404); try
174
+ // each service's own $discovery endpoint too, as scripts/fetch-discovery does.
180
175
  const urls = [
181
176
  `https://www.googleapis.com/discovery/v1/apis/${spec.id}/${spec.version}/rest`,
182
177
  `https://${spec.id}.googleapis.com/$discovery/rest?version=${spec.version}`,
@@ -217,20 +212,11 @@ export function clearDiscoveryMemoryCache() {
217
212
  // GA4-style report execution (runReport, batchRunPivotReports, runAccessReport)
218
213
  // and check* predicates are POSTs purely for the request-body size — reads.
219
214
  const POST_READ_VERB = /^(get|list|search|query|lookup|count|batchGet|generateIds|export|download|inspect|check|suggest|(batch)?run\w*report)/i;
220
- // `cancelWipe` must be matched here, BEFORE `cancel` reaches the delete list:
221
- // it calls off a pending wipe, which is the opposite of one. Update is tested
222
- // before delete for exactly that reason.
223
- // State transitions on something that already exists: reversible, and none of
224
- // them creates anything, so `create` was both wrong and the most permissive
225
- // class available. `(batch)?` mirrors the delete list, without which every
226
- // `batchUpdate*` POST fell through to `create`.
215
+ // Update, tested before delete, so cancelWipe (calling off a wipe) doesn't
216
+ // reach the delete list via its "cancel" suffix. See docs/internals.md.
227
217
  const POST_UPDATE_VERB = /^(cancelWipe|(batch)?(untrash|undelete|restore|modify|move|set|sort|merge|unmerge|replace|resize|publish|resolve|update|patch|write|format|change|close|reopen|enable|disable|hide|unhide|accept|approve|decline|reassign|reactivate|renew|mark|turn|suspend|activate|make|return|reclaim|complete))/i;
228
- // archive sits with the deletes: in GA4 archiving a custom dimension/metric is
229
- // permanent, so the most restrictive write class is the safe classification.
230
- // So do the teardown verbs: `stop` and `cancel` tear down a push channel or a
231
- // long-running operation, `wipe`/`invalidate`/`signOut` destroy device data,
232
- // codes and sessions, `reset` discards a configuration, and `end` terminates a
233
- // live conference. Each removes something that existed.
218
+ // archive/wipe/reset/stop/cancel/end etc. all remove or permanently discard
219
+ // something that existed, so the most restrictive class is the safe one. See docs/internals.md.
234
220
  const POST_DELETE_VERB = /^(batch)?(delete|remove|trash|clear|empty|obliterate|purge|revoke|wipeout|archive|wipe|invalidate|signOut|stop|cancel|unregister|unreserve|reset|end)/i;
235
221
  export function cudFromMethod(method) {
236
222
  switch (method.httpMethod) {
@@ -262,13 +248,8 @@ export function expandPath(template, pathParams) {
262
248
  if (value === undefined) {
263
249
  throw new Error(`Missing required path parameter "${name}"`);
264
250
  }
265
- // A blank segment collapses the path and Google routes the resulting
266
- // trailing-slash URL to the COLLECTION: `sites.get` with siteUrl:"" came
267
- // back as the whole site LIST, reported as success. Dot segments resolve
268
- // at URL-parse time, so `../v1/contactGroups` retargets the call at a
269
- // different endpoint and returns ITS response as success. Both are
270
- // silent-wrong, so refuse locally rather than send a different request.
271
- // `{+param}` keeps its slashes, so every segment has to be checked.
251
+ // Both a blank segment and a "." / ".." segment silently retarget the
252
+ // request to a different, successfully-answered endpoint. See docs/internals.md.
272
253
  const segments = plus ? String(value).split('/') : [String(value)];
273
254
  for (const segment of segments) {
274
255
  if (segment.trim() === '') {
package/dist/doctor.d.ts CHANGED
@@ -52,10 +52,10 @@ export interface HttpProbeResult {
52
52
  unreachable?: boolean;
53
53
  problem?: string;
54
54
  }
55
- /** Console deep-link to enable one API (section-6 hint, error taxonomy B10). */
55
+ /** Console deep-link to enable one API (section-6 hint). */
56
56
  export declare function apiEnableLink(api: string): string;
57
57
  /** One line, state AND source, so the people being measured can see both
58
- * here and in `diagnose` (metrics spec section 2). Read-only. */
58
+ * here and in `diagnose`. Read-only. */
59
59
  export declare function usageMetricsStatusLine(env: Record<string, string | undefined>): string;
60
60
  /** Roll section verdicts to an overall verdict. `unknown` never worsens it. */
61
61
  export declare function overallVerdict(sections: DiagnosticSection[]): Verdict;
@@ -64,7 +64,7 @@ export declare function runDiagnostics(deps?: DiagnosticsDeps, opts?: {
64
64
  }): Promise<DiagnosticsReport>;
65
65
  /** doctor CLI (brew-doctor style): glyph-coded human report. */
66
66
  export declare function renderDoctorText(report: DiagnosticsReport): string;
67
- /** Redacted, paste-ready bug report (BR8): verdicts + provenance labels + token
67
+ /** Redacted, paste-ready bug report: verdicts + provenance labels + token
68
68
  * statuses + slugs; NO token values, secrets, keys, or full email addresses. */
69
69
  export declare function renderReport(report: DiagnosticsReport, version: string): string;
70
70
  /** Exit non-zero when any section FAILs; with strict, WARN counts too. */
package/dist/doctor.js CHANGED
@@ -35,9 +35,8 @@ const DEFAULT_DEPS = {
35
35
  probeApi: (alias) => probeApiEnablement(alias),
36
36
  probeHttp: (cfg) => probeHttpEndpoints(cfg),
37
37
  };
38
- /** §7 live check: the advertised OAuth metadata must derive from MCP_PUBLIC_URL
39
- * exactly — one mismatch between PRM `resource` / AS `issuer` and what clients
40
- * compute from the public URL is the perpetual-401 interop bug (BR4). */
38
+ // §7 live check: a mismatch between PRM/AS metadata and what clients compute
39
+ // from the public URL is the perpetual-401 interop bug.
41
40
  async function probeHttpEndpoints(cfg) {
42
41
  try {
43
42
  const prmRes = await fetch(`${cfg.publicUrl}/.well-known/oauth-protected-resource`, {
@@ -66,7 +65,7 @@ async function probeHttpEndpoints(cfg) {
66
65
  return { ok: false, unreachable: true };
67
66
  }
68
67
  }
69
- /** Console deep-link to enable one API (section-6 hint, error taxonomy B10). */
68
+ /** Console deep-link to enable one API (section-6 hint). */
70
69
  export function apiEnableLink(api) {
71
70
  return `https://console.cloud.google.com/apis/library/${api}.googleapis.com`;
72
71
  }
@@ -75,8 +74,8 @@ function sectionRuntime(deps, tenantScope = false) {
75
74
  const major = Number.parseInt(deps.nodeVersion.split('.')[0] ?? '0', 10);
76
75
  if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
77
76
  if (tenantScope) {
78
- // The host version is operator infrastructure, not the tenant's (E7
79
- // residue): no version string, and the fix is the operator's to apply.
77
+ // The host version is operator infrastructure, not the tenant's: no
78
+ // version string, and the fix is the operator's to apply.
80
79
  const rendered = hints()?.render({ kind: 'operator_only', slug: 'E_NODE_TOO_OLD' });
81
80
  return {
82
81
  id: 1,
@@ -150,7 +149,7 @@ function sectionConfig(deps, set, tenantScope = false) {
150
149
  return { id: 2, title: 'Config', verdict, lines, ...(hint ? { hint } : {}), ...(slug ? { slug } : {}) };
151
150
  }
152
151
  /** One line, state AND source, so the people being measured can see both
153
- * here and in `diagnose` (metrics spec section 2). Read-only. */
152
+ * here and in `diagnose`. Read-only. */
154
153
  export function usageMetricsStatusLine(env) {
155
154
  const envSrc = envValueSource('GOOGLE_USAGE_METRICS');
156
155
  let configValue;
@@ -240,8 +239,7 @@ async function sectionApiEnablement(deps, aliases, tenantScope = false) {
240
239
  }
241
240
  catch (e) {
242
241
  // Network / transient: WARN with the target, never crash the report. The
243
- // raw text can name an egress or proxy host (E7 residue): tenant scope
244
- // drops it.
242
+ // raw text can name an egress or proxy host: tenant scope drops it.
245
243
  return {
246
244
  id: 6,
247
245
  title: 'API enablement',
@@ -338,10 +336,8 @@ export function overallVerdict(sections) {
338
336
  return worst;
339
337
  }
340
338
  export async function runDiagnostics(deps = DEFAULT_DEPS, opts = {}) {
341
- // Tenant scope omits the OPERATOR sections: §3 (key provenance = box
342
- // infrastructure) and §7 (its owner-gate lines print MCP_OWNER_EMAILS
343
- // values — other people's addresses from a tenant's seat), and trims §2 to
344
- // the context's own account count.
339
+ // Tenant scope omits the OPERATOR sections (§3 key provenance, §7 owner-gate
340
+ // emails) and trims §2 to the context's own account count.
345
341
  const tenantScope = opts.scope === 'tenant';
346
342
  const sections = [];
347
343
  sections.push(sectionRuntime(deps, tenantScope));
@@ -381,10 +377,8 @@ export function renderDoctorText(report) {
381
377
  }
382
378
  const EMAIL_LOCAL_CHAR = /[A-Za-z0-9._%+-]/;
383
379
  const EMAIL_DOMAIN_CHAR = /[A-Za-z0-9.-]/;
384
- /** Mask the local-part of an email so a report never carries a full address
385
- * (BR8). A linear scan, not a regex: the `local* @domain` pattern backtracks
386
- * polynomially on long local-charset runs without an @ (CodeQL
387
- * js/polynomial-redos), and report text can embed arbitrary error strings. */
380
+ // A linear scan, not a regex: `local*@domain` backtracks polynomially on long
381
+ // local-charset runs without an @ (CodeQL js/polynomial-redos).
388
382
  function maskEmails(text) {
389
383
  let out = '';
390
384
  let i = 0;
@@ -409,7 +403,7 @@ function maskEmails(text) {
409
403
  }
410
404
  return out;
411
405
  }
412
- /** Redacted, paste-ready bug report (BR8): verdicts + provenance labels + token
406
+ /** Redacted, paste-ready bug report: verdicts + provenance labels + token
413
407
  * statuses + slugs; NO token values, secrets, keys, or full email addresses. */
414
408
  export function renderReport(report, version) {
415
409
  const out = [`mcp-google-multi doctor report (v${version})`, `overall: ${report.verdict}`, ''];
package/dist/env-load.js CHANGED
@@ -3,11 +3,8 @@ import path from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
- // Precedence: real env > CWD .env > package-root .env > ~/.config .env.
7
- // process.loadEnvFile never overwrites keys already in process.env (pinned by
8
- // tests/env-load.test.ts T4), so highest-priority file loads FIRST and real
9
- // env wins by already being present; the boot snapshot re-assert is a second
10
- // line of defense should those semantics ever change.
6
+ // Precedence: real env > CWD .env > package-root .env > ~/.config .env;
7
+ // the boot snapshot re-assert below is a second line of defense.
11
8
  export function loadEnvFiles(paths = {}) {
12
9
  // loadEnvFile exists since 20.12, so a feature check alone would let 20.12-21.x through.
13
10
  const major = Number(process.versions.node.split('.', 1)[0]);
package/dist/executor.js CHANGED
@@ -91,10 +91,8 @@ export async function executeApiMethod(method, args, deps = {}) {
91
91
  });
92
92
  const text = JSON.stringify(res.data ?? null);
93
93
  if (text.length > MAX_RESPONSE_CHARS) {
94
- // Budget the ENCODED head: jsonResult re-stringifies it, so every quote
95
- // and backslash inside doubles. Slicing raw characters made the emitted
96
- // result exceed the cap this tool advertises, by up to 15 percent on a
97
- // quote-dense listing.
94
+ // Budget the ENCODED head: jsonResult re-stringifies it, so slicing raw
95
+ // characters overshot the advertised cap on a quote-dense listing.
98
96
  return jsonResult({
99
97
  truncated: true,
100
98
  totalChars: text.length,
package/dist/fanout.js CHANGED
@@ -4,19 +4,13 @@ import { echoName, echoNames, MAX_SUGGESTED_KEYS } from './arg-strict.js';
4
4
  import { hints } from './hint-provider.js';
5
5
  export const CSV_RE = /^[a-zA-Z0-9_-]+(\s*,\s*[a-zA-Z0-9_-]+)+$/;
6
6
  const FANOUT_CONCURRENCY = 5;
7
- // `aliases` is required (no global default): the field is baked into a
8
- // registry's inputSchema, so it must read THAT registry's account set or a
9
- // '*' would advertise and accept another registry's aliases. It is a getter:
10
- // the set is read at parse time, so an alias added mid-session (account_add,
11
- // a tenant link) is valid on tools registered before it existed.
7
+ // `aliases` is a getter read at parse time (not baked), so a registry only
8
+ // ever sees ITS OWN account set, and a mid-session alias (account_add) is valid on tools registered before it existed.
12
9
  export function fanoutAccountField(description, aliases) {
13
10
  const boot = aliases();
14
11
  const csvExample = boot.length > 1 ? `; or a CSV subset like "${boot.slice(0, 2).join(',')}"` : '';
15
- // The union's OWN error only fires when every branch aborts at the type
16
- // check (a non-string). For a string, zod surfaces the single non-aborted
17
- // branch verbatim, which is the CSV regex, so a mistyped alias used to read
18
- // as "Invalid string: must match pattern /^[a-zA-Z0-9_-]+(,...)/" and never
19
- // named an alias. Hence the same message on BOTH.
12
+ // Same message on both union branches: for a string, zod surfaces the
13
+ // single non-aborted branch (the CSV regex) verbatim, which never named the alias.
20
14
  const bad = () => unknownAliasMessage(aliases(), true);
21
15
  return z
22
16
  // The selector branch stays first: tools/list injects the live
@@ -46,9 +40,8 @@ export function noAccountsResult() {
46
40
  export function parseAccountSelector(value, accounts = getAccountSet().aliases) {
47
41
  if (value === '*')
48
42
  return { ok: true, fanout: true, aliases: [...accounts] };
49
- // A single alias used to skip validation entirely and reach getClient, whose
50
- // throw landed on the mapper's floor as an unclassified upstream_error. Same
51
- // check and same envelope wherever the alias was typed.
43
+ // A single alias used to skip validation and reach getClient, whose throw
44
+ // landed on the mapper's floor as an unclassified upstream_error.
52
45
  if (!value.includes(',')) {
53
46
  return accounts.includes(value)
54
47
  ? { ok: true, fanout: false, aliases: [value] }
@@ -132,10 +125,8 @@ export async function runFanout(handler, args, aliases) {
132
125
  await Promise.all(Array.from({ length: Math.min(FANOUT_CONCURRENCY, aliases.length) }, () => worker()));
133
126
  const failed = results.filter((r) => !r.ok).length;
134
127
  if (failed === results.length && results.length > 0) {
135
- // Every account failed, so this IS an error result, and used to carry
136
- // `isError` with no top-level error, hint or retriable: the agent saw a
137
- // successful-looking object flagged as an error, and metrics bucketed it
138
- // as `other`. Hoist a real envelope, keeping the per-account detail.
128
+ // Every account failed, so this IS an error result: hoist a real
129
+ // top-level envelope (error/hint/retriable), keeping the per-account detail.
139
130
  const slugs = [...new Set(results.map((r) => r.error?.error ?? 'internal'))];
140
131
  const hints = [...new Set(results.map((r) => r.error?.hint).filter(Boolean))];
141
132
  const retriable = results.every((r) => r.error?.retriable === true);
@@ -1,6 +1,3 @@
1
- /** Which process instance holds a lock: PID namespace id, start time in
2
- * clock ticks since boot, and a boot id prefix. A part this process cannot
3
- * read is `host` or `0` (all three without /proc). */
4
1
  interface LockIdentity {
5
2
  ns: string;
6
3
  start: string;
package/dist/fs-atomic.js CHANGED
@@ -129,9 +129,8 @@ function holderDead(holder, now) {
129
129
  // Two unreadable parts compare equal without naming the same namespace or
130
130
  // boot, so only a fully known identity is looked up by start time.
131
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
133
- // look up: a restarted container reuses both the id and the pid, but not
134
- // the start time.
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.
135
134
  const start = startOf(pid);
136
135
  if (start !== undefined)
137
136
  return start !== owner.start;
@@ -141,9 +140,8 @@ function holderDead(holder, now) {
141
140
  return holder.mtimeMs < PROCESS_START_MS;
142
141
  return probeDead(pid) || leaseOver;
143
142
  }
144
- // One waiter at a time breaks a dead lock: two breakers could otherwise both
145
- // see the dead holder, and the second would unlink the first one's new lock.
146
- // False when another waiter is breaking it.
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.
147
145
  function breakLock(lock, holder) {
148
146
  const breaker = `${lock}.break`;
149
147
  // A token, not the inode: a breaker created after ours was removed can
@@ -198,10 +196,8 @@ function releaseBreaker(breaker, token) {
198
196
  throw error;
199
197
  }
200
198
  }
201
- // Hardlink-based advisory lock, generalized path-keyed from the token store's
202
- // alias-keyed original. The lock body is the bare pid, as older readers expect;
203
- // the owner file stays linked while held and names the holder's process
204
- // instance. See docs/internals.md.
199
+ // Hardlink-based advisory lock; the lock body is the bare pid, as older
200
+ // readers expect. See docs/internals.md.
205
201
  export function withFileLock(absPath, fn, label = `lock: ${absPath}`) {
206
202
  const dir = path.dirname(absPath);
207
203
  const lock = path.join(dir, `.${path.basename(absPath)}.lock`);
@@ -1,9 +1,5 @@
1
- // A plain array literal, not derived from the union: deriving it would need a
2
- // value-level mirror of the type anyway, and this way a missing member is
3
- // caught by the completeness check below instead of silently compiling.
4
- // `as const` so that check narrows to the literal tuple rather than widening
5
- // every element back to the whole HintCase['kind'] union (which would make
6
- // it pass trivially, whatever was listed).
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.
7
3
  export const HINT_KINDS = [
8
4
  'reauth',
9
5
  'legacy_plaintext_token',
@@ -43,10 +39,8 @@ let installed = null;
43
39
  export function setHintProvider(p) {
44
40
  installed = p;
45
41
  }
46
- // A throwing provider must never take down the call site: every call site
47
- // does `hints()?.render(...)?.hint ?? <default>`, so a render that cannot
48
- // produce a Rendered falls back to the same literal a missing provider
49
- // would, and the provider's own error text never reaches a client.
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.
50
44
  export function hints() {
51
45
  if (!installed)
52
46
  return null;
@@ -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
  */
@@ -12,9 +12,9 @@ export type AuthOutcome = {
12
12
  body: string;
13
13
  headers?: Record<string, string>;
14
14
  };
15
- /** 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. */
16
16
  export type Authenticator = (req: IncomingMessage) => AuthOutcome | Promise<AuthOutcome>;
17
- /** 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. */
18
18
  export type RouteHandler = (req: IncomingMessage, res: ServerResponse, url: URL) => boolean | Promise<boolean>;
19
19
  /** What a verified subject resolves to: ITS server plus the per-request hooks
20
20
  * bound to that server's registry (tool shapes, declared keys, the default
@@ -27,7 +27,7 @@ export interface ServerTarget {
27
27
  validationEnvelope?: ValidationEnvelopeOptions;
28
28
  }
29
29
  export interface HttpHostOptions {
30
- /** 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). */
31
31
  server: McpServer;
32
32
  config: HttpConfig;
33
33
  version: string;
@@ -53,7 +53,7 @@ export interface HttpHostOptions {
53
53
  message: string;
54
54
  };
55
55
  };
56
- /** Extra routes keyed by exact pathname (AS endpoints mount here in B13). */
56
+ /** Extra routes keyed by exact pathname (AS endpoints mount here). */
57
57
  routes?: Record<string, RouteHandler>;
58
58
  log?: (line: string) => void;
59
59
  /** Max /mcp JSON body bytes (DoS guard). */
@@ -81,7 +81,7 @@ export interface HttpHostOptions {
81
81
  export declare function parseOwnerEmails(env?: NodeJS.ProcessEnv): string[];
82
82
  /** Front guard: an Origin, if present, must be allowlisted; a Host must be
83
83
  * allowlisted. Absent Origin passes (native/CLI/backend clients — the claude.ai
84
- * connector calls /mcp server-to-server with no Origin, BV-4). */
84
+ * connector calls /mcp server-to-server with no Origin). */
85
85
  export declare function originAllowed(origin: string | undefined, allowed: string[]): boolean;
86
86
  export declare function hostAllowed(host: string | undefined, allowed: string[]): boolean;
87
87
  /** JSON-RPC method name(s) for an observability log line: no params, no PII.