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
package/dist/registry.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
1
|
import { z } from 'zod';
|
|
3
2
|
import { isAllowed, writeDisabledResult, IRREVERSIBLE_TOOLS } from './write-control.js';
|
|
4
3
|
import { getAccountSet, refreshAccountSetIfStale } from './accounts.js';
|
|
5
4
|
import { compactResult, trimEnabled } from './trim.js';
|
|
6
5
|
import { fanoutAccountField, invalidAccountsResult, parseAccountSelector, runFanout } from './fanout.js';
|
|
7
6
|
import { MAX_RESPONSE_CHARS } from './executor.js';
|
|
7
|
+
import { suggestKeys } from './arg-strict.js';
|
|
8
|
+
import { classifyScope } from './scope-observability.js';
|
|
9
|
+
/** Nothing granted, no profile: asks the BUNDLE CATALOG whether a scope is
|
|
10
|
+
* reachable at all, independently of any account. */
|
|
11
|
+
const EMPTY_SCOPES = new Set();
|
|
8
12
|
// Client-side result budget advertised for tools that do not declare their own
|
|
9
13
|
// (fat readers do; see trim.ts). ~50k chars stays well inside a default client
|
|
10
14
|
// context limit while leaving room for real list payloads.
|
|
@@ -38,6 +42,11 @@ const SERVICE_OVERRIDES = {
|
|
|
38
42
|
};
|
|
39
43
|
// read tools that write local files — same savePath fanned across accounts would clobber
|
|
40
44
|
const FANOUT_EXCLUDE = new Set(['gmail_download_attachment', 'drive_download', 'drive_export']);
|
|
45
|
+
/** Account-management tools: the `account` they take is the SUBJECT of the
|
|
46
|
+
* operation, not the identity it runs as, so injecting the configured default
|
|
47
|
+
* would silently retarget them. `account_add` would adopt the default as the
|
|
48
|
+
* new alias, and `account_reauth` would re-authenticate the wrong account. */
|
|
49
|
+
const DEFAULT_ACCOUNT_EXCLUDE = new Set(['account_add', 'account_reauth']);
|
|
41
50
|
/** Unwrap optional/default/nullable to the declared scalar kind (zod 4 defs). */
|
|
42
51
|
function scalarKindOf(field) {
|
|
43
52
|
let cur = field;
|
|
@@ -81,12 +90,16 @@ export function inferCud(name) {
|
|
|
81
90
|
}
|
|
82
91
|
export class ToolRegistry {
|
|
83
92
|
server;
|
|
93
|
+
metrics;
|
|
84
94
|
tools = [];
|
|
85
95
|
policy;
|
|
86
96
|
registerTool;
|
|
87
97
|
revealed = new Set();
|
|
88
98
|
jsonSchemaCache = new Map();
|
|
89
99
|
argShapeCache = new Map();
|
|
100
|
+
/** Memoized isUngrantable verdict per tool: the answer depends on the frozen
|
|
101
|
+
* bundle catalog only, so it never changes within a process. */
|
|
102
|
+
ungrantable = new Map();
|
|
90
103
|
compactOutput = trimEnabled();
|
|
91
104
|
registeringMeta = false;
|
|
92
105
|
/** Configured visibility mode (GOOGLE_DISCOVERY); default lazy = v5 exact. */
|
|
@@ -94,8 +107,9 @@ export class ToolRegistry {
|
|
|
94
107
|
/** Agent-toggled runtime overlay (discover_all / discover_reset): lifts a
|
|
95
108
|
* lazy surface to curated without touching the configured mode. */
|
|
96
109
|
expanded = false;
|
|
97
|
-
constructor(server, policy, mode = resolveDiscoveryMode()) {
|
|
110
|
+
constructor(server, policy, mode = resolveDiscoveryMode(), metrics = null) {
|
|
98
111
|
this.server = server;
|
|
112
|
+
this.metrics = metrics;
|
|
99
113
|
this.policy = policy;
|
|
100
114
|
this.mode = mode;
|
|
101
115
|
this.registerTool = ((name, config, handler) => {
|
|
@@ -125,7 +139,7 @@ export class ToolRegistry {
|
|
|
125
139
|
// never fan out meta tools: google_api_call infers cud=read but executes writes
|
|
126
140
|
let inputShape = config.inputSchema ?? {};
|
|
127
141
|
let baseHandler = handler;
|
|
128
|
-
const hasAccountField = 'account' in inputShape;
|
|
142
|
+
const hasAccountField = 'account' in inputShape && !DEFAULT_ACCOUNT_EXCLUDE.has(name);
|
|
129
143
|
if (cud === 'read' && !this.registeringMeta && !FANOUT_EXCLUDE.has(name) && isAccountEnum(inputShape.account)) {
|
|
130
144
|
const description = inputShape.account.description ?? 'Google account alias';
|
|
131
145
|
inputShape = { ...inputShape, account: fanoutAccountField(description) };
|
|
@@ -154,9 +168,11 @@ export class ToolRegistry {
|
|
|
154
168
|
});
|
|
155
169
|
const guarded = cud === 'read'
|
|
156
170
|
? baseHandler
|
|
157
|
-
: (...args) => isAllowed({ name, service, cud }, policy)
|
|
171
|
+
: (...args) => isAllowed({ name, service, cud, scopes: config.requiredScopes }, policy)
|
|
158
172
|
? baseHandler(...args)
|
|
159
|
-
: writeDisabledResult({ name, service, cud }, policy
|
|
173
|
+
: writeDisabledResult({ name, service, cud, scopes: config.requiredScopes }, policy,
|
|
174
|
+
// Concrete by here: default-account injection wraps this.
|
|
175
|
+
args[0]?.account);
|
|
160
176
|
// A2: the ONE default-account injection site — outside the CUD gate and
|
|
161
177
|
// the fan-out parse so both observe a concrete alias; NOT meta-skipped
|
|
162
178
|
// (that is what covers google_api_call with zero bespoke code). Explicit
|
|
@@ -194,8 +210,21 @@ export class ToolRegistry {
|
|
|
194
210
|
const finalHandler = this.compactOutput
|
|
195
211
|
? async (...args) => compactResult(await withDefault(...args))
|
|
196
212
|
: withDefault;
|
|
213
|
+
// Usage metrics wrap OUTERMOST and only when enabled: off means no
|
|
214
|
+
// wrapper exists and the chain is byte-identical to the pre-metrics
|
|
215
|
+
// chain. Fan-out width is derived here (bucketed in the module) so the
|
|
216
|
+
// metrics module never imports fanout.
|
|
217
|
+
const instrumented = this.metrics
|
|
218
|
+
? this.metrics.wrap({ name, service, meta: this.registeringMeta, generated: config.cud !== undefined }, finalHandler, (a) => {
|
|
219
|
+
const v = a?.account;
|
|
220
|
+
if (typeof v !== 'string' || (v !== '*' && !v.includes(',')))
|
|
221
|
+
return 1;
|
|
222
|
+
const sel = parseAccountSelector(v);
|
|
223
|
+
return sel.ok ? sel.aliases.length : 1;
|
|
224
|
+
})
|
|
225
|
+
: finalHandler;
|
|
197
226
|
const { cud: _cud, ...sdkConfig } = config;
|
|
198
|
-
return server.registerTool(name, { ...sdkConfig, inputSchema: inputShape, annotations },
|
|
227
|
+
return server.registerTool(name, { ...sdkConfig, inputSchema: inputShape, annotations }, instrumented);
|
|
199
228
|
});
|
|
200
229
|
}
|
|
201
230
|
registerMeta = ((name, config, handler) => {
|
|
@@ -225,6 +254,51 @@ export class ToolRegistry {
|
|
|
225
254
|
this.argShapeCache.set(name, shape);
|
|
226
255
|
return shape;
|
|
227
256
|
}
|
|
257
|
+
/** Declared argument keys for a tool, in declaration order; undefined when
|
|
258
|
+
* the tool is not registered. Backs unknown-argument screening, which needs
|
|
259
|
+
* the full key list rather than argShape's scalar-kind subset view. */
|
|
260
|
+
declaredKeys(name) {
|
|
261
|
+
const entry = this.tools.find((t) => t.name === name);
|
|
262
|
+
return entry ? Object.keys(entry.inputShape) : undefined;
|
|
263
|
+
}
|
|
264
|
+
/** Keys spelling a similar concept elsewhere in the same service, for the
|
|
265
|
+
* hint on a call whose key matched nothing. Kept only when a key is declared
|
|
266
|
+
* by at least two tools in the service or by a curated one, so one-off
|
|
267
|
+
* generated parameters do not become advice. */
|
|
268
|
+
siblingSpellings(tool, unknownKeys) {
|
|
269
|
+
const self = this.tools.find((t) => t.name === tool);
|
|
270
|
+
if (!self)
|
|
271
|
+
return [];
|
|
272
|
+
const declared = new Set(Object.keys(self.inputShape));
|
|
273
|
+
const byKey = new Map();
|
|
274
|
+
for (const t of this.tools) {
|
|
275
|
+
// Only tools the agent can actually see in tools/list. A hint naming a
|
|
276
|
+
// registered-but-unadvertised generated tool turns a recoverable error
|
|
277
|
+
// into a dead end: in curated mode 353 tools are registered and 181 are
|
|
278
|
+
// advertised, and the hint used to reach for any of them.
|
|
279
|
+
if (t.service !== self.service || t.name === tool || !this.isVisible(t))
|
|
280
|
+
continue;
|
|
281
|
+
for (const key of Object.keys(t.inputShape)) {
|
|
282
|
+
if (declared.has(key))
|
|
283
|
+
continue;
|
|
284
|
+
const e = byKey.get(key) ?? { tools: [], curated: false };
|
|
285
|
+
e.tools.push(t.name);
|
|
286
|
+
if (!t.generated)
|
|
287
|
+
e.curated = true;
|
|
288
|
+
byKey.set(key, e);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const candidates = [...byKey.entries()].filter(([, e]) => e.tools.length >= 2 || e.curated);
|
|
292
|
+
// Reuse the tiered matcher rather than a substring test: the motivating
|
|
293
|
+
// case (parentId against parentFolderId) fails containment and edit
|
|
294
|
+
// distance alike, which is the whole reason that matcher exists.
|
|
295
|
+
const keys = candidates.map(([key]) => key);
|
|
296
|
+
const hits = new Set(unknownKeys.flatMap((k) => suggestKeys(k, keys)));
|
|
297
|
+
return candidates
|
|
298
|
+
.filter(([key]) => hits.has(key))
|
|
299
|
+
.slice(0, 2)
|
|
300
|
+
.map(([key, e]) => ({ key, tools: e.tools }));
|
|
301
|
+
}
|
|
228
302
|
catalog(service, query) {
|
|
229
303
|
const q = query?.trim().toLowerCase();
|
|
230
304
|
return this.tools
|
|
@@ -235,8 +309,38 @@ export class ToolRegistry {
|
|
|
235
309
|
summary: t.description,
|
|
236
310
|
args: Object.keys(t.inputShape),
|
|
237
311
|
cud: t.cud,
|
|
312
|
+
...(this.isUngrantable(t) ? { unreachable: true } : {}),
|
|
238
313
|
}));
|
|
239
314
|
}
|
|
315
|
+
/** The metrics recorder, for hook sites (escape hatch); null when off. */
|
|
316
|
+
get usageMetrics() {
|
|
317
|
+
return this.metrics;
|
|
318
|
+
}
|
|
319
|
+
/** Every registered tool name, hidden ones included: they stay callable, so
|
|
320
|
+
* a did-you-mean may legitimately point at one. */
|
|
321
|
+
toolNames() {
|
|
322
|
+
return this.tools.map((t) => t.name);
|
|
323
|
+
}
|
|
324
|
+
/** Membership test against the REGISTERED tool set (hidden tools included:
|
|
325
|
+
* they stay callable). The metrics tap uses this so a client-supplied name
|
|
326
|
+
* can never enter the closed vocabulary. */
|
|
327
|
+
hasTool(name) {
|
|
328
|
+
return this.tools.some((t) => t.name === name);
|
|
329
|
+
}
|
|
330
|
+
/** Op-name vocabulary for a service, split by provenance so the capped
|
|
331
|
+
* discover descriptions can list curated ops and only summarize the
|
|
332
|
+
* generated long tail. */
|
|
333
|
+
opNames(service) {
|
|
334
|
+
const strip = (n) => (n.startsWith(`${service}_`) ? n.slice(service.length + 1) : n);
|
|
335
|
+
const curated = [];
|
|
336
|
+
const generated = [];
|
|
337
|
+
for (const t of this.tools) {
|
|
338
|
+
if (t.meta || t.service !== service)
|
|
339
|
+
continue;
|
|
340
|
+
(t.generated ? generated : curated).push(strip(t.name));
|
|
341
|
+
}
|
|
342
|
+
return { curated: [...new Set(curated)], generated: [...new Set(generated)] };
|
|
343
|
+
}
|
|
240
344
|
reveal(service) {
|
|
241
345
|
if (this.revealed.has(service))
|
|
242
346
|
return false;
|
|
@@ -269,8 +373,40 @@ export class ToolRegistry {
|
|
|
269
373
|
return this.mode;
|
|
270
374
|
return this.expanded ? 'curated' : 'lazy';
|
|
271
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* True when NO bundle in the catalog grants any of the method's alternative
|
|
378
|
+
* scopes, so the call cannot succeed on any account under any configuration.
|
|
379
|
+
* Static: it asks the catalog, not an account, so it is safe to cache.
|
|
380
|
+
* `classifyScope` with nothing granted and no profile reports `add_bundle`
|
|
381
|
+
* for a scope some bundle could supply and `unknown_scope` only when none
|
|
382
|
+
* can, which is exactly the distinction wanted here.
|
|
383
|
+
*/
|
|
384
|
+
isUngrantable(tool) {
|
|
385
|
+
if (!tool.requiredScopes || tool.requiredScopes.length === 0)
|
|
386
|
+
return false;
|
|
387
|
+
const cached = this.ungrantable.get(tool.name);
|
|
388
|
+
if (cached !== undefined)
|
|
389
|
+
return cached;
|
|
390
|
+
// EVERY alternative must be ungrantable. `classifyMethodScopes` cannot
|
|
391
|
+
// answer this: `add_bundle` and `unknown_scope` share the
|
|
392
|
+
// `not_requestable` rank, so its first-best match hides a grantable
|
|
393
|
+
// alternative that appears later in the list. Discovery scope lists are
|
|
394
|
+
// ANY-OF, so one grantable alternative makes the method reachable.
|
|
395
|
+
const verdict = tool.requiredScopes.every((scope) => {
|
|
396
|
+
const c = classifyScope(scope, EMPTY_SCOPES, EMPTY_SCOPES);
|
|
397
|
+
return c.state === 'not_requestable' && c.reason === 'unknown_scope';
|
|
398
|
+
});
|
|
399
|
+
this.ungrantable.set(tool.name, verdict);
|
|
400
|
+
return verdict;
|
|
401
|
+
}
|
|
272
402
|
isVisible(tool) {
|
|
273
403
|
const mode = this.effectiveMode();
|
|
404
|
+
// Advertising a tool that can never succeed spends context budget to hand
|
|
405
|
+
// the agent a dead end. It stays registered and callable, so calling it by
|
|
406
|
+
// name still produces the real scope error plus its remediation, and
|
|
407
|
+
// {service}_discover still lists it, marked.
|
|
408
|
+
if (!tool.meta && this.isUngrantable(tool))
|
|
409
|
+
return false;
|
|
274
410
|
return (tool.meta ||
|
|
275
411
|
mode === 'eager' ||
|
|
276
412
|
this.revealed.has(tool.service) ||
|
|
@@ -287,7 +423,11 @@ export class ToolRegistry {
|
|
|
287
423
|
if (this.tools.length === 0) {
|
|
288
424
|
throw new Error('installListHandler() requires at least one registered tool');
|
|
289
425
|
}
|
|
290
|
-
this.server.server.setRequestHandler(
|
|
426
|
+
this.server.server.setRequestHandler('tools/list', async () => ({
|
|
427
|
+
// The wire Tool is hand-built because the SDK's own types drop the
|
|
428
|
+
// anthropic/* _meta keys honoring clients read. z.toJSONSchema emits a
|
|
429
|
+
// valid draft-7 object schema by construction, which the SDK's recursive
|
|
430
|
+
// JSON-Schema type cannot infer from our cached `unknown`.
|
|
291
431
|
tools: this.tools.filter((t) => this.isVisible(t)).map((t) => this.toToolJson(t)),
|
|
292
432
|
}));
|
|
293
433
|
}
|
package/dist/scope-catalog.js
CHANGED
|
@@ -108,6 +108,22 @@ export const BUNDLE_CATALOG = {
|
|
|
108
108
|
description: 'Read Gmail Postmaster Tools deliverability data.',
|
|
109
109
|
risk: 'low',
|
|
110
110
|
},
|
|
111
|
+
analytics: {
|
|
112
|
+
scopes: ['https://www.googleapis.com/auth/analytics.readonly'],
|
|
113
|
+
description: 'Read Google Analytics (GA4): run reports and inspect accounts, properties and their configuration.',
|
|
114
|
+
risk: 'low',
|
|
115
|
+
},
|
|
116
|
+
// Includes readonly so it is self-sufficient: analytics.edit alone does not
|
|
117
|
+
// authorize Data API reads. The smaller `analytics` bundle stays the hint
|
|
118
|
+
// target for read scopes (scope-observability sorts bundles by size).
|
|
119
|
+
analytics_write: {
|
|
120
|
+
scopes: [
|
|
121
|
+
'https://www.googleapis.com/auth/analytics.readonly',
|
|
122
|
+
'https://www.googleapis.com/auth/analytics.edit',
|
|
123
|
+
],
|
|
124
|
+
description: 'Edit Google Analytics (GA4) configuration: properties, data streams, key events, custom dimensions and metrics. Includes read access.',
|
|
125
|
+
risk: 'medium',
|
|
126
|
+
},
|
|
111
127
|
groupssettings: {
|
|
112
128
|
scopes: ['https://www.googleapis.com/auth/apps.groups.settings'],
|
|
113
129
|
description: 'Change Google Groups settings for the domain.',
|
package/dist/services.d.ts
CHANGED
|
@@ -9,3 +9,11 @@ export declare const GENERATED_GATES: Record<string, {
|
|
|
9
9
|
enabled: () => boolean;
|
|
10
10
|
hint: string;
|
|
11
11
|
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Message for a `tools/call` naming a tool that is not registered. The SDK's
|
|
14
|
+
* own answer is a bare "Tool X not found", which reads identically for a typo,
|
|
15
|
+
* for a service gated behind a scope bundle, and for an API this server has
|
|
16
|
+
* never heard of. Those need different next steps, and the server knows which
|
|
17
|
+
* is which: it composed the enabled-service list at boot.
|
|
18
|
+
*/
|
|
19
|
+
export declare function unknownToolMessage(registry: ToolRegistry, name: string): string;
|
package/dist/services.js
CHANGED
|
@@ -11,7 +11,10 @@ import { registerSlidesTools } from './tools/slides.js';
|
|
|
11
11
|
import { registerFormsTools } from './tools/forms.js';
|
|
12
12
|
import { registerChatTools } from './tools/chat.js';
|
|
13
13
|
import { registerAdminTools } from './tools/admin.js';
|
|
14
|
+
import { registerAnalyticsTools } from './tools/analytics.js';
|
|
14
15
|
import { getOptionalBundles, getAdminAccounts } from './auth.js';
|
|
16
|
+
import { GENERATED_SERVICES } from './tools/generated/index.js';
|
|
17
|
+
import { suggestKeys } from './arg-strict.js';
|
|
15
18
|
export const SERVICES = [
|
|
16
19
|
{ name: 'gmail', register: registerGmailTools },
|
|
17
20
|
{ name: 'drive', register: registerDriveTools },
|
|
@@ -25,9 +28,10 @@ export const SERVICES = [
|
|
|
25
28
|
{ name: 'slides', register: registerSlidesTools, enabled: () => new Set(getOptionalBundles()).has('slides') },
|
|
26
29
|
{ name: 'forms', register: registerFormsTools, enabled: () => new Set(getOptionalBundles()).has('forms') },
|
|
27
30
|
{ name: 'chat', register: registerChatTools, enabled: () => new Set(getOptionalBundles()).has('chat') },
|
|
31
|
+
{ name: 'analytics', register: registerAnalyticsTools, enabled: () => { const b = new Set(getOptionalBundles()); return b.has('analytics') || b.has('analytics_write'); } },
|
|
28
32
|
{ name: 'admin', register: registerAdminTools, enabled: () => getAdminAccounts().length > 0 },
|
|
29
33
|
];
|
|
30
|
-
// Generated-only services with opt-in scopes; admin/forms/chat reuse their curated gate in buildRegistry,
|
|
34
|
+
// Generated-only services with opt-in scopes; admin/forms/chat/analytics reuse their curated gate in buildRegistry,
|
|
31
35
|
// and workspaceevents is deliberately absent — no dedicated scope (subscriptions use resource scopes).
|
|
32
36
|
const bundleGate = (name) => ({
|
|
33
37
|
enabled: () => new Set(getOptionalBundles()).has(name),
|
|
@@ -49,3 +53,26 @@ export const GENERATED_GATES = {
|
|
|
49
53
|
script: bundleGate('script'),
|
|
50
54
|
vault: bundleGate('vault'),
|
|
51
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Message for a `tools/call` naming a tool that is not registered. The SDK's
|
|
58
|
+
* own answer is a bare "Tool X not found", which reads identically for a typo,
|
|
59
|
+
* for a service gated behind a scope bundle, and for an API this server has
|
|
60
|
+
* never heard of. Those need different next steps, and the server knows which
|
|
61
|
+
* is which: it composed the enabled-service list at boot.
|
|
62
|
+
*/
|
|
63
|
+
export function unknownToolMessage(registry, name) {
|
|
64
|
+
const near = suggestKeys(name, registry.toolNames(), 3);
|
|
65
|
+
if (near.length > 0)
|
|
66
|
+
return `Tool ${name} not found. Did you mean: ${near.join(', ')}?`;
|
|
67
|
+
// A service that EXISTS in the build but registered nothing is gated, not
|
|
68
|
+
// misspelled, and the fix is a scope bundle rather than a different name.
|
|
69
|
+
const service = name.split('_')[0];
|
|
70
|
+
const known = SERVICES.some((s) => s.name === service) || GENERATED_SERVICES.some((s) => s.name === service);
|
|
71
|
+
if (known && !registry.services().includes(service)) {
|
|
72
|
+
const hint = service === 'admin'
|
|
73
|
+
? 'set admin on an account/profile (or GOOGLE_ADMIN_ACCOUNTS), then re-auth'
|
|
74
|
+
: (GENERATED_GATES[service]?.hint ?? `add "${service}" to an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES), then re-auth`);
|
|
75
|
+
return `Tool ${name} not found: the "${service}" service is not enabled in this deployment. To enable it, ${hint}. Until then, google_api_call can reach the same API.`;
|
|
76
|
+
}
|
|
77
|
+
return `Tool ${name} not found. Call {service}_discover to list a service's tools, or google_api_search to find a method on any Google API.`;
|
|
78
|
+
}
|
package/dist/setup-prompt.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { McpServer } from
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
2
|
export interface SetupOptions {
|
|
3
3
|
/** Public base URL when serving over HTTP; used for the Web client redirect. */
|
|
4
4
|
publicUrl?: string;
|
package/dist/setup-prompt.js
CHANGED
|
@@ -47,7 +47,7 @@ export function registerSetupPrompt(server) {
|
|
|
47
47
|
server.registerPrompt('setup', {
|
|
48
48
|
title: 'Set up mcp-google-multi',
|
|
49
49
|
description: 'Guided Google Cloud Console prelude: project, APIs, consent screen, OAuth client, and credentials. The one-time browser setup that has no API.',
|
|
50
|
-
argsSchema: { publicUrl: z.string().optional().describe('Public base URL when serving over HTTP (for the Web OAuth client redirect). Omit for stdio.') },
|
|
50
|
+
argsSchema: z.object({ publicUrl: z.string().optional().describe('Public base URL when serving over HTTP (for the Web OAuth client redirect). Omit for stdio.') }),
|
|
51
51
|
}, (args) => ({
|
|
52
52
|
messages: [
|
|
53
53
|
{
|
package/dist/tools/_coerce.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare function coerceArray<T extends z.ZodTypeAny>(element: T): z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<T>>;
|
|
3
3
|
export declare function coerceJson<T extends z.ZodTypeAny>(schema: T): z.ZodPipe<z.ZodTransform<unknown, unknown>, T>;
|
|
4
|
+
/** Numeric args arrive string-encoded from some clients. Arg normalization
|
|
5
|
+
* runs on BOTH transports, but it only coerces keys it RENAMED, so a value
|
|
6
|
+
* sent under the correct key never passes through it and the coercion has to
|
|
7
|
+
* live in the schema. */
|
|
8
|
+
export declare function coerceNumber<T extends z.ZodTypeAny>(schema: T): z.ZodPipe<z.ZodTransform<unknown, unknown>, T>;
|
|
4
9
|
export declare const coerceBoolean: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>;
|
package/dist/tools/_coerce.js
CHANGED
|
@@ -22,6 +22,17 @@ export function coerceArray(element) {
|
|
|
22
22
|
export function coerceJson(schema) {
|
|
23
23
|
return z.preprocess((val) => (typeof val === 'string' ? parseJsonLoose(val) : val), schema);
|
|
24
24
|
}
|
|
25
|
+
/** Numeric args arrive string-encoded from some clients. Arg normalization
|
|
26
|
+
* runs on BOTH transports, but it only coerces keys it RENAMED, so a value
|
|
27
|
+
* sent under the correct key never passes through it and the coercion has to
|
|
28
|
+
* live in the schema. */
|
|
29
|
+
export function coerceNumber(schema) {
|
|
30
|
+
return z.preprocess((val) => {
|
|
31
|
+
if (typeof val === 'string' && val.trim() !== '' && !Number.isNaN(Number(val)))
|
|
32
|
+
return Number(val);
|
|
33
|
+
return val;
|
|
34
|
+
}, schema);
|
|
35
|
+
}
|
|
25
36
|
export const coerceBoolean = z.preprocess((val) => {
|
|
26
37
|
if (typeof val === 'boolean')
|
|
27
38
|
return val;
|
package/dist/tools/_errors.d.ts
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
import type { Account } from '../accounts.js';
|
|
2
|
+
/** A 403 means "missing scope" and "you cannot touch this resource" alike, and
|
|
3
|
+
* a single hint slot for both is what told callers to add a bundle they had
|
|
4
|
+
* already granted. Services now supply one hint per meaning. A bare string
|
|
5
|
+
* stays accepted and keeps its old meaning, the scope one. */
|
|
6
|
+
export interface ServiceHints {
|
|
7
|
+
scope?: string;
|
|
8
|
+
resource?: string;
|
|
9
|
+
}
|
|
2
10
|
export interface ErrorEnvelope {
|
|
3
11
|
error: string;
|
|
4
12
|
message: string;
|
|
5
13
|
hint?: string;
|
|
6
14
|
retriable: boolean;
|
|
7
|
-
account
|
|
15
|
+
/** Optional because some failures genuinely precede account resolution (an
|
|
16
|
+
* undeclared argument, a wizard step before the alias is known). Every
|
|
17
|
+
* producer that HAS an account must still pass it. */
|
|
18
|
+
account?: string;
|
|
8
19
|
}
|
|
9
|
-
|
|
20
|
+
/** The ONE extraction point for every Google-originated message. Without the
|
|
21
|
+
* raw-body guard, gaxios copies the entire response body into error.message,
|
|
22
|
+
* which put 8.5 KB of Google's HTML front-end page inside a JSON envelope. */
|
|
23
|
+
export declare function safeMessage(error: any, payload?: any, status?: number | undefined): string;
|
|
24
|
+
export declare function mapGoogleError(error: any, account: Account, serviceHints?: string | ServiceHints, scopeContext?: () => {
|
|
10
25
|
hint: string;
|
|
11
26
|
retriable: boolean;
|
|
12
27
|
} | null): ErrorEnvelope;
|
|
13
|
-
|
|
28
|
+
/** Bound the SERIALIZED envelope, not just the message: an authored hint can
|
|
29
|
+
* be long too, and the error path had no cap at all before this. */
|
|
30
|
+
export declare function stringifyEnvelope(envelope: ErrorEnvelope): string;
|
|
31
|
+
export declare function handleGoogleApiError(error: any, account: Account, serviceHints?: string | ServiceHints, scopeContext?: () => {
|
|
14
32
|
hint: string;
|
|
15
33
|
retriable: boolean;
|
|
16
34
|
} | null): {
|
|
@@ -20,3 +38,13 @@ export declare function handleGoogleApiError(error: any, account: Account, forbi
|
|
|
20
38
|
}[];
|
|
21
39
|
isError: true;
|
|
22
40
|
};
|
|
41
|
+
/** Local argument rejection, before any Google call. Same envelope as
|
|
42
|
+
* mapGoogleError so a caller parses one shape. The slug literal stays inline
|
|
43
|
+
* because the KNOWN_ERROR_SLUGS honesty test greps for `error: '<slug>'`. */
|
|
44
|
+
export declare function invalidParams(account: Account | undefined, message: string, hint: string): {
|
|
45
|
+
content: {
|
|
46
|
+
type: "text";
|
|
47
|
+
text: string;
|
|
48
|
+
}[];
|
|
49
|
+
isError: true;
|
|
50
|
+
};
|