mcp-google-multi 6.0.0-beta.1 → 6.0.0-beta.2
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 -3
- package/dist/accounts.d.ts +5 -0
- package/dist/accounts.js +12 -1
- package/dist/api-probe.js +3 -0
- package/dist/arg-normalize.d.ts +61 -4
- package/dist/arg-normalize.js +228 -4
- package/dist/arg-strict.d.ts +54 -0
- package/dist/arg-strict.js +163 -0
- package/dist/auth.js +15 -72
- package/dist/client.js +15 -5
- package/dist/config-file.d.ts +1 -0
- package/dist/config-file.js +4 -0
- package/dist/discover.js +35 -14
- package/dist/discovery-client.d.ts +8 -1
- package/dist/discovery-client.js +96 -17
- package/dist/doctor.d.ts +9 -1
- package/dist/doctor.js +32 -4
- package/dist/env-load.d.ts +11 -0
- package/dist/env-load.js +23 -1
- package/dist/executor.js +12 -1
- package/dist/fanout.js +43 -8
- package/dist/http-transport.d.ts +11 -2
- package/dist/http-transport.js +10 -5
- package/dist/index.js +112 -12
- package/dist/metrics-cli.d.ts +21 -0
- package/dist/metrics-cli.js +206 -0
- package/dist/metrics-tap.d.ts +12 -0
- package/dist/metrics-tap.js +95 -0
- package/dist/oauth-consent.d.ts +25 -10
- package/dist/oauth-consent.js +85 -39
- package/dist/outbound-allowlist.d.ts +34 -0
- package/dist/outbound-allowlist.js +108 -0
- package/dist/registry.d.ts +49 -2
- package/dist/registry.js +147 -7
- package/dist/scope-catalog.js +16 -0
- package/dist/services.d.ts +8 -0
- package/dist/services.js +28 -1
- package/dist/setup-prompt.d.ts +1 -1
- package/dist/setup-prompt.js +1 -1
- package/dist/tools/_coerce.d.ts +5 -0
- package/dist/tools/_coerce.js +11 -0
- package/dist/tools/_errors.d.ts +31 -3
- package/dist/tools/_errors.js +307 -43
- package/dist/tools/_local-files.js +7 -2
- package/dist/tools/account-wizard.d.ts +24 -1
- package/dist/tools/account-wizard.js +152 -51
- package/dist/tools/admin.js +7 -6
- package/dist/tools/analytics.d.ts +18 -0
- package/dist/tools/analytics.js +284 -0
- package/dist/tools/calendar.d.ts +7 -0
- package/dist/tools/calendar.js +75 -15
- package/dist/tools/chat.js +7 -4
- package/dist/tools/contacts.js +57 -30
- package/dist/tools/docs.js +56 -54
- package/dist/tools/drive.js +132 -70
- package/dist/tools/forms.js +7 -7
- package/dist/tools/generated/_shared.d.ts +6 -0
- package/dist/tools/generated/_shared.js +11 -1
- package/dist/tools/generated/admin.js +345 -214
- package/dist/tools/generated/analytics.d.ts +2 -0
- package/dist/tools/generated/analytics.js +981 -0
- package/dist/tools/generated/appsmarket.js +4 -4
- package/dist/tools/generated/calendar.js +28 -28
- package/dist/tools/generated/chat.js +83 -56
- package/dist/tools/generated/classroom.js +258 -216
- package/dist/tools/generated/cloudidentity.js +76 -68
- package/dist/tools/generated/cloudsearch.js +40 -39
- package/dist/tools/generated/contacts.js +20 -11
- package/dist/tools/generated/drive.js +62 -53
- package/dist/tools/generated/drivelabels.js +60 -32
- package/dist/tools/generated/forms.js +7 -7
- package/dist/tools/generated/gmail.js +136 -109
- package/dist/tools/generated/groupsmigration.js +1 -1
- package/dist/tools/generated/groupssettings.js +3 -3
- package/dist/tools/generated/index.js +2 -0
- package/dist/tools/generated/keep.js +8 -7
- package/dist/tools/generated/licensing.js +39 -23
- package/dist/tools/generated/meet.js +14 -14
- package/dist/tools/generated/method-map.d.ts +2 -0
- package/dist/tools/generated/method-map.js +894 -0
- package/dist/tools/generated/postmaster.js +3 -3
- package/dist/tools/generated/reseller.js +34 -28
- package/dist/tools/generated/script.js +29 -19
- package/dist/tools/generated/searchconsole.js +5 -3
- package/dist/tools/generated/sheets.js +10 -9
- package/dist/tools/generated/tasks.js +10 -4
- package/dist/tools/generated/vault.js +65 -56
- package/dist/tools/generated/workspaceevents.js +17 -16
- package/dist/tools/gmail.d.ts +10 -1
- package/dist/tools/gmail.js +50 -18
- package/dist/tools/google-api.js +48 -19
- package/dist/tools/meet.js +5 -2
- package/dist/tools/searchconsole.js +10 -10
- package/dist/tools/sheets.js +36 -48
- package/dist/tools/slides.js +6 -6
- package/dist/tools/tasks.js +16 -16
- package/dist/trim.d.ts +21 -0
- package/dist/trim.js +47 -0
- package/dist/usage-metrics.d.ts +155 -0
- package/dist/usage-metrics.js +603 -0
- package/dist/write-control.d.ts +27 -1
- package/dist/write-control.js +90 -11
- package/package.json +8 -3
|
@@ -5,14 +5,16 @@ import { writeToken } from '../token-store.js';
|
|
|
5
5
|
import { resolveScopesForAccount } from '../auth.js';
|
|
6
6
|
import { BUNDLE_CATALOG, closestBundle, resolveBundleAliases } from '../scope-catalog.js';
|
|
7
7
|
import { openUrl } from '../open-url.js';
|
|
8
|
-
import {
|
|
8
|
+
import { coerceBoolean } from './_coerce.js';
|
|
9
|
+
import { safeMessage, stringifyEnvelope } from './_errors.js';
|
|
10
|
+
import { buildConsentClient, openLoopbackConsent, hasClientCredentials, TESTING_MODE_WARNING, } from '../oauth-consent.js';
|
|
9
11
|
import { detectClients, buildServerEntry, renderInstruction, applyFileEntry, resolveMode, DEFAULT_SERVER_NAME, } from '../client-config.js';
|
|
10
12
|
// B7: the elicitation-driven account_add / account_reauth wizard. It rebuilds
|
|
11
13
|
// interactive account management on the mutable config.json registry so a
|
|
12
14
|
// running server can add an account without the deployer editing env + running
|
|
13
15
|
// a CLI. Google consent reuses the loopback flow (leg C); the HTTP-transport
|
|
14
16
|
// consent path (${BASE}/authorize) is owned by the OAuth AS and lands with that
|
|
15
|
-
// cluster
|
|
17
|
+
// cluster. account_add over http is gated behind it.
|
|
16
18
|
const ALIAS_RE = /^[a-zA-Z0-9_-]+$/;
|
|
17
19
|
// The common optional bundles get their own checkbox (MCP elicitation has no
|
|
18
20
|
// multi-select, so a boolean per bundle IS the checklist); the long tail stays
|
|
@@ -44,13 +46,13 @@ export function validateAddForm(input, existingAliases) {
|
|
|
44
46
|
const alias = (input.alias ?? '').trim();
|
|
45
47
|
const email = (input.email ?? '').trim();
|
|
46
48
|
if (!ALIAS_RE.test(alias)) {
|
|
47
|
-
return { ok: false, slug: 'E_VALIDATION', message: `Invalid alias "${alias}"
|
|
49
|
+
return { ok: false, slug: 'E_VALIDATION', message: `Invalid alias "${alias}".`, hint: 'Use letters, digits, "_" or "-".', alias };
|
|
48
50
|
}
|
|
49
51
|
if (existingAliases.includes(alias)) {
|
|
50
|
-
return { ok: false, slug: 'E_ALIAS_EXISTS', message: `Alias "${alias}" already exists
|
|
52
|
+
return { ok: false, slug: 'E_ALIAS_EXISTS', message: `Alias "${alias}" already exists.`, hint: 'Use account_reauth to re-authenticate it, or pick another name.', alias };
|
|
51
53
|
}
|
|
52
54
|
if (email === '') {
|
|
53
|
-
return { ok: false, slug: 'E_VALIDATION', message: 'Email is required
|
|
55
|
+
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 };
|
|
54
56
|
}
|
|
55
57
|
if (input.allBundles === true) {
|
|
56
58
|
// "All optional scopes" supersedes the individual picks.
|
|
@@ -61,15 +63,46 @@ export function validateAddForm(input, existingAliases) {
|
|
|
61
63
|
const bundles = [...new Set(resolveBundleAliases([...picked, ...rawOther]))];
|
|
62
64
|
for (const b of bundles) {
|
|
63
65
|
if (b === 'admin') {
|
|
64
|
-
return { ok: false, slug: 'E_UNKNOWN_BUNDLE', message: '"admin" is not a bundle
|
|
66
|
+
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 };
|
|
65
67
|
}
|
|
66
68
|
if (!(b in BUNDLE_CATALOG)) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
+
const closest = closestBundle(b);
|
|
70
|
+
const known = Object.keys(BUNDLE_CATALOG).filter((n) => n !== 'admin').join(', ');
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
slug: 'E_UNKNOWN_BUNDLE',
|
|
74
|
+
message: `Unknown bundle "${b}".`,
|
|
75
|
+
hint: `${closest ? `Did you mean "${closest}"? ` : ''}Known bundles: ${known}.`,
|
|
76
|
+
alias,
|
|
77
|
+
};
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
return { ok: true, alias, email, bundles, admin: input.admin === true };
|
|
72
81
|
}
|
|
82
|
+
/** URL-mode elicitation params. `elicitationId` is REQUIRED by the spec
|
|
83
|
+
* schema; omitting it made every url-mode request fail client-side
|
|
84
|
+
* validation, so the branch silently never worked. Pure, so the shape is
|
|
85
|
+
* testable against the SDK's own schema without a live client. */
|
|
86
|
+
export function urlElicitationParams(alias, url, elicitationId) {
|
|
87
|
+
return {
|
|
88
|
+
mode: 'url',
|
|
89
|
+
elicitationId,
|
|
90
|
+
message: `Authorize the "${alias}" Google account in your browser.`,
|
|
91
|
+
url,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Map direct tool arguments onto the elicitation form shape: the argument-
|
|
95
|
+
* mode fallback for clients without form elicitation. All bundle picks travel
|
|
96
|
+
* through otherBundles, which validateAddForm resolves and validates. Pure. */
|
|
97
|
+
export function argsToAddForm(a) {
|
|
98
|
+
return {
|
|
99
|
+
alias: a.alias ?? a.account ?? '',
|
|
100
|
+
email: a.email ?? '',
|
|
101
|
+
allBundles: a.allBundles === true,
|
|
102
|
+
otherBundles: a.bundles ?? '',
|
|
103
|
+
admin: a.admin === true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
73
106
|
/** Scopes requested by the profile but NOT granted at consent (granular
|
|
74
107
|
* consent / unchecked bundles). Pure. */
|
|
75
108
|
export function scopeGrantDiff(requested, grantedScope) {
|
|
@@ -92,8 +125,23 @@ function writeAccountRow(alias, email, bundles, admin) {
|
|
|
92
125
|
return next;
|
|
93
126
|
});
|
|
94
127
|
}
|
|
95
|
-
function textResult(text
|
|
96
|
-
return { content: [{ type: 'text', text }]
|
|
128
|
+
function textResult(text) {
|
|
129
|
+
return { content: [{ type: 'text', text }] };
|
|
130
|
+
}
|
|
131
|
+
/** Wizard failures carry the same envelope as every other tool's, so a client
|
|
132
|
+
* can branch on the slug and metrics can classify them. Success stays prose:
|
|
133
|
+
* it is a human-readable onboarding report, not data. */
|
|
134
|
+
function errorResult(slug, message, hint, account) {
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: 'text', text: stringifyEnvelope({
|
|
137
|
+
error: slug,
|
|
138
|
+
message,
|
|
139
|
+
...(hint ? { hint } : {}),
|
|
140
|
+
retriable: false,
|
|
141
|
+
...(account ? { account } : {}),
|
|
142
|
+
}) }],
|
|
143
|
+
isError: true,
|
|
144
|
+
};
|
|
97
145
|
}
|
|
98
146
|
/** Run Google consent for `alias` and persist the token. Opens the browser via
|
|
99
147
|
* URL-mode elicitation when the client supports it, else server-side + prints
|
|
@@ -101,22 +149,41 @@ function textResult(text, isError = false) {
|
|
|
101
149
|
async function runConsent(server, alias) {
|
|
102
150
|
const { randomBytes } = await import('node:crypto');
|
|
103
151
|
const cfg = getAccountSet().configs[alias];
|
|
104
|
-
if (!cfg)
|
|
105
|
-
return {
|
|
106
|
-
|
|
152
|
+
if (!cfg) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
slug: 'E_VALIDATION',
|
|
156
|
+
message: `Account "${alias}" is not in the live registry.`,
|
|
157
|
+
hint: 'Accounts sourced from GOOGLE_ACCOUNTS are not editable here. Run `mcp-google-multi migrate-config` to move them into config.json.',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Bind the ephemeral loopback listener BEFORE building the auth URL: the
|
|
161
|
+
// redirect URI needs the assigned port, and listening first means the
|
|
162
|
+
// callback can't race the browser.
|
|
163
|
+
const loop = await openLoopbackConsent();
|
|
164
|
+
const client = buildConsentClient(loop.redirect);
|
|
107
165
|
const expectedState = randomBytes(32).toString('hex');
|
|
108
166
|
const scopes = resolveScopesForAccount(alias);
|
|
109
167
|
const url = client.generateAuthUrl({ access_type: 'offline', prompt: 'consent', scope: scopes, login_hint: cfg.email, state: expectedState });
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
168
|
+
const consent = loop.finish(client, expectedState);
|
|
169
|
+
// Capability probe: the spec advertises each elicitation mode as a PRESENT
|
|
170
|
+
// object, not a boolean, so test presence rather than truthiness.
|
|
113
171
|
const caps = server.server.getClientCapabilities?.();
|
|
114
172
|
let opened = false;
|
|
115
|
-
if (caps?.elicitation?.url) {
|
|
173
|
+
if (caps?.elicitation?.url !== undefined) {
|
|
116
174
|
try {
|
|
117
|
-
|
|
175
|
+
// elicitationId is REQUIRED by the spec schema; omitting it made every
|
|
176
|
+
// url-mode request fail client-side validation, so this branch always
|
|
177
|
+
// fell through to the server-side browser open.
|
|
178
|
+
const r = await server.server.elicitInput(urlElicitationParams(alias, url, randomBytes(16).toString('hex')));
|
|
118
179
|
if (r.action !== 'accept') {
|
|
119
|
-
|
|
180
|
+
loop.close();
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
slug: 'confirmation_declined',
|
|
184
|
+
message: 'Consent was cancelled, so no token was stored.',
|
|
185
|
+
hint: 'The account row was kept and diagnose reports its token as "missing". Run account_reauth to finish authorizing it.',
|
|
186
|
+
};
|
|
120
187
|
}
|
|
121
188
|
opened = true;
|
|
122
189
|
}
|
|
@@ -132,15 +199,23 @@ async function runConsent(server, alias) {
|
|
|
132
199
|
tokens = await consent;
|
|
133
200
|
}
|
|
134
201
|
catch (e) {
|
|
135
|
-
|
|
202
|
+
// Always surface the URL: the browser hand-off can succeed and consent
|
|
203
|
+
// still fail (declined, timed out), and the URL is the only recovery.
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
slug: 'auth_required',
|
|
207
|
+
message: safeMessage(e),
|
|
208
|
+
hint: `The browser hand-off can succeed and consent still fail, so this URL is the only recovery. Open it to authorize: ${url}`,
|
|
209
|
+
};
|
|
136
210
|
}
|
|
137
211
|
writeToken(alias, tokens);
|
|
138
212
|
return { ok: true, missing: scopeGrantDiff(scopes, typeof tokens.scope === 'string' ? tokens.scope : undefined) };
|
|
139
213
|
}
|
|
140
214
|
function s4Text(alias, missing) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
215
|
+
const outcome = missing.length === 0
|
|
216
|
+
? `✔ "${alias}" authenticated; all requested scopes granted. It is now usable without a restart.`
|
|
217
|
+
: `⚠ "${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}`;
|
|
144
219
|
}
|
|
145
220
|
const REQUIRES_INTERACTION = { 'anthropic/requiresUserInteraction': true };
|
|
146
221
|
export function registerAccountWizardTools(registry, server) {
|
|
@@ -151,43 +226,63 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
151
226
|
registerMeta('account_add', {
|
|
152
227
|
_meta: REQUIRES_INTERACTION,
|
|
153
228
|
annotations: { openWorldHint: true },
|
|
154
|
-
description: 'Add a new Google account
|
|
155
|
-
inputSchema: {
|
|
156
|
-
|
|
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).',
|
|
230
|
+
inputSchema: {
|
|
231
|
+
alias: z.string().optional().describe('Account alias (letters, digits, _ or -). Pass with email to add directly, skipping the form.'),
|
|
232
|
+
account: z.string().optional().describe('Alias for the new account (same as `alias`; every other tool spells it `account`)'),
|
|
233
|
+
email: z.string().optional().describe("The account's Google address (used as the login hint)"),
|
|
234
|
+
bundles: z.string().optional().describe('Optional scope bundles, comma-separated (e.g. "forms,chat"); blank = base scopes only'),
|
|
235
|
+
allBundles: coerceBoolean.optional().describe('Grant every optional bundle (biggest consent screen); overrides bundles'),
|
|
236
|
+
admin: coerceBoolean.optional().describe('Grant Workspace admin scopes (super-admin accounts only)'),
|
|
237
|
+
},
|
|
238
|
+
}, async (args) => {
|
|
157
239
|
try {
|
|
158
240
|
// Env-sourced registry: GOOGLE_ACCOUNTS is the exclusive source and
|
|
159
241
|
// config.json accounts are ignored, so a wizard add would be a phantom
|
|
160
242
|
// write. Refuse up front (BR-10) rather than write a row nothing reads.
|
|
161
243
|
if (process.env.GOOGLE_ACCOUNTS?.trim()) {
|
|
162
|
-
return
|
|
244
|
+
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.');
|
|
163
245
|
}
|
|
164
246
|
if (!hasClientCredentials()) {
|
|
165
|
-
return
|
|
247
|
+
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.');
|
|
166
248
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
249
|
+
// S1: collect the registry row. Arguments win over the form so the
|
|
250
|
+
// wizard still works in clients without form elicitation (where the
|
|
251
|
+
// interactive path used to dead-end).
|
|
252
|
+
const a = (args ?? {});
|
|
253
|
+
let input;
|
|
254
|
+
if (a.alias?.trim() || a.account?.trim() || a.email?.trim()) {
|
|
255
|
+
input = argsToAddForm(a);
|
|
170
256
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
257
|
+
else {
|
|
258
|
+
// Modes are advertised as PRESENT objects, not booleans.
|
|
259
|
+
const caps = server.server.getClientCapabilities?.();
|
|
260
|
+
if (caps?.elicitation?.form === undefined) {
|
|
261
|
+
return errorResult('elicitation_unsupported', 'This client does not support the interactive form, and no arguments were supplied, so there is nothing to add.', 'Call account_add again with arguments instead, e.g. {"alias": "work", "email": "you@example.com"} (optional: "bundles" as a comma-separated list, "allBundles": true, "admin": true).');
|
|
262
|
+
}
|
|
263
|
+
const form = await server.server.elicitInput({ message: 'Add a Google account', requestedSchema: addFormSchema() });
|
|
264
|
+
if (form.action !== 'accept') {
|
|
265
|
+
return textResult('confirmation_declined: no account was added.');
|
|
266
|
+
}
|
|
267
|
+
input = (form.content ?? {});
|
|
175
268
|
}
|
|
176
|
-
const validated = validateAddForm(
|
|
269
|
+
const validated = validateAddForm(input, getAccountSet().aliases);
|
|
177
270
|
if (!validated.ok)
|
|
178
|
-
return
|
|
271
|
+
return errorResult(validated.slug, validated.message, validated.hint, validated.alias);
|
|
179
272
|
// S2: atomic write + make the alias callable without a restart (BR3).
|
|
180
273
|
writeAccountRow(validated.alias, validated.email, validated.bundles, validated.admin);
|
|
181
274
|
invalidateAccountSet();
|
|
182
275
|
// S3 + S4: consent + validate.
|
|
183
276
|
const consent = await runConsent(server, validated.alias);
|
|
184
277
|
if (!consent.ok)
|
|
185
|
-
return
|
|
278
|
+
return errorResult(consent.slug, consent.message, consent.hint, validated.alias);
|
|
186
279
|
// B15: offer to register the server with another MCP client.
|
|
187
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).`);
|
|
188
281
|
}
|
|
189
282
|
catch (e) {
|
|
190
|
-
|
|
283
|
+
// safeMessage, not error.message: an arbitrary throw here can carry a
|
|
284
|
+
// token or a whole response body.
|
|
285
|
+
return errorResult('internal', `account_add failed: ${safeMessage(e)}`);
|
|
191
286
|
}
|
|
192
287
|
});
|
|
193
288
|
registerMeta('account_reauth', {
|
|
@@ -197,24 +292,30 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
197
292
|
inputSchema: {
|
|
198
293
|
// Plain string (NOT the account enum) so this never joins the
|
|
199
294
|
// multi-account fan-out path; validated against the registry below.
|
|
200
|
-
alias: z.string().describe('Existing account alias to re-authenticate'),
|
|
295
|
+
alias: z.string().min(1).optional().describe('Existing account alias to re-authenticate'),
|
|
296
|
+
// Every other tool in the server spells this `account`, so that is
|
|
297
|
+
// what a caller reaches for. Accepting both removes a dead end that
|
|
298
|
+
// no did-you-mean can rescue: `account` is 5 edits from `alias`, so
|
|
299
|
+
// the matcher cannot bridge them.
|
|
300
|
+
account: z.string().min(1).optional().describe('Alias of the account to re-authenticate (same as `alias`)'),
|
|
201
301
|
},
|
|
202
302
|
}, async (args) => {
|
|
203
303
|
try {
|
|
204
|
-
const
|
|
304
|
+
const a = args;
|
|
305
|
+
const alias = a.alias ?? a.account ?? '';
|
|
205
306
|
if (!hasClientCredentials()) {
|
|
206
|
-
return
|
|
307
|
+
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.');
|
|
207
308
|
}
|
|
208
309
|
if (!getAccountSet().aliases.includes(alias)) {
|
|
209
|
-
return
|
|
310
|
+
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);
|
|
210
311
|
}
|
|
211
312
|
const consent = await runConsent(server, alias);
|
|
212
313
|
if (!consent.ok)
|
|
213
|
-
return
|
|
314
|
+
return errorResult(consent.slug, consent.message, consent.hint, alias);
|
|
214
315
|
return textResult(s4Text(alias, consent.missing));
|
|
215
316
|
}
|
|
216
317
|
catch (e) {
|
|
217
|
-
return
|
|
318
|
+
return errorResult('internal', `account_reauth failed: ${safeMessage(e)}`);
|
|
218
319
|
}
|
|
219
320
|
});
|
|
220
321
|
registerMeta('account_write_config', {
|
|
@@ -246,7 +347,7 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
246
347
|
entry = buildServerEntry({ name, mode, resourceUri });
|
|
247
348
|
}
|
|
248
349
|
catch (e) {
|
|
249
|
-
return
|
|
350
|
+
return errorResult('internal', safeMessage(e));
|
|
250
351
|
}
|
|
251
352
|
let clients = detectClients();
|
|
252
353
|
if (a.client)
|
|
@@ -257,16 +358,16 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
257
358
|
for (const client of targets) {
|
|
258
359
|
const instr = renderInstruction(client, name, entry);
|
|
259
360
|
if (client.managed === 'cli') {
|
|
260
|
-
blocks.push(`${client.label}
|
|
361
|
+
blocks.push(`${client.label}: run\n ${instr.text}`);
|
|
261
362
|
}
|
|
262
363
|
else if (a.write) {
|
|
263
364
|
const res = applyFileEntry(client, name, entry);
|
|
264
365
|
blocks.push(res.ok
|
|
265
|
-
? `${client.label}
|
|
266
|
-
: `${client.label}
|
|
366
|
+
? `${client.label}: ${res.action} "${name}" in ${res.path}${res.backup ? ` (backup ${res.backup})` : ''}`
|
|
367
|
+
: `${client.label}: ${res.message}\n${res.snippet ?? ''}`);
|
|
267
368
|
}
|
|
268
369
|
else {
|
|
269
|
-
blocks.push(`${client.label}
|
|
370
|
+
blocks.push(`${client.label}: add to ${instr.path}\n${instr.text}`);
|
|
270
371
|
}
|
|
271
372
|
}
|
|
272
373
|
if (targets.length === 0) {
|
|
@@ -278,7 +379,7 @@ export function registerAccountWizardTools(registry, server) {
|
|
|
278
379
|
return textResult(`${blocks.join('\n\n')}\n\n${note}`);
|
|
279
380
|
}
|
|
280
381
|
catch (e) {
|
|
281
|
-
return
|
|
382
|
+
return errorResult('internal', `account_write_config failed: ${safeMessage(e)}`);
|
|
282
383
|
}
|
|
283
384
|
});
|
|
284
385
|
}
|
package/dist/tools/admin.js
CHANGED
|
@@ -3,7 +3,7 @@ import { coerceBoolean } from './_coerce.js';
|
|
|
3
3
|
import { admin as adminClient } from '@googleapis/admin';
|
|
4
4
|
import { accountAliasSchema } from '../accounts.js';
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
|
-
import { handleGoogleApiError } from './_errors.js';
|
|
6
|
+
import { handleGoogleApiError, invalidParams } from './_errors.js';
|
|
7
7
|
const accountEnum = accountAliasSchema.optional();
|
|
8
8
|
// Admin SDK requires Workspace super-admin (or delegated admin) on the account — personal @gmail.com accounts 403 on every endpoint.
|
|
9
9
|
export function registerAdminTools(server) {
|
|
@@ -98,7 +98,7 @@ export function registerAdminTools(server) {
|
|
|
98
98
|
description: 'Get a single Workspace user by email or user ID',
|
|
99
99
|
inputSchema: {
|
|
100
100
|
account: accountEnum.describe('Google account alias (must be a Workspace admin)'),
|
|
101
|
-
userKey: z.string().describe('User email or ID'),
|
|
101
|
+
userKey: z.string().min(1).describe('User email or ID'),
|
|
102
102
|
projection: z.enum(['basic', 'custom', 'full']).optional(),
|
|
103
103
|
},
|
|
104
104
|
}, async ({ account, userKey, projection }) => {
|
|
@@ -121,7 +121,7 @@ export function registerAdminTools(server) {
|
|
|
121
121
|
description: 'Update a Workspace user (PATCH semantics). Gated by write-control (a CUD tool).',
|
|
122
122
|
inputSchema: {
|
|
123
123
|
account: accountEnum.describe('Google account alias (must be a Workspace admin)'),
|
|
124
|
-
userKey: z.string().describe('User email or ID'),
|
|
124
|
+
userKey: z.string().min(1).describe('User email or ID'),
|
|
125
125
|
givenName: z.string().optional(),
|
|
126
126
|
familyName: z.string().optional(),
|
|
127
127
|
suspended: coerceBoolean.optional(),
|
|
@@ -150,7 +150,7 @@ export function registerAdminTools(server) {
|
|
|
150
150
|
if (orgUnitPath !== undefined)
|
|
151
151
|
requestBody.orgUnitPath = orgUnitPath;
|
|
152
152
|
if (Object.keys(requestBody).length === 0) {
|
|
153
|
-
return
|
|
153
|
+
return invalidParams(account, 'No fields to update: every optional field was omitted, so the request would have been a no-op.', 'Pass at least one of: givenName, familyName, suspended, password, changePasswordAtNextLogin, orgUnitPath.');
|
|
154
154
|
}
|
|
155
155
|
const res = await directory.users.patch({ userKey, requestBody });
|
|
156
156
|
return {
|
|
@@ -197,7 +197,7 @@ export function registerAdminTools(server) {
|
|
|
197
197
|
description: 'List members of a Workspace group',
|
|
198
198
|
inputSchema: {
|
|
199
199
|
account: accountEnum.describe('Google account alias (must be a Workspace admin)'),
|
|
200
|
-
groupKey: z.string().describe('Group email or ID'),
|
|
200
|
+
groupKey: z.string().min(1).describe('Group email or ID'),
|
|
201
201
|
roles: z.string().optional().describe('Comma-separated roles to include (OWNER, MANAGER, MEMBER)'),
|
|
202
202
|
includeDerivedMembership: coerceBoolean.optional(),
|
|
203
203
|
maxResults: z.number().min(1).max(200).optional(),
|
|
@@ -224,5 +224,6 @@ export function registerAdminTools(server) {
|
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
226
|
function handleAdminError(error, account) {
|
|
227
|
-
|
|
227
|
+
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 });
|
|
228
229
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
/** Accepts "213025502" or "properties/213025502"; rejects the identifiers
|
|
3
|
+
* people paste by mistake (G-… measurement IDs, UA-… properties) with a
|
|
4
|
+
* pointer to the right one. */
|
|
5
|
+
export declare function normalizeProperty(input: string): {
|
|
6
|
+
name: string;
|
|
7
|
+
} | {
|
|
8
|
+
hint: string;
|
|
9
|
+
};
|
|
10
|
+
/** GA4 report rows ({dimensionValues:[{value}], metricValues:[{value}]}) are
|
|
11
|
+
* verbose; merge each row into one {name: value} object (dimension and metric
|
|
12
|
+
* API names never collide). Shared by runReport and runRealtimeReport. */
|
|
13
|
+
export declare function shapeReport(data: any): Record<string, unknown>;
|
|
14
|
+
export declare function shapeAccountSummaries(data: any): Record<string, unknown>;
|
|
15
|
+
/** Full metadata descriptions run to paragraphs; the first ~160 chars carry
|
|
16
|
+
* the disambiguation the model needs without bloating a ~300-entry list. */
|
|
17
|
+
export declare function shapeMetadata(data: any): Record<string, unknown>;
|
|
18
|
+
export declare function registerAnalyticsTools(server: ToolRegistry): void;
|