mcp-google-multi 6.0.0-beta.2 → 6.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/accounts.d.ts +20 -4
- package/dist/accounts.js +68 -44
- package/dist/api-probe.d.ts +5 -0
- package/dist/api-probe.js +20 -15
- package/dist/arg-normalize.js +19 -38
- package/dist/arg-strict.d.ts +7 -0
- package/dist/arg-strict.js +38 -28
- package/dist/auth.d.ts +5 -4
- package/dist/auth.js +16 -23
- package/dist/boot-gates.d.ts +26 -0
- package/dist/boot-gates.js +36 -0
- package/dist/cimd-cache.d.ts +38 -0
- package/dist/cimd-cache.js +111 -0
- package/dist/client-config.d.ts +2 -2
- package/dist/client-config.js +8 -14
- package/dist/client.d.ts +32 -2
- package/dist/client.js +0 -0
- package/dist/compose.d.ts +18 -0
- package/dist/compose.js +109 -0
- package/dist/config-file.d.ts +13 -0
- package/dist/config-file.js +41 -10
- package/dist/discover.js +7 -10
- package/dist/discovery-client.d.ts +3 -0
- package/dist/discovery-client.js +36 -32
- package/dist/doctor.d.ts +9 -5
- package/dist/doctor.js +119 -29
- package/dist/env-load.js +2 -5
- package/dist/executor.d.ts +3 -0
- package/dist/executor.js +6 -6
- package/dist/fanout.d.ts +9 -1
- package/dist/fanout.js +47 -22
- package/dist/fs-atomic.d.ts +8 -0
- package/dist/fs-atomic.js +216 -29
- package/dist/hint-provider.d.ts +129 -0
- package/dist/hint-provider.js +58 -0
- package/dist/http-config.d.ts +2 -2
- package/dist/http-config.js +3 -7
- package/dist/http-transport.d.ts +67 -9
- package/dist/http-transport.js +318 -52
- package/dist/identity.d.ts +6 -3
- package/dist/identity.js +24 -8
- package/dist/index.js +76 -160
- package/dist/master-key.d.ts +3 -3
- package/dist/master-key.js +16 -19
- package/dist/mcp-token.d.ts +126 -19
- package/dist/mcp-token.js +557 -76
- package/dist/metrics-cli.d.ts +1 -1
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-tap.js +2 -5
- package/dist/migrate-config.d.ts +1 -2
- package/dist/migrate-config.js +5 -7
- package/dist/net-tuning.js +2 -4
- package/dist/oauth-as.d.ts +104 -4
- package/dist/oauth-as.js +368 -114
- package/dist/oauth-consent.d.ts +10 -0
- package/dist/oauth-consent.js +20 -10
- package/dist/outbound-allowlist.js +9 -11
- package/dist/reauth-hint.d.ts +1 -1
- package/dist/reauth-hint.js +11 -10
- package/dist/registry-transfer.d.ts +5 -0
- package/dist/registry-transfer.js +63 -11
- package/dist/registry.d.ts +13 -4
- package/dist/registry.js +100 -72
- package/dist/scope-catalog.d.ts +16 -0
- package/dist/scope-catalog.js +40 -13
- package/dist/scope-observability.d.ts +10 -9
- package/dist/scope-observability.js +34 -24
- package/dist/services.d.ts +18 -10
- package/dist/services.js +61 -22
- package/dist/ssrf-guard.d.ts +23 -9
- package/dist/ssrf-guard.js +106 -78
- package/dist/tenant-purge.d.ts +26 -0
- package/dist/tenant-purge.js +65 -0
- package/dist/token-store.d.ts +20 -2
- package/dist/token-store.js +0 -0
- package/dist/tools/_coerce.d.ts +4 -4
- package/dist/tools/_errors.d.ts +3 -0
- package/dist/tools/_errors.js +72 -60
- package/dist/tools/_local-files.d.ts +9 -0
- package/dist/tools/_local-files.js +22 -8
- package/dist/tools/account-wizard.d.ts +13 -3
- package/dist/tools/account-wizard.js +107 -56
- package/dist/tools/accounts-tool.d.ts +8 -1
- package/dist/tools/accounts-tool.js +35 -19
- package/dist/tools/admin.d.ts +10 -1
- package/dist/tools/admin.js +14 -11
- package/dist/tools/analytics.d.ts +10 -1
- package/dist/tools/analytics.js +14 -10
- package/dist/tools/calendar.d.ts +2 -1
- package/dist/tools/calendar.js +19 -18
- package/dist/tools/chat.d.ts +10 -1
- package/dist/tools/chat.js +12 -8
- package/dist/tools/contacts.d.ts +2 -1
- package/dist/tools/contacts.js +22 -19
- package/dist/tools/docs.d.ts +2 -1
- package/dist/tools/docs.js +35 -36
- package/dist/tools/drive.d.ts +2 -1
- package/dist/tools/drive.js +80 -74
- package/dist/tools/forms.d.ts +10 -1
- package/dist/tools/forms.js +18 -12
- package/dist/tools/generated/_shared.d.ts +2 -1
- package/dist/tools/generated/_shared.js +7 -3
- package/dist/tools/generated/admin.d.ts +2 -1
- package/dist/tools/generated/admin.js +265 -265
- package/dist/tools/generated/analytics.d.ts +2 -1
- package/dist/tools/generated/analytics.js +125 -125
- package/dist/tools/generated/appsmarket.d.ts +2 -1
- package/dist/tools/generated/appsmarket.js +5 -5
- package/dist/tools/generated/calendar.d.ts +2 -1
- package/dist/tools/generated/calendar.js +55 -55
- package/dist/tools/generated/chat.d.ts +2 -1
- package/dist/tools/generated/chat.js +95 -95
- package/dist/tools/generated/classroom.d.ts +2 -1
- package/dist/tools/generated/classroom.js +209 -209
- package/dist/tools/generated/cloudidentity.d.ts +2 -1
- package/dist/tools/generated/cloudidentity.js +133 -133
- package/dist/tools/generated/cloudsearch.d.ts +2 -1
- package/dist/tools/generated/cloudsearch.js +99 -99
- package/dist/tools/generated/contacts.d.ts +2 -1
- package/dist/tools/generated/contacts.js +29 -29
- package/dist/tools/generated/drive.d.ts +2 -1
- package/dist/tools/generated/drive.js +71 -71
- package/dist/tools/generated/driveactivity.d.ts +2 -1
- package/dist/tools/generated/driveactivity.js +3 -3
- package/dist/tools/generated/drivelabels.d.ts +2 -1
- package/dist/tools/generated/drivelabels.js +53 -53
- package/dist/tools/generated/forms.d.ts +2 -1
- package/dist/tools/generated/forms.js +7 -7
- package/dist/tools/generated/gmail.d.ts +2 -1
- package/dist/tools/generated/gmail.js +117 -117
- package/dist/tools/generated/groupsmigration.d.ts +2 -1
- package/dist/tools/generated/groupsmigration.js +3 -3
- package/dist/tools/generated/groupssettings.d.ts +2 -1
- package/dist/tools/generated/groupssettings.js +7 -7
- package/dist/tools/generated/index.d.ts +2 -1
- package/dist/tools/generated/keep.d.ts +2 -1
- package/dist/tools/generated/keep.js +15 -15
- package/dist/tools/generated/licensing.d.ts +2 -1
- package/dist/tools/generated/licensing.js +15 -15
- package/dist/tools/generated/meet.d.ts +2 -1
- package/dist/tools/generated/meet.js +27 -27
- package/dist/tools/generated/postmaster.d.ts +2 -1
- package/dist/tools/generated/postmaster.js +9 -9
- package/dist/tools/generated/reseller.d.ts +2 -1
- package/dist/tools/generated/reseller.js +35 -35
- package/dist/tools/generated/script.d.ts +2 -1
- package/dist/tools/generated/script.js +33 -33
- package/dist/tools/generated/searchconsole.d.ts +2 -1
- package/dist/tools/generated/searchconsole.js +3 -3
- package/dist/tools/generated/sheets.d.ts +2 -1
- package/dist/tools/generated/sheets.js +15 -15
- package/dist/tools/generated/tasks.d.ts +2 -1
- package/dist/tools/generated/tasks.js +5 -5
- package/dist/tools/generated/vault.d.ts +2 -1
- package/dist/tools/generated/vault.js +67 -67
- package/dist/tools/generated/workspaceevents.d.ts +2 -1
- package/dist/tools/generated/workspaceevents.js +31 -31
- package/dist/tools/gmail-mime.d.ts +1 -1
- package/dist/tools/gmail-mime.js +7 -12
- package/dist/tools/gmail.d.ts +4 -3
- package/dist/tools/gmail.js +67 -69
- package/dist/tools/google-api.d.ts +2 -3
- package/dist/tools/google-api.js +34 -22
- package/dist/tools/meet.d.ts +10 -1
- package/dist/tools/meet.js +12 -9
- package/dist/tools/searchconsole.d.ts +2 -1
- package/dist/tools/searchconsole.js +16 -13
- package/dist/tools/sheets.d.ts +2 -1
- package/dist/tools/sheets.js +35 -32
- package/dist/tools/slides.d.ts +10 -1
- package/dist/tools/slides.js +16 -10
- package/dist/tools/tasks.d.ts +2 -1
- package/dist/tools/tasks.js +18 -15
- package/dist/trim.d.ts +5 -0
- package/dist/trim.js +19 -0
- package/dist/usage-metrics.d.ts +23 -0
- package/dist/usage-metrics.js +67 -56
- package/dist/write-control.d.ts +4 -8
- package/dist/write-control.js +13 -6
- package/package.json +47 -21
package/dist/doctor.js
CHANGED
|
@@ -2,16 +2,19 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as readline from 'node:readline';
|
|
4
4
|
import { getAccountSet } from './accounts.js';
|
|
5
|
-
import { deriveAccountHealth } from './tools/accounts-tool.js';
|
|
5
|
+
import { accountHealthDepsFor, deriveAccountHealth } from './tools/accounts-tool.js';
|
|
6
|
+
import { isOwnerContext } from './identity.js';
|
|
6
7
|
import { peekMasterKeyProvenance, deleteMasterKeyMaterial } from './master-key.js';
|
|
7
8
|
import { hasToken } from './token-store.js';
|
|
8
9
|
import { configDir, loadConfigFile } from './config-file.js';
|
|
9
10
|
import { envValueSource } from './env-load.js';
|
|
11
|
+
import { reauthHint } from './reauth-hint.js';
|
|
10
12
|
import { describeMetricsDir, resolveUsageMetrics, sourceLabel } from './usage-metrics.js';
|
|
11
|
-
import { probeApiEnablement } from './api-probe.js';
|
|
13
|
+
import { apiProbeDepsFor, probeApiEnablement } from './api-probe.js';
|
|
12
14
|
import { safeMessage, stringifyEnvelope } from './tools/_errors.js';
|
|
13
15
|
import { resolveHttpConfig, HttpConfigError } from './http-config.js';
|
|
14
16
|
import { parseOwnerEmails } from './http-transport.js';
|
|
17
|
+
import { hints } from './hint-provider.js';
|
|
15
18
|
const MIN_NODE_MAJOR = 22;
|
|
16
19
|
const DEFAULT_DEPS = {
|
|
17
20
|
nodeVersion: process.versions.node,
|
|
@@ -32,9 +35,8 @@ const DEFAULT_DEPS = {
|
|
|
32
35
|
probeApi: (alias) => probeApiEnablement(alias),
|
|
33
36
|
probeHttp: (cfg) => probeHttpEndpoints(cfg),
|
|
34
37
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* 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.
|
|
38
40
|
async function probeHttpEndpoints(cfg) {
|
|
39
41
|
try {
|
|
40
42
|
const prmRes = await fetch(`${cfg.publicUrl}/.well-known/oauth-protected-resource`, {
|
|
@@ -63,14 +65,27 @@ async function probeHttpEndpoints(cfg) {
|
|
|
63
65
|
return { ok: false, unreachable: true };
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
|
-
/** Console deep-link to enable one API (section-6 hint
|
|
68
|
+
/** Console deep-link to enable one API (section-6 hint). */
|
|
67
69
|
export function apiEnableLink(api) {
|
|
68
70
|
return `https://console.cloud.google.com/apis/library/${api}.googleapis.com`;
|
|
69
71
|
}
|
|
70
72
|
const LEGACY_ENV_KEYS = ['GOOGLE_ACCOUNTS', 'GOOGLE_OPTIONAL_SCOPES', 'GOOGLE_ADMIN_ACCOUNTS'];
|
|
71
|
-
function sectionRuntime(deps) {
|
|
73
|
+
function sectionRuntime(deps, tenantScope = false) {
|
|
72
74
|
const major = Number.parseInt(deps.nodeVersion.split('.')[0] ?? '0', 10);
|
|
73
75
|
if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
|
|
76
|
+
if (tenantScope) {
|
|
77
|
+
// The host version is operator infrastructure, not the tenant's: no
|
|
78
|
+
// version string, and the fix is the operator's to apply.
|
|
79
|
+
const rendered = hints()?.render({ kind: 'operator_only', slug: 'E_NODE_TOO_OLD' });
|
|
80
|
+
return {
|
|
81
|
+
id: 1,
|
|
82
|
+
title: 'Runtime',
|
|
83
|
+
verdict: 'fail',
|
|
84
|
+
slug: 'E_NODE_TOO_OLD',
|
|
85
|
+
lines: ['Runtime below the supported version'],
|
|
86
|
+
hint: rendered?.hint ?? 'Tell the server operator the code E_NODE_TOO_OLD.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
74
89
|
return {
|
|
75
90
|
id: 1,
|
|
76
91
|
title: 'Runtime',
|
|
@@ -80,23 +95,36 @@ function sectionRuntime(deps) {
|
|
|
80
95
|
hint: `Upgrade to Node ${MIN_NODE_MAJOR} LTS or newer.`,
|
|
81
96
|
};
|
|
82
97
|
}
|
|
83
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
id: 1,
|
|
100
|
+
title: 'Runtime',
|
|
101
|
+
verdict: 'ok',
|
|
102
|
+
lines: [tenantScope ? 'Runtime ok' : `Node.js ${deps.nodeVersion} (>= ${MIN_NODE_MAJOR})`],
|
|
103
|
+
};
|
|
84
104
|
}
|
|
85
|
-
function sectionConfig(deps, set) {
|
|
105
|
+
function sectionConfig(deps, set, tenantScope = false) {
|
|
86
106
|
const lines = [];
|
|
87
107
|
let verdict = 'ok';
|
|
88
108
|
let hint;
|
|
89
109
|
let slug;
|
|
90
110
|
if (!set || set.aliases.length === 0) {
|
|
111
|
+
const tenantHint = tenantScope ? (hints()?.render({ kind: 'no_accounts' })?.hint ?? '') : undefined;
|
|
91
112
|
return {
|
|
92
113
|
id: 2,
|
|
93
114
|
title: 'Config',
|
|
94
115
|
verdict: 'fail',
|
|
95
116
|
slug: 'E_NO_ACCOUNTS_CONFIGURED',
|
|
96
117
|
lines: ['No accounts configured.'],
|
|
97
|
-
|
|
118
|
+
...(tenantScope
|
|
119
|
+
? (tenantHint ? { hint: tenantHint } : {})
|
|
120
|
+
: { hint: 'Add an account: run `npx mcp-google-multi migrate-config` or set GOOGLE_ACCOUNTS.' }),
|
|
98
121
|
};
|
|
99
122
|
}
|
|
123
|
+
// The config file, process env, working directory and metrics store below
|
|
124
|
+
// are the operator's host, not the context's: a tenant sees its own count.
|
|
125
|
+
if (tenantScope) {
|
|
126
|
+
return { id: 2, title: 'Config', verdict: 'ok', lines: [`${set.aliases.length} account(s) configured`] };
|
|
127
|
+
}
|
|
100
128
|
const cfgPath = path.join(configDir(), 'config.json');
|
|
101
129
|
lines.push(deps.fileExists(cfgPath) ? `config.json present (${set.aliases.length} account(s))` : `config.json absent — accounts sourced from env (${set.aliases.length})`);
|
|
102
130
|
const legacyEnv = LEGACY_ENV_KEYS.filter((k) => deps.env[k]);
|
|
@@ -121,7 +149,7 @@ function sectionConfig(deps, set) {
|
|
|
121
149
|
return { id: 2, title: 'Config', verdict, lines, ...(hint ? { hint } : {}), ...(slug ? { slug } : {}) };
|
|
122
150
|
}
|
|
123
151
|
/** One line, state AND source, so the people being measured can see both
|
|
124
|
-
* here and in `diagnose
|
|
152
|
+
* here and in `diagnose`. Read-only. */
|
|
125
153
|
export function usageMetricsStatusLine(env) {
|
|
126
154
|
const envSrc = envValueSource('GOOGLE_USAGE_METRICS');
|
|
127
155
|
let configValue;
|
|
@@ -181,7 +209,8 @@ function sectionsTokensAndScopes(deps, aliases) {
|
|
|
181
209
|
if (scopeVerdict === 'ok')
|
|
182
210
|
scopeVerdict = 'warn';
|
|
183
211
|
scopeLines.push(`${alias}: ${r.callable.length} callable, ${r.requestable.length} requested-not-granted`);
|
|
184
|
-
|
|
212
|
+
const rendered = hints()?.render({ kind: 'scopes_missing', account: alias });
|
|
213
|
+
scopeHint = rendered?.hint ?? `Re-auth to grant missing scopes. ${reauthHint(alias)}`;
|
|
185
214
|
}
|
|
186
215
|
else {
|
|
187
216
|
scopeLines.push(`${alias}: ${r.callable.length} callable, all profile scopes granted`);
|
|
@@ -192,7 +221,7 @@ function sectionsTokensAndScopes(deps, aliases) {
|
|
|
192
221
|
{ id: 5, title: 'Scopes', verdict: scopeVerdict, lines: scopeLines, ...(scopeHint ? { hint: scopeHint } : {}), ...(scopeVerdict === 'warn' ? { slug: 'E_SCOPE_NOT_GRANTED' } : {}) },
|
|
193
222
|
];
|
|
194
223
|
}
|
|
195
|
-
async function sectionApiEnablement(deps, aliases) {
|
|
224
|
+
async function sectionApiEnablement(deps, aliases, tenantScope = false) {
|
|
196
225
|
if (!deps.probeApi) {
|
|
197
226
|
return { id: 6, title: 'API enablement', verdict: 'unknown', lines: ['Probe not run (no live account probe configured).'] };
|
|
198
227
|
}
|
|
@@ -209,8 +238,14 @@ async function sectionApiEnablement(deps, aliases) {
|
|
|
209
238
|
results = await deps.probeApi(healthy);
|
|
210
239
|
}
|
|
211
240
|
catch (e) {
|
|
212
|
-
// Network / transient: WARN with the target, never crash the report.
|
|
213
|
-
|
|
241
|
+
// Network / transient: WARN with the target, never crash the report. The
|
|
242
|
+
// raw text can name an egress or proxy host: tenant scope drops it.
|
|
243
|
+
return {
|
|
244
|
+
id: 6,
|
|
245
|
+
title: 'API enablement',
|
|
246
|
+
verdict: 'warn',
|
|
247
|
+
lines: [tenantScope ? 'Probe could not complete.' : `Probe could not complete: ${e?.message ?? e}`],
|
|
248
|
+
};
|
|
214
249
|
}
|
|
215
250
|
if (results.length === 0) {
|
|
216
251
|
return { id: 6, title: 'API enablement', verdict: 'unknown', lines: [`No probeable service scopes granted on "${healthy}".`] };
|
|
@@ -218,13 +253,14 @@ async function sectionApiEnablement(deps, aliases) {
|
|
|
218
253
|
const disabled = results.filter((r) => r.notEnabled);
|
|
219
254
|
const lines = results.map((r) => `${r.service}: ${r.ok ? 'enabled' : r.notEnabled ? 'NOT ENABLED' : `unknown (${r.message ?? 'error'})`}`);
|
|
220
255
|
if (disabled.length > 0) {
|
|
256
|
+
const rendered = hints()?.render({ kind: 'apis_disabled', services: disabled.map((r) => ({ service: r.service, api: r.api })) });
|
|
221
257
|
return {
|
|
222
258
|
id: 6,
|
|
223
259
|
title: 'API enablement',
|
|
224
260
|
verdict: 'fail',
|
|
225
261
|
slug: 'E_API_NOT_ENABLED',
|
|
226
262
|
lines,
|
|
227
|
-
hint: disabled.map((r) => `Enable ${r.service}: ${apiEnableLink(r.api)}`).join('\n'),
|
|
263
|
+
hint: rendered?.hint ?? disabled.map((r) => `Enable ${r.service}: ${apiEnableLink(r.api)}`).join('\n'),
|
|
228
264
|
};
|
|
229
265
|
}
|
|
230
266
|
return { id: 6, title: 'API enablement', verdict: 'ok', lines: lines.length ? lines : ['(probed account, all enabled)'] };
|
|
@@ -299,21 +335,27 @@ export function overallVerdict(sections) {
|
|
|
299
335
|
}
|
|
300
336
|
return worst;
|
|
301
337
|
}
|
|
302
|
-
export async function runDiagnostics(deps = DEFAULT_DEPS) {
|
|
338
|
+
export async function runDiagnostics(deps = DEFAULT_DEPS, opts = {}) {
|
|
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.
|
|
341
|
+
const tenantScope = opts.scope === 'tenant';
|
|
303
342
|
const sections = [];
|
|
304
|
-
sections.push(sectionRuntime(deps));
|
|
343
|
+
sections.push(sectionRuntime(deps, tenantScope));
|
|
305
344
|
const set = deps.accountSet();
|
|
306
|
-
sections.push(sectionConfig(deps, set));
|
|
345
|
+
sections.push(sectionConfig(deps, set, tenantScope));
|
|
307
346
|
const aliases = set?.aliases ?? [];
|
|
308
|
-
|
|
347
|
+
if (!tenantScope)
|
|
348
|
+
sections.push(sectionKeys(deps, aliases));
|
|
309
349
|
if (aliases.length > 0) {
|
|
310
350
|
const [tokens, scopes] = sectionsTokensAndScopes(deps, aliases);
|
|
311
351
|
sections.push(tokens, scopes);
|
|
312
|
-
sections.push(await sectionApiEnablement(deps, aliases));
|
|
352
|
+
sections.push(await sectionApiEnablement(deps, aliases, tenantScope));
|
|
353
|
+
}
|
|
354
|
+
if (!tenantScope) {
|
|
355
|
+
const http = await sectionHttp(deps, aliases);
|
|
356
|
+
if (http)
|
|
357
|
+
sections.push(http);
|
|
313
358
|
}
|
|
314
|
-
const http = await sectionHttp(deps, aliases);
|
|
315
|
-
if (http)
|
|
316
|
-
sections.push(http);
|
|
317
359
|
return { verdict: overallVerdict(sections), sections };
|
|
318
360
|
}
|
|
319
361
|
// --- skins -----------------------------------------------------------------
|
|
@@ -333,11 +375,35 @@ export function renderDoctorText(report) {
|
|
|
333
375
|
out.push(`Overall: ${report.verdict.toUpperCase()}`);
|
|
334
376
|
return out.join('\n');
|
|
335
377
|
}
|
|
336
|
-
|
|
378
|
+
const EMAIL_LOCAL_CHAR = /[A-Za-z0-9._%+-]/;
|
|
379
|
+
const EMAIL_DOMAIN_CHAR = /[A-Za-z0-9.-]/;
|
|
380
|
+
// A linear scan, not a regex: `local*@domain` backtracks polynomially on long
|
|
381
|
+
// local-charset runs without an @ (CodeQL js/polynomial-redos).
|
|
337
382
|
function maskEmails(text) {
|
|
338
|
-
|
|
383
|
+
let out = '';
|
|
384
|
+
let i = 0;
|
|
385
|
+
while (i < text.length) {
|
|
386
|
+
if (text[i] === '@') {
|
|
387
|
+
let j = i + 1;
|
|
388
|
+
while (j < text.length && EMAIL_DOMAIN_CHAR.test(text[j]))
|
|
389
|
+
j += 1;
|
|
390
|
+
let k = out.length;
|
|
391
|
+
while (k > 0 && EMAIL_LOCAL_CHAR.test(out[k - 1]))
|
|
392
|
+
k -= 1;
|
|
393
|
+
if (j > i + 1 && k < out.length) {
|
|
394
|
+
// keep the first local char, mask the rest ('*' is outside the local
|
|
395
|
+
// charset, so a masked run can never be re-masked by a later @)
|
|
396
|
+
out = `${out.slice(0, k + 1)}***${text.slice(i, j)}`;
|
|
397
|
+
i = j;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
out += text[i];
|
|
402
|
+
i += 1;
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
339
405
|
}
|
|
340
|
-
/** Redacted, paste-ready bug report
|
|
406
|
+
/** Redacted, paste-ready bug report: verdicts + provenance labels + token
|
|
341
407
|
* statuses + slugs; NO token values, secrets, keys, or full email addresses. */
|
|
342
408
|
export function renderReport(report, version) {
|
|
343
409
|
const out = [`mcp-google-multi doctor report (v${version})`, `overall: ${report.verdict}`, ''];
|
|
@@ -356,6 +422,26 @@ export function exitCodeFor(report, strict) {
|
|
|
356
422
|
return 1;
|
|
357
423
|
return 0;
|
|
358
424
|
}
|
|
425
|
+
/** The engine's account, token and probe reads bound to one context, so a
|
|
426
|
+
* report never shows (or probes with) another context's accounts. */
|
|
427
|
+
function diagnosticsDepsFor(ctx) {
|
|
428
|
+
const health = accountHealthDepsFor(ctx.tokenStore, () => ctx.accounts);
|
|
429
|
+
const probe = apiProbeDepsFor(ctx.tokenStore.readToken, ctx.getClient);
|
|
430
|
+
return {
|
|
431
|
+
...DEFAULT_DEPS,
|
|
432
|
+
accountSet: () => {
|
|
433
|
+
try {
|
|
434
|
+
return ctx.accounts;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
accountHealth: (alias) => deriveAccountHealth(alias, health),
|
|
441
|
+
anyTokensExist: (aliases) => aliases.some((a) => ctx.tokenStore.hasToken(a)),
|
|
442
|
+
probeApi: (alias) => probeApiEnablement(alias, probe),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
359
445
|
/** Agent-callable structured health report (read-only). Mirrors `doctor`'s
|
|
360
446
|
* engine. Registered as a META tool, like `account_list`: it introspects this
|
|
361
447
|
* server rather than Google data, and as a normal tool it became a service of
|
|
@@ -363,14 +449,18 @@ export function exitCodeFor(report, strict) {
|
|
|
363
449
|
* from already-registered services, and this runs after that), so it was
|
|
364
450
|
* advertised only once something else expanded the surface. The README sends
|
|
365
451
|
* people here when they are stuck, so it has to be findable. */
|
|
366
|
-
export function registerDiagnoseTool(registry) {
|
|
452
|
+
export function registerDiagnoseTool(registry, ctx) {
|
|
453
|
+
// A non-owner context gets the tenant-scoped report; only the owner context
|
|
454
|
+
// (or the context-free CLI surface) keeps the full operator report.
|
|
455
|
+
const scope = ctx && !isOwnerContext(ctx) ? 'tenant' : 'operator';
|
|
456
|
+
const deps = ctx ? diagnosticsDepsFor(ctx) : DEFAULT_DEPS;
|
|
367
457
|
registry.registerMeta('diagnose', {
|
|
368
458
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
369
459
|
description: 'Health report for this server: runtime, config, keys, per-account token status, scope grants, and API enablement. Read-only; returns copy-pasteable fixes for anything wrong. Call this to diagnose auth/config failures.',
|
|
370
460
|
inputSchema: {},
|
|
371
461
|
}, async () => {
|
|
372
462
|
try {
|
|
373
|
-
const result = await runDiagnostics();
|
|
463
|
+
const result = await runDiagnostics(deps, { scope });
|
|
374
464
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
375
465
|
}
|
|
376
466
|
catch (e) {
|
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
|
-
//
|
|
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.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getClient } from './client.js';
|
|
2
|
+
import { type ScopeSetDeps } from './scope-observability.js';
|
|
2
3
|
export declare const MAX_RESPONSE_CHARS = 100000;
|
|
3
4
|
export type QueryValue = string | number | boolean;
|
|
4
5
|
export type QueryParams = Record<string, QueryValue | QueryValue[]>;
|
|
@@ -13,6 +14,8 @@ export interface ApiMethodRef {
|
|
|
13
14
|
}
|
|
14
15
|
export interface ExecuteDeps {
|
|
15
16
|
getClientFn?: typeof getClient;
|
|
17
|
+
/** Token + profile view behind a 403 scope hint; absent = the single owner. */
|
|
18
|
+
scopeDeps?: ScopeSetDeps;
|
|
16
19
|
}
|
|
17
20
|
export interface ExecuteArgs {
|
|
18
21
|
account: string;
|
package/dist/executor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { expandPath, isGoogleApiUrl } from './discovery-client.js';
|
|
|
4
4
|
import { handleGoogleApiError } from './tools/_errors.js';
|
|
5
5
|
import { scopeHintForMethod } from './scope-observability.js';
|
|
6
6
|
import { checkOutboundForMethod } from './outbound-allowlist.js';
|
|
7
|
+
import { hints } from './hint-provider.js';
|
|
7
8
|
export const MAX_RESPONSE_CHARS = 100_000;
|
|
8
9
|
export function jsonResult(payload, isError = false) {
|
|
9
10
|
const base = { content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
@@ -70,10 +71,11 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
70
71
|
}, true);
|
|
71
72
|
}
|
|
72
73
|
if (!isGoogleApiUrl(url)) {
|
|
74
|
+
const rendered = hints()?.render({ kind: 'untrusted_host' });
|
|
73
75
|
return jsonResult({
|
|
74
76
|
error: 'untrusted_host',
|
|
75
77
|
message: `Refusing to send credentials to a non-googleapis.com host for "${method.id}".`,
|
|
76
|
-
hint: 'The discovery cache may be corrupt; delete DISCOVERY_CACHE_PATH and retry.',
|
|
78
|
+
hint: rendered?.hint ?? 'The discovery cache may be corrupt; delete DISCOVERY_CACHE_PATH and retry.',
|
|
77
79
|
retriable: false,
|
|
78
80
|
account: args.account,
|
|
79
81
|
}, true);
|
|
@@ -89,10 +91,8 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
89
91
|
});
|
|
90
92
|
const text = JSON.stringify(res.data ?? null);
|
|
91
93
|
if (text.length > MAX_RESPONSE_CHARS) {
|
|
92
|
-
// Budget the ENCODED head: jsonResult re-stringifies it, so
|
|
93
|
-
//
|
|
94
|
-
// result exceed the cap this tool advertises, by up to 15 percent on a
|
|
95
|
-
// 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.
|
|
96
96
|
return jsonResult({
|
|
97
97
|
truncated: true,
|
|
98
98
|
totalChars: text.length,
|
|
@@ -103,6 +103,6 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
103
103
|
return { content: [{ type: 'text', text }] };
|
|
104
104
|
}
|
|
105
105
|
catch (error) {
|
|
106
|
-
return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account) : null);
|
|
106
|
+
return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account, deps.scopeDeps) : null);
|
|
107
107
|
}
|
|
108
108
|
}
|
package/dist/fanout.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const CSV_RE: RegExp;
|
|
3
|
-
export declare function fanoutAccountField(description: string): z.ZodType;
|
|
3
|
+
export declare function fanoutAccountField(description: string, aliases: () => readonly string[]): z.ZodType;
|
|
4
|
+
/** '*' over an empty account set would fan out to nothing and report success. */
|
|
5
|
+
export declare function noAccountsResult(): {
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
isError: true;
|
|
11
|
+
};
|
|
4
12
|
export type AccountSelector = {
|
|
5
13
|
ok: true;
|
|
6
14
|
fanout: boolean;
|
package/dist/fanout.js
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { getAccountSet, liveAccountCheck, unknownAliasMessage } from './accounts.js';
|
|
3
|
+
import { echoName, echoNames, MAX_SUGGESTED_KEYS } from './arg-strict.js';
|
|
4
|
+
import { hints } from './hint-provider.js';
|
|
3
5
|
export const CSV_RE = /^[a-zA-Z0-9_-]+(\s*,\s*[a-zA-Z0-9_-]+)+$/;
|
|
4
6
|
const FANOUT_CONCURRENCY = 5;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
const bad = () => unknownAliasMessage(
|
|
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.
|
|
9
|
+
export function fanoutAccountField(description, aliases) {
|
|
10
|
+
const boot = aliases();
|
|
11
|
+
const csvExample = boot.length > 1 ? `; or a CSV subset like "${boot.slice(0, 2).join(',')}"` : '';
|
|
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.
|
|
14
|
+
const bad = () => unknownAliasMessage(aliases(), true);
|
|
13
15
|
return z
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
.union([z.
|
|
16
|
+
// The selector branch stays first: tools/list injects the live
|
|
17
|
+
// '*' + alias enum into it (registry withLiveAccountEnum).
|
|
18
|
+
.union([z.string().check(liveAccountCheck(aliases, true)), z.string().regex(CSV_RE, { error: bad })], { error: bad })
|
|
17
19
|
.optional()
|
|
18
20
|
.describe(`${description}; "*" = all accounts${csvExample}; omit for the default account`);
|
|
19
21
|
}
|
|
22
|
+
/** '*' over an empty account set would fan out to nothing and report success. */
|
|
23
|
+
export function noAccountsResult() {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: JSON.stringify({
|
|
29
|
+
error: 'validation_error',
|
|
30
|
+
message: 'No Google account is configured, so "*" selects nothing.',
|
|
31
|
+
hint: unknownAliasMessage([]),
|
|
32
|
+
retriable: false,
|
|
33
|
+
account: '*',
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
isError: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
20
40
|
export function parseAccountSelector(value, accounts = getAccountSet().aliases) {
|
|
21
41
|
if (value === '*')
|
|
22
42
|
return { ok: true, fanout: true, aliases: [...accounts] };
|
|
23
|
-
// A single alias used to skip validation
|
|
24
|
-
//
|
|
25
|
-
// 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.
|
|
26
45
|
if (!value.includes(',')) {
|
|
27
46
|
return accounts.includes(value)
|
|
28
47
|
? { ok: true, fanout: false, aliases: [value] }
|
|
@@ -53,10 +72,11 @@ export function invalidAccountsResult(invalid, accounts = getAccountSet().aliase
|
|
|
53
72
|
type: 'text',
|
|
54
73
|
text: JSON.stringify({
|
|
55
74
|
error: 'validation_error',
|
|
56
|
-
|
|
75
|
+
// The caller's CSV can hold any number of tokens: echo a prefix.
|
|
76
|
+
message: `Unknown account alias(es): ${echoNames(invalid)}.`,
|
|
57
77
|
hint: unknownAliasMessage(accounts, true),
|
|
58
78
|
retriable: false,
|
|
59
|
-
account: invalid.join(','),
|
|
79
|
+
account: invalid.slice(0, MAX_SUGGESTED_KEYS).map(echoName).join(','),
|
|
60
80
|
}),
|
|
61
81
|
},
|
|
62
82
|
],
|
|
@@ -87,10 +107,17 @@ export async function runFanout(handler, args, aliases) {
|
|
|
87
107
|
results[i] = res?.isError ? { account, ok: false, error: payload } : { account, ok: true, data: payload };
|
|
88
108
|
}
|
|
89
109
|
catch (err) {
|
|
110
|
+
const rendered = hints()?.render({ kind: 'internal', account });
|
|
90
111
|
results[i] = {
|
|
91
112
|
account,
|
|
92
113
|
ok: false,
|
|
93
|
-
error: {
|
|
114
|
+
error: {
|
|
115
|
+
error: 'internal',
|
|
116
|
+
message: rendered?.message ?? err.message,
|
|
117
|
+
hint: rendered?.hint,
|
|
118
|
+
retriable: false,
|
|
119
|
+
account,
|
|
120
|
+
},
|
|
94
121
|
};
|
|
95
122
|
}
|
|
96
123
|
}
|
|
@@ -98,10 +125,8 @@ export async function runFanout(handler, args, aliases) {
|
|
|
98
125
|
await Promise.all(Array.from({ length: Math.min(FANOUT_CONCURRENCY, aliases.length) }, () => worker()));
|
|
99
126
|
const failed = results.filter((r) => !r.ok).length;
|
|
100
127
|
if (failed === results.length && results.length > 0) {
|
|
101
|
-
// Every account failed, so this IS an error result
|
|
102
|
-
//
|
|
103
|
-
// successful-looking object flagged as an error, and metrics bucketed it
|
|
104
|
-
// 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.
|
|
105
130
|
const slugs = [...new Set(results.map((r) => r.error?.error ?? 'internal'))];
|
|
106
131
|
const hints = [...new Set(results.map((r) => r.error?.hint).filter(Boolean))];
|
|
107
132
|
const retriable = results.every((r) => r.error?.retriable === true);
|
package/dist/fs-atomic.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
interface LockIdentity {
|
|
2
|
+
ns: string;
|
|
3
|
+
start: string;
|
|
4
|
+
boot: string;
|
|
5
|
+
}
|
|
6
|
+
/** Test hook: act as a process with this identity (undefined restores the real one). */
|
|
7
|
+
export declare function __setLockIdentityForTest(identity?: LockIdentity): void;
|
|
1
8
|
export declare function withFileLock<T>(absPath: string, fn: () => T, label?: string): T;
|
|
2
9
|
export declare function atomicWriteFileSync(absPath: string, contents: string, mode?: number): void;
|
|
3
10
|
export declare function atomicWriteWithLock(absPath: string, contents: string, mode?: number): void;
|
|
11
|
+
export {};
|