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/compose.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Registry composition, extracted from index.ts so tests can build registries
|
|
2
|
+
// without touching the CLI entrypoint. No import-time side effects of its own.
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
4
|
+
import { GENERATED_SERVICES } from './tools/generated/index.js';
|
|
5
|
+
import { GENERATED_GATES, SERVICES, servicesAwaitingRestart, unknownToolMessage } from './services.js';
|
|
6
|
+
import { argNormalizationEnabled } from './arg-normalize.js';
|
|
7
|
+
import { unknownArgMode } from './arg-strict.js';
|
|
8
|
+
import { ToolRegistry } from './registry.js';
|
|
9
|
+
import { registerDiscoverTools } from './discover.js';
|
|
10
|
+
import { registerEscapeTools } from './tools/google-api.js';
|
|
11
|
+
import { accountHealthDepsFor, registerAccountTools } from './tools/accounts-tool.js';
|
|
12
|
+
import { registerDiagnoseTool } from './doctor.js';
|
|
13
|
+
import { registerAccountWizardTools } from './tools/account-wizard.js';
|
|
14
|
+
import { getToolsets, toolsetEnabled } from './toolsets.js';
|
|
15
|
+
import { isOwnerContext } from './identity.js';
|
|
16
|
+
import { resolveScopesForAccount } from './auth.js';
|
|
17
|
+
/** The one place the MCP server is constructed, so an embedding host can pass
|
|
18
|
+
* per-session `instructions` without re-deriving the server identity. */
|
|
19
|
+
export function createMcpServer(version, opts = {}) {
|
|
20
|
+
const instructions = opts.instructions?.trim() ? opts.instructions : undefined;
|
|
21
|
+
return new McpServer({ name: 'mcp-google-multi', version }, instructions ? { instructions } : undefined);
|
|
22
|
+
}
|
|
23
|
+
export function buildRegistry(server, ctx, mode, metrics = null) {
|
|
24
|
+
const policy = ctx.policy;
|
|
25
|
+
// The registry's account view IS the context's, not the process global.
|
|
26
|
+
const registry = new ToolRegistry(server, policy, mode, metrics, () => ctx.accounts);
|
|
27
|
+
// ctx.accounts is read per call, never snapshotted here, so a live getter stays live.
|
|
28
|
+
const owner = isOwnerContext(ctx);
|
|
29
|
+
const clientDeps = { getClientFn: ctx.getClient, localFiles: owner };
|
|
30
|
+
const execDeps = {
|
|
31
|
+
getClientFn: ctx.getClient,
|
|
32
|
+
scopeDeps: {
|
|
33
|
+
readTokenFn: ctx.tokenStore.readToken,
|
|
34
|
+
profileFn: (alias) => resolveScopesForAccount(alias, ctx.accounts),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const toolsets = getToolsets();
|
|
38
|
+
if (toolsets !== 'all') {
|
|
39
|
+
const known = new Set([...SERVICES.map((s) => s.name), ...GENERATED_SERVICES.map((s) => s.name)]);
|
|
40
|
+
for (const requested of toolsets) {
|
|
41
|
+
if (!known.has(requested)) {
|
|
42
|
+
process.stderr.write(`GOOGLE_TOOLSETS: unknown service "${requested}" ignored\n`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const svc of SERVICES) {
|
|
47
|
+
if (!toolsetEnabled(toolsets, svc.name))
|
|
48
|
+
continue;
|
|
49
|
+
if (svc.enabled && !svc.enabled(ctx.accounts)) {
|
|
50
|
+
if (toolsets !== 'all') {
|
|
51
|
+
const hint = svc.name === 'admin' ? 'set admin on an account/profile (or GOOGLE_ADMIN_ACCOUNTS)' : `add "${svc.name}" to an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES)`;
|
|
52
|
+
process.stderr.write(`GOOGLE_TOOLSETS: "${svc.name}" requested but not enabled — ${hint}\n`);
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
svc.register(registry, clientDeps);
|
|
57
|
+
}
|
|
58
|
+
for (const gen of GENERATED_SERVICES) {
|
|
59
|
+
if (!toolsetEnabled(toolsets, gen.name))
|
|
60
|
+
continue;
|
|
61
|
+
const curated = SERVICES.find((s) => s.name === gen.name);
|
|
62
|
+
const gate = curated?.enabled ?? GENERATED_GATES[gen.name]?.enabled;
|
|
63
|
+
if (gate && !gate(ctx.accounts)) {
|
|
64
|
+
if (!curated && toolsets !== 'all') {
|
|
65
|
+
process.stderr.write(`GOOGLE_TOOLSETS: "${gen.name}" requested but not enabled — ${GENERATED_GATES[gen.name].hint}\n`);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
gen.register(registry, execDeps);
|
|
70
|
+
}
|
|
71
|
+
if (registry.services().length === 0) {
|
|
72
|
+
const known = [...new Set([...SERVICES.map((s) => s.name), ...GENERATED_SERVICES.map((s) => s.name)])].sort();
|
|
73
|
+
throw new Error(`GOOGLE_TOOLSETS="${process.env.GOOGLE_TOOLSETS ?? ''}" selected no enabled services. ` +
|
|
74
|
+
`Known services: ${known.join(', ')}. ` +
|
|
75
|
+
`Note: optional services need their bundle in an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES); admin needs an admin account/profile.`);
|
|
76
|
+
}
|
|
77
|
+
registerDiscoverTools(registry, policy);
|
|
78
|
+
registerEscapeTools(registry, policy, execDeps);
|
|
79
|
+
registerAccountTools(registry, accountHealthDepsFor(ctx.tokenStore));
|
|
80
|
+
registerDiagnoseTool(registry, ctx);
|
|
81
|
+
// The wizard edits the owner's config.json and token store; any other
|
|
82
|
+
// context has neither.
|
|
83
|
+
if (owner)
|
|
84
|
+
registerAccountWizardTools(registry, server, () => servicesAwaitingRestart(registry));
|
|
85
|
+
return registry;
|
|
86
|
+
}
|
|
87
|
+
/** The per-request hooks bound to one registry: argument shapes, unknown-
|
|
88
|
+
* argument screening and the account a validation envelope names. Both
|
|
89
|
+
* transports and every resolved server target build them here, so a mistyped
|
|
90
|
+
* argument behaves the same wherever it arrives. */
|
|
91
|
+
export function requestHooksFor(registry, ctx, metrics = null, env = process.env) {
|
|
92
|
+
const mode = unknownArgMode(env);
|
|
93
|
+
return {
|
|
94
|
+
argShapeFor: argNormalizationEnabled(env) ? (tool) => registry.argShape(tool) : undefined,
|
|
95
|
+
strictArgs: mode === 'off'
|
|
96
|
+
? undefined
|
|
97
|
+
: {
|
|
98
|
+
mode,
|
|
99
|
+
declaredFor: (tool) => registry.declaredKeys(tool),
|
|
100
|
+
siblingsFor: (tool, keys) => registry.siblingSpellings(tool, keys),
|
|
101
|
+
unknownTool: (name) => unknownToolMessage(registry, name),
|
|
102
|
+
onDrop: metrics ? (tool, keys) => metrics.recordArgDrop(tool, keys) : undefined,
|
|
103
|
+
},
|
|
104
|
+
validationEnvelope: {
|
|
105
|
+
isKnownTool: (name) => registry.hasTool(name),
|
|
106
|
+
defaultAccount: () => ctx.accounts.defaultAccount,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
package/dist/config-file.d.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
export declare const CONFIG_VERSION = 1;
|
|
2
2
|
export declare function configDir(env?: NodeJS.ProcessEnv): string;
|
|
3
3
|
export declare function configFilePath(env?: NodeJS.ProcessEnv): string;
|
|
4
|
+
export declare function tenantsDir(env?: NodeJS.ProcessEnv): string;
|
|
5
|
+
export declare function tenantConfigFilePath(tenantId: string, env?: NodeJS.ProcessEnv): string;
|
|
6
|
+
export declare function tenantTokenDir(tenantId: string, env?: NodeJS.ProcessEnv): string;
|
|
7
|
+
/** Create and permission-harden the tenant directory chain. mkdirSync's
|
|
8
|
+
* recursive mode only stamps the DEEPEST newly-created dir, so every level
|
|
9
|
+
* gets an explicit chmod: the shared tenants/ root (created by whichever
|
|
10
|
+
* tenant provisions first) must never stay group-readable. */
|
|
11
|
+
export declare function ensureTenantDirs(tenantId: string, env?: NodeJS.ProcessEnv): {
|
|
12
|
+
configFile: string;
|
|
13
|
+
tokenDir: string;
|
|
14
|
+
};
|
|
4
15
|
export declare const ALIAS_RE: RegExp;
|
|
16
|
+
export declare const RESERVED_ALIASES: string[];
|
|
17
|
+
export declare function isReservedAlias(name: string): boolean;
|
|
5
18
|
export interface ConfigFile {
|
|
6
19
|
version: number;
|
|
7
20
|
accounts?: Record<string, {
|
package/dist/config-file.js
CHANGED
|
@@ -10,6 +10,38 @@ export function configDir(env = process.env) {
|
|
|
10
10
|
export function configFilePath(env = process.env) {
|
|
11
11
|
return path.join(configDir(env), 'config.json');
|
|
12
12
|
}
|
|
13
|
+
export function tenantsDir(env = process.env) {
|
|
14
|
+
return path.join(configDir(env), 'tenants');
|
|
15
|
+
}
|
|
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.
|
|
18
|
+
function assertTenantId(tenantId) {
|
|
19
|
+
if (!ALIAS_RE.test(tenantId) || isReservedAlias(tenantId) || tenantId === 'owner') {
|
|
20
|
+
throw new Error(`E_TENANT_ID_INVALID: invalid tenant id "${tenantId}".`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function tenantConfigFilePath(tenantId, env = process.env) {
|
|
24
|
+
assertTenantId(tenantId);
|
|
25
|
+
return path.join(tenantsDir(env), tenantId, 'config.json');
|
|
26
|
+
}
|
|
27
|
+
export function tenantTokenDir(tenantId, env = process.env) {
|
|
28
|
+
assertTenantId(tenantId);
|
|
29
|
+
return path.join(tenantsDir(env), tenantId, 'tokens');
|
|
30
|
+
}
|
|
31
|
+
/** Create and permission-harden the tenant directory chain. mkdirSync's
|
|
32
|
+
* recursive mode only stamps the DEEPEST newly-created dir, so every level
|
|
33
|
+
* gets an explicit chmod: the shared tenants/ root (created by whichever
|
|
34
|
+
* tenant provisions first) must never stay group-readable. */
|
|
35
|
+
export function ensureTenantDirs(tenantId, env = process.env) {
|
|
36
|
+
const tokenDir = tenantTokenDir(tenantId, env);
|
|
37
|
+
const levels = [configDir(env), tenantsDir(env), path.join(tenantsDir(env), tenantId), tokenDir];
|
|
38
|
+
for (const dir of levels) {
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
40
|
+
if (process.platform !== 'win32')
|
|
41
|
+
fs.chmodSync(dir, 0o700);
|
|
42
|
+
}
|
|
43
|
+
return { configFile: tenantConfigFilePath(tenantId, env), tokenDir };
|
|
44
|
+
}
|
|
13
45
|
// Alias charset is load-bearing security, not cosmetics: the alias becomes a
|
|
14
46
|
// tokenDir path segment (encPath), so this guard blocks path traversal.
|
|
15
47
|
export const ALIAS_RE = /^[a-zA-Z0-9_-]+$/;
|
|
@@ -19,10 +51,11 @@ const accountEntrySchema = z.strictObject({
|
|
|
19
51
|
admin: z.boolean().optional(),
|
|
20
52
|
});
|
|
21
53
|
// .strict() everywhere: any unknown or secret-shaped key (clientSecret,
|
|
22
|
-
// masterKey, jwtKey, clientId) fails validation, structurally enforcing
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
// masterKey, jwtKey, clientId) fails validation, structurally enforcing secrets-env-only.
|
|
55
|
+
export const RESERVED_ALIASES = ['__proto__', 'constructor', 'prototype'];
|
|
56
|
+
export function isReservedAlias(name) {
|
|
57
|
+
return RESERVED_ALIASES.includes(name);
|
|
58
|
+
}
|
|
26
59
|
const configSchema = z.strictObject({
|
|
27
60
|
version: z.number().int(),
|
|
28
61
|
accounts: z.record(z.string().regex(ALIAS_RE), accountEntrySchema).optional(),
|
|
@@ -36,9 +69,8 @@ const configSchema = z.strictObject({
|
|
|
36
69
|
defaultAccount: z.string().optional(),
|
|
37
70
|
discovery: z.enum(['lazy', 'curated', 'eager']).optional(),
|
|
38
71
|
toolsets: z.string().optional(),
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// (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.
|
|
42
74
|
usageMetrics: z.boolean().optional(),
|
|
43
75
|
});
|
|
44
76
|
export function failStartup(slug, message) {
|
|
@@ -89,9 +121,8 @@ export function loadConfigFile(filePath = configFilePath(), onInvalid = 'exit')
|
|
|
89
121
|
const issue = result.error.issues[0];
|
|
90
122
|
fail('E_CONFIG_INVALID', `${filePath}: ${issue.path.join('.') || '(root)'}: ${issue.message}. Secrets never belong in config.json.`);
|
|
91
123
|
}
|
|
92
|
-
// Post-schema
|
|
93
|
-
//
|
|
94
|
-
// 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.
|
|
95
126
|
const data = result.data;
|
|
96
127
|
for (const [section, keys] of [
|
|
97
128
|
['accounts', Object.keys(data.accounts ?? {})],
|
package/dist/discover.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { describePolicy } from './write-control.js';
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
// CAPPED vocabulary and the complete catalog stays in this tool's RESULT.
|
|
3
|
+
import { hints } from './hint-provider.js';
|
|
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.
|
|
6
6
|
const CURATED_OPS_CAP = 10;
|
|
7
7
|
const GENERATED_GROUPS_CAP = 6;
|
|
8
8
|
function opVocabulary(registry, service) {
|
|
@@ -14,8 +14,7 @@ function opVocabulary(registry, service) {
|
|
|
14
14
|
parts.push(`${shown.join(', ')}${more > 0 ? ` +${more} more` : ''}`);
|
|
15
15
|
}
|
|
16
16
|
if (generated.length > 0) {
|
|
17
|
-
//
|
|
18
|
-
// more selective than any truncated name list.
|
|
17
|
+
// Compresses to resource groups: denser than any truncated name list.
|
|
19
18
|
const counts = new Map();
|
|
20
19
|
for (const op of generated) {
|
|
21
20
|
const group = op.split('_')[0];
|
|
@@ -30,10 +29,7 @@ function opVocabulary(registry, service) {
|
|
|
30
29
|
}
|
|
31
30
|
export function registerDiscoverTools(registry, policy) {
|
|
32
31
|
const registerMeta = registry.registerMeta;
|
|
33
|
-
//
|
|
34
|
-
// curated quality layer for heavy Google work, collapse to reclaim the
|
|
35
|
-
// name/schema context budget after. stdio-only semantics — over stateless
|
|
36
|
-
// 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.
|
|
37
33
|
registerMeta('discover_all', {
|
|
38
34
|
description: 'Reveal ALL curated Google tools at once (instead of per-service discovery). Use when ' +
|
|
39
35
|
'starting substantial Google work; prefer these over google_api_call. Pair with discover_reset.',
|
|
@@ -91,6 +87,7 @@ export function registerDiscoverTools(registry, policy) {
|
|
|
91
87
|
}, async ({ query }) => {
|
|
92
88
|
const operations = registry.catalog(service, query);
|
|
93
89
|
registry.reveal(service);
|
|
90
|
+
const rendered = hints()?.render({ kind: 'policy_summary', policy });
|
|
94
91
|
return {
|
|
95
92
|
content: [
|
|
96
93
|
{
|
|
@@ -98,7 +95,7 @@ export function registerDiscoverTools(registry, policy) {
|
|
|
98
95
|
text: JSON.stringify({
|
|
99
96
|
service,
|
|
100
97
|
operations,
|
|
101
|
-
writeControl: describePolicy(policy),
|
|
98
|
+
writeControl: rendered?.hint ?? describePolicy(policy),
|
|
102
99
|
next: operations.length > 0
|
|
103
100
|
? 'Call the chosen tool by name; it is now listed and callable.'
|
|
104
101
|
: `No ${service} operation matches "${query}". Call again without query for the full catalog.`,
|
|
@@ -49,5 +49,8 @@ export declare function loadMethodIndex(api: string, deps?: DiscoveryDeps): Prom
|
|
|
49
49
|
export declare function clearDiscoveryMemoryCache(): void;
|
|
50
50
|
export declare function cudFromMethod(method: Pick<DiscoveryMethod, 'httpMethod' | 'id'>): 'read' | 'create' | 'update' | 'delete';
|
|
51
51
|
export declare function expandPath(template: string, pathParams: Record<string, string>): string;
|
|
52
|
+
/** Search keywords are a few words; the caps keep a caller-sized query from
|
|
53
|
+
* driving index-size x token-count substring scans on the event loop. */
|
|
54
|
+
export declare const MAX_SEARCH_QUERY = 500;
|
|
52
55
|
export declare function searchMethods(index: DiscoveryMethod[], query: string, limit?: number): DiscoveryMethod[];
|
|
53
56
|
export {};
|
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,6 +72,20 @@ 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
|
}
|
|
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.
|
|
77
|
+
function tightenOwnDir(dir) {
|
|
78
|
+
if (process.platform === 'win32')
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const st = fs.statSync(dir);
|
|
82
|
+
if (st.uid === process.getuid?.() && (st.mode & 0o077) !== 0)
|
|
83
|
+
fs.chmodSync(dir, st.mode & ~0o077 & 0o7777);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// the cache still works with the directory's own mode
|
|
87
|
+
}
|
|
88
|
+
}
|
|
77
89
|
// A poisoned cache file could repoint baseUrl off-Google; refuse to send the
|
|
78
90
|
// user's Bearer token anywhere but a googleapis.com host.
|
|
79
91
|
export function isGoogleApiUrl(url) {
|
|
@@ -95,9 +107,8 @@ export function buildMethodIndex(doc, api) {
|
|
|
95
107
|
if (!method.id || !method.httpMethod || !method.path)
|
|
96
108
|
continue;
|
|
97
109
|
const params = method.parameters ?? {};
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// 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.
|
|
101
112
|
const order = method.parameterOrder ?? [];
|
|
102
113
|
const pos = (n) => {
|
|
103
114
|
const i = order.indexOf(n);
|
|
@@ -135,6 +146,7 @@ export async function loadMethodIndex(api, deps = {}) {
|
|
|
135
146
|
throw new Error(`Unknown api "${api}". Known: ${Object.keys(SUPPORTED_APIS).join(', ')}`);
|
|
136
147
|
}
|
|
137
148
|
const fetchFn = deps.fetchFn ?? ((url) => fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }));
|
|
149
|
+
const defaultDir = deps.cacheDir === undefined && process.env.DISCOVERY_CACHE_PATH === undefined;
|
|
138
150
|
const cacheDir = deps.cacheDir ?? discoveryCacheDir();
|
|
139
151
|
const cacheFile = path.join(cacheDir, `${api}.json`);
|
|
140
152
|
const readCache = () => {
|
|
@@ -158,9 +170,8 @@ export async function loadMethodIndex(api, deps = {}) {
|
|
|
158
170
|
if (cacheFresh())
|
|
159
171
|
doc = readCache();
|
|
160
172
|
if (!doc) {
|
|
161
|
-
// Newer APIs
|
|
162
|
-
//
|
|
163
|
-
// 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.
|
|
164
175
|
const urls = [
|
|
165
176
|
`https://www.googleapis.com/discovery/v1/apis/${spec.id}/${spec.version}/rest`,
|
|
166
177
|
`https://${spec.id}.googleapis.com/$discovery/rest?version=${spec.version}`,
|
|
@@ -172,7 +183,9 @@ export async function loadMethodIndex(api, deps = {}) {
|
|
|
172
183
|
if (!res.ok)
|
|
173
184
|
throw new Error(`HTTP ${res.status}`);
|
|
174
185
|
doc = (await res.json());
|
|
175
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
186
|
+
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
|
|
187
|
+
if (defaultDir)
|
|
188
|
+
tightenOwnDir(cacheDir);
|
|
176
189
|
fs.writeFileSync(cacheFile, JSON.stringify(doc), { mode: 0o600 });
|
|
177
190
|
break;
|
|
178
191
|
}
|
|
@@ -199,20 +212,11 @@ export function clearDiscoveryMemoryCache() {
|
|
|
199
212
|
// GA4-style report execution (runReport, batchRunPivotReports, runAccessReport)
|
|
200
213
|
// and check* predicates are POSTs purely for the request-body size — reads.
|
|
201
214
|
const POST_READ_VERB = /^(get|list|search|query|lookup|count|batchGet|generateIds|export|download|inspect|check|suggest|(batch)?run\w*report)/i;
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
// before delete for exactly that reason.
|
|
205
|
-
// State transitions on something that already exists: reversible, and none of
|
|
206
|
-
// them creates anything, so `create` was both wrong and the most permissive
|
|
207
|
-
// class available. `(batch)?` mirrors the delete list, without which every
|
|
208
|
-
// `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.
|
|
209
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;
|
|
210
|
-
// archive
|
|
211
|
-
//
|
|
212
|
-
// So do the teardown verbs: `stop` and `cancel` tear down a push channel or a
|
|
213
|
-
// long-running operation, `wipe`/`invalidate`/`signOut` destroy device data,
|
|
214
|
-
// codes and sessions, `reset` discards a configuration, and `end` terminates a
|
|
215
|
-
// 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.
|
|
216
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;
|
|
217
221
|
export function cudFromMethod(method) {
|
|
218
222
|
switch (method.httpMethod) {
|
|
@@ -244,13 +248,8 @@ export function expandPath(template, pathParams) {
|
|
|
244
248
|
if (value === undefined) {
|
|
245
249
|
throw new Error(`Missing required path parameter "${name}"`);
|
|
246
250
|
}
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// back as the whole site LIST, reported as success. Dot segments resolve
|
|
250
|
-
// at URL-parse time, so `../v1/contactGroups` retargets the call at a
|
|
251
|
-
// different endpoint and returns ITS response as success. Both are
|
|
252
|
-
// silent-wrong, so refuse locally rather than send a different request.
|
|
253
|
-
// `{+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.
|
|
254
253
|
const segments = plus ? String(value).split('/') : [String(value)];
|
|
255
254
|
for (const segment of segments) {
|
|
256
255
|
if (segment.trim() === '') {
|
|
@@ -263,8 +262,13 @@ export function expandPath(template, pathParams) {
|
|
|
263
262
|
return segments.map(encodeURIComponent).join('/');
|
|
264
263
|
});
|
|
265
264
|
}
|
|
265
|
+
/** Search keywords are a few words; the caps keep a caller-sized query from
|
|
266
|
+
* driving index-size x token-count substring scans on the event loop. */
|
|
267
|
+
export const MAX_SEARCH_QUERY = 500;
|
|
268
|
+
const MAX_SEARCH_TOKENS = 16;
|
|
266
269
|
export function searchMethods(index, query, limit = 10) {
|
|
267
|
-
const
|
|
270
|
+
const words = query.slice(0, MAX_SEARCH_QUERY).toLowerCase().split(/\s+/).filter(Boolean);
|
|
271
|
+
const tokens = [...new Set(words)].slice(0, MAX_SEARCH_TOKENS);
|
|
268
272
|
if (tokens.length === 0)
|
|
269
273
|
return index.slice(0, limit);
|
|
270
274
|
const scored = index
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolRegistry } from './registry.js';
|
|
2
2
|
import { getAccountSet } from './accounts.js';
|
|
3
3
|
import { type AccountHealth } from './tools/accounts-tool.js';
|
|
4
|
+
import { type IdentityContext } from './identity.js';
|
|
4
5
|
import { peekMasterKeyProvenance } from './master-key.js';
|
|
5
6
|
import { type HttpConfig } from './http-config.js';
|
|
6
7
|
export type Verdict = 'ok' | 'warn' | 'fail' | 'unknown';
|
|
@@ -51,21 +52,24 @@ export interface HttpProbeResult {
|
|
|
51
52
|
unreachable?: boolean;
|
|
52
53
|
problem?: string;
|
|
53
54
|
}
|
|
54
|
-
/** Console deep-link to enable one API (section-6 hint
|
|
55
|
+
/** Console deep-link to enable one API (section-6 hint). */
|
|
55
56
|
export declare function apiEnableLink(api: string): string;
|
|
56
57
|
/** One line, state AND source, so the people being measured can see both
|
|
57
|
-
* here and in `diagnose
|
|
58
|
+
* here and in `diagnose`. Read-only. */
|
|
58
59
|
export declare function usageMetricsStatusLine(env: Record<string, string | undefined>): string;
|
|
59
60
|
/** Roll section verdicts to an overall verdict. `unknown` never worsens it. */
|
|
60
61
|
export declare function overallVerdict(sections: DiagnosticSection[]): Verdict;
|
|
61
|
-
export declare function runDiagnostics(deps?: DiagnosticsDeps
|
|
62
|
+
export declare function runDiagnostics(deps?: DiagnosticsDeps, opts?: {
|
|
63
|
+
scope?: 'operator' | 'tenant';
|
|
64
|
+
}): Promise<DiagnosticsReport>;
|
|
62
65
|
/** doctor CLI (brew-doctor style): glyph-coded human report. */
|
|
63
66
|
export declare function renderDoctorText(report: DiagnosticsReport): string;
|
|
64
|
-
/** Redacted, paste-ready bug report
|
|
67
|
+
/** Redacted, paste-ready bug report: verdicts + provenance labels + token
|
|
65
68
|
* statuses + slugs; NO token values, secrets, keys, or full email addresses. */
|
|
66
69
|
export declare function renderReport(report: DiagnosticsReport, version: string): string;
|
|
67
70
|
/** Exit non-zero when any section FAILs; with strict, WARN counts too. */
|
|
68
71
|
export declare function exitCodeFor(report: DiagnosticsReport, strict: boolean): number;
|
|
72
|
+
export type DiagnoseContext = Pick<IdentityContext, 'subject' | 'accounts' | 'getClient' | 'tokenStore'>;
|
|
69
73
|
/** Agent-callable structured health report (read-only). Mirrors `doctor`'s
|
|
70
74
|
* engine. Registered as a META tool, like `account_list`: it introspects this
|
|
71
75
|
* server rather than Google data, and as a normal tool it became a service of
|
|
@@ -73,7 +77,7 @@ export declare function exitCodeFor(report: DiagnosticsReport, strict: boolean):
|
|
|
73
77
|
* from already-registered services, and this runs after that), so it was
|
|
74
78
|
* advertised only once something else expanded the surface. The README sends
|
|
75
79
|
* people here when they are stuck, so it has to be findable. */
|
|
76
|
-
export declare function registerDiagnoseTool(registry: ToolRegistry): void;
|
|
80
|
+
export declare function registerDiagnoseTool(registry: ToolRegistry, ctx?: DiagnoseContext): void;
|
|
77
81
|
export interface ResetOptions {
|
|
78
82
|
account?: string;
|
|
79
83
|
regenerateKey: boolean;
|