mcp-google-multi 6.0.0-alpha.20 → 6.0.0-alpha.22
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/dist/discover.js +35 -14
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +14 -0
- package/dist/tools/drive.js +3 -3
- package/dist/tools/gmail.d.ts +10 -1
- package/dist/tools/gmail.js +21 -3
- package/dist/tools/google-api.js +2 -1
- package/package.json +1 -1
package/dist/discover.js
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { describePolicy } from './write-control.js';
|
|
3
|
+
// The description budget is the whole point of lazy mode: a full op list per
|
|
4
|
+
// service put lazy tools/list at ~11.5k tokens (RQ2), so descriptions carry a
|
|
5
|
+
// CAPPED vocabulary and the complete catalog stays in this tool's RESULT.
|
|
6
|
+
const CURATED_OPS_CAP = 10;
|
|
7
|
+
const GENERATED_GROUPS_CAP = 6;
|
|
3
8
|
function opVocabulary(registry, service) {
|
|
4
|
-
const
|
|
5
|
-
|
|
9
|
+
const { curated, generated } = registry.opNames(service);
|
|
10
|
+
const parts = [];
|
|
11
|
+
if (curated.length > 0) {
|
|
12
|
+
const shown = curated.slice(0, CURATED_OPS_CAP);
|
|
13
|
+
const more = curated.length - shown.length;
|
|
14
|
+
parts.push(`${shown.join(', ')}${more > 0 ? ` +${more} more` : ''}`);
|
|
15
|
+
}
|
|
16
|
+
if (generated.length > 0) {
|
|
17
|
+
// The generated long tail compresses to its resource groups: denser and
|
|
18
|
+
// more selective than any truncated name list.
|
|
19
|
+
const counts = new Map();
|
|
20
|
+
for (const op of generated) {
|
|
21
|
+
const group = op.split('_')[0];
|
|
22
|
+
counts.set(group, (counts.get(group) ?? 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
const groups = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
25
|
+
const shown = groups.slice(0, GENERATED_GROUPS_CAP).map(([g]) => g);
|
|
26
|
+
const more = groups.length - shown.length;
|
|
27
|
+
parts.push(`${generated.length} generated ops: ${shown.join(', ')}${more > 0 ? ` +${more} areas` : ''}`);
|
|
28
|
+
}
|
|
29
|
+
return parts.join('; ');
|
|
6
30
|
}
|
|
7
31
|
export function registerDiscoverTools(registry, policy) {
|
|
8
32
|
const registerMeta = registry.registerMeta;
|
|
@@ -11,9 +35,8 @@ export function registerDiscoverTools(registry, policy) {
|
|
|
11
35
|
// name/schema context budget after. stdio-only semantics — over stateless
|
|
12
36
|
// HTTP the mode is forced curated and these are no-ops.
|
|
13
37
|
registerMeta('discover_all', {
|
|
14
|
-
description: 'Reveal ALL curated Google tools at once (instead of per-service discovery). ' +
|
|
15
|
-
'
|
|
16
|
-
'directly callable; prefer them over google_api_call. Pair with discover_reset when done.',
|
|
38
|
+
description: 'Reveal ALL curated Google tools at once (instead of per-service discovery). Use when ' +
|
|
39
|
+
'starting substantial Google work; prefer these over google_api_call. Pair with discover_reset.',
|
|
17
40
|
inputSchema: {},
|
|
18
41
|
_meta: { 'anthropic/alwaysLoad': true },
|
|
19
42
|
}, async () => {
|
|
@@ -35,9 +58,8 @@ export function registerDiscoverTools(registry, policy) {
|
|
|
35
58
|
};
|
|
36
59
|
});
|
|
37
60
|
registerMeta('discover_reset', {
|
|
38
|
-
description: 'Collapse the tool surface back to the configured default
|
|
39
|
-
'
|
|
40
|
-
'callable by name after collapsing.',
|
|
61
|
+
description: 'Collapse the tool surface back to the configured default, reclaiming context budget ' +
|
|
62
|
+
'after heavy Google work. All tools remain callable by name after collapsing.',
|
|
41
63
|
inputSchema: {},
|
|
42
64
|
_meta: { 'anthropic/alwaysLoad': true },
|
|
43
65
|
}, async () => {
|
|
@@ -58,13 +80,12 @@ export function registerDiscoverTools(registry, policy) {
|
|
|
58
80
|
});
|
|
59
81
|
for (const service of registry.services()) {
|
|
60
82
|
registerMeta(`${service}_discover`, {
|
|
61
|
-
description:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`Operations: ${opVocabulary(registry, service)}.`,
|
|
83
|
+
description: (registry.mode === 'lazy'
|
|
84
|
+
? `Discover ${service}: lists the catalog and reveals its hidden tools; call first, then call the tool by name. `
|
|
85
|
+
: `List the ${service} catalog (reveals any still-hidden ${service} tools). `) +
|
|
86
|
+
`Ops: ${opVocabulary(registry, service)}.`,
|
|
66
87
|
inputSchema: {
|
|
67
|
-
query: z.string().optional().describe('
|
|
88
|
+
query: z.string().optional().describe('Filter keyword'),
|
|
68
89
|
},
|
|
69
90
|
_meta: { 'anthropic/alwaysLoad': true },
|
|
70
91
|
}, async ({ query }) => {
|
package/dist/registry.d.ts
CHANGED
|
@@ -54,6 +54,13 @@ export declare class ToolRegistry {
|
|
|
54
54
|
* normalization; the kind drives value coercion on renamed keys). */
|
|
55
55
|
argShape(name: string): ArgShape | undefined;
|
|
56
56
|
catalog(service: string, query?: string): CatalogOperation[];
|
|
57
|
+
/** Op-name vocabulary for a service, split by provenance so the capped
|
|
58
|
+
* discover descriptions can list curated ops and only summarize the
|
|
59
|
+
* generated long tail. */
|
|
60
|
+
opNames(service: string): {
|
|
61
|
+
curated: string[];
|
|
62
|
+
generated: string[];
|
|
63
|
+
};
|
|
57
64
|
reveal(service: string): boolean;
|
|
58
65
|
/** discover_all: advertise the full curated set at once. Idempotent. */
|
|
59
66
|
expand(): boolean;
|
package/dist/registry.js
CHANGED
|
@@ -237,6 +237,20 @@ export class ToolRegistry {
|
|
|
237
237
|
cud: t.cud,
|
|
238
238
|
}));
|
|
239
239
|
}
|
|
240
|
+
/** Op-name vocabulary for a service, split by provenance so the capped
|
|
241
|
+
* discover descriptions can list curated ops and only summarize the
|
|
242
|
+
* generated long tail. */
|
|
243
|
+
opNames(service) {
|
|
244
|
+
const strip = (n) => (n.startsWith(`${service}_`) ? n.slice(service.length + 1) : n);
|
|
245
|
+
const curated = [];
|
|
246
|
+
const generated = [];
|
|
247
|
+
for (const t of this.tools) {
|
|
248
|
+
if (t.meta || t.service !== service)
|
|
249
|
+
continue;
|
|
250
|
+
(t.generated ? generated : curated).push(strip(t.name));
|
|
251
|
+
}
|
|
252
|
+
return { curated: [...new Set(curated)], generated: [...new Set(generated)] };
|
|
253
|
+
}
|
|
240
254
|
reveal(service) {
|
|
241
255
|
if (this.revealed.has(service))
|
|
242
256
|
return false;
|
package/dist/tools/drive.js
CHANGED
|
@@ -129,8 +129,8 @@ export function registerDriveTools(server) {
|
|
|
129
129
|
inputSchema: {
|
|
130
130
|
account: accountEnum.describe('Google account alias'),
|
|
131
131
|
query: z.string().describe('A plain keyword (full-text search) or Drive query syntax, e.g. "name contains \'MoU\'"'),
|
|
132
|
-
maxResults: z.number().min(1).max(100).default(
|
|
133
|
-
.describe('Max results to return (default:
|
|
132
|
+
maxResults: z.number().min(1).max(100).default(10).optional()
|
|
133
|
+
.describe('Max results to return (default: 10, max: 100)'),
|
|
134
134
|
driveId: z.string().optional().describe('Optional shared drive ID'),
|
|
135
135
|
},
|
|
136
136
|
}, async ({ account, query, maxResults, driveId }) => {
|
|
@@ -139,7 +139,7 @@ export function registerDriveTools(server) {
|
|
|
139
139
|
const drive = driveClient({ version: 'v3', auth });
|
|
140
140
|
const params = {
|
|
141
141
|
q: normalizeDriveQuery(query),
|
|
142
|
-
pageSize: maxResults ??
|
|
142
|
+
pageSize: maxResults ?? 10,
|
|
143
143
|
fields: 'files(id,name,mimeType,modifiedTime,webViewLink,size,parents,driveId)',
|
|
144
144
|
supportsAllDrives: true,
|
|
145
145
|
includeItemsFromAllDrives: true,
|
package/dist/tools/gmail.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
2
|
import type { Account } from '../accounts.js';
|
|
3
3
|
import { type ComposeAttachment } from './gmail-mime.js';
|
|
4
|
-
import type { GmailMessageFull } from '../types.js';
|
|
4
|
+
import type { GmailMessageHeader, GmailMessageFull } from '../types.js';
|
|
5
5
|
/** Re: prefix unless the subject already carries one (case-insensitive, after
|
|
6
6
|
* trimming); never double-prefix. */
|
|
7
7
|
export declare function deriveReplySubject(sourceSubject: string): string;
|
|
@@ -33,5 +33,14 @@ export declare function parseMessage(msg: any, bodyCap?: number, opts?: {
|
|
|
33
33
|
* ordered per-id entries followed by a trailing counts summary.
|
|
34
34
|
*/
|
|
35
35
|
export declare function readBatch(gmail: any, account: Account, ids: string[], full: boolean, rawHtml: boolean): Promise<any[]>;
|
|
36
|
+
/** Compact gmail_search row: just the pick-one-message selection signal, with
|
|
37
|
+
* the snippet flattened to one bounded line. Pure. */
|
|
38
|
+
export declare function compactMessageRow(m: GmailMessageHeader): {
|
|
39
|
+
id: string;
|
|
40
|
+
from: string;
|
|
41
|
+
subject: string;
|
|
42
|
+
date: string;
|
|
43
|
+
snippet: string;
|
|
44
|
+
};
|
|
36
45
|
export declare function registerGmailTools(server: ToolRegistry): void;
|
|
37
46
|
export {};
|
package/dist/tools/gmail.js
CHANGED
|
@@ -423,16 +423,30 @@ export async function readBatch(gmail, account, ids, full, rawHtml) {
|
|
|
423
423
|
const summary = { counts: { ok, failed }, ...(truncatedAny ? { truncated: true } : {}) };
|
|
424
424
|
return [...entries, summary];
|
|
425
425
|
}
|
|
426
|
+
/** Compact gmail_search row: just the pick-one-message selection signal, with
|
|
427
|
+
* the snippet flattened to one bounded line. Pure. */
|
|
428
|
+
export function compactMessageRow(m) {
|
|
429
|
+
const snippet = m.snippet.replace(/\s+/g, ' ').trim();
|
|
430
|
+
return {
|
|
431
|
+
id: m.id,
|
|
432
|
+
from: m.from,
|
|
433
|
+
subject: m.subject,
|
|
434
|
+
date: m.date,
|
|
435
|
+
snippet: snippet.length > 120 ? `${snippet.slice(0, 119)}…` : snippet,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
426
438
|
export function registerGmailTools(server) {
|
|
427
439
|
server.registerTool('gmail_search', {
|
|
428
|
-
description: 'Search messages in a Gmail account'
|
|
440
|
+
description: 'Search messages in a Gmail account. Returns compact rows (id, from, subject, date, snippet); ' +
|
|
441
|
+
'pass full=true for threadId, to, labelIds and the untruncated snippet.',
|
|
429
442
|
inputSchema: {
|
|
430
443
|
account: accountEnum.describe('Google account alias'),
|
|
431
444
|
query: z.string().describe('Gmail search syntax, e.g. "from:monaam is:unread"'),
|
|
432
445
|
maxResults: z.number().min(1).max(100).default(20).optional()
|
|
433
446
|
.describe('Max results to return (default: 20, max: 100)'),
|
|
447
|
+
full: coerceBoolean.optional().describe('Return the full row shape instead of the compact default'),
|
|
434
448
|
},
|
|
435
|
-
}, async ({ account, query, maxResults }) => {
|
|
449
|
+
}, async ({ account, query, maxResults, full }) => {
|
|
436
450
|
try {
|
|
437
451
|
const auth = await getClient(account);
|
|
438
452
|
const gmail = gmailClient({ version: 'v1', auth });
|
|
@@ -466,8 +480,12 @@ export function registerGmailTools(server) {
|
|
|
466
480
|
});
|
|
467
481
|
}
|
|
468
482
|
}
|
|
483
|
+
// Search is a pick-one-message step ~always followed by gmail_read;
|
|
484
|
+
// the compact row is the selection signal, the rest was measured burn
|
|
485
|
+
// (p90 14.4k chars per call). full=true restores the pre-6.0 shape.
|
|
486
|
+
const rows = full === true ? results : results.map(compactMessageRow);
|
|
469
487
|
return {
|
|
470
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
488
|
+
content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
|
|
471
489
|
};
|
|
472
490
|
}
|
|
473
491
|
catch (error) {
|
package/dist/tools/google-api.js
CHANGED
|
@@ -117,7 +117,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
117
117
|
registry.registerMeta('google_api_call', {
|
|
118
118
|
description: 'Invoke any Google Workspace REST method by Discovery id (escape hatch for operations without a ' +
|
|
119
119
|
'dedicated tool). Find methods with google_api_search first. Subject to the same write-control ' +
|
|
120
|
-
'policy as named tools.
|
|
120
|
+
'policy as named tools. On reads, pass a `fields` query param (Google partial response) to keep ' +
|
|
121
|
+
'the payload small. Returns JSON only — for binary/file content (media downloads, ' +
|
|
121
122
|
'drive.files.export) use drive_download / drive_export instead.',
|
|
122
123
|
inputSchema: {
|
|
123
124
|
account: accountEnum.describe('Google account alias (omit for the default account)'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "6.0.0-alpha.
|
|
3
|
+
"version": "6.0.0-alpha.22",
|
|
4
4
|
"description": "Local MCP server for Google Workspace (Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Search Console, +Forms/Chat/Admin) across multiple accounts — OAuth-only, encrypted token storage, deny-by-default writes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|