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