mcp-google-multi 6.0.0-alpha.71 → 6.0.0-alpha.72
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.
- package/README.md +5 -5
- package/dist/accounts.d.ts +3 -3
- package/dist/accounts.js +11 -11
- package/dist/arg-strict.js +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +3 -3
- package/dist/boot-gates.d.ts +1 -1
- package/dist/boot-gates.js +2 -2
- package/dist/cimd-cache.js +1 -1
- package/dist/client-config.d.ts +2 -2
- package/dist/client-config.js +4 -4
- package/dist/compose.js +1 -1
- package/dist/discover.js +2 -2
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.js +8 -9
- package/dist/http-config.d.ts +2 -2
- package/dist/http-config.js +4 -5
- package/dist/http-transport.d.ts +5 -5
- package/dist/http-transport.js +7 -7
- package/dist/identity.d.ts +2 -2
- package/dist/identity.js +4 -4
- package/dist/index.js +9 -9
- package/dist/master-key.d.ts +3 -3
- package/dist/master-key.js +5 -5
- package/dist/mcp-token.d.ts +3 -3
- package/dist/mcp-token.js +6 -6
- package/dist/migrate-config.js +1 -1
- package/dist/oauth-as.d.ts +1 -1
- package/dist/oauth-as.js +11 -11
- package/dist/oauth-consent.js +3 -3
- package/dist/outbound-allowlist.js +2 -2
- package/dist/reauth-hint.js +3 -3
- package/dist/registry-transfer.d.ts +1 -1
- package/dist/registry-transfer.js +2 -2
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +4 -4
- package/dist/scope-catalog.d.ts +5 -0
- package/dist/scope-catalog.js +13 -0
- package/dist/scope-observability.d.ts +1 -1
- package/dist/scope-observability.js +1 -1
- package/dist/ssrf-guard.js +3 -4
- package/dist/tenant-purge.js +1 -1
- package/dist/tools/_errors.js +4 -4
- package/dist/tools/account-wizard.d.ts +4 -3
- package/dist/tools/account-wizard.js +27 -12
- package/dist/tools/accounts-tool.d.ts +1 -1
- package/dist/tools/accounts-tool.js +1 -1
- package/dist/tools/admin.js +1 -1
- package/dist/tools/analytics.js +1 -1
- package/dist/tools/calendar.js +1 -1
- package/dist/tools/chat.js +1 -1
- package/dist/tools/contacts.js +4 -4
- package/dist/tools/docs.js +1 -1
- package/dist/tools/drive.js +1 -1
- package/dist/tools/forms.js +1 -1
- package/dist/tools/gmail-mime.d.ts +1 -1
- package/dist/tools/gmail-mime.js +3 -3
- package/dist/tools/gmail.d.ts +2 -2
- package/dist/tools/gmail.js +8 -8
- package/dist/tools/google-api.js +1 -1
- package/dist/tools/meet.js +1 -1
- package/dist/tools/searchconsole.js +1 -1
- package/dist/tools/sheets.js +1 -1
- package/dist/tools/slides.js +1 -1
- package/dist/tools/tasks.js +1 -1
- package/dist/usage-metrics.js +1 -1
- package/dist/write-control.d.ts +4 -5
- package/dist/write-control.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ New to all this? It's written for someone who just installed Claude Code and has
|
|
|
44
44
|
5. **Add it to Claude Code, then restart Claude Code:**
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
claude mcp add google-multi -s user -- npx -y mcp-google-multi
|
|
47
|
+
claude mcp add mcp-google-multi -s user -- npx -y mcp-google-multi
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
**Stuck at any point? Run `mcp-google-multi doctor`.** It inspects every part and prints the exact fix for anything wrong (a missing sign-in, a Google API you still need to switch on, and so on). Once it reads all-green, just talk to Claude: *"summarize my unread email."*
|
|
@@ -55,14 +55,14 @@ New to all this? It's written for someone who just installed Claude Code and has
|
|
|
55
55
|
|
|
56
56
|
**Go deeper:** [Configuration reference](./docs/configuration.md) · [What's covered](./COVERAGE.md) · [Features tour](./docs/features.md) · [Remote / HTTP setup](./docs/http-setup.md) · [Secrets in a vault](./docs/secrets.md) · [Local usage metrics](./docs/usage-metrics.md) · [Migrating to v6](./MIGRATION-v6.md) · [Security policy](./SECURITY.md) · [Roadmap](https://github.com/bakissation/mcp-google-multi/milestones)
|
|
57
57
|
|
|
58
|
-
## Maintainer &
|
|
58
|
+
## Maintainer, sponsor & license
|
|
59
59
|
|
|
60
60
|
Built and maintained by **Abdelbaki Berkati** — [berkati.xyz](https://berkati.xyz) · [@bakissation](https://github.com/bakissation). [Read the case study →](https://berkati.xyz/case-studies/mcp-google-multi/)
|
|
61
61
|
|
|
62
62
|
Development is **funded by [IdeaCrafters](https://ideacrafters.com)** ([@IdeaCraftersHQ](https://github.com/IdeaCraftersHQ)) — the studio that pays for this OSS to exist.
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
An Enterprise Edition (hosted, multi-tenant) is available: ask through [berkati.xyz](https://berkati.xyz) (email or LinkedIn there).
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Thanks to contributors [@obatried](https://github.com/obatried), [@trevor-commits](https://github.com/trevor-commits), and [@mjreddy](https://github.com/mjreddy). The project is maintainer-led (roadmap on [Milestones](https://github.com/bakissation/mcp-google-multi/milestones); bug reports welcome, feature PRs by prior agreement — see [CONTRIBUTING.md](./CONTRIBUTING.md)). **Feedback shapes the roadmap: [open an issue](https://github.com/bakissation/mcp-google-multi/issues/new/choose) with bugs, pain points, or what you wish it did.** Security reports go to [SECURITY.md](./SECURITY.md), never a public issue.
|
|
67
67
|
|
|
68
|
-
[MIT](./LICENSE)
|
|
68
|
+
Licensed under [MIT](./LICENSE).
|
package/dist/accounts.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare function getAccountSet(): AccountSet;
|
|
|
28
28
|
/** Re-resolve after a config.json mutation and swap the live set. */
|
|
29
29
|
export declare function invalidateAccountSet(): AccountSet;
|
|
30
30
|
export declare function refreshAccountSetIfStale(): void;
|
|
31
|
-
/** Cross-process staleness probe
|
|
31
|
+
/** Cross-process staleness probe: one stat, compared against the stamp. */
|
|
32
32
|
export declare function isAccountSetStale(): boolean;
|
|
33
33
|
/** Account aliases (possibly empty on a fresh install).
|
|
34
34
|
* Snapshot from the initial load; enums widen only when the registry is
|
|
@@ -43,7 +43,7 @@ export declare function unknownAliasMessage(aliases: readonly string[], selector
|
|
|
43
43
|
* The `account` param schema, empty-registry-safe. A `z.enum` requires at least
|
|
44
44
|
* one value, so a fresh install (zero aliases) would throw at schema-build time
|
|
45
45
|
* (module load) and take down every CLI — including `doctor`, which is meant to
|
|
46
|
-
* REPORT the empty registry
|
|
46
|
+
* REPORT the empty registry. Fall back to a plain string when there
|
|
47
47
|
* are no aliases: no alias exists to enumerate, the server refuses to boot empty
|
|
48
48
|
* anyway, and dispatch validates the account against the live set. */
|
|
49
49
|
export declare function accountAliasSchemaFor(aliases: readonly string[]): z.ZodType<string>;
|
|
@@ -62,7 +62,7 @@ export declare function liveAccountCheck(aliases: () => readonly string[], selec
|
|
|
62
62
|
/** True for a field built on liveAccountCheck, optional or not. */
|
|
63
63
|
export declare function isLiveAccountField(field: unknown): boolean;
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
65
|
+
* The stdio/http SERVER never boots with an empty registry — a fresh user
|
|
66
66
|
* bootstraps via env / `migrate-config` / `account import` / `auth` first. The
|
|
67
67
|
* bootstrap and diagnostic CLIs return before this guard, so it gates only the
|
|
68
68
|
* server path (called from index.ts after the CLI branches). */
|
package/dist/accounts.js
CHANGED
|
@@ -67,7 +67,7 @@ function noAccountsMessage() {
|
|
|
67
67
|
`or set GOOGLE_ACCOUNTS=work:user@company.com,personal:user@gmail.com in the environment.${envHint}`);
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
|
-
* Registry resolution
|
|
70
|
+
* Registry resolution: a non-empty GOOGLE_ACCOUNTS env takes the WHOLE
|
|
71
71
|
* registry from env (12-factor override, never merged key-by-key); otherwise
|
|
72
72
|
* the registry comes from config.json. GOOGLE_ADMIN_ACCOUNTS, when NON-EMPTY,
|
|
73
73
|
* overrides per-account admin flags from the file (env var > config field);
|
|
@@ -88,7 +88,7 @@ opts = {}) {
|
|
|
88
88
|
};
|
|
89
89
|
// Validate the legacy global override AT BOOT, whatever GOOGLE_TOOLSETS
|
|
90
90
|
// selects — otherwise a typo only surfaces at dispatch time inside
|
|
91
|
-
// account_list instead of the promised startup error
|
|
91
|
+
// account_list instead of the promised startup error.
|
|
92
92
|
const legacyNames = parseCsv(env.GOOGLE_OPTIONAL_SCOPES);
|
|
93
93
|
if (legacyNames.length > 0) {
|
|
94
94
|
for (const bundle of resolveBundleAliases(legacyNames)) {
|
|
@@ -130,9 +130,9 @@ opts = {}) {
|
|
|
130
130
|
// Gap #23: an empty registry is NOT fatal at resolve time — the bootstrap and
|
|
131
131
|
// diagnostic CLIs (doctor/reset/account import/migrate-config/config check)
|
|
132
132
|
// must run on a fresh install. Only the stdio/http SERVER refuses to boot
|
|
133
|
-
// empty
|
|
133
|
+
// empty, enforced by assertServerAccountsConfigured() in index.ts.
|
|
134
134
|
// The dispatch-path reload ('throw') still throws so a mid-session emptied
|
|
135
|
-
// config.json keeps the last-good registry
|
|
135
|
+
// config.json keeps the last-good registry instead of dropping tools.
|
|
136
136
|
if (onInvalid === 'throw' && !opts.emptyOk) {
|
|
137
137
|
throw new Error(`E_NO_ACCOUNTS_CONFIGURED: ${noAccountsMessage()}`);
|
|
138
138
|
}
|
|
@@ -144,7 +144,7 @@ opts = {}) {
|
|
|
144
144
|
stamp: `${config?.version ?? CONFIG_VERSION}:${preStamp.split(':')[1]}`,
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
|
-
//
|
|
147
|
+
// Unknown bundle names fail loudly (a mis-scoped token is worse than a
|
|
148
148
|
// clear error); v5 silently filtered them. Null prototype: profile names are
|
|
149
149
|
// user input and must never collide with Object.prototype members.
|
|
150
150
|
const scopeProfiles = Object.create(null);
|
|
@@ -187,7 +187,7 @@ opts = {}) {
|
|
|
187
187
|
...def,
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
|
-
/**
|
|
190
|
+
/** env GOOGLE_DEFAULT_ACCOUNT > config.defaultAccount > unset. A configured
|
|
191
191
|
* default naming an unknown alias refuses to start (E_DEFAULT_ACCOUNT_UNKNOWN —
|
|
192
192
|
* deliberately NOT E_CONFIG_INVALID: the config is schema-valid). */
|
|
193
193
|
function resolveDefaultAccount(env, fromConfig, aliases, fail) {
|
|
@@ -208,7 +208,7 @@ export function fileStamp(filePath, version) {
|
|
|
208
208
|
return `${version}:0`;
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
-
// First-run shim
|
|
211
|
+
// First-run shim: env is set and no config.json exists yet — materialize
|
|
212
212
|
// the file so the wizard has something to edit. Env still wins this session;
|
|
213
213
|
// a write failure must never block boot (warn on stderr and continue).
|
|
214
214
|
function materializeFirstRun(aliases, configs, filePath) {
|
|
@@ -251,7 +251,7 @@ export function invalidateAccountSet() {
|
|
|
251
251
|
return current;
|
|
252
252
|
}
|
|
253
253
|
/**
|
|
254
|
-
* Dispatch-path reload
|
|
254
|
+
* Dispatch-path reload: NEVER exits and never throws — a mid-edit,
|
|
255
255
|
* corrupt, or deleted config.json keeps the last-good set and warns once per
|
|
256
256
|
* distinct failure on stderr. failStartup semantics are boot/CLI-only.
|
|
257
257
|
*/
|
|
@@ -271,7 +271,7 @@ export function refreshAccountSetIfStale() {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
/** Cross-process staleness probe
|
|
274
|
+
/** Cross-process staleness probe: one stat, compared against the stamp. */
|
|
275
275
|
export function isAccountSetStale() {
|
|
276
276
|
if (current.source !== 'file')
|
|
277
277
|
return false;
|
|
@@ -297,7 +297,7 @@ export function unknownAliasMessage(aliases, selectors = false) {
|
|
|
297
297
|
* The `account` param schema, empty-registry-safe. A `z.enum` requires at least
|
|
298
298
|
* one value, so a fresh install (zero aliases) would throw at schema-build time
|
|
299
299
|
* (module load) and take down every CLI — including `doctor`, which is meant to
|
|
300
|
-
* REPORT the empty registry
|
|
300
|
+
* REPORT the empty registry. Fall back to a plain string when there
|
|
301
301
|
* are no aliases: no alias exists to enumerate, the server refuses to boot empty
|
|
302
302
|
* anyway, and dispatch validates the account against the live set. */
|
|
303
303
|
export function accountAliasSchemaFor(aliases) {
|
|
@@ -339,7 +339,7 @@ export function isLiveAccountField(field) {
|
|
|
339
339
|
return def?.checks?.some((c) => LIVE_ACCOUNT_CHECKS.has(c)) ?? false;
|
|
340
340
|
}
|
|
341
341
|
/**
|
|
342
|
-
*
|
|
342
|
+
* The stdio/http SERVER never boots with an empty registry — a fresh user
|
|
343
343
|
* bootstraps via env / `migrate-config` / `account import` / `auth` first. The
|
|
344
344
|
* bootstrap and diagnostic CLIs return before this guard, so it gates only the
|
|
345
345
|
* server path (called from index.ts after the CLI branches). */
|
package/dist/arg-strict.js
CHANGED
|
@@ -43,7 +43,7 @@ export const STRICT_EXEMPT_TOOLS = new Set();
|
|
|
43
43
|
* no arguments; the others are call-plumbing that bridges and proxies have
|
|
44
44
|
* been observed to fold into `arguments` instead of `params._meta`, where the
|
|
45
45
|
* spec puts them. Rejecting these would fail a call the model got right.
|
|
46
|
-
* Measured against the full
|
|
46
|
+
* Measured against the full tool surface: none is a declared key. */
|
|
47
47
|
const CLIENT_ARTIFACT_KEYS = new Set([
|
|
48
48
|
'random_string',
|
|
49
49
|
'toolCallId',
|
package/dist/auth.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const BASE_SCOPES: string[];
|
|
|
4
4
|
export declare const OPTIONAL_SCOPE_BUNDLES: Record<string, string[]>;
|
|
5
5
|
export { ADMIN_SCOPES };
|
|
6
6
|
/** Union of every account's resolved bundles: a service registers if ANY
|
|
7
|
-
* account can authorize it; per-account authz happens at call time
|
|
7
|
+
* account can authorize it; per-account authz happens at call time. */
|
|
8
8
|
export declare function getOptionalBundles(set?: AccountSet): string[];
|
|
9
9
|
/** Aliases granted ADMIN_SCOPES: per-account admin flag (env
|
|
10
10
|
* GOOGLE_ADMIN_ACCOUNTS overrides config.json at resolve) OR the account's
|
package/dist/auth.js
CHANGED
|
@@ -32,9 +32,9 @@ function parseCsvEnv(name) {
|
|
|
32
32
|
.filter(Boolean);
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
* Legacy global override
|
|
35
|
+
* Legacy global override: a set GOOGLE_OPTIONAL_SCOPES acts as an
|
|
36
36
|
* implicit "legacy-global" profile applied to every account (env wins over
|
|
37
|
-
* file profiles
|
|
37
|
+
* file profiles). Unknown names now fail loudly where v5
|
|
38
38
|
* silently dropped them — the intended migration signal.
|
|
39
39
|
*/
|
|
40
40
|
function legacyGlobalProfile() {
|
|
@@ -65,7 +65,7 @@ function profileForAccount(alias, set = getAccountSet()) {
|
|
|
65
65
|
return Object.hasOwn(set.scopeProfiles, name) ? set.scopeProfiles[name] : { bundles: [] };
|
|
66
66
|
}
|
|
67
67
|
/** Union of every account's resolved bundles: a service registers if ANY
|
|
68
|
-
* account can authorize it; per-account authz happens at call time
|
|
68
|
+
* account can authorize it; per-account authz happens at call time. */
|
|
69
69
|
export function getOptionalBundles(set = getAccountSet()) {
|
|
70
70
|
const legacy = legacyGlobalProfile();
|
|
71
71
|
if (legacy)
|
package/dist/boot-gates.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface MtBootGates {
|
|
|
11
11
|
export declare function setMtBootGates(g: MtBootGates | null): void;
|
|
12
12
|
export declare function mtBootGates(): MtBootGates | null;
|
|
13
13
|
export declare function isMultiTenantBoot(): boolean;
|
|
14
|
-
/**
|
|
14
|
+
/** The empty-registry guard applies to stdio always, and to HTTP only when single-owner: a
|
|
15
15
|
* multi-tenant HTTP deploy starts empty by design. */
|
|
16
16
|
export declare function accountsAssertRequired(wantStdio: boolean, multiTenant: boolean): boolean;
|
|
17
17
|
/** A set GOOGLE_ACCOUNTS on a multi-tenant boot is refused at DEPLOY time:
|
package/dist/boot-gates.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Multi-tenant boot gates. The free core never sets these — every
|
|
2
2
|
// helper reduces to today's single-owner behavior when no gates are
|
|
3
3
|
// installed. Deliberately a side-effect-free module (accounts.ts/auth.ts
|
|
4
4
|
// resolve env at import time) so the tenancy module can import it cheaply.
|
|
@@ -13,7 +13,7 @@ export function mtBootGates() {
|
|
|
13
13
|
export function isMultiTenantBoot() {
|
|
14
14
|
return gates?.multiTenant === true;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/** The empty-registry guard applies to stdio always, and to HTTP only when single-owner: a
|
|
17
17
|
* multi-tenant HTTP deploy starts empty by design. */
|
|
18
18
|
export function accountsAssertRequired(wantStdio, multiTenant) {
|
|
19
19
|
return wantStdio || !multiTenant;
|
package/dist/cimd-cache.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// GET /authorize is unauthenticated, and a client_id on an allowlisted
|
|
2
2
|
// issuer with a path never seen before forces an outbound fetch. So the
|
|
3
3
|
// fetches running at once are capped, one client_id is fetched once however
|
|
4
4
|
// many requests wait on it, and only a document that passed validation is
|
package/dist/client-config.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface EntryOptions {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* The client `mcpServers` entry. stdio needs no secret env: the server loads
|
|
41
|
-
* its own .env from ~/.config/mcp-google-multi
|
|
41
|
+
* its own .env from ~/.config/mcp-google-multi, so we never
|
|
42
42
|
* inline MASTER_KEY / GOOGLE_CLIENT_SECRET into a world-readable client config.
|
|
43
43
|
*/
|
|
44
44
|
export declare function buildServerEntry(opts: EntryOptions): ServerEntry;
|
|
@@ -73,7 +73,7 @@ export type ApplyResult = {
|
|
|
73
73
|
* the prior file, update-in-place if the key exists.
|
|
74
74
|
*/
|
|
75
75
|
export declare function applyFileEntry(client: ClientInfo, name: string, entry: ServerEntry, deps?: ClientDeps): ApplyResult;
|
|
76
|
-
/** Resolve stdio-vs-http mode from the current transport config
|
|
76
|
+
/** Resolve stdio-vs-http mode from the current transport config.
|
|
77
77
|
* http/both → a `url` entry; stdio → a `command` entry. */
|
|
78
78
|
export declare function resolveMode(httpConfig?: {
|
|
79
79
|
transport: 'stdio' | 'http' | 'both';
|
package/dist/client-config.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// After the setup wizard finalizes an account, offer to register the
|
|
2
2
|
// server with the user's MCP client so they never hand-edit JSON. Pure core
|
|
3
3
|
// (detection + entry building + idempotent apply), deps-injectable for tests.
|
|
4
4
|
// Consent + non-TTY behavior live in the CLI/tool surfaces; secrets are NEVER
|
|
5
5
|
// inlined — the server self-loads its .env from ~/.config/mcp-google-multi, so
|
|
6
|
-
// a stdio entry needs no secret env at all
|
|
6
|
+
// a stdio entry needs no secret env at all.
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
8
|
import * as readline from 'node:readline';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -78,7 +78,7 @@ export function detectClients(deps = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* The client `mcpServers` entry. stdio needs no secret env: the server loads
|
|
81
|
-
* its own .env from ~/.config/mcp-google-multi
|
|
81
|
+
* its own .env from ~/.config/mcp-google-multi, so we never
|
|
82
82
|
* inline MASTER_KEY / GOOGLE_CLIENT_SECRET into a world-readable client config.
|
|
83
83
|
*/
|
|
84
84
|
export function buildServerEntry(opts) {
|
|
@@ -166,7 +166,7 @@ export function applyFileEntry(client, name, entry, deps = {}) {
|
|
|
166
166
|
r.writeFile(p, `${JSON.stringify(next, null, 2)}\n`);
|
|
167
167
|
return { ok: true, client: client.id, action: had ? 'updated' : 'added', path: p, backup };
|
|
168
168
|
}
|
|
169
|
-
/** Resolve stdio-vs-http mode from the current transport config
|
|
169
|
+
/** Resolve stdio-vs-http mode from the current transport config.
|
|
170
170
|
* http/both → a `url` entry; stdio → a `command` entry. */
|
|
171
171
|
export function resolveMode(httpConfig) {
|
|
172
172
|
if (httpConfig && (httpConfig.transport === 'http' || httpConfig.transport === 'both')) {
|
package/dist/compose.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Registry composition, extracted from index.ts so the tenancy module
|
|
2
2
|
// (and tests) can build registries without touching the CLI entrypoint. This
|
|
3
3
|
// module must stay free of import-time side effects of its own: no argv
|
|
4
4
|
// parsing, no process.exit, no prints (accounts.js module-load resolution is
|
package/dist/discover.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { describePolicy } from './write-control.js';
|
|
3
3
|
import { hints } from './hint-provider.js';
|
|
4
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
|
|
5
|
+
// service put lazy tools/list at ~11.5k tokens, so descriptions carry a
|
|
6
6
|
// CAPPED vocabulary and the complete catalog stays in this tool's RESULT.
|
|
7
7
|
const CURATED_OPS_CAP = 10;
|
|
8
8
|
const GENERATED_GROUPS_CAP = 6;
|
|
@@ -31,7 +31,7 @@ function opVocabulary(registry, service) {
|
|
|
31
31
|
}
|
|
32
32
|
export function registerDiscoverTools(registry, policy) {
|
|
33
33
|
const registerMeta = registry.registerMeta;
|
|
34
|
-
// Agent-controllable runtime expansion
|
|
34
|
+
// Agent-controllable runtime expansion: reveal the whole
|
|
35
35
|
// curated quality layer for heavy Google work, collapse to reclaim the
|
|
36
36
|
// name/schema context budget after. stdio-only semantics — over stateless
|
|
37
37
|
// HTTP the mode is forced curated and these are no-ops.
|
package/dist/doctor.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface HttpProbeResult {
|
|
|
52
52
|
unreachable?: boolean;
|
|
53
53
|
problem?: string;
|
|
54
54
|
}
|
|
55
|
-
/** Console deep-link to enable one API (section-6 hint
|
|
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
58
|
* here and in `diagnose` (metrics spec section 2). Read-only. */
|
|
@@ -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
|
|
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
|
@@ -37,7 +37,7 @@ const DEFAULT_DEPS = {
|
|
|
37
37
|
};
|
|
38
38
|
/** §7 live check: the advertised OAuth metadata must derive from MCP_PUBLIC_URL
|
|
39
39
|
* exactly — one mismatch between PRM `resource` / AS `issuer` and what clients
|
|
40
|
-
* compute from the public URL is the perpetual-401 interop bug
|
|
40
|
+
* compute from the public URL is the perpetual-401 interop bug. */
|
|
41
41
|
async function probeHttpEndpoints(cfg) {
|
|
42
42
|
try {
|
|
43
43
|
const prmRes = await fetch(`${cfg.publicUrl}/.well-known/oauth-protected-resource`, {
|
|
@@ -66,7 +66,7 @@ async function probeHttpEndpoints(cfg) {
|
|
|
66
66
|
return { ok: false, unreachable: true };
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
/** Console deep-link to enable one API (section-6 hint
|
|
69
|
+
/** Console deep-link to enable one API (section-6 hint). */
|
|
70
70
|
export function apiEnableLink(api) {
|
|
71
71
|
return `https://console.cloud.google.com/apis/library/${api}.googleapis.com`;
|
|
72
72
|
}
|
|
@@ -75,8 +75,8 @@ function sectionRuntime(deps, tenantScope = false) {
|
|
|
75
75
|
const major = Number.parseInt(deps.nodeVersion.split('.')[0] ?? '0', 10);
|
|
76
76
|
if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
|
|
77
77
|
if (tenantScope) {
|
|
78
|
-
// The host version is operator infrastructure, not the tenant's
|
|
79
|
-
//
|
|
78
|
+
// The host version is operator infrastructure, not the tenant's: no
|
|
79
|
+
// version string, and the fix is the operator's to apply.
|
|
80
80
|
const rendered = hints()?.render({ kind: 'operator_only', slug: 'E_NODE_TOO_OLD' });
|
|
81
81
|
return {
|
|
82
82
|
id: 1,
|
|
@@ -240,8 +240,7 @@ async function sectionApiEnablement(deps, aliases, tenantScope = false) {
|
|
|
240
240
|
}
|
|
241
241
|
catch (e) {
|
|
242
242
|
// Network / transient: WARN with the target, never crash the report. The
|
|
243
|
-
// raw text can name an egress or proxy host
|
|
244
|
-
// drops it.
|
|
243
|
+
// raw text can name an egress or proxy host: tenant scope drops it.
|
|
245
244
|
return {
|
|
246
245
|
id: 6,
|
|
247
246
|
title: 'API enablement',
|
|
@@ -381,8 +380,8 @@ export function renderDoctorText(report) {
|
|
|
381
380
|
}
|
|
382
381
|
const EMAIL_LOCAL_CHAR = /[A-Za-z0-9._%+-]/;
|
|
383
382
|
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
|
-
*
|
|
383
|
+
/** Mask the local-part of an email so a report never carries a full address.
|
|
384
|
+
* A linear scan, not a regex: the `local* @domain` pattern backtracks
|
|
386
385
|
* polynomially on long local-charset runs without an @ (CodeQL
|
|
387
386
|
* js/polynomial-redos), and report text can embed arbitrary error strings. */
|
|
388
387
|
function maskEmails(text) {
|
|
@@ -409,7 +408,7 @@ function maskEmails(text) {
|
|
|
409
408
|
}
|
|
410
409
|
return out;
|
|
411
410
|
}
|
|
412
|
-
/** Redacted, paste-ready bug report
|
|
411
|
+
/** Redacted, paste-ready bug report: verdicts + provenance labels + token
|
|
413
412
|
* statuses + slugs; NO token values, secrets, keys, or full email addresses. */
|
|
414
413
|
export function renderReport(report, version) {
|
|
415
414
|
const out = [`mcp-google-multi doctor report (v${version})`, `overall: ${report.verdict}`, ''];
|
package/dist/http-config.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface HttpConfig {
|
|
|
11
11
|
transport: Transport;
|
|
12
12
|
host: string;
|
|
13
13
|
port: number;
|
|
14
|
-
/** Canonical public base URL
|
|
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
|
|
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
|
*/
|
package/dist/http-config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Transport selection + public-base-URL / Origin / host config plumbing
|
|
2
|
-
// Pure, resolved once at boot; the HTTP host
|
|
3
|
-
//
|
|
4
|
-
// Owns the canonicalization rule (cc-transport-hosting BR4): a single mismatch
|
|
1
|
+
// Transport selection + public-base-URL / Origin / host config plumbing.
|
|
2
|
+
// Pure, resolved once at boot; consumed by the HTTP host and the AS metadata.
|
|
3
|
+
// Owns the canonicalization rule: a single mismatch
|
|
5
4
|
// between the advertised resource URI and the client's `resource`/`aud` = a
|
|
6
5
|
// perpetual 401 loop (the #1 interop bug), so it is normalized exactly once.
|
|
7
6
|
export const DEFAULT_HTTP_HOST = '127.0.0.1';
|
|
@@ -30,7 +29,7 @@ export function resolveTransport(env = process.env) {
|
|
|
30
29
|
throw new HttpConfigError('E_INVALID_TRANSPORT', `MCP_TRANSPORT must be one of stdio|http|both (got "${env.MCP_TRANSPORT}")`);
|
|
31
30
|
}
|
|
32
31
|
/**
|
|
33
|
-
* Canonicalize a public base URL
|
|
32
|
+
* Canonicalize a public base URL. WHATWG `URL` already lowercases the
|
|
34
33
|
* scheme+host and omits a default port from `.host`; this adds the HTTPS-only
|
|
35
34
|
* scheme check, strips fragment/query/userinfo, and removes a trailing slash.
|
|
36
35
|
*/
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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.
|
package/dist/http-transport.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
|
|
2
2
|
// node:http server, the route table, the front guard (Host / Origin / DNS-rebind),
|
|
3
3
|
// and the stateless per-request /mcp dispatch. The OAuth AS endpoints and Bearer
|
|
4
|
-
// verification are a seam filled by
|
|
5
|
-
//
|
|
4
|
+
// verification are a seam filled by oauth-as.ts; the default is a loopback-owner
|
|
5
|
+
// authenticator so the local-HTTP model works without the AS.
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { withArgNormalization, withValidationEnvelope } from './arg-normalize.js';
|
|
8
8
|
import { logSafe } from './trim.js';
|
|
@@ -16,7 +16,7 @@ const DISPATCH_TIMEOUT_DEFAULT = 120_000;
|
|
|
16
16
|
* of a batch synchronously in one tick: without a cap one body multiplies the
|
|
17
17
|
* per-call work by however many frames fit in it. */
|
|
18
18
|
const MAX_BATCH_FRAMES = 16;
|
|
19
|
-
// A resolveServer refusal's body reaches the wire verbatim
|
|
19
|
+
// A resolveServer refusal's body reaches the wire verbatim: validated
|
|
20
20
|
// so a host cannot accidentally leak an operator path/env var through a wrongly
|
|
21
21
|
// shaped slug or an overlong message. No new slug literal enters core itself —
|
|
22
22
|
// the slug is always the host's own value.
|
|
@@ -42,7 +42,7 @@ export function parseOwnerEmails(env = process.env) {
|
|
|
42
42
|
}
|
|
43
43
|
/** Front guard: an Origin, if present, must be allowlisted; a Host must be
|
|
44
44
|
* allowlisted. Absent Origin passes (native/CLI/backend clients — the claude.ai
|
|
45
|
-
* connector calls /mcp server-to-server with no Origin
|
|
45
|
+
* connector calls /mcp server-to-server with no Origin). */
|
|
46
46
|
export function originAllowed(origin, allowed) {
|
|
47
47
|
if (!origin)
|
|
48
48
|
return true;
|
|
@@ -375,8 +375,8 @@ export class HttpTransportHost {
|
|
|
375
375
|
return;
|
|
376
376
|
}
|
|
377
377
|
const sub = auth.sub ?? 'owner';
|
|
378
|
-
// Tenant resolution (
|
|
379
|
-
// server. Absent resolver = the one boot server for every subject.
|
|
378
|
+
// Tenant resolution (a multi-tenant host fills this seam): map the
|
|
379
|
+
// verified subject to ITS server. Absent resolver = the one boot server for every subject.
|
|
380
380
|
let target = {
|
|
381
381
|
server: this.opts.server,
|
|
382
382
|
argShapeFor: this.opts.argShapeFor,
|
|
@@ -538,7 +538,7 @@ export class HttpTransportHost {
|
|
|
538
538
|
health(res) {
|
|
539
539
|
const { config, ownerConfigured, version } = this.opts;
|
|
540
540
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
541
|
-
// No secrets, no owner emails
|
|
541
|
+
// No secrets, no owner emails.
|
|
542
542
|
res.end(JSON.stringify({
|
|
543
543
|
status: 'ok',
|
|
544
544
|
transport: config.transport,
|
package/dist/identity.d.ts
CHANGED
|
@@ -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";
|
|
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
|
@@ -11,8 +11,8 @@ export function isOwnerContext(ctx) {
|
|
|
11
11
|
return ctx !== undefined && ownerContexts.has(ctx);
|
|
12
12
|
}
|
|
13
13
|
export function buildIdentityContext(env = process.env, opts = {}) {
|
|
14
|
-
// Real closures over the single-owner state (the SAME factories
|
|
15
|
-
// instantiates per tenant), bound here to the global registry + token dir —
|
|
14
|
+
// Real closures over the single-owner state (the SAME factories a
|
|
15
|
+
// multi-tenant host instantiates per tenant), bound here to the global registry + token dir —
|
|
16
16
|
// behavior-identical to the module-level functions for the free core.
|
|
17
17
|
const tokenStore = createTokenStore(getTokenDir());
|
|
18
18
|
const ctx = {
|
|
@@ -22,8 +22,8 @@ export function buildIdentityContext(env = process.env, opts = {}) {
|
|
|
22
22
|
get accounts() {
|
|
23
23
|
return getAccountSet();
|
|
24
24
|
},
|
|
25
|
-
// The dispatch transport rides into the resolved policy as a reserved seam
|
|
26
|
-
//
|
|
25
|
+
// The dispatch transport rides into the resolved policy as a reserved seam;
|
|
26
|
+
// it does not change any write-control verdict.
|
|
27
27
|
policy: resolvePolicy(env, { transport: opts.transport }),
|
|
28
28
|
getClient: makeGetClient('owner', {
|
|
29
29
|
accounts: getAccountSet,
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
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';
|
|
@@ -139,7 +139,7 @@ async function main() {
|
|
|
139
139
|
}
|
|
140
140
|
const wantStdio = httpCfg.transport === 'stdio' || httpCfg.transport === 'both';
|
|
141
141
|
const wantHttp = transportIncludesHttp(httpCfg.transport);
|
|
142
|
-
//
|
|
142
|
+
// The SERVER never boots with an empty registry — stdio always, HTTP
|
|
143
143
|
// when single-owner. The bootstrap and diagnostic CLIs above already
|
|
144
144
|
// returned. A multi-tenant boot instead refuses the two process-wide env
|
|
145
145
|
// vectors that would leak one operator's accounts/scopes into every tenant.
|
|
@@ -186,8 +186,8 @@ async function main() {
|
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
// Build one McpServer + registry per transport at boot (
|
|
190
|
-
//
|
|
189
|
+
// Build one McpServer + registry per transport at boot (never
|
|
190
|
+
// rebuilt per request); `both` runs the two concurrently.
|
|
191
191
|
if (wantStdio) {
|
|
192
192
|
const server = createMcpServer(pkg.version);
|
|
193
193
|
const stdioMetrics = initMetricsFor('stdio', resolveDiscoveryMode());
|
|
@@ -226,14 +226,14 @@ async function main() {
|
|
|
226
226
|
gates.assertProvisioningGate();
|
|
227
227
|
}
|
|
228
228
|
else {
|
|
229
|
-
//
|
|
229
|
+
// The owner allowlist is the entire multi-tenant collapse;
|
|
230
230
|
// refuse to open an ungated HTTP endpoint.
|
|
231
231
|
if (owners.length === 0) {
|
|
232
232
|
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');
|
|
233
233
|
process.exit(1);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
-
//
|
|
236
|
+
// Stateless HTTP cannot push tools/list_changed, so it forces curated.
|
|
237
237
|
const configuredMode = (process.env.GOOGLE_DISCOVERY ?? '').trim().toLowerCase();
|
|
238
238
|
if (configuredMode && configuredMode !== 'curated') {
|
|
239
239
|
process.stderr.write(`GOOGLE_DISCOVERY="${configuredMode}" is ignored over HTTP; the stateless transport forces "curated".\n`);
|
|
@@ -244,7 +244,7 @@ async function main() {
|
|
|
244
244
|
const registry = buildRegistry(httpServer, httpCtx, 'curated', httpMetrics);
|
|
245
245
|
registry.installListHandler();
|
|
246
246
|
registerSetupPrompt(httpServer);
|
|
247
|
-
//
|
|
247
|
+
// Mount the OAuth 2.1 AS (MCP-client tokens + owner login) + the Bearer authenticator.
|
|
248
248
|
const { buildAuthServer, verifiedEmailFromIdToken } = await import('./oauth-as.js');
|
|
249
249
|
const { jwtSecretFrom } = await import('./mcp-token.js');
|
|
250
250
|
const { resolveJwtKey, resolveMasterKeyForDispatch } = await import('./master-key.js');
|
|
@@ -287,7 +287,7 @@ async function main() {
|
|
|
287
287
|
aliasEmail: (alias) => getAccountSet().configs[alias]?.email,
|
|
288
288
|
log: (l) => process.stderr.write(`[as] ${l}\n`),
|
|
289
289
|
});
|
|
290
|
-
//
|
|
290
|
+
// Over HTTP, a dead/missing per-account token surfaces a clickable,
|
|
291
291
|
// signed re-auth link into the AS's alias_reauth flow instead of a CLI hint.
|
|
292
292
|
const { setHttpReauthLink } = await import('./reauth-hint.js');
|
|
293
293
|
setHttpReauthLink((alias) => authServer.reauthLink(alias));
|
|
@@ -313,7 +313,7 @@ async function main() {
|
|
|
313
313
|
});
|
|
314
314
|
await host.start();
|
|
315
315
|
process.stderr.write(`HTTP transport listening on http://${httpCfg.host}:${httpCfg.port} (public ${httpCfg.publicUrl})\n`);
|
|
316
|
-
// Graceful shutdown so `docker run --init`
|
|
316
|
+
// Graceful shutdown so `docker run --init` forwards SIGTERM cleanly.
|
|
317
317
|
const shutdown = () => {
|
|
318
318
|
host.close().finally(() => process.exit(0));
|
|
319
319
|
};
|