mcp-google-multi 6.0.0-beta.3 → 6.0.0-beta.5

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 (126) hide show
  1. package/README.md +20 -6
  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 +6 -12
  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 +104 -22
  26. package/dist/hint-provider.d.ts +4 -1
  27. package/dist/hint-provider.js +4 -10
  28. package/dist/http-config.d.ts +2 -2
  29. package/dist/http-config.js +3 -7
  30. package/dist/http-transport.d.ts +5 -5
  31. package/dist/http-transport.js +39 -92
  32. package/dist/identity.d.ts +2 -2
  33. package/dist/identity.js +6 -9
  34. package/dist/index.js +19 -31
  35. package/dist/master-key.d.ts +3 -3
  36. package/dist/master-key.js +11 -22
  37. package/dist/mcp-token.d.ts +40 -16
  38. package/dist/mcp-token.js +121 -44
  39. package/dist/metrics-cli.d.ts +1 -1
  40. package/dist/metrics-cli.js +1 -1
  41. package/dist/metrics-tap.js +2 -5
  42. package/dist/migrate-config.d.ts +1 -2
  43. package/dist/migrate-config.js +3 -5
  44. package/dist/net-tuning.js +2 -4
  45. package/dist/oauth-as.d.ts +7 -2
  46. package/dist/oauth-as.js +45 -78
  47. package/dist/oauth-consent.js +4 -10
  48. package/dist/outbound-allowlist.js +4 -9
  49. package/dist/reauth-hint.js +2 -5
  50. package/dist/registry-transfer.d.ts +1 -1
  51. package/dist/registry-transfer.js +5 -13
  52. package/dist/registry.d.ts +3 -9
  53. package/dist/registry.js +97 -76
  54. package/dist/scope-catalog.d.ts +5 -0
  55. package/dist/scope-catalog.js +17 -7
  56. package/dist/scope-observability.d.ts +2 -3
  57. package/dist/scope-observability.js +8 -14
  58. package/dist/services.d.ts +3 -0
  59. package/dist/services.js +5 -10
  60. package/dist/ssrf-guard.js +11 -22
  61. package/dist/tenant-purge.js +2 -4
  62. package/dist/token-store.js +0 -0
  63. package/dist/tools/_coerce.d.ts +4 -4
  64. package/dist/tools/_errors.js +22 -39
  65. package/dist/tools/_local-files.js +4 -8
  66. package/dist/tools/account-wizard.d.ts +4 -3
  67. package/dist/tools/account-wizard.js +48 -54
  68. package/dist/tools/accounts-tool.d.ts +8 -1
  69. package/dist/tools/accounts-tool.js +18 -7
  70. package/dist/tools/admin.js +8 -4
  71. package/dist/tools/analytics.js +32 -16
  72. package/dist/tools/calendar.d.ts +5 -0
  73. package/dist/tools/calendar.js +63 -7
  74. package/dist/tools/chat.js +2 -3
  75. package/dist/tools/contacts.js +19 -14
  76. package/dist/tools/docs.js +45 -21
  77. package/dist/tools/drive.js +72 -39
  78. package/dist/tools/forms.js +2 -3
  79. package/dist/tools/generated/gmail.js +0 -14
  80. package/dist/tools/generated/method-map.js +1 -1
  81. package/dist/tools/gmail-mime.d.ts +1 -1
  82. package/dist/tools/gmail-mime.js +7 -12
  83. package/dist/tools/gmail.d.ts +3 -3
  84. package/dist/tools/gmail.js +135 -52
  85. package/dist/tools/google-api.js +5 -7
  86. package/dist/tools/meet.js +2 -3
  87. package/dist/tools/searchconsole.js +2 -3
  88. package/dist/tools/sheets.d.ts +10 -0
  89. package/dist/tools/sheets.js +111 -5
  90. package/dist/tools/slides.js +2 -3
  91. package/dist/tools/tasks.js +2 -3
  92. package/dist/trim.js +2 -3
  93. package/dist/usage-metrics.js +5 -12
  94. package/dist/write-control.d.ts +4 -8
  95. package/dist/write-control.js +18 -6
  96. package/package.json +24 -22
  97. package/skills/google-workspace/SKILL.md +162 -0
  98. package/skills/google-workspace/reference/admin.md +105 -0
  99. package/skills/google-workspace/reference/analytics.md +70 -0
  100. package/skills/google-workspace/reference/appsmarket.md +5 -0
  101. package/skills/google-workspace/reference/calendar.md +175 -0
  102. package/skills/google-workspace/reference/chat.md +59 -0
  103. package/skills/google-workspace/reference/classroom.md +5 -0
  104. package/skills/google-workspace/reference/cloudidentity.md +5 -0
  105. package/skills/google-workspace/reference/cloudsearch.md +5 -0
  106. package/skills/google-workspace/reference/contacts.md +138 -0
  107. package/skills/google-workspace/reference/docs.md +395 -0
  108. package/skills/google-workspace/reference/drive.md +485 -0
  109. package/skills/google-workspace/reference/driveactivity.md +5 -0
  110. package/skills/google-workspace/reference/drivelabels.md +5 -0
  111. package/skills/google-workspace/reference/forms.md +92 -0
  112. package/skills/google-workspace/reference/gmail.md +305 -0
  113. package/skills/google-workspace/reference/groupsmigration.md +5 -0
  114. package/skills/google-workspace/reference/groupssettings.md +5 -0
  115. package/skills/google-workspace/reference/keep.md +5 -0
  116. package/skills/google-workspace/reference/licensing.md +5 -0
  117. package/skills/google-workspace/reference/meet.md +68 -0
  118. package/skills/google-workspace/reference/postmaster.md +5 -0
  119. package/skills/google-workspace/reference/reseller.md +5 -0
  120. package/skills/google-workspace/reference/script.md +5 -0
  121. package/skills/google-workspace/reference/searchconsole.md +128 -0
  122. package/skills/google-workspace/reference/sheets.md +430 -0
  123. package/skills/google-workspace/reference/slides.md +67 -0
  124. package/skills/google-workspace/reference/tasks.md +167 -0
  125. package/skills/google-workspace/reference/vault.md +5 -0
  126. package/skills/google-workspace/reference/workspaceevents.md +5 -0
@@ -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.
@@ -1,30 +1,21 @@
1
1
  import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
2
- // node:http server, the route table, the front guard (Host / Origin / DNS-rebind),
3
- // and the stateless per-request /mcp dispatch. The OAuth AS endpoints and Bearer
4
- // verification are a seam filled by B13 (oauth-authorization-server); B12 ships a
5
- // loopback-owner authenticator so the local-HTTP model works before the AS lands.
6
2
  import { createServer } from 'node:http';
7
3
  import { withArgNormalization, withValidationEnvelope } from './arg-normalize.js';
8
4
  import { logSafe } from './trim.js';
9
5
  // A host wrapping the log callbacks it hands core needs this, and trim has no package export.
10
6
  export { logSafe } from './trim.js';
11
7
  // A hung handler that keeps the connection open would otherwise hold the global
12
- // serialize() lock forever. Generous by default so slow-but-valid calls (large
13
- // Drive exports, fan-out) still finish; the point is only to guarantee release.
8
+ // serialize() lock forever; generous by default so slow-but-valid calls still finish.
14
9
  const DISPATCH_TIMEOUT_DEFAULT = 120_000;
15
- /** MCP 2025-06-18 dropped JSON-RPC batching, and the SDK screens every frame
16
- * of a batch synchronously in one tick: without a cap one body multiplies the
17
- * per-call work by however many frames fit in it. */
10
+ // Without a cap, one body multiplies per-call work by however many
11
+ // JSON-RPC frames the SDK's synchronous batch screening finds in it.
18
12
  const MAX_BATCH_FRAMES = 16;
19
- // A resolveServer refusal's body reaches the wire verbatim (H3 PR1): validated
20
- // so a host cannot accidentally leak an operator path/env var through a wrongly
21
- // shaped slug or an overlong message. No new slug literal enters core itself —
22
- // the slug is always the host's own value.
13
+ // A refusal's body reaches the wire verbatim, so it's validated: no new slug
14
+ // literal enters core, and an overlong message can't leak an operator path/env var.
23
15
  const REFUSAL_SLUG_RE = /^[a-z][a-z0-9_]{0,39}$/;
24
16
  const MAX_REFUSAL_MESSAGE_CHARS = 300;
25
- // C0/C1 controls, format chars (includes bidi overrides like U+202E and
26
- // zero-width joiners) and the line/paragraph separators; ordinary non-ASCII
27
- // letters stay allowed.
17
+ // C0/C1 controls, format chars (bidi overrides, zero-width joiners) and
18
+ // line/paragraph separators; ordinary non-ASCII letters stay allowed.
28
19
  const NON_PRINTABLE_RE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
29
20
  function isValidRefusal(r) {
30
21
  return (typeof r.error === 'string' &&
@@ -42,7 +33,7 @@ export function parseOwnerEmails(env = process.env) {
42
33
  }
43
34
  /** Front guard: an Origin, if present, must be allowlisted; a Host must be
44
35
  * allowlisted. Absent Origin passes (native/CLI/backend clients — the claude.ai
45
- * connector calls /mcp server-to-server with no Origin, BV-4). */
36
+ * connector calls /mcp server-to-server with no Origin). */
46
37
  export function originAllowed(origin, allowed) {
47
38
  if (!origin)
48
39
  return true;
@@ -81,25 +72,17 @@ export function jsonRpcMethod(body) {
81
72
  /** setTimeout fires at once for a delay above this, so a longer grace would
82
73
  * cut every connection immediately. */
83
74
  const MAX_GRACE_MS = 2 ** 31 - 1;
84
- // A lingering close: a socket destroyed while its client is still sending (a
85
- // body pipelined after the close) is reset by the kernel, which drops whatever
86
- // of the last response is still unsent. So the socket only ends its side and
87
- // waits for the client's FIN, or for the grace to end.
75
+ // A lingering close (ends the socket, never destroys it); see docs/internals.md (Graceful close).
88
76
  function endAfterFlush(socket) {
89
77
  if (socket.destroyed || socket.writableEnded)
90
78
  return;
91
- // Node arms a keep-alive timer when a response finishes and destroys the
92
- // socket when it fires, which would cut a slow reader's unread tail just as
93
- // an early destroy does. Only the client's FIN or the grace ends it now.
79
+ // Disarm the keep-alive timer Node arms on finish, or it destroys the
80
+ // socket ~6s later and cuts a slow reader's unread tail the same way an early destroy does.
94
81
  socket.setTimeout(0);
95
82
  socket.end();
96
83
  }
97
- // A request that arrives while closing is never answered, so Node keeps it
98
- // until the socket closes, and a flood pipelined behind a held response would
99
- // pile up without limit. Reading stops for good instead, which lets TCP flow
100
- // control stop the client. Node resumes a socket after every parsed request
101
- // and on drain, so resume becomes a no-op. Such a socket never sees the
102
- // client's FIN either: the grace ends it.
84
+ // Stops reading for good so TCP flow control stops the client, instead of
85
+ // piling up a flood pipelined behind a held response; see docs/internals.md (Graceful close).
103
86
  function stopReading(socket) {
104
87
  socket.pause();
105
88
  socket.resume = () => socket;
@@ -117,18 +100,12 @@ function pathOf(req) {
117
100
  export class HttpTransportHost {
118
101
  opts;
119
102
  httpServer;
120
- // Serialize the connect→dispatch critical section PER SERVER: a McpServer
121
- // captures its transport per request (protocol.js), but the gap between
122
- // connect() and the dispatch capturing it would still race under true
123
- // concurrency, so all traffic to ONE server runs under one mutex. The free
124
- // core has one lane (the boot server); with resolveServer each resolved
125
- // server gets its own, however subjects map onto servers.
103
+ // Per-server dispatch mutex; see docs/internals.md.
126
104
  locks = new Map();
127
105
  // Requests holding a lane slot, per server; used only with maxQueuedPerLane.
128
106
  depth = new Map();
129
- // What a graceful close waits on: open sockets, per socket the requests it
130
- // has accepted whose response has not settled and the last of them, and
131
- // every handle() that has not settled.
107
+ // What a graceful close waits on: open sockets and every handle() not yet
108
+ // settled; see docs/internals.md (Graceful close).
132
109
  sockets = new Set();
133
110
  pending = new Map();
134
111
  // Sockets that have accepted a request: nothing pending means the last
@@ -145,9 +122,8 @@ export class HttpTransportHost {
145
122
  const run = prev.then(fn, fn);
146
123
  const tail = run.then(() => undefined, () => undefined);
147
124
  this.locks.set(key, tail);
148
- // An idle lane drops out, so the map holds only in-flight subjects rather
149
- // than every subject ever seen. A request that chained on this tail has
150
- // already replaced it, so the identity check never frees a live lane.
125
+ // An idle lane drops out of the map; the identity check below skips a
126
+ // later request that already replaced this tail, so it never frees a live lane.
151
127
  void tail.then(() => {
152
128
  if (this.locks.get(key) === tail)
153
129
  this.locks.delete(key);
@@ -161,9 +137,8 @@ export class HttpTransportHost {
161
137
  const { config } = this.opts;
162
138
  const server = createServer((req, res) => {
163
139
  const socket = req.socket;
164
- // Once closing, a connection ends after the last request it accepted
165
- // before the close, so one arriving now is never handled (RFC 9112 9.6).
166
- // Its body is left unread: nothing reads the socket again.
140
+ // Once closing, a request arriving now is never handled (RFC 9112 §9.6)
141
+ // and its body is left unread.
167
142
  if (this.closing) {
168
143
  stopReading(socket);
169
144
  return;
@@ -190,10 +165,8 @@ export class HttpTransportHost {
190
165
  };
191
166
  res.once('finish', settle);
192
167
  res.once('close', settle);
193
- // Never rejects: close() waits on it, and a catch-all that itself threw
194
- // must neither reject close() nor surface as an unhandled rejection.
195
- // The thrown message can name server paths (a lock timeout does), so it
196
- // goes only to the host's log.
168
+ // Never rejects (close() waits on it); the thrown message can name
169
+ // server paths, so it goes only to the host's log.
197
170
  const done = this.handle(req, res)
198
171
  .catch((e) => {
199
172
  try {
@@ -265,20 +238,13 @@ export class HttpTransportHost {
265
238
  }
266
239
  async closeGracefully(s, graceMs) {
267
240
  this.closing = true;
268
- // Node's close() also destroys every connection it deems idle, and that
269
- // includes one whose response has ended but is still flushing: a large
270
- // body would be cut short. Idle sockets are closed below instead.
241
+ // Node's close() also destroys idle connections, which would cut short a
242
+ // large response still flushing; idle sockets are closed below instead.
271
243
  const nodeIdleCloser = s.closeIdleConnections;
272
244
  s.closeIdleConnections = () => undefined;
273
245
  const closed = new Promise((resolve) => s.close(() => resolve()));
274
246
  s.closeIdleConnections = nodeIdleCloser;
275
- // Every request accepted before now is answered, and a connection ends
276
- // after the last of them: that one says so if it has not started, and the
277
- // settle hook in start() ends the socket once it finishes. Node itself
278
- // destroys a socket once a Connection: close response is flushed, which
279
- // resets it just as endAfterFlush explains, so that end lingers too. A
280
- // socket with nothing pending may still be delivering the tail of its
281
- // last response, so only one that never accepted a request is destroyed.
247
+ // Socket bookkeeping below; see docs/internals.md (Graceful close).
282
248
  for (const socket of this.sockets) {
283
249
  const entry = this.pending.get(socket);
284
250
  if (!entry && !this.served.has(socket)) {
@@ -357,10 +323,7 @@ export class HttpTransportHost {
357
323
  }
358
324
  if (!this.frontGuard(req, res))
359
325
  return;
360
- // Registered before anything awaits: a client that leaves while its request
361
- // waits on the lane must not have it dispatched once its turn comes. A
362
- // response queued behind another on its connection never emits 'close', so
363
- // the socket is checked too.
326
+ // Registered before anything awaits (client-gone skip); see docs/internals.md.
364
327
  let gone = false;
365
328
  res.once('close', () => (gone = true));
366
329
  const socket = req.socket;
@@ -375,8 +338,8 @@ export class HttpTransportHost {
375
338
  return;
376
339
  }
377
340
  const sub = auth.sub ?? 'owner';
378
- // Tenant resolution (the seam EE fills): map the verified subject to ITS
379
- // server. Absent resolver = the one boot server for every subject.
341
+ // Tenant resolution (a multi-tenant host fills this seam): map the
342
+ // verified subject to ITS server. Absent resolver = the one boot server for every subject.
380
343
  let target = {
381
344
  server: this.opts.server,
382
345
  argShapeFor: this.opts.argShapeFor,
@@ -400,8 +363,7 @@ export class HttpTransportHost {
400
363
  return;
401
364
  }
402
365
  // An invalid shape falls through to today's exact answer rather than
403
- // forwarding something unvalidated; a refusal never reaches dispatch
404
- // either way.
366
+ // forwarding something unvalidated.
405
367
  resolved = null;
406
368
  }
407
369
  target = resolved;
@@ -437,9 +399,8 @@ export class HttpTransportHost {
437
399
  }
438
400
  }
439
401
  async admitted(req, res, target, sub, clientGone) {
440
- // Thread the verified subject to tool handlers: the Node transport forwards
441
- // req.auth verbatim as ctx.http.authInfo. Token/clientId stay empty — the
442
- // bearer value must not re-enter the dispatch path via handler context.
402
+ // Thread the verified subject to tool handlers via ctx.http.authInfo;
403
+ // token/clientId stay empty so the bearer value never re-enters the dispatch path.
443
404
  req.auth = {
444
405
  token: '',
445
406
  clientId: '',
@@ -457,28 +418,16 @@ export class HttpTransportHost {
457
418
  if (Array.isArray(body) && body.length > maxFrames) {
458
419
  return this.fail(res, 400, 'batch_too_large', `a JSON-RPC batch may hold at most ${maxFrames} messages`);
459
420
  }
460
- // Host/Origin are enforced by the front guard above (uniformly for /mcp and
461
- // the mounted AS routes), so the SDK's own DNS-rebind guard is disabled: its
462
- // exact-Host match is stricter than the front guard and would 403 valid
463
- // Hosts (double enforcement, differing rules).
421
+ // The SDK's own DNS-rebind guard is disabled: Host/Origin are already
422
+ // enforced above, and its stricter exact-Host match would 403 valid Hosts.
464
423
  const transport = new NodeStreamableHTTPServerTransport({
465
424
  sessionIdGenerator: undefined,
466
425
  enableJsonResponse: true,
467
426
  enableDnsRebindingProtection: false,
468
427
  });
469
- // The shared McpServer holds exactly one connected transport at a time, so
470
- // connect + dispatch + DISCONNECT all run inside the mutex: the next request
471
- // can never hit "Already connected". The serialized section is guaranteed to
472
- // settle three ways — the handler finishes, the client disconnects (res
473
- // 'close'), or a dispatch deadline fires — so neither a client that holds the
474
- // connection open nor a hung upstream can wedge the lock. The transport is
475
- // closed in a finally (which resets server._transport) before the lock
476
- // releases. (A shared initialized-state persists across stateless requests;
477
- // benign for the single-owner design.)
428
+ // connect + dispatch + disconnect run inside the per-server mutex, keyed
429
+ // on the server identity, never the subject; see docs/internals.md.
478
430
  const deadlineMs = this.opts.dispatchTimeoutMs ?? DISPATCH_TIMEOUT_DEFAULT;
479
- // The lock lane is the SERVER identity, never the subject: two subjects
480
- // reaching one server (the boot server, or a resolver mapping several
481
- // subjects to one) would otherwise race it through the connect gap.
482
431
  const { server: mcpServer, argShapeFor, strictArgs, validationEnvelope } = target;
483
432
  await this.serializeFor(mcpServer, async () => {
484
433
  if (clientGone()) {
@@ -491,16 +440,14 @@ export class HttpTransportHost {
491
440
  timer = setTimeout(() => resolve('timeout'), deadlineMs);
492
441
  timer.unref?.();
493
442
  });
494
- // Same composition as the stdio leg: the envelope rewrite is innermost,
495
- // so the tap above it still classifies the original validation prose.
443
+ // Envelope rewrite innermost, as on stdio; see docs/internals.md (Argument normalization).
496
444
  const enveloped = withValidationEnvelope(transport, validationEnvelope ?? {});
497
445
  const tapped = this.opts.metricsTap ? this.opts.metricsTap(enveloped) : enveloped;
498
446
  await mcpServer.connect(argShapeFor || strictArgs
499
447
  ? withArgNormalization(tapped, argShapeFor ?? (() => undefined), this.opts.log, this.opts.onArgRename, strictArgs)
500
448
  : tapped);
501
- // Reflect the dispatch into a non-rejecting arm: if the deadline wins the
502
- // race, an orphaned handler settling later must not surface as an unhandled
503
- // rejection — but a genuine dispatch error still propagates (rethrown below).
449
+ // Non-rejecting arm: an orphaned handler settling after the deadline wins
450
+ // must not surface as an unhandled rejection; a real error is rethrown below.
504
451
  let dispatchErr;
505
452
  const dispatchArm = transport
506
453
  .handleRequest(req, res, body)
@@ -538,7 +485,7 @@ export class HttpTransportHost {
538
485
  health(res) {
539
486
  const { config, ownerConfigured, version } = this.opts;
540
487
  res.writeHead(200, { 'Content-Type': 'application/json' });
541
- // No secrets, no owner emails (cc-transport-hosting /health rule).
488
+ // No secrets, no owner emails.
542
489
  res.end(JSON.stringify({
543
490
  status: 'ok',
544
491
  transport: config.transport,
@@ -4,8 +4,8 @@ import { hasToken, readToken, updateToken, writeToken } from './token-store.js';
4
4
  import { type Policy, type Transport } from './write-control.js';
5
5
  /**
6
6
  * The one forward-compat seam (frozen public API): the free core builds
7
- * exactly one context with subject "owner"; EE later instantiates N contexts
8
- * from N registries. Nothing here may assume a global current-account.
7
+ * exactly one context with subject "owner"; a multi-tenant host instantiates
8
+ * N contexts from N registries. Nothing here may assume a global current-account.
9
9
  * `subject` is a string (not the literal 'owner') so a context can carry a
10
10
  * tenant id; the free core still always builds 'owner'.
11
11
  */
package/dist/identity.js CHANGED
@@ -2,18 +2,15 @@ import { getAccountSet, getTokenDir, refreshAccountSetIfStale } from './accounts
2
2
  import { makeGetClient } from './client.js';
3
3
  import { createTokenStore } from './token-store.js';
4
4
  import { resolvePolicy } from './write-control.js';
5
- // Only buildIdentityContext mints the owner brand. The subject string alone
6
- // never unlocks the owner-only surfaces (account wizard, host-file tools, the
7
- // operator diagnose report): a hand-built context claiming 'owner' stays a
8
- // non-owner context.
5
+ // Only buildIdentityContext mints the owner brand: the subject string alone
6
+ // never unlocks owner-only surfaces, so a hand-built context claiming 'owner' stays non-owner.
9
7
  const ownerContexts = new WeakSet();
10
8
  export function isOwnerContext(ctx) {
11
9
  return ctx !== undefined && ownerContexts.has(ctx);
12
10
  }
13
11
  export function buildIdentityContext(env = process.env, opts = {}) {
14
- // Real closures over the single-owner state (the SAME factories EE
15
- // instantiates per tenant), bound here to the global registry + token dir —
16
- // behavior-identical to the module-level functions for the free core.
12
+ // The SAME factories a multi-tenant host instantiates per tenant, bound here
13
+ // to the global registry + token dir.
17
14
  const tokenStore = createTokenStore(getTokenDir());
18
15
  const ctx = {
19
16
  subject: 'owner',
@@ -22,8 +19,8 @@ export function buildIdentityContext(env = process.env, opts = {}) {
22
19
  get accounts() {
23
20
  return getAccountSet();
24
21
  },
25
- // The dispatch transport rides into the resolved policy as a reserved seam
26
- // (cc-write-control B14); it does not change any write-control verdict.
22
+ // The dispatch transport rides into the resolved policy as a reserved seam;
23
+ // it does not change any write-control verdict.
27
24
  policy: resolvePolicy(env, { transport: opts.transport }),
28
25
  getClient: makeGetClient('owner', {
29
26
  accounts: getAccountSet,
package/dist/index.js CHANGED
@@ -1,17 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  // First import: triggers accounts.js module load (env files + registry) before
3
- // anything else. Named to also pull the server-only empty-registry guard (BR-4).
3
+ // anything else. Named to also pull the server-only empty-registry guard.
4
4
  import { assertServerAccountsConfigured } from './accounts.js';
5
5
  import { accountsAssertRequired, assertNoEnvAccountsMode, assertNoEnvOptionalScopesMode, isMultiTenantBoot, mtBootGates } from './boot-gates.js';
6
6
  import { readFileSync } from 'node:fs';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import path from 'node:path';
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
10
- import { McpServer } from "@modelcontextprotocol/server";
11
10
  import { resolveDiscoveryMode } from './registry.js';
12
11
  import { isAllowed, describePolicy } from './write-control.js';
13
12
  import { buildIdentityContext } from './identity.js';
14
- import { buildRegistry, requestHooksFor } from './compose.js';
13
+ import { buildRegistry, createMcpServer, requestHooksFor } from './compose.js';
15
14
  import { registerSetupPrompt } from './setup-prompt.js';
16
15
  import { applyNetTuning } from './net-tuning.js';
17
16
  import { withArgNormalization, withValidationEnvelope } from './arg-normalize.js';
@@ -81,7 +80,7 @@ async function main() {
81
80
  if (process.argv.includes('config') && process.argv.includes('check')) {
82
81
  const ctx = buildIdentityContext();
83
82
  const policy = ctx.policy;
84
- const registry = buildRegistry(new McpServer({ name: 'mcp-google-multi', version: pkg.version }), ctx);
83
+ const registry = buildRegistry(createMcpServer(pkg.version), ctx);
85
84
  const cud = registry.tools.filter((t) => t.cud !== 'read');
86
85
  const disabled = cud.filter((t) => !isAllowed(t, policy));
87
86
  const counts = registry.visibleCount();
@@ -140,10 +139,8 @@ async function main() {
140
139
  }
141
140
  const wantStdio = httpCfg.transport === 'stdio' || httpCfg.transport === 'both';
142
141
  const wantHttp = transportIncludesHttp(httpCfg.transport);
143
- // BR-4: the SERVER never boots with an empty registry — stdio always, HTTP
144
- // when single-owner. The bootstrap and diagnostic CLIs above already
145
- // returned. A multi-tenant boot instead refuses the two process-wide env
146
- // vectors that would leak one operator's accounts/scopes into every tenant.
142
+ // The SERVER never boots with an empty registry (stdio always, HTTP when
143
+ // single-owner); a multi-tenant boot instead refuses the two process-wide env vectors.
147
144
  const mt = isMultiTenantBoot();
148
145
  if (accountsAssertRequired(wantStdio, mt))
149
146
  assertServerAccountsConfigured();
@@ -155,9 +152,7 @@ async function main() {
155
152
  // specced "fatal at startup", never mid-dispatch.
156
153
  const { resolveMasterKey } = await import('./master-key.js');
157
154
  resolveMasterKey();
158
- // Local usage metrics: fail-closed resolve, self-announcing source, null
159
- // when off (no wrapper, no dir, nothing initializes). One instance per
160
- // transport so `boots` keys stay honest under `both`.
155
+ // One Metrics instance per transport so `boots` keys stay honest under `both`.
161
156
  const envSrc = envValueSource('GOOGLE_USAGE_METRICS');
162
157
  const metricsState = resolveUsageMetrics(process.env, loadConfigFile()?.usageMetrics, envSrc?.kind === 'file' ? envSrc.file : undefined);
163
158
  if (metricsState.warning)
@@ -187,21 +182,17 @@ async function main() {
187
182
  });
188
183
  }
189
184
  }
190
- // Build one McpServer + registry per transport at boot (P1 / BV gap #4:
191
- // never rebuilt per request); `both` runs the two concurrently.
185
+ // Build one McpServer + registry per transport at boot (never
186
+ // rebuilt per request); `both` runs the two concurrently.
192
187
  if (wantStdio) {
193
- const server = new McpServer({ name: 'mcp-google-multi', version: pkg.version });
188
+ const server = createMcpServer(pkg.version);
194
189
  const stdioMetrics = initMetricsFor('stdio', resolveDiscoveryMode());
195
190
  const stdioCtx = buildIdentityContext(process.env, { transport: 'stdio' });
196
191
  const registry = buildRegistry(server, stdioCtx, undefined, stdioMetrics);
197
192
  registry.installListHandler();
198
193
  registerSetupPrompt(server);
199
194
  const stdioHooks = requestHooksFor(registry, stdioCtx, stdioMetrics);
200
- // Outbound runs outermost-first, so the composition is deliberate: the
201
- // envelope rewrite is INNERMOST (last to touch the frame), the tap sits
202
- // above it (classifying the ORIGINAL validation prose), arg normalization
203
- // outermost. The tap only counts outbound JSON-RPC error frames (no
204
- // handler ran) plus id->tool names.
195
+ // Composition order (outbound runs outermost-first): see docs/internals.md (Argument normalization).
205
196
  let transport = withValidationEnvelope(new StdioServerTransport(), stdioHooks.validationEnvelope);
206
197
  if (stdioMetrics) {
207
198
  const { tapUsageMetrics } = await import('./metrics-tap.js');
@@ -209,10 +200,8 @@ async function main() {
209
200
  }
210
201
  await server.connect(stdioHooks.argShapeFor || stdioHooks.strictArgs
211
202
  ? withArgNormalization(transport,
212
- // Gated exactly as the HTTP leg gates `argShapeFor`. Passing the
213
- // shape unconditionally made stdio rename keys while
214
- // GOOGLE_ARG_NORMALIZE=off, so the same call succeeded on stdio
215
- // and failed on HTTP once screening rejects.
203
+ // Gated exactly as the HTTP leg gates argShapeFor, or GOOGLE_ARG_NORMALIZE=off
204
+ // would rename keys on stdio while HTTP's screening rejects the same call.
216
205
  stdioHooks.argShapeFor ?? (() => undefined), undefined, stdioMetrics ? (tool, n) => stdioMetrics.recordArgFix(tool, n) : undefined, stdioHooks.strictArgs)
217
206
  : transport);
218
207
  }
@@ -221,31 +210,30 @@ async function main() {
221
210
  const owners = parseOwnerEmails(process.env);
222
211
  const gates = mtBootGates();
223
212
  if (gates?.multiTenant) {
224
- // Same fail-fast SHAPE, different condition: an ungated HTTP endpoint
225
- // must never boot; under tenancy the gate is "a valid provisioning
213
+ // Same fail-fast shape as below, but the gate is "a valid provisioning
226
214
  // mechanism exists", not a flat owner allowlist.
227
215
  gates.assertProvisioningGate();
228
216
  }
229
217
  else {
230
- // BR3 / C13: the owner allowlist is the entire multi-tenant collapse;
218
+ // The owner allowlist is the entire multi-tenant collapse;
231
219
  // refuse to open an ungated HTTP endpoint.
232
220
  if (owners.length === 0) {
233
221
  process.stderr.write('E_OWNER_EMAILS_REQUIRED: MCP_TRANSPORT includes http but MCP_OWNER_EMAILS is empty. Set MCP_OWNER_EMAILS to the Google email(s) allowed to authenticate.\n');
234
222
  process.exit(1);
235
223
  }
236
224
  }
237
- // BR7: stateless HTTP cannot push tools/list_changed, so it forces curated.
225
+ // Stateless HTTP cannot push tools/list_changed, so it forces curated.
238
226
  const configuredMode = (process.env.GOOGLE_DISCOVERY ?? '').trim().toLowerCase();
239
227
  if (configuredMode && configuredMode !== 'curated') {
240
228
  process.stderr.write(`GOOGLE_DISCOVERY="${configuredMode}" is ignored over HTTP; the stateless transport forces "curated".\n`);
241
229
  }
242
- const httpServer = new McpServer({ name: 'mcp-google-multi', version: pkg.version });
230
+ const httpServer = createMcpServer(pkg.version);
243
231
  const httpMetrics = initMetricsFor('http', 'curated');
244
232
  const httpCtx = buildIdentityContext(process.env, { transport: 'http' });
245
233
  const registry = buildRegistry(httpServer, httpCtx, 'curated', httpMetrics);
246
234
  registry.installListHandler();
247
235
  registerSetupPrompt(httpServer);
248
- // B13: mount the OAuth 2.1 AS (legs A + B) + the Bearer authenticator.
236
+ // Mount the OAuth 2.1 AS (MCP-client tokens + owner login) + the Bearer authenticator.
249
237
  const { buildAuthServer, verifiedEmailFromIdToken } = await import('./oauth-as.js');
250
238
  const { jwtSecretFrom } = await import('./mcp-token.js');
251
239
  const { resolveJwtKey, resolveMasterKeyForDispatch } = await import('./master-key.js');
@@ -288,7 +276,7 @@ async function main() {
288
276
  aliasEmail: (alias) => getAccountSet().configs[alias]?.email,
289
277
  log: (l) => process.stderr.write(`[as] ${l}\n`),
290
278
  });
291
- // BV-1: over HTTP, a dead/missing per-account token surfaces a clickable,
279
+ // Over HTTP, a dead/missing per-account token surfaces a clickable,
292
280
  // signed re-auth link into the AS's alias_reauth flow instead of a CLI hint.
293
281
  const { setHttpReauthLink } = await import('./reauth-hint.js');
294
282
  setHttpReauthLink((alias) => authServer.reauthLink(alias));
@@ -314,7 +302,7 @@ async function main() {
314
302
  });
315
303
  await host.start();
316
304
  process.stderr.write(`HTTP transport listening on http://${httpCfg.host}:${httpCfg.port} (public ${httpCfg.publicUrl})\n`);
317
- // Graceful shutdown so `docker run --init` (B16) forwards SIGTERM cleanly.
305
+ // Graceful shutdown so `docker run --init` forwards SIGTERM cleanly.
318
306
  const shutdown = () => {
319
307
  host.close().finally(() => process.exit(0));
320
308
  };
@@ -15,7 +15,7 @@ export interface KeyDeps {
15
15
  hasAnyToken?: () => boolean;
16
16
  }
17
17
  export declare function __setKeychainFactoryForTest(fn: ((account: string) => KeychainEntry | null) | null): void;
18
- /** BR-1 single choke point: no other module may read the env vars directly.
18
+ /** Single choke point: no other module may read the env vars directly.
19
19
  * Boot/CLI form: failures exit with a clean stderr slug ("fatal at startup"). */
20
20
  export declare function resolveMasterKey(deps?: KeyDeps): ResolvedKey;
21
21
  /** Dispatch form: identical resolution, but failures THROW (caught by the tool
@@ -28,14 +28,14 @@ export declare function peekMasterKeyProvenance(deps?: KeyDeps): KeyProvenance |
28
28
  * outstanding MCP tokens and clients re-auth silently. */
29
29
  export declare function resolveJwtKey(deps?: KeyDeps): ResolvedKey;
30
30
  /**
31
- * BR-5: after the FIRST successful token decrypt with an env-sourced key,
31
+ * After the FIRST successful token decrypt with an env-sourced key,
32
32
  * mirror it into the keychain (only when the keychain holds no entry) so the
33
33
  * key survives an env loss. Gating on a successful decrypt means a stale or
34
34
  * wrong env key is never mirrored. Best-effort: failures are doctor material,
35
35
  * never fatal.
36
36
  */
37
37
  export declare function noteSuccessfulDecrypt(): void;
38
- /** B6 reset: drop the generated MASTER_KEY material (0600 file + keychain
38
+ /** Reset: drop the generated MASTER_KEY material (0600 file + keychain
39
39
  * entry) so the next run re-provisions. Never removes an env-provided key (the
40
40
  * deployer owns that; `env:true` flags it so reset can warn). The caller MUST
41
41
  * have wiped every .enc token first — regenerating with tokens present would