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
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { getAccountSet, invalidateAccountSet } from '../accounts.js';
|
|
3
|
-
import { mutateConfigFile } from '../config-file.js';
|
|
2
|
+
import { getAccountSet, invalidateAccountSet, refreshAccountSetIfStale } from '../accounts.js';
|
|
3
|
+
import { ALIAS_RE, mutateConfigFile } from '../config-file.js';
|
|
4
4
|
import { writeToken } from '../token-store.js';
|
|
5
5
|
import { resolveScopesForAccount } from '../auth.js';
|
|
6
|
-
import { BUNDLE_CATALOG, closestBundle, resolveBundleAliases } from '../scope-catalog.js';
|
|
6
|
+
import { BUNDLE_CATALOG, closestBundle, isKnownBundle, refusedBundlesFor, resolveBundleAliases } from '../scope-catalog.js';
|
|
7
7
|
import { openUrl } from '../open-url.js';
|
|
8
8
|
import { coerceBoolean } from './_coerce.js';
|
|
9
9
|
import { safeMessage, stringifyEnvelope } from './_errors.js';
|
|
10
10
|
import { buildConsentClient, openLoopbackConsent, hasClientCredentials, TESTING_MODE_WARNING, } from '../oauth-consent.js';
|
|
11
11
|
import { detectClients, buildServerEntry, renderInstruction, applyFileEntry, resolveMode, DEFAULT_SERVER_NAME, } from '../client-config.js';
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// running server can add an account without the deployer editing env + running
|
|
15
|
-
// a CLI. Google consent reuses the loopback flow (leg C); the HTTP-transport
|
|
16
|
-
// consent path (${BASE}/authorize) is owned by the OAuth AS and lands with that
|
|
17
|
-
// cluster. account_add over http is gated behind it.
|
|
18
|
-
const ALIAS_RE = /^[a-zA-Z0-9_-]+$/;
|
|
19
|
-
// The common optional bundles get their own checkbox (MCP elicitation has no
|
|
20
|
-
// multi-select, so a boolean per bundle IS the checklist); the long tail stays
|
|
21
|
-
// a comma-separated field for power users, and `allBundles` grants every one.
|
|
12
|
+
// Each common bundle gets its own checkbox since MCP elicitation has no
|
|
13
|
+
// multi-select; the long tail stays a comma-separated field, `allBundles` grants all.
|
|
22
14
|
const CHECKBOX_BUNDLES = ['forms', 'chat'];
|
|
23
|
-
/** Every optional bundle (admin is granted via its own checkbox, not here)
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
/** Every optional bundle (admin is granted via its own checkbox, not here),
|
|
16
|
+
* less those Google refuses for the account at `email` when one is given. */
|
|
17
|
+
export function allOptionalBundles(email) {
|
|
18
|
+
const all = Object.keys(BUNDLE_CATALOG).filter((n) => n !== 'admin');
|
|
19
|
+
if (email === undefined)
|
|
20
|
+
return all;
|
|
21
|
+
const refused = new Set(refusedBundlesFor(email, all));
|
|
22
|
+
return all.filter((n) => !refused.has(n));
|
|
26
23
|
}
|
|
27
24
|
/** elicitation/create form schema (flat primitives only, per the MCP spec). */
|
|
28
25
|
export function addFormSchema() {
|
|
@@ -55,8 +52,7 @@ export function validateAddForm(input, existingAliases) {
|
|
|
55
52
|
return { ok: false, slug: 'E_VALIDATION', message: 'Email is required.', hint: "It is used as the Google login hint, so it must be the account's real Google address.", alias };
|
|
56
53
|
}
|
|
57
54
|
if (input.allBundles === true) {
|
|
58
|
-
|
|
59
|
-
return { ok: true, alias, email, bundles: allOptionalBundles(), admin: input.admin === true };
|
|
55
|
+
return { ok: true, alias, email, bundles: allOptionalBundles(email), admin: input.admin === true };
|
|
60
56
|
}
|
|
61
57
|
const picked = CHECKBOX_BUNDLES.filter((b) => input[b] === true);
|
|
62
58
|
const rawOther = (input.otherBundles ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
@@ -65,7 +61,7 @@ export function validateAddForm(input, existingAliases) {
|
|
|
65
61
|
if (b === 'admin') {
|
|
66
62
|
return { ok: false, slug: 'E_UNKNOWN_BUNDLE', message: '"admin" is not a scope bundle.', hint: 'Use the admin checkbox instead, or pass "admin": true.', alias };
|
|
67
63
|
}
|
|
68
|
-
if (!(b
|
|
64
|
+
if (!isKnownBundle(b)) {
|
|
69
65
|
const closest = closestBundle(b);
|
|
70
66
|
const known = Object.keys(BUNDLE_CATALOG).filter((n) => n !== 'admin').join(', ');
|
|
71
67
|
return {
|
|
@@ -77,6 +73,16 @@ export function validateAddForm(input, existingAliases) {
|
|
|
77
73
|
};
|
|
78
74
|
}
|
|
79
75
|
}
|
|
76
|
+
const refused = refusedBundlesFor(email, bundles);
|
|
77
|
+
if (refused.length > 0) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
slug: 'E_VALIDATION',
|
|
81
|
+
message: `Google does not let a personal account grant ${refused.map((b) => `"${b}"`).join(', ')}.`,
|
|
82
|
+
hint: 'Leave it out, or link a Google Workspace account for it.',
|
|
83
|
+
alias,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
80
86
|
return { ok: true, alias, email, bundles, admin: input.admin === true };
|
|
81
87
|
}
|
|
82
88
|
/** URL-mode elicitation params. `elicitationId` is REQUIRED by the spec
|
|
@@ -109,8 +115,6 @@ export function scopeGrantDiff(requested, grantedScope) {
|
|
|
109
115
|
const granted = new Set((grantedScope ?? '').split(' ').filter(Boolean));
|
|
110
116
|
return requested.filter((s) => !granted.has(s));
|
|
111
117
|
}
|
|
112
|
-
/** Persist a new account row (+ a per-account scope profile when bundles/admin
|
|
113
|
-
* were chosen) through the atomic registry mutation path (BR2). */
|
|
114
118
|
function writeAccountRow(alias, email, bundles, admin) {
|
|
115
119
|
mutateConfigFile((current) => {
|
|
116
120
|
const next = { ...current, accounts: { ...(current.accounts ?? {}) } };
|
|
@@ -128,9 +132,8 @@ function writeAccountRow(alias, email, bundles, admin) {
|
|
|
128
132
|
function textResult(text) {
|
|
129
133
|
return { content: [{ type: 'text', text }] };
|
|
130
134
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
* it is a human-readable onboarding report, not data. */
|
|
135
|
+
// Errors use the shared envelope so clients can branch on slug; success stays
|
|
136
|
+
// plain prose (a human-readable report, not data).
|
|
134
137
|
function errorResult(slug, message, hint, account) {
|
|
135
138
|
return {
|
|
136
139
|
content: [{ type: 'text', text: stringifyEnvelope({
|
|
@@ -143,9 +146,24 @@ function errorResult(slug, message, hint, account) {
|
|
|
143
146
|
isError: true,
|
|
144
147
|
};
|
|
145
148
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
let httpConsent = null;
|
|
150
|
+
export function setWizardHttpConsent(ctx) {
|
|
151
|
+
httpConsent = ctx;
|
|
152
|
+
}
|
|
153
|
+
// Services register tools at startup: an account's first-enabled service
|
|
154
|
+
// stays unregistered until a restart, so say so rather than claim it just works.
|
|
155
|
+
function restartNote(pending) {
|
|
156
|
+
if (pending.length === 0)
|
|
157
|
+
return '';
|
|
158
|
+
const names = pending.map((s) => `"${s}"`).join(', ');
|
|
159
|
+
const what = pending.length > 1 ? `services ${names} register their` : `service ${names} registers its`;
|
|
160
|
+
return `\nThe ${what} tools when the server starts: restart the server to use them. Until then, google_api_call can reach the same API.`;
|
|
161
|
+
}
|
|
162
|
+
function pendingText(alias, url, pending) {
|
|
163
|
+
return (`Account "${alias}" is ready to authorize. Open this link in a browser (it belongs to whoever owns the Google account):\n${url}\n` +
|
|
164
|
+
`On completion the token is stored server-side and the account is usable immediately — no restart. ` +
|
|
165
|
+
`If the link expires, run account_reauth for a fresh one.${restartNote(pending)}\n${TESTING_MODE_WARNING}`);
|
|
166
|
+
}
|
|
149
167
|
async function runConsent(server, alias) {
|
|
150
168
|
const { randomBytes } = await import('node:crypto');
|
|
151
169
|
const cfg = getAccountSet().configs[alias];
|
|
@@ -157,9 +175,30 @@ async function runConsent(server, alias) {
|
|
|
157
175
|
hint: 'Accounts sourced from GOOGLE_ACCOUNTS are not editable here. Run `mcp-google-multi migrate-config` to move them into config.json.',
|
|
158
176
|
};
|
|
159
177
|
}
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
178
|
+
// HTTP: consent completes out-of-band at the AS /callback; never bind a
|
|
179
|
+
// loopback listener; surface the URL up front, elicitation is just a convenience.
|
|
180
|
+
const http = httpConsent;
|
|
181
|
+
if (http) {
|
|
182
|
+
let url;
|
|
183
|
+
try {
|
|
184
|
+
url = await http.mintConsentUrl(alias);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
return { ok: false, slug: 'internal', message: safeMessage(e) };
|
|
188
|
+
}
|
|
189
|
+
const caps = server.server.getClientCapabilities?.();
|
|
190
|
+
if (caps?.elicitation?.url !== undefined) {
|
|
191
|
+
try {
|
|
192
|
+
await server.server.elicitInput(urlElicitationParams(alias, url, randomBytes(16).toString('hex')));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// pending text has the recovery URL
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { ok: 'pending', url };
|
|
199
|
+
}
|
|
200
|
+
// Bind the loopback listener before building the auth URL: the redirect URI
|
|
201
|
+
// needs the assigned port, and listening first avoids a race with the callback.
|
|
163
202
|
const loop = await openLoopbackConsent();
|
|
164
203
|
const client = buildConsentClient(loop.redirect);
|
|
165
204
|
const expectedState = randomBytes(32).toString('hex');
|
|
@@ -172,9 +211,8 @@ async function runConsent(server, alias) {
|
|
|
172
211
|
let opened = false;
|
|
173
212
|
if (caps?.elicitation?.url !== undefined) {
|
|
174
213
|
try {
|
|
175
|
-
// elicitationId is REQUIRED by the spec
|
|
176
|
-
//
|
|
177
|
-
// fell through to the server-side browser open.
|
|
214
|
+
// elicitationId is REQUIRED by the spec; omitting it silently fails
|
|
215
|
+
// client-side validation, falling through to the server-side browser open.
|
|
178
216
|
const r = await server.server.elicitInput(urlElicitationParams(alias, url, randomBytes(16).toString('hex')));
|
|
179
217
|
if (r.action !== 'accept') {
|
|
180
218
|
loop.close();
|
|
@@ -211,22 +249,21 @@ async function runConsent(server, alias) {
|
|
|
211
249
|
writeToken(alias, tokens);
|
|
212
250
|
return { ok: true, missing: scopeGrantDiff(scopes, typeof tokens.scope === 'string' ? tokens.scope : undefined) };
|
|
213
251
|
}
|
|
214
|
-
function s4Text(alias, missing) {
|
|
252
|
+
function s4Text(alias, missing, pending) {
|
|
215
253
|
const outcome = missing.length === 0
|
|
216
254
|
? `✔ "${alias}" authenticated; all requested scopes granted. It is now usable without a restart.`
|
|
217
255
|
: `⚠ "${alias}" authenticated, but ${missing.length} requested scope(s) were NOT granted (E_SCOPE_NOT_GRANTED). You may have unchecked some on the consent screen. Re-run account_reauth to grant them. The account is usable for the granted scopes.`;
|
|
218
|
-
return `${outcome}\n${TESTING_MODE_WARNING}`;
|
|
256
|
+
return `${outcome}${restartNote(pending)}\n${TESTING_MODE_WARNING}`;
|
|
219
257
|
}
|
|
220
258
|
const REQUIRES_INTERACTION = { 'anthropic/requiresUserInteraction': true };
|
|
221
|
-
export function registerAccountWizardTools(registry, server) {
|
|
222
|
-
// Registered as
|
|
223
|
-
//
|
|
224
|
-
// preserves the requiresUserInteraction clientMeta and skips the fan-out path.
|
|
259
|
+
export function registerAccountWizardTools(registry, server, awaitingRestart = () => []) {
|
|
260
|
+
// Registered as meta (like account_list) so onboarding tools are reachable in
|
|
261
|
+
// lazy mode without discover_all; this still preserves requiresUserInteraction and skips fan-out.
|
|
225
262
|
const registerMeta = registry.registerMeta;
|
|
226
263
|
registerMeta('account_add', {
|
|
227
264
|
_meta: REQUIRES_INTERACTION,
|
|
228
265
|
annotations: { openWorldHint: true },
|
|
229
|
-
description: 'Add a new Google account: pass alias + email directly (plus optional bundles/allBundles/admin), or pass nothing for an interactive form where the client supports elicitation. Writes the registry and runs Google consent in the browser. No file editing or restart needed. Requires GOOGLE_CLIENT_ID/SECRET (run the `setup` prompt first if missing).',
|
|
266
|
+
description: 'Add a new Google account: pass alias + email directly (plus optional bundles/allBundles/admin), or pass nothing for an interactive form where the client supports elicitation. Writes the registry and runs Google consent in the browser. No file editing or restart needed to use the account (tools of a service it newly enables register at the next restart). Requires GOOGLE_CLIENT_ID/SECRET (run the `setup` prompt first if missing).',
|
|
230
267
|
inputSchema: {
|
|
231
268
|
alias: z.string().optional().describe('Account alias (letters, digits, _ or -). Pass with email to add directly, skipping the form.'),
|
|
232
269
|
account: z.string().optional().describe('Alias for the new account (same as `alias`; every other tool spells it `account`)'),
|
|
@@ -237,18 +274,16 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
237
274
|
},
|
|
238
275
|
}, async (args) => {
|
|
239
276
|
try {
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// write. Refuse up front (BR-10) rather than write a row nothing reads.
|
|
277
|
+
// GOOGLE_ACCOUNTS is the exclusive registry source when set; config.json
|
|
278
|
+
// is ignored, so refuse here rather than write a row nothing reads.
|
|
243
279
|
if (process.env.GOOGLE_ACCOUNTS?.trim()) {
|
|
244
280
|
return errorResult('E_ENV_ACCOUNTS_MODE', 'Accounts are defined by the GOOGLE_ACCOUNTS environment variable, so a new account cannot be added interactively: config.json is ignored while it is set, and the row would be written where nothing reads it.', 'Either add the alias to GOOGLE_ACCOUNTS and run account_reauth, or run `mcp-google-multi migrate-config` to move accounts into config.json and unset GOOGLE_ACCOUNTS.');
|
|
245
281
|
}
|
|
246
282
|
if (!hasClientCredentials()) {
|
|
247
283
|
return errorResult('invalid_client', 'GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are not set, so there is no OAuth client to run consent against.', 'Run the `setup` prompt (/mcp__google-multi__setup) to create an OAuth client, then set them. See docs/google-cloud-setup.md.');
|
|
248
284
|
}
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// interactive path used to dead-end).
|
|
285
|
+
// Arguments win over the form so the wizard still works for clients
|
|
286
|
+
// without form elicitation support.
|
|
252
287
|
const a = (args ?? {});
|
|
253
288
|
let input;
|
|
254
289
|
if (a.alias?.trim() || a.account?.trim() || a.email?.trim()) {
|
|
@@ -269,15 +304,23 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
269
304
|
const validated = validateAddForm(input, getAccountSet().aliases);
|
|
270
305
|
if (!validated.ok)
|
|
271
306
|
return errorResult(validated.slug, validated.message, validated.hint, validated.alias);
|
|
272
|
-
//
|
|
307
|
+
// Atomic write + make the alias callable without a restart.
|
|
273
308
|
writeAccountRow(validated.alias, validated.email, validated.bundles, validated.admin);
|
|
274
309
|
invalidateAccountSet();
|
|
275
|
-
//
|
|
310
|
+
// Account args already validate against the refreshed registry; the
|
|
311
|
+
// nudge just gets clients to re-fetch tools/list for the new account enum.
|
|
312
|
+
try {
|
|
313
|
+
server.sendToolListChanged();
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// harmless if unsupported
|
|
317
|
+
}
|
|
276
318
|
const consent = await runConsent(server, validated.alias);
|
|
319
|
+
if (consent.ok === 'pending')
|
|
320
|
+
return textResult(pendingText(validated.alias, consent.url, awaitingRestart()));
|
|
277
321
|
if (!consent.ok)
|
|
278
322
|
return errorResult(consent.slug, consent.message, consent.hint, validated.alias);
|
|
279
|
-
|
|
280
|
-
return textResult(`${s4Text(validated.alias, consent.missing)}\nTip: run account_write_config to register this server with another MCP client (Claude Desktop / Cursor / Claude Code).`);
|
|
323
|
+
return textResult(`${s4Text(validated.alias, consent.missing, awaitingRestart())}\nTip: run account_write_config to register this server with another MCP client (Claude Desktop / Cursor / Claude Code).`);
|
|
281
324
|
}
|
|
282
325
|
catch (e) {
|
|
283
326
|
// safeMessage, not error.message: an arbitrary throw here can carry a
|
|
@@ -293,16 +336,17 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
293
336
|
// Plain string (NOT the account enum) so this never joins the
|
|
294
337
|
// multi-account fan-out path; validated against the registry below.
|
|
295
338
|
alias: z.string().min(1).optional().describe('Existing account alias to re-authenticate'),
|
|
296
|
-
// Every other tool
|
|
297
|
-
//
|
|
298
|
-
// no did-you-mean can rescue: `account` is 5 edits from `alias`, so
|
|
299
|
-
// the matcher cannot bridge them.
|
|
339
|
+
// Every other tool spells this `account`; accepting both avoids a dead
|
|
340
|
+
// end no did-you-mean can bridge (too many edits from `alias`).
|
|
300
341
|
account: z.string().min(1).optional().describe('Alias of the account to re-authenticate (same as `alias`)'),
|
|
301
342
|
},
|
|
302
343
|
}, async (args) => {
|
|
303
344
|
try {
|
|
304
345
|
const a = args;
|
|
305
346
|
const alias = a.alias ?? a.account ?? '';
|
|
347
|
+
// No default-account wrapper runs for this tool, so pick up a
|
|
348
|
+
// hand-edited config.json here: that edit is why a re-auth is asked for.
|
|
349
|
+
refreshAccountSetIfStale();
|
|
306
350
|
if (!hasClientCredentials()) {
|
|
307
351
|
return errorResult('invalid_client', 'GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are not set, so there is no OAuth client to run consent against.', 'Run the `setup` prompt (/mcp__google-multi__setup) to create an OAuth client, then set them. See docs/google-cloud-setup.md.');
|
|
308
352
|
}
|
|
@@ -310,9 +354,11 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
310
354
|
return errorResult('validation_error', `Unknown account "${alias}".`, `Pass one of the configured aliases: ${getAccountSet().aliases.join(', ')}. Use account_add to create a new one.`, alias);
|
|
311
355
|
}
|
|
312
356
|
const consent = await runConsent(server, alias);
|
|
357
|
+
if (consent.ok === 'pending')
|
|
358
|
+
return textResult(pendingText(alias, consent.url, awaitingRestart()));
|
|
313
359
|
if (!consent.ok)
|
|
314
360
|
return errorResult(consent.slug, consent.message, consent.hint, alias);
|
|
315
|
-
return textResult(s4Text(alias, consent.missing));
|
|
361
|
+
return textResult(s4Text(alias, consent.missing, awaitingRestart()));
|
|
316
362
|
}
|
|
317
363
|
catch (e) {
|
|
318
364
|
return errorResult('internal', `account_reauth failed: ${safeMessage(e)}`);
|
|
@@ -340,7 +386,7 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
340
386
|
resourceUri = m.resourceUri;
|
|
341
387
|
}
|
|
342
388
|
catch {
|
|
343
|
-
//
|
|
389
|
+
// fallback to stdio on error
|
|
344
390
|
}
|
|
345
391
|
let entry;
|
|
346
392
|
try {
|
|
@@ -360,6 +406,11 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
360
406
|
if (client.managed === 'cli') {
|
|
361
407
|
blocks.push(`${client.label}: run\n ${instr.text}`);
|
|
362
408
|
}
|
|
409
|
+
else if (a.write && mode === 'http') {
|
|
410
|
+
// Over HTTP this runs on the daemon, not the caller's machine: write:true
|
|
411
|
+
// would edit the operator's own client configs (a tenancy hazard), so snippet only.
|
|
412
|
+
blocks.push(`${client.label}: write:true is ignored over HTTP — this tool runs on the server, not your machine. Add to ${instr.path}\n${instr.text}`);
|
|
413
|
+
}
|
|
363
414
|
else if (a.write) {
|
|
364
415
|
const res = applyFileEntry(client, name, entry);
|
|
365
416
|
blocks.push(res.ok
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
import { type AccountSet } from '../accounts.js';
|
|
2
3
|
import { type ScopesReport } from '../scope-observability.js';
|
|
4
|
+
import { type TokenStore } from '../token-store.js';
|
|
3
5
|
export interface AccountHealthDeps {
|
|
4
6
|
hasToken: (alias: string) => boolean;
|
|
5
7
|
readToken: (alias: string) => {
|
|
@@ -10,10 +12,15 @@ export interface AccountHealthDeps {
|
|
|
10
12
|
fileExists: (p: string) => boolean;
|
|
11
13
|
now: () => number;
|
|
12
14
|
/** Scopes required by registered tools — feeds doctor's tool-grained
|
|
13
|
-
* "registered but not requestable" view
|
|
15
|
+
* "registered but not requestable" view. account_list stays compact:
|
|
14
16
|
* its report universe is profile ∪ granted only. */
|
|
15
17
|
registeredScopes?: () => string[];
|
|
18
|
+
/** Account view health is derived against; absent = the global registry. */
|
|
19
|
+
accounts?: () => AccountSet;
|
|
16
20
|
}
|
|
21
|
+
/** Health deps over one context's token store (and, optionally, its account
|
|
22
|
+
* view); the module-level functions are the single owner's store. */
|
|
23
|
+
export declare function accountHealthDepsFor(store: Pick<TokenStore, 'hasToken' | 'readToken'>, accounts?: () => AccountSet): AccountHealthDeps;
|
|
17
24
|
export type TokenStatus = 'ok' | 'expired_refreshable' | 'needs_reauth' | 'missing' | 'decrypt_error';
|
|
18
25
|
export interface AccountHealth {
|
|
19
26
|
alias: string;
|
|
@@ -1,19 +1,28 @@
|
|
|
1
|
+
import { reauthHint } from '../reauth-hint.js';
|
|
1
2
|
import * as fs from 'node:fs';
|
|
2
3
|
import { getAccountSet, refreshAccountSetIfStale } from '../accounts.js';
|
|
3
4
|
import { buildScopesReport } from '../scope-observability.js';
|
|
4
5
|
import { getAdminAccounts, resolveScopesForAccount } from '../auth.js';
|
|
5
6
|
import { hasToken, readToken } from '../token-store.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import { hints } from '../hint-provider.js';
|
|
8
|
+
/** Health deps over one context's token store (and, optionally, its account
|
|
9
|
+
* view); the module-level functions are the single owner's store. */
|
|
10
|
+
export function accountHealthDepsFor(store, accounts) {
|
|
11
|
+
return {
|
|
12
|
+
hasToken: store.hasToken,
|
|
13
|
+
readToken: store.readToken,
|
|
14
|
+
fileExists: fs.existsSync,
|
|
15
|
+
now: Date.now,
|
|
16
|
+
...(accounts ? { accounts } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const DEFAULT_DEPS = accountHealthDepsFor({ hasToken, readToken });
|
|
12
20
|
export function deriveAccountHealth(alias, deps = DEFAULT_DEPS) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
21
|
+
const set = (deps.accounts ?? getAccountSet)();
|
|
22
|
+
const config = set.configs[alias];
|
|
23
|
+
const admin = getAdminAccounts(set).includes(alias);
|
|
24
|
+
const configured = resolveScopesForAccount(alias, set);
|
|
25
|
+
// Env-sourced accounts are not persisted in config.json, so the
|
|
17
26
|
// account wizard cannot edit them — the deployer edits env instead.
|
|
18
27
|
const base = {
|
|
19
28
|
alias,
|
|
@@ -29,13 +38,17 @@ export function deriveAccountHealth(alias, deps = DEFAULT_DEPS) {
|
|
|
29
38
|
const noScopes = buildScopesReport(new Set(), profileSet, registered);
|
|
30
39
|
if (!deps.hasToken(alias)) {
|
|
31
40
|
const legacy = deps.fileExists(config.tokenPath);
|
|
41
|
+
const rendered = legacy
|
|
42
|
+
? hints()?.render({ kind: 'legacy_plaintext_token', account: alias })
|
|
43
|
+
: hints()?.render({ kind: 'reauth', account: alias });
|
|
32
44
|
return {
|
|
33
45
|
...base,
|
|
34
46
|
token: {
|
|
35
47
|
status: 'missing',
|
|
36
|
-
hint:
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
hint: rendered?.hint ??
|
|
49
|
+
(legacy
|
|
50
|
+
? 'Plaintext token.json found — run: npx mcp-google-multi migrate-tokens'
|
|
51
|
+
: reauthHint(alias)),
|
|
39
52
|
},
|
|
40
53
|
scopes: noScopes,
|
|
41
54
|
};
|
|
@@ -45,9 +58,10 @@ export function deriveAccountHealth(alias, deps = DEFAULT_DEPS) {
|
|
|
45
58
|
token = deps.readToken(alias);
|
|
46
59
|
}
|
|
47
60
|
catch {
|
|
61
|
+
const rendered = hints()?.render({ kind: 'token_unreadable', account: alias });
|
|
48
62
|
return {
|
|
49
63
|
...base,
|
|
50
|
-
token: { status: 'decrypt_error', hint: 'Wrong MASTER_KEY or corrupt token file — re-auth or restore the key.' },
|
|
64
|
+
token: { status: 'decrypt_error', hint: rendered?.hint ?? 'Wrong MASTER_KEY or corrupt token file — re-auth or restore the key.' },
|
|
51
65
|
scopes: noScopes,
|
|
52
66
|
};
|
|
53
67
|
}
|
|
@@ -69,9 +83,9 @@ export function deriveAccountHealth(alias, deps = DEFAULT_DEPS) {
|
|
|
69
83
|
status,
|
|
70
84
|
expiryDate: expiry !== undefined ? new Date(expiry).toISOString() : undefined,
|
|
71
85
|
hint: status === 'needs_reauth'
|
|
72
|
-
?
|
|
86
|
+
? (hints()?.render({ kind: 'reauth', account: alias })?.hint ?? reauthHint(alias))
|
|
73
87
|
: missing.length > 0
|
|
74
|
-
? 'Re-auth to grant the missing scopes'
|
|
88
|
+
? (hints()?.render({ kind: 'scopes_missing', account: alias })?.hint ?? 'Re-auth to grant the missing scopes')
|
|
75
89
|
: undefined,
|
|
76
90
|
},
|
|
77
91
|
scopes: report,
|
|
@@ -87,14 +101,16 @@ export function registerAccountTools(registry, deps = DEFAULT_DEPS) {
|
|
|
87
101
|
_meta: { 'anthropic/alwaysLoad': true },
|
|
88
102
|
}, async () => {
|
|
89
103
|
refreshAccountSetIfStale();
|
|
104
|
+
const set = registry.accountSet();
|
|
105
|
+
const health = { ...deps, accounts: () => set };
|
|
90
106
|
return {
|
|
91
107
|
content: [
|
|
92
108
|
{
|
|
93
109
|
type: 'text',
|
|
94
110
|
text: JSON.stringify({
|
|
95
|
-
defaultAccount:
|
|
96
|
-
defaultAccountSource:
|
|
97
|
-
accounts:
|
|
111
|
+
defaultAccount: set.defaultAccount ?? null,
|
|
112
|
+
defaultAccountSource: set.defaultAccountSource ?? null,
|
|
113
|
+
accounts: set.aliases.map((alias) => deriveAccountHealth(alias, health)),
|
|
98
114
|
}),
|
|
99
115
|
},
|
|
100
116
|
],
|
package/dist/tools/admin.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import type { Account } from '../accounts.js';
|
|
3
|
+
import { type CuratedToolDeps } from '../client.js';
|
|
4
|
+
export declare function registerAdminTools(server: ToolRegistry, deps?: CuratedToolDeps): void;
|
|
5
|
+
export declare function handleAdminError(error: any, account: Account): {
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
isError: true;
|
|
11
|
+
};
|
package/dist/tools/admin.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { coerceBoolean } from './_coerce.js';
|
|
3
3
|
import { admin as adminClient } from '@googleapis/admin';
|
|
4
|
-
import {
|
|
4
|
+
import { accountArgLive } from '../accounts.js';
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError, invalidParams } from './_errors.js';
|
|
7
|
-
const accountEnum = accountAliasSchema.optional();
|
|
8
7
|
// Admin SDK requires Workspace super-admin (or delegated admin) on the account — personal @gmail.com accounts 403 on every endpoint.
|
|
9
|
-
export function registerAdminTools(server) {
|
|
8
|
+
export function registerAdminTools(server, deps = {}) {
|
|
9
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
10
|
+
// the registry's account view at parse time, not a process-global default.
|
|
11
|
+
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
12
|
+
const getClientFn = deps.getClientFn ?? getClient;
|
|
10
13
|
// ─── Reports / audit log ───────────────────────────────────────────────
|
|
11
14
|
server.registerTool('reports_activities_list', {
|
|
12
15
|
description: 'List Admin Activity audit log entries. Filter by application (login, drive, gmail, admin, token, etc.), user, date range, event. The single most useful admin endpoint for SMB visibility.',
|
|
@@ -34,7 +37,7 @@ export function registerAdminTools(server) {
|
|
|
34
37
|
},
|
|
35
38
|
}, async ({ account, applicationName, userKey, startTime, endTime, eventName, actorIpAddress, filters, orgUnitID, groupIdFilter, customerId, maxResults, pageToken }) => {
|
|
36
39
|
try {
|
|
37
|
-
const auth = await
|
|
40
|
+
const auth = await getClientFn(account);
|
|
38
41
|
const reports = adminClient({ version: 'reports_v1', auth });
|
|
39
42
|
const res = await reports.activities.list({
|
|
40
43
|
applicationName,
|
|
@@ -74,7 +77,7 @@ export function registerAdminTools(server) {
|
|
|
74
77
|
},
|
|
75
78
|
}, async ({ account, customer, domain, query, maxResults, pageToken, orderBy, showDeleted, projection }) => {
|
|
76
79
|
try {
|
|
77
|
-
const auth = await
|
|
80
|
+
const auth = await getClientFn(account);
|
|
78
81
|
const directory = adminClient({ version: 'directory_v1', auth });
|
|
79
82
|
const res = await directory.users.list({
|
|
80
83
|
customer: customer ?? 'my_customer',
|
|
@@ -103,7 +106,7 @@ export function registerAdminTools(server) {
|
|
|
103
106
|
},
|
|
104
107
|
}, async ({ account, userKey, projection }) => {
|
|
105
108
|
try {
|
|
106
|
-
const auth = await
|
|
109
|
+
const auth = await getClientFn(account);
|
|
107
110
|
const directory = adminClient({ version: 'directory_v1', auth });
|
|
108
111
|
const res = await directory.users.get({
|
|
109
112
|
userKey,
|
|
@@ -131,7 +134,7 @@ export function registerAdminTools(server) {
|
|
|
131
134
|
},
|
|
132
135
|
}, async ({ account, userKey, givenName, familyName, suspended, password, changePasswordAtNextLogin, orgUnitPath }) => {
|
|
133
136
|
try {
|
|
134
|
-
const auth = await
|
|
137
|
+
const auth = await getClientFn(account);
|
|
135
138
|
const directory = adminClient({ version: 'directory_v1', auth });
|
|
136
139
|
const requestBody = {};
|
|
137
140
|
if (givenName !== undefined || familyName !== undefined) {
|
|
@@ -175,7 +178,7 @@ export function registerAdminTools(server) {
|
|
|
175
178
|
},
|
|
176
179
|
}, async ({ account, customer, domain, userKey, query, maxResults, pageToken }) => {
|
|
177
180
|
try {
|
|
178
|
-
const auth = await
|
|
181
|
+
const auth = await getClientFn(account);
|
|
179
182
|
const directory = adminClient({ version: 'directory_v1', auth });
|
|
180
183
|
const res = await directory.groups.list({
|
|
181
184
|
customer: customer ?? 'my_customer',
|
|
@@ -205,7 +208,7 @@ export function registerAdminTools(server) {
|
|
|
205
208
|
},
|
|
206
209
|
}, async ({ account, groupKey, roles, includeDerivedMembership, maxResults, pageToken }) => {
|
|
207
210
|
try {
|
|
208
|
-
const auth = await
|
|
211
|
+
const auth = await getClientFn(account);
|
|
209
212
|
const directory = adminClient({ version: 'directory_v1', auth });
|
|
210
213
|
const res = await directory.members.list({
|
|
211
214
|
groupKey,
|
|
@@ -223,7 +226,7 @@ export function registerAdminTools(server) {
|
|
|
223
226
|
}
|
|
224
227
|
});
|
|
225
228
|
}
|
|
226
|
-
function handleAdminError(error, account) {
|
|
229
|
+
export function handleAdminError(error, account) {
|
|
227
230
|
const admin = 'Admin tools require Workspace super-admin privileges AND the account must be listed as an admin account (then re-authenticated). Personal Gmail accounts cannot use these endpoints.';
|
|
228
|
-
return handleGoogleApiError(error, account, { scope: admin, resource: admin });
|
|
231
|
+
return handleGoogleApiError(error, account, { scope: admin, resource: admin, bundle: 'admin' });
|
|
229
232
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
import type { Account } from '../accounts.js';
|
|
3
|
+
import { type CuratedToolDeps } from '../client.js';
|
|
2
4
|
/** Accepts "213025502" or "properties/213025502"; rejects the identifiers
|
|
3
5
|
* people paste by mistake (G-… measurement IDs, UA-… properties) with a
|
|
4
6
|
* pointer to the right one. */
|
|
@@ -15,4 +17,11 @@ export declare function shapeAccountSummaries(data: any): Record<string, unknown
|
|
|
15
17
|
/** Full metadata descriptions run to paragraphs; the first ~160 chars carry
|
|
16
18
|
* the disambiguation the model needs without bloating a ~300-entry list. */
|
|
17
19
|
export declare function shapeMetadata(data: any): Record<string, unknown>;
|
|
18
|
-
export declare function registerAnalyticsTools(server: ToolRegistry): void;
|
|
20
|
+
export declare function registerAnalyticsTools(server: ToolRegistry, deps?: CuratedToolDeps): void;
|
|
21
|
+
export declare function handleAnalyticsError(error: any, account: Account): {
|
|
22
|
+
content: {
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
}[];
|
|
26
|
+
isError: true;
|
|
27
|
+
};
|
package/dist/tools/analytics.js
CHANGED
|
@@ -2,10 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
|
|
3
3
|
import { analyticsdata as analyticsdataClient } from '@googleapis/analyticsdata';
|
|
4
4
|
import { analyticsadmin as analyticsadminClient } from '@googleapis/analyticsadmin';
|
|
5
|
-
import {
|
|
5
|
+
import { accountArgLive } from '../accounts.js';
|
|
6
6
|
import { getClient } from '../client.js';
|
|
7
7
|
import { handleGoogleApiError } from './_errors.js';
|
|
8
|
-
const accountEnum = accountAliasSchema.optional();
|
|
9
8
|
/** Accepts "213025502" or "properties/213025502"; rejects the identifiers
|
|
10
9
|
* people paste by mistake (G-… measurement IDs, UA-… properties) with a
|
|
11
10
|
* pointer to the right one. */
|
|
@@ -105,7 +104,11 @@ export function shapeMetadata(data) {
|
|
|
105
104
|
const propertySchema = z
|
|
106
105
|
.string()
|
|
107
106
|
.describe('GA4 property: numeric ID like "213025502" or "properties/213025502" — NOT a "G-..." measurement ID and not "UA-...". Find yours with analytics_account_summaries.');
|
|
108
|
-
export function registerAnalyticsTools(server) {
|
|
107
|
+
export function registerAnalyticsTools(server, deps = {}) {
|
|
108
|
+
// Per-registry, live account enum + injectable client: the schema follows
|
|
109
|
+
// the registry's account view at parse time, not a process-global default.
|
|
110
|
+
const accountEnum = accountArgLive(() => server.accountAliases()).optional();
|
|
111
|
+
const getClientFn = deps.getClientFn ?? getClient;
|
|
109
112
|
server.registerTool('analytics_account_summaries', {
|
|
110
113
|
description: 'List every Google Analytics (GA4) account and property this Google account can access, with their numeric property IDs. The starting point for any Analytics question ("what properties do I have?").',
|
|
111
114
|
inputSchema: {
|
|
@@ -115,7 +118,7 @@ export function registerAnalyticsTools(server) {
|
|
|
115
118
|
},
|
|
116
119
|
}, async ({ account, pageSize, pageToken }) => {
|
|
117
120
|
try {
|
|
118
|
-
const auth = await
|
|
121
|
+
const auth = await getClientFn(account);
|
|
119
122
|
const admin = analyticsadminClient({ version: 'v1beta', auth });
|
|
120
123
|
const res = await admin.accountSummaries.list({ pageSize, pageToken });
|
|
121
124
|
return {
|
|
@@ -159,7 +162,7 @@ export function registerAnalyticsTools(server) {
|
|
|
159
162
|
if ('hint' in prop)
|
|
160
163
|
return invalidProperty(prop.hint, account);
|
|
161
164
|
try {
|
|
162
|
-
const auth = await
|
|
165
|
+
const auth = await getClientFn(account);
|
|
163
166
|
const dataApi = analyticsdataClient({ version: 'v1beta', auth });
|
|
164
167
|
const requestBody = {
|
|
165
168
|
dateRanges: [{ startDate, endDate }],
|
|
@@ -216,7 +219,7 @@ export function registerAnalyticsTools(server) {
|
|
|
216
219
|
if ('hint' in prop)
|
|
217
220
|
return invalidProperty(prop.hint, account);
|
|
218
221
|
try {
|
|
219
|
-
const auth = await
|
|
222
|
+
const auth = await getClientFn(account);
|
|
220
223
|
const dataApi = analyticsdataClient({ version: 'v1beta', auth });
|
|
221
224
|
const requestBody = { metrics: metrics.map((name) => ({ name })) };
|
|
222
225
|
if (dimensions?.length)
|
|
@@ -251,7 +254,7 @@ export function registerAnalyticsTools(server) {
|
|
|
251
254
|
if ('hint' in prop)
|
|
252
255
|
return invalidProperty(prop.hint, account);
|
|
253
256
|
try {
|
|
254
|
-
const auth = await
|
|
257
|
+
const auth = await getClientFn(account);
|
|
255
258
|
const dataApi = analyticsdataClient({ version: 'v1beta', auth });
|
|
256
259
|
const res = await dataApi.properties.getMetadata({ name: `${prop.name}/metadata` });
|
|
257
260
|
return {
|
|
@@ -274,11 +277,12 @@ function invalidProperty(hint, account) {
|
|
|
274
277
|
isError: true,
|
|
275
278
|
};
|
|
276
279
|
}
|
|
277
|
-
function handleAnalyticsError(error, account) {
|
|
280
|
+
export function handleAnalyticsError(error, account) {
|
|
278
281
|
return handleGoogleApiError(error, account, {
|
|
279
282
|
scope: 'Needs the "analytics" bundle on this account: add it to the scope profile, then re-auth.',
|
|
280
|
-
// GA4
|
|
281
|
-
//
|
|
283
|
+
// GA4 returns PERMISSION_DENIED for a property-access issue too, not just
|
|
284
|
+
// a missing scope; keep the hints distinct, or callers get sent to re-auth a scope they already have.
|
|
282
285
|
resource: 'The scope is not the problem: this Google account has no access to that GA4 property. Grant it in GA4 Admin > Property Access Management, or call analytics_account_summaries to see the properties it can read.',
|
|
286
|
+
bundle: 'analytics',
|
|
283
287
|
});
|
|
284
288
|
}
|
package/dist/tools/calendar.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { type CuratedToolDeps } from '../client.js';
|
|
3
|
+
export declare function registerCalendarTools(server: ToolRegistry, deps?: CuratedToolDeps): void;
|
|
3
4
|
/** Google disagrees with itself on a backwards window: events.list answers 200
|
|
4
5
|
* with [], which reads as "nothing scheduled", while freebusy.query 400s on the
|
|
5
6
|
* identical input. Decide locally so an empty list always means empty. Returns
|