mcp-google-multi 6.0.0-beta.4 → 6.0.0
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 +15 -1
- package/dist/discover.js +2 -3
- package/dist/fs-atomic.js +98 -12
- package/dist/hint-provider.d.ts +4 -1
- package/dist/mcp-token.d.ts +37 -3
- package/dist/mcp-token.js +105 -12
- package/dist/oauth-as.d.ts +5 -0
- package/dist/oauth-as.js +13 -5
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +59 -10
- package/dist/tools/accounts-tool.d.ts +7 -0
- package/dist/tools/accounts-tool.js +17 -6
- package/dist/tools/admin.js +6 -1
- package/dist/tools/analytics.js +28 -11
- package/dist/tools/calendar.d.ts +5 -0
- package/dist/tools/calendar.js +59 -0
- package/dist/tools/contacts.js +11 -5
- package/dist/tools/docs.js +41 -12
- package/dist/tools/drive.js +54 -12
- package/dist/tools/generated/gmail.js +0 -14
- package/dist/tools/generated/method-map.js +1 -1
- package/dist/tools/gmail.d.ts +1 -1
- package/dist/tools/gmail.js +109 -12
- package/dist/tools/sheets.d.ts +10 -0
- package/dist/tools/sheets.js +109 -2
- package/dist/write-control.js +15 -2
- package/package.json +4 -2
- package/skills/google-workspace/SKILL.md +162 -0
- package/skills/google-workspace/reference/admin.md +105 -0
- package/skills/google-workspace/reference/analytics.md +70 -0
- package/skills/google-workspace/reference/appsmarket.md +5 -0
- package/skills/google-workspace/reference/calendar.md +175 -0
- package/skills/google-workspace/reference/chat.md +59 -0
- package/skills/google-workspace/reference/classroom.md +5 -0
- package/skills/google-workspace/reference/cloudidentity.md +5 -0
- package/skills/google-workspace/reference/cloudsearch.md +5 -0
- package/skills/google-workspace/reference/contacts.md +138 -0
- package/skills/google-workspace/reference/docs.md +395 -0
- package/skills/google-workspace/reference/drive.md +485 -0
- package/skills/google-workspace/reference/driveactivity.md +5 -0
- package/skills/google-workspace/reference/drivelabels.md +5 -0
- package/skills/google-workspace/reference/forms.md +92 -0
- package/skills/google-workspace/reference/gmail.md +305 -0
- package/skills/google-workspace/reference/groupsmigration.md +5 -0
- package/skills/google-workspace/reference/groupssettings.md +5 -0
- package/skills/google-workspace/reference/keep.md +5 -0
- package/skills/google-workspace/reference/licensing.md +5 -0
- package/skills/google-workspace/reference/meet.md +68 -0
- package/skills/google-workspace/reference/postmaster.md +5 -0
- package/skills/google-workspace/reference/reseller.md +5 -0
- package/skills/google-workspace/reference/script.md +5 -0
- package/skills/google-workspace/reference/searchconsole.md +128 -0
- package/skills/google-workspace/reference/sheets.md +430 -0
- package/skills/google-workspace/reference/slides.md +67 -0
- package/skills/google-workspace/reference/tasks.md +167 -0
- package/skills/google-workspace/reference/vault.md +5 -0
- package/skills/google-workspace/reference/workspaceevents.md +5 -0
|
@@ -18,20 +18,6 @@ export function registerGmailGeneratedTools(registry, deps = {}) {
|
|
|
18
18
|
["https://mail.google.com/", "https://www.googleapis.com/auth/gmail.metadata", "https://www.googleapis.com/auth/gmail.modify", "https://www.googleapis.com/auth/gmail.readonly"],
|
|
19
19
|
["https://mail.google.com/"],
|
|
20
20
|
];
|
|
21
|
-
registerGeneratedTool(registry, {
|
|
22
|
-
name: "gmail_users_drafts_delete",
|
|
23
|
-
cud: "delete",
|
|
24
|
-
description: "Immediately and permanently deletes the specified draft. Does not simply trash it. For more information, see [Create and send draft emails](https://developers.g",
|
|
25
|
-
method: { id: "gmail.users.drafts.delete", httpMethod: "DELETE", path: "gmail/v1/users/{userId}/drafts/{id}", baseUrl: "https://gmail.googleapis.com/", requiredParams: ["userId", "id"], scopes: S_gmail_v1[0] },
|
|
26
|
-
params: [{ "field": "id", "api": "id", "location": "path" }, { "field": "userId", "api": "userId", "location": "path" }, { "field": "fields", "api": "fields", "location": "query" }],
|
|
27
|
-
hasBody: false,
|
|
28
|
-
shape: {
|
|
29
|
-
account: accountField(() => registry.accountAliases()),
|
|
30
|
-
id: z.string().min(1).describe("The ID of the draft to delete."),
|
|
31
|
-
userId: z.string().min(1).describe("The user's email address. The special value `me` can be used to indicate the authenticated user."),
|
|
32
|
-
fields: z.string().optional().describe('Response field mask.'),
|
|
33
|
-
},
|
|
34
|
-
}, deps);
|
|
35
21
|
registerGeneratedTool(registry, {
|
|
36
22
|
name: "gmail_users_drafts_update",
|
|
37
23
|
cud: "update",
|
|
@@ -550,7 +550,6 @@ export const GENERATED_METHOD_TOOLS = {
|
|
|
550
550
|
"forms.forms.watches.create": "forms_forms_watches_create",
|
|
551
551
|
"forms.forms.watches.delete": "forms_forms_watches_delete",
|
|
552
552
|
"forms.forms.watches.renew": "forms_forms_watches_renew",
|
|
553
|
-
"gmail.users.drafts.delete": "gmail_users_drafts_delete",
|
|
554
553
|
"gmail.users.drafts.update": "gmail_users_drafts_update",
|
|
555
554
|
"gmail.users.labels.get": "gmail_users_labels_get",
|
|
556
555
|
"gmail.users.labels.patch": "gmail_users_labels_patch",
|
|
@@ -818,6 +817,7 @@ export const CURATED_METHOD_IDS = [
|
|
|
818
817
|
"forms.forms.setPublishSettings",
|
|
819
818
|
"forms.forms.watches.list",
|
|
820
819
|
"gmail.users.drafts.create",
|
|
820
|
+
"gmail.users.drafts.delete",
|
|
821
821
|
"gmail.users.drafts.get",
|
|
822
822
|
"gmail.users.drafts.list",
|
|
823
823
|
"gmail.users.drafts.send",
|
package/dist/tools/gmail.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare function parseMessage(msg: any, bodyCap?: number, opts?: {
|
|
|
33
33
|
* is rethrown so the caller maps it to a whole-call isError. Returns the
|
|
34
34
|
* ordered per-id entries followed by a trailing counts summary.
|
|
35
35
|
*/
|
|
36
|
-
export declare function readBatch(gmail: any, account: Account, ids: string[], full: boolean, rawHtml: boolean): Promise<any[]>;
|
|
36
|
+
export declare function readBatch(gmail: any, account: Account, ids: string[], full: boolean, rawHtml: boolean, metaHeaders?: string[]): Promise<any[]>;
|
|
37
37
|
/** Compact gmail_search row: just the pick-one-message selection signal, with
|
|
38
38
|
* the snippet flattened to one bounded line. Pure. */
|
|
39
39
|
export declare function compactMessageRow(m: GmailMessageHeader): {
|
package/dist/tools/gmail.js
CHANGED
|
@@ -17,6 +17,12 @@ import * as fs from 'fs';
|
|
|
17
17
|
function getHeader(headers, name) {
|
|
18
18
|
return headers?.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ?? '';
|
|
19
19
|
}
|
|
20
|
+
function pickHeaders(headers, names) {
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const name of names)
|
|
23
|
+
out[name] = getHeader(headers, name);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
20
26
|
function collectTextParts(part, plain, html, topLevel = false) {
|
|
21
27
|
if (!part)
|
|
22
28
|
return;
|
|
@@ -176,6 +182,21 @@ export async function resolveReply(gmail, account, primaryEmail, replyToMessageI
|
|
|
176
182
|
subject: deriveReplySubject(getHeader(headers, 'Subject')),
|
|
177
183
|
};
|
|
178
184
|
}
|
|
185
|
+
// Gmail's after:/before: operators take epoch seconds; Date.parse accepts both
|
|
186
|
+
// an ISO date and a full date-time, so one parse covers both input shapes.
|
|
187
|
+
function parseEpochSeconds(value) {
|
|
188
|
+
// A date-only value is interpreted as 00:00 UTC.
|
|
189
|
+
// A date-time must include Z or an offset (+HH:MM or -HH:MM) to be unambiguous.
|
|
190
|
+
if (value.includes('T')) {
|
|
191
|
+
const hasZ = value.endsWith('Z');
|
|
192
|
+
const hasOffset = /[+-]\d{2}:\d{2}$/.test(value);
|
|
193
|
+
if (!hasZ && !hasOffset) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const ms = Date.parse(value);
|
|
198
|
+
return Number.isNaN(ms) ? null : Math.floor(ms / 1000);
|
|
199
|
+
}
|
|
179
200
|
const BODY_CAP_CHARS = 50_000;
|
|
180
201
|
// gmail_read_batch aggregate ceiling: bounds the summed serialized output so a
|
|
181
202
|
// 100-message batch can't blow the context window (per-message bodies are capped at BODY_CAP_CHARS first).
|
|
@@ -331,13 +352,14 @@ export function parseMessage(msg, bodyCap, opts) {
|
|
|
331
352
|
* is rethrown so the caller maps it to a whole-call isError. Returns the
|
|
332
353
|
* ordered per-id entries followed by a trailing counts summary.
|
|
333
354
|
*/
|
|
334
|
-
export async function readBatch(gmail, account, ids, full, rawHtml) {
|
|
355
|
+
export async function readBatch(gmail, account, ids, full, rawHtml, metaHeaders) {
|
|
335
356
|
if (ids.length === 0) {
|
|
336
357
|
throw new GmailComposeError('validation_error', '`ids` must be a non-empty array of 1..100 message IDs.');
|
|
337
358
|
}
|
|
338
359
|
if (ids.length > 100) {
|
|
339
360
|
throw new GmailComposeError('validation_error', `\`ids\` accepts at most 100 message IDs per call (got ${ids.length}).`);
|
|
340
361
|
}
|
|
362
|
+
const metadataMode = metaHeaders !== undefined && metaHeaders.length > 0;
|
|
341
363
|
const cap = full ? undefined : BODY_CAP_CHARS;
|
|
342
364
|
const CHUNK_SIZE = 10;
|
|
343
365
|
const entries = new Array(ids.length);
|
|
@@ -347,7 +369,9 @@ export async function readBatch(gmail, account, ids, full, rawHtml) {
|
|
|
347
369
|
const slice = ids.slice(i, i + CHUNK_SIZE);
|
|
348
370
|
const settled = await Promise.all(slice.map(async (id, j) => {
|
|
349
371
|
try {
|
|
350
|
-
const res =
|
|
372
|
+
const res = metadataMode
|
|
373
|
+
? await gmail.users.messages.get({ userId: 'me', id, format: 'metadata', metadataHeaders: metaHeaders })
|
|
374
|
+
: await gmail.users.messages.get({ userId: 'me', id, format: 'full' });
|
|
351
375
|
return { idx: i + j, id, ok: true, data: res.data };
|
|
352
376
|
}
|
|
353
377
|
catch (err) {
|
|
@@ -356,7 +380,14 @@ export async function readBatch(gmail, account, ids, full, rawHtml) {
|
|
|
356
380
|
}));
|
|
357
381
|
for (const s of settled) {
|
|
358
382
|
if (s.ok) {
|
|
359
|
-
entries[s.idx] =
|
|
383
|
+
entries[s.idx] = metadataMode
|
|
384
|
+
? {
|
|
385
|
+
id: s.data.id ?? '',
|
|
386
|
+
threadId: s.data.threadId ?? '',
|
|
387
|
+
labelIds: s.data.labelIds ?? [],
|
|
388
|
+
headers: pickHeaders(s.data.payload?.headers, metaHeaders),
|
|
389
|
+
}
|
|
390
|
+
: parseMessage(s.data, cap, { rawHtml });
|
|
360
391
|
ok++;
|
|
361
392
|
}
|
|
362
393
|
else {
|
|
@@ -374,10 +405,11 @@ export async function readBatch(gmail, account, ids, full, rawHtml) {
|
|
|
374
405
|
}
|
|
375
406
|
// Aggregate guard: walk in order accumulating serialized size; once the running
|
|
376
407
|
// total would exceed the budget, cap this and every later success body so the batch never blows the context window.
|
|
408
|
+
// Metadata-mode entries carry no body, so there is nothing to cap.
|
|
377
409
|
let running = 0;
|
|
378
410
|
let truncatedAny = false;
|
|
379
411
|
let exhausted = false;
|
|
380
|
-
for (const e of entries) {
|
|
412
|
+
for (const e of metadataMode ? [] : entries) {
|
|
381
413
|
if (!e || e.error) {
|
|
382
414
|
running += JSON.stringify(e ?? {}).length;
|
|
383
415
|
continue;
|
|
@@ -434,27 +466,50 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
434
466
|
const registerHostFileTool = localFiles ? server.registerTool : (() => undefined);
|
|
435
467
|
server.registerTool('gmail_search', {
|
|
436
468
|
description: 'Search messages in a Gmail account. Returns compact rows (id, from, subject, date, snippet); ' +
|
|
437
|
-
'pass full=true for threadId, to, labelIds and the untruncated snippet.'
|
|
469
|
+
'pass full=true for threadId, to, labelIds and the untruncated snippet. ' +
|
|
470
|
+
'For a recurring "what is new" sweep, prefer gmail_list_history over polling this with since.',
|
|
438
471
|
inputSchema: {
|
|
439
472
|
account: accountEnum.describe('Google account alias'),
|
|
440
473
|
query: z.string().describe('Gmail search syntax, e.g. "from:alice is:unread"'),
|
|
441
474
|
maxResults: coerceNumber(z.number().min(1).max(100)).optional()
|
|
442
475
|
.describe('Max results to return (default: 20, max: 100)'),
|
|
443
476
|
full: coerceBoolean.optional().describe('Return the full row shape instead of the compact default'),
|
|
477
|
+
since: z.string().optional()
|
|
478
|
+
.describe('Only messages on/after this ISO 8601 date (00:00 UTC) or date-time with Z or offset, added to the query as after:<epoch seconds>'),
|
|
479
|
+
until: z.string().optional()
|
|
480
|
+
.describe('Only messages before this ISO 8601 date (00:00 UTC) or date-time with Z or offset, added to the query as before:<epoch seconds>'),
|
|
481
|
+
includeSpamTrash: coerceBoolean.optional()
|
|
482
|
+
.describe('Include Spam and Trash in the search. Gmail excludes both by default, even for in:anywhere.'),
|
|
444
483
|
},
|
|
445
|
-
}, async ({ account, query, maxResults, full }) => {
|
|
484
|
+
}, async ({ account, query, maxResults, full, since, until, includeSpamTrash }) => {
|
|
446
485
|
try {
|
|
447
486
|
// Gmail treats a blank `q` as no filter, so a caller passing an unset
|
|
448
487
|
// variable got the whole mailbox back as if it had matched.
|
|
449
488
|
if (query.trim() === '') {
|
|
450
489
|
return invalidParams(account, '`query` is empty, and Gmail reads an empty query as "match everything".', 'Pass a real Gmail search term such as "is:unread" or "from:someone@example.com". Returning the whole mailbox for a blank query would look like a successful search.');
|
|
451
490
|
}
|
|
491
|
+
let effectiveQuery = query;
|
|
492
|
+
if (since !== undefined) {
|
|
493
|
+
const epoch = parseEpochSeconds(since);
|
|
494
|
+
if (epoch === null) {
|
|
495
|
+
return invalidParams(account, `\`since\` is not a parsable ISO 8601 date or date-time: "${since}".`, 'Pass an ISO 8601 date (e.g. "2026-01-01") or a date-time with Z or offset (e.g. "2026-01-01T09:00:00Z").');
|
|
496
|
+
}
|
|
497
|
+
effectiveQuery = `${effectiveQuery} after:${epoch}`;
|
|
498
|
+
}
|
|
499
|
+
if (until !== undefined) {
|
|
500
|
+
const epoch = parseEpochSeconds(until);
|
|
501
|
+
if (epoch === null) {
|
|
502
|
+
return invalidParams(account, `\`until\` is not a parsable ISO 8601 date or date-time: "${until}".`, 'Pass an ISO 8601 date (e.g. "2026-01-01") or a date-time with Z or offset (e.g. "2026-01-01T09:00:00Z").');
|
|
503
|
+
}
|
|
504
|
+
effectiveQuery = `${effectiveQuery} before:${epoch}`;
|
|
505
|
+
}
|
|
452
506
|
const auth = await getClientFn(account);
|
|
453
507
|
const gmail = gmailClient({ version: 'v1', auth });
|
|
454
508
|
const listRes = await gmail.users.messages.list({
|
|
455
509
|
userId: 'me',
|
|
456
|
-
q:
|
|
510
|
+
q: effectiveQuery,
|
|
457
511
|
maxResults: maxResults ?? 20,
|
|
512
|
+
...(includeSpamTrash !== undefined ? { includeSpamTrash } : {}),
|
|
458
513
|
});
|
|
459
514
|
const messages = listRes.data.messages ?? [];
|
|
460
515
|
const results = [];
|
|
@@ -495,18 +550,38 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
495
550
|
});
|
|
496
551
|
server.registerTool('gmail_read', {
|
|
497
552
|
_meta: { 'anthropic/maxResultSizeChars': 50_000 },
|
|
498
|
-
description: 'Read a full Gmail message by ID (body capped at 50k chars unless full=true)'
|
|
553
|
+
description: 'Read a full Gmail message by ID (body capped at 50k chars unless full=true). ' +
|
|
554
|
+
'Pass `headers` for metadata mode: no body decoding, just id, threadId, labelIds and the named headers.',
|
|
499
555
|
inputSchema: {
|
|
500
556
|
account: accountEnum.describe('Google account alias'),
|
|
501
557
|
messageId: z.string().min(1).describe('Gmail message ID'),
|
|
502
558
|
full: coerceBoolean.optional().describe('Return the entire body without the character cap'),
|
|
503
559
|
rawHtml: coerceBoolean.optional()
|
|
504
560
|
.describe('Return the HTML body unconverted instead of the plain-text rendering (HTML-only messages)'),
|
|
561
|
+
headers: coerceArray(z.string()).optional()
|
|
562
|
+
.describe('Header names to fetch in metadata mode instead of the full body (e.g. ["Subject", "From"])'),
|
|
505
563
|
},
|
|
506
|
-
}, async ({ account, messageId, full, rawHtml }) => {
|
|
564
|
+
}, async ({ account, messageId, full, rawHtml, headers }) => {
|
|
507
565
|
try {
|
|
508
566
|
const auth = await getClientFn(account);
|
|
509
567
|
const gmail = gmailClient({ version: 'v1', auth });
|
|
568
|
+
if (headers !== undefined && headers.length > 0) {
|
|
569
|
+
const metaRes = await gmail.users.messages.get({
|
|
570
|
+
userId: 'me',
|
|
571
|
+
id: messageId,
|
|
572
|
+
format: 'metadata',
|
|
573
|
+
metadataHeaders: headers,
|
|
574
|
+
});
|
|
575
|
+
const metaResult = {
|
|
576
|
+
id: metaRes.data.id ?? '',
|
|
577
|
+
threadId: metaRes.data.threadId ?? '',
|
|
578
|
+
labelIds: metaRes.data.labelIds ?? [],
|
|
579
|
+
headers: pickHeaders(metaRes.data.payload?.headers, headers),
|
|
580
|
+
};
|
|
581
|
+
return {
|
|
582
|
+
content: [{ type: 'text', text: JSON.stringify(metaResult, null, 2) }],
|
|
583
|
+
};
|
|
584
|
+
}
|
|
510
585
|
const res = await gmail.users.messages.get({
|
|
511
586
|
userId: 'me',
|
|
512
587
|
id: messageId,
|
|
@@ -576,19 +651,21 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
576
651
|
server.registerTool('gmail_read_batch', {
|
|
577
652
|
_meta: { 'anthropic/maxResultSizeChars': BATCH_MAX_RESULT_CHARS },
|
|
578
653
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
579
|
-
description: 'Read many Gmail messages by ID in one call, collapsing the search→read triage loop. Bodies are capped at 50k chars each (unless full=true) and the aggregate output is bounded. Returns one ordered entry per id plus a trailing counts summary; a single failed id does NOT fail the batch.',
|
|
654
|
+
description: 'Read many Gmail messages by ID in one call, collapsing the search→read triage loop. Bodies are capped at 50k chars each (unless full=true) and the aggregate output is bounded. Returns one ordered entry per id plus a trailing counts summary; a single failed id does NOT fail the batch. Pass `headers` for metadata mode: no body decoding, just id, threadId, labelIds and the named headers per entry.',
|
|
580
655
|
inputSchema: {
|
|
581
656
|
account: accountEnum.describe('Google account alias'),
|
|
582
657
|
ids: coerceArray(z.string()).describe('Gmail message IDs to read (1..100). Comma-separated string or JSON array.'),
|
|
583
658
|
full: coerceBoolean.optional().describe('Return entire bodies without the per-message 50k character cap'),
|
|
584
659
|
rawHtml: coerceBoolean.optional()
|
|
585
660
|
.describe('Return HTML bodies unconverted instead of the plain-text rendering (HTML-only messages)'),
|
|
661
|
+
headers: coerceArray(z.string()).optional()
|
|
662
|
+
.describe('Header names to fetch in metadata mode instead of full bodies (e.g. ["Subject", "From"])'),
|
|
586
663
|
},
|
|
587
|
-
}, async ({ account, ids, full, rawHtml }) => {
|
|
664
|
+
}, async ({ account, ids, full, rawHtml, headers }) => {
|
|
588
665
|
try {
|
|
589
666
|
const auth = await getClientFn(account);
|
|
590
667
|
const gmail = gmailClient({ version: 'v1', auth });
|
|
591
|
-
const result = await readBatch(gmail, account, ids ?? [], full === true, rawHtml === true);
|
|
668
|
+
const result = await readBatch(gmail, account, ids ?? [], full === true, rawHtml === true, headers);
|
|
592
669
|
return {
|
|
593
670
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
594
671
|
};
|
|
@@ -993,6 +1070,26 @@ export function registerGmailTools(server, deps = {}) {
|
|
|
993
1070
|
return mapped ?? handleGmailError(error, account);
|
|
994
1071
|
}
|
|
995
1072
|
});
|
|
1073
|
+
server.registerTool('gmail_delete_draft', {
|
|
1074
|
+
description: 'Permanently delete a Gmail draft by its draft ID',
|
|
1075
|
+
inputSchema: {
|
|
1076
|
+
account: accountEnum.describe('Google account alias'),
|
|
1077
|
+
draftId: z.string().min(1).describe('Draft ID to delete'),
|
|
1078
|
+
},
|
|
1079
|
+
}, async ({ account, draftId }) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const auth = await getClientFn(account);
|
|
1082
|
+
const gmail = gmailClient({ version: 'v1', auth });
|
|
1083
|
+
await gmail.users.drafts.delete({ userId: 'me', id: draftId });
|
|
1084
|
+
return {
|
|
1085
|
+
content: [{ type: 'text', text: JSON.stringify({ deleted: true, draftId }, null, 2) }],
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
catch (error) {
|
|
1089
|
+
const mapped = composeErrorResult(error, account);
|
|
1090
|
+
return mapped ?? handleGmailError(error, account);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
996
1093
|
server.registerTool('gmail_list_labels', {
|
|
997
1094
|
description: 'List all Gmail labels (system and user-defined). Use to get label IDs for gmail_modify_labels.',
|
|
998
1095
|
inputSchema: {
|
package/dist/tools/sheets.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
2
|
import { type CuratedToolDeps } from '../client.js';
|
|
3
3
|
export declare function registerSheetsTools(server: ToolRegistry, deps?: CuratedToolDeps): void;
|
|
4
|
+
interface GridRangeIndexes {
|
|
5
|
+
startRowIndex?: number;
|
|
6
|
+
endRowIndex?: number;
|
|
7
|
+
startColumnIndex?: number;
|
|
8
|
+
endColumnIndex?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseA1Range(a1: string): GridRangeIndexes & {
|
|
11
|
+
sheetName: string;
|
|
12
|
+
};
|
|
4
13
|
export declare function buildCellFormat(input: {
|
|
5
14
|
backgroundColor?: {
|
|
6
15
|
red?: number;
|
|
@@ -33,3 +42,4 @@ export declare function buildCellFormat(input: {
|
|
|
33
42
|
format: any;
|
|
34
43
|
fields: string;
|
|
35
44
|
};
|
|
45
|
+
export {};
|
package/dist/tools/sheets.js
CHANGED
|
@@ -369,6 +369,31 @@ export function registerSheetsTools(server, deps = {}) {
|
|
|
369
369
|
return handleSheetsError(error, account);
|
|
370
370
|
}
|
|
371
371
|
});
|
|
372
|
+
server.registerTool('sheets_copy_sheet_to', {
|
|
373
|
+
description: 'Copy a tab into another spreadsheet (which may belong to a different account)',
|
|
374
|
+
inputSchema: {
|
|
375
|
+
account: accountEnum.describe('Google account alias'),
|
|
376
|
+
spreadsheetId: z.string().min(1).describe('Source spreadsheet ID'),
|
|
377
|
+
sheetId: z.number().describe('Source sheet ID'),
|
|
378
|
+
destinationSpreadsheetId: z.string().min(1).describe('Destination spreadsheet ID'),
|
|
379
|
+
},
|
|
380
|
+
}, async ({ account, spreadsheetId, sheetId, destinationSpreadsheetId }) => {
|
|
381
|
+
try {
|
|
382
|
+
const auth = await getClientFn(account);
|
|
383
|
+
const sheets = sheetsClient({ version: 'v4', auth });
|
|
384
|
+
const res = await sheets.spreadsheets.sheets.copyTo({
|
|
385
|
+
spreadsheetId,
|
|
386
|
+
sheetId,
|
|
387
|
+
requestBody: { destinationSpreadsheetId },
|
|
388
|
+
});
|
|
389
|
+
return {
|
|
390
|
+
content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
return handleSheetsError(error, account);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
372
397
|
server.registerTool('sheets_update_sheet_properties', {
|
|
373
398
|
description: 'Rename a tab, change tab color, freeze rows/columns, hide/show, or change grid dimensions',
|
|
374
399
|
inputSchema: {
|
|
@@ -457,7 +482,8 @@ export function registerSheetsTools(server, deps = {}) {
|
|
|
457
482
|
inputSchema: {
|
|
458
483
|
account: accountEnum.describe('Google account alias'),
|
|
459
484
|
spreadsheetId: z.string().min(1).describe('Spreadsheet ID'),
|
|
460
|
-
range:
|
|
485
|
+
range: z.union([gridRangeSchema, z.string().min(1)])
|
|
486
|
+
.describe('Target range: a GridRange object (half-open indexes), or an A1 string such as "Sheet1!A1:D10" (the sheet title is resolved to a sheetId with one extra call)'),
|
|
461
487
|
backgroundColor: rgbColorSchema.optional().describe('Cell background (RGB 0..1)'),
|
|
462
488
|
textFormat: z.object({
|
|
463
489
|
foregroundColor: rgbColorSchema.optional(),
|
|
@@ -484,12 +510,16 @@ export function registerSheetsTools(server, deps = {}) {
|
|
|
484
510
|
if (built.fields.length === 0) {
|
|
485
511
|
return invalidParams(account, 'No formatting to apply: every optional format property was omitted.', 'Pass at least one of: backgroundColor, textFormat, horizontalAlignment, verticalAlignment, wrapStrategy, numberFormat.');
|
|
486
512
|
}
|
|
513
|
+
const targetRange = typeof range === 'string' ? await resolveA1GridRange(sheets, spreadsheetId, range) : range;
|
|
514
|
+
if ('error' in targetRange) {
|
|
515
|
+
return invalidParams(account, targetRange.error, targetRange.hint);
|
|
516
|
+
}
|
|
487
517
|
await sheets.spreadsheets.batchUpdate({
|
|
488
518
|
spreadsheetId,
|
|
489
519
|
requestBody: {
|
|
490
520
|
requests: [{
|
|
491
521
|
repeatCell: {
|
|
492
|
-
range,
|
|
522
|
+
range: targetRange,
|
|
493
523
|
cell: { userEnteredFormat: built.format },
|
|
494
524
|
fields: built.fields,
|
|
495
525
|
},
|
|
@@ -1118,6 +1148,83 @@ const gridRangeSchema = z.object({
|
|
|
1118
1148
|
startColumnIndex: z.number().min(0).optional(),
|
|
1119
1149
|
endColumnIndex: z.number().min(0).optional(),
|
|
1120
1150
|
});
|
|
1151
|
+
function columnLettersToIndex(letters) {
|
|
1152
|
+
let n = 0;
|
|
1153
|
+
for (const ch of letters.toUpperCase())
|
|
1154
|
+
n = n * 26 + (ch.charCodeAt(0) - 64);
|
|
1155
|
+
return n - 1;
|
|
1156
|
+
}
|
|
1157
|
+
const A1_CELL_RE = /^([A-Za-z]+)?(\d+)?$/;
|
|
1158
|
+
// Either side of ':', and either half of a side, may be omitted: whole column, row or sheet.
|
|
1159
|
+
export function parseA1Range(a1) {
|
|
1160
|
+
const bang = a1.lastIndexOf('!');
|
|
1161
|
+
const sheetPart = bang === -1 ? a1 : a1.slice(0, bang);
|
|
1162
|
+
const rangePart = bang === -1 ? '' : a1.slice(bang + 1);
|
|
1163
|
+
const sheetName = sheetPart.startsWith("'") && sheetPart.endsWith("'")
|
|
1164
|
+
? sheetPart.slice(1, -1).replace(/''/g, "'")
|
|
1165
|
+
: sheetPart;
|
|
1166
|
+
if (rangePart === '')
|
|
1167
|
+
return { sheetName };
|
|
1168
|
+
const [startRaw, endRaw] = rangePart.split(':');
|
|
1169
|
+
const m1 = startRaw.match(A1_CELL_RE);
|
|
1170
|
+
if (!m1 || (!m1[1] && !m1[2]))
|
|
1171
|
+
throw new Error(`invalid A1 range "${a1}"`);
|
|
1172
|
+
const [, col1, row1] = m1;
|
|
1173
|
+
const out = { sheetName };
|
|
1174
|
+
if (endRaw === undefined) {
|
|
1175
|
+
if (col1) {
|
|
1176
|
+
const c = columnLettersToIndex(col1);
|
|
1177
|
+
out.startColumnIndex = c;
|
|
1178
|
+
out.endColumnIndex = c + 1;
|
|
1179
|
+
}
|
|
1180
|
+
if (row1) {
|
|
1181
|
+
const r = Number(row1) - 1;
|
|
1182
|
+
out.startRowIndex = r;
|
|
1183
|
+
out.endRowIndex = r + 1;
|
|
1184
|
+
}
|
|
1185
|
+
return out;
|
|
1186
|
+
}
|
|
1187
|
+
const m2 = endRaw.match(A1_CELL_RE);
|
|
1188
|
+
if (!m2 || (!m2[1] && !m2[2]))
|
|
1189
|
+
throw new Error(`invalid A1 range "${a1}"`);
|
|
1190
|
+
const [, col2, row2] = m2;
|
|
1191
|
+
if (col1)
|
|
1192
|
+
out.startColumnIndex = columnLettersToIndex(col1);
|
|
1193
|
+
if (col2)
|
|
1194
|
+
out.endColumnIndex = columnLettersToIndex(col2) + 1;
|
|
1195
|
+
if (row1)
|
|
1196
|
+
out.startRowIndex = Number(row1) - 1;
|
|
1197
|
+
if (row2)
|
|
1198
|
+
out.endRowIndex = Number(row2);
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
async function resolveA1GridRange(sheets, spreadsheetId, a1) {
|
|
1202
|
+
let parsed;
|
|
1203
|
+
try {
|
|
1204
|
+
parsed = parseA1Range(a1);
|
|
1205
|
+
}
|
|
1206
|
+
catch {
|
|
1207
|
+
return {
|
|
1208
|
+
error: `Could not parse A1 range "${a1}".`,
|
|
1209
|
+
hint: 'Use a form like "Sheet1!A1:D10", "Sheet1!A:D", or "Sheet1" for the whole sheet.',
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
const meta = await sheets.spreadsheets.get({ spreadsheetId, fields: 'sheets.properties(sheetId,title)' });
|
|
1213
|
+
const match = meta.data.sheets?.find((s) => s.properties?.title === parsed.sheetName);
|
|
1214
|
+
if (!match || match.properties?.sheetId == null) {
|
|
1215
|
+
return {
|
|
1216
|
+
error: `No sheet/tab named "${parsed.sheetName}" in this spreadsheet.`,
|
|
1217
|
+
hint: 'Check the tab name, or use sheets_get to list sheet titles and IDs.',
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
sheetId: match.properties.sheetId,
|
|
1222
|
+
startRowIndex: parsed.startRowIndex,
|
|
1223
|
+
endRowIndex: parsed.endRowIndex,
|
|
1224
|
+
startColumnIndex: parsed.startColumnIndex,
|
|
1225
|
+
endColumnIndex: parsed.endColumnIndex,
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1121
1228
|
const borderSchema = z.object({
|
|
1122
1229
|
style: z.enum(['DOTTED', 'DASHED', 'SOLID', 'SOLID_MEDIUM', 'SOLID_THICK', 'NONE', 'DOUBLE']),
|
|
1123
1230
|
color: rgbColorSchema.optional(),
|
package/dist/write-control.js
CHANGED
|
@@ -13,12 +13,12 @@ export const IRREVERSIBLE_TOOLS = new Set([
|
|
|
13
13
|
'gmail_send',
|
|
14
14
|
'gmail_send_draft',
|
|
15
15
|
'gmail_delete',
|
|
16
|
+
'gmail_delete_draft',
|
|
16
17
|
'gmail_batch_delete',
|
|
17
18
|
'drive_delete',
|
|
18
19
|
'drive_empty_trash',
|
|
19
20
|
// generated twins (permanent deletes / real sends)
|
|
20
21
|
'gmail_users_threads_delete',
|
|
21
|
-
'gmail_users_drafts_delete',
|
|
22
22
|
'chat_spaces_messages_delete',
|
|
23
23
|
'cloudidentity_customers_userinvitations_send',
|
|
24
24
|
'vault_matters_holds_delete',
|
|
@@ -82,13 +82,26 @@ export function privilegeOf(tool) {
|
|
|
82
82
|
return 'privileged_scope';
|
|
83
83
|
return undefined;
|
|
84
84
|
}
|
|
85
|
+
/** Moves an item to Trash rather than deleting it permanently: the owner can
|
|
86
|
+
* still recover it, so safe-writes treats this like its other reversible
|
|
87
|
+
* writes despite the delete-shaped verb. Named explicitly rather than matched
|
|
88
|
+
* by verb, so a synthetic delete-classified check (drive_transfer's move
|
|
89
|
+
* flag) never qualifies by accident, and so emptying Trash, which the same
|
|
90
|
+
* verb regex also classifies as delete, is never swept in by a suffix match. */
|
|
91
|
+
const TRASH_MOVE_TOOLS = new Set([
|
|
92
|
+
'gmail_trash',
|
|
93
|
+
'drive_trash',
|
|
94
|
+
'gmail_users_threads_trash',
|
|
95
|
+
]);
|
|
85
96
|
function profileAllows(profile, tool) {
|
|
86
97
|
if (profile === 'full-writes')
|
|
87
98
|
return true;
|
|
88
99
|
if (profile === 'safe-writes') {
|
|
89
100
|
if (privilegeOf(tool) !== undefined)
|
|
90
101
|
return false;
|
|
91
|
-
|
|
102
|
+
if (tool.cud === 'create' || tool.cud === 'update')
|
|
103
|
+
return true;
|
|
104
|
+
return tool.cud === 'delete' && TRASH_MOVE_TOOLS.has(tool.name);
|
|
92
105
|
}
|
|
93
106
|
return false;
|
|
94
107
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "6.0.0
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"mcpName": "io.github.bakissation/mcp-google-multi",
|
|
5
5
|
"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.",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"./package.json": "./package.json"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
|
-
"dist"
|
|
39
|
+
"dist",
|
|
40
|
+
"skills"
|
|
40
41
|
],
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=22"
|
|
@@ -85,6 +86,7 @@
|
|
|
85
86
|
"gen:discovery": "tsx scripts/fetch-discovery.ts",
|
|
86
87
|
"gen:tools": "tsx scripts/gen-tools.ts",
|
|
87
88
|
"gen:coverage": "tsx scripts/gen-coverage.ts",
|
|
89
|
+
"gen:skill": "tsx scripts/gen-skill.ts",
|
|
88
90
|
"eval:contract": "npx --yes promptfoo@0.123.1 eval -c eval/contract/promptfooconfig.yaml --no-progress-bar",
|
|
89
91
|
"measure:lazy": "node scripts/measure-tools.mjs",
|
|
90
92
|
"pack:mcpb": "mcpb pack . mcp-google-multi.mcpb",
|