google-tools-mcp 1.2.6 → 1.2.7
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/helpers.js +38 -0
- package/dist/tools/drive/getRevision.js +57 -0
- package/dist/tools/drive/index.js +6 -0
- package/dist/tools/drive/listRevisions.js +50 -0
- package/dist/tools/drive/updateRevision.js +51 -0
- package/dist/tools/gmail/labels.js +0 -20
- package/dist/tools/gmail/messages.js +56 -75
- package/dist/tools/gmail/threads.js +49 -40
- package/package.json +1 -1
package/dist/helpers.js
CHANGED
|
@@ -212,3 +212,41 @@ export const getPlainTextBody = (messagePart) => {
|
|
|
212
212
|
}
|
|
213
213
|
return '';
|
|
214
214
|
};
|
|
215
|
+
|
|
216
|
+
export const formatMessageClean = (message, maxBodyChars = 3000) => {
|
|
217
|
+
const headers = message.payload?.headers || [];
|
|
218
|
+
const get = (name) => findHeader(headers, name);
|
|
219
|
+
let body = getPlainTextBody(message.payload) || '';
|
|
220
|
+
const totalChars = body.length;
|
|
221
|
+
const truncated = maxBodyChars > 0 && body.length > maxBodyChars;
|
|
222
|
+
if (truncated) body = body.slice(0, maxBodyChars);
|
|
223
|
+
return {
|
|
224
|
+
id: message.id,
|
|
225
|
+
threadId: message.threadId,
|
|
226
|
+
labelIds: message.labelIds,
|
|
227
|
+
snippet: message.snippet,
|
|
228
|
+
from: get('from'),
|
|
229
|
+
to: get('to'),
|
|
230
|
+
cc: get('cc'),
|
|
231
|
+
subject: get('subject'),
|
|
232
|
+
date: get('date'),
|
|
233
|
+
body,
|
|
234
|
+
...(truncated ? { bodyTruncated: true, totalChars } : {}),
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const formatMessageMetadata = (message) => {
|
|
239
|
+
const headers = message.payload?.headers || [];
|
|
240
|
+
const get = (name) => findHeader(headers, name);
|
|
241
|
+
return {
|
|
242
|
+
id: message.id,
|
|
243
|
+
threadId: message.threadId,
|
|
244
|
+
labelIds: message.labelIds,
|
|
245
|
+
snippet: message.snippet,
|
|
246
|
+
from: get('from'),
|
|
247
|
+
to: get('to'),
|
|
248
|
+
cc: get('cc'),
|
|
249
|
+
subject: get('subject'),
|
|
250
|
+
date: get('date'),
|
|
251
|
+
};
|
|
252
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'getRevision',
|
|
7
|
+
description: 'Gets details about a specific revision of a Drive file, including who made it, when, and an optional temporary download link to retrieve the content at that point in time.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
fileId: z.string().describe('The ID of the Drive file.'),
|
|
10
|
+
revisionId: z.string().describe('The revision ID (from listRevisions).'),
|
|
11
|
+
includeDownloadLink: z.boolean().optional().default(false).describe('If true, includes a short-lived download URL for the revision content.'),
|
|
12
|
+
}),
|
|
13
|
+
execute: async (args, { log }) => {
|
|
14
|
+
const drive = await getDriveClient();
|
|
15
|
+
log.info(`Getting revision ${args.revisionId} for file: ${args.fileId}`);
|
|
16
|
+
try {
|
|
17
|
+
const fields = [
|
|
18
|
+
'id', 'modifiedTime', 'lastModifyingUser(displayName,emailAddress)',
|
|
19
|
+
'keepForever', 'published', 'publishedOutsideDomain', 'publishAuto',
|
|
20
|
+
'mimeType', 'size',
|
|
21
|
+
...(args.includeDownloadLink ? ['exportLinks', 'downloadUrl'] : []),
|
|
22
|
+
].join(',');
|
|
23
|
+
const response = await drive.revisions.get({
|
|
24
|
+
fileId: args.fileId,
|
|
25
|
+
revisionId: args.revisionId,
|
|
26
|
+
fields,
|
|
27
|
+
});
|
|
28
|
+
const r = response.data;
|
|
29
|
+
const result = {
|
|
30
|
+
revisionId: r.id,
|
|
31
|
+
modifiedTime: r.modifiedTime,
|
|
32
|
+
modifiedBy: r.lastModifyingUser?.displayName || null,
|
|
33
|
+
modifiedByEmail: r.lastModifyingUser?.emailAddress || null,
|
|
34
|
+
mimeType: r.mimeType,
|
|
35
|
+
sizeBytes: r.size ? parseInt(r.size, 10) : null,
|
|
36
|
+
keepForever: r.keepForever || false,
|
|
37
|
+
published: r.published || false,
|
|
38
|
+
publishedOutsideDomain: r.publishedOutsideDomain || false,
|
|
39
|
+
publishAuto: r.publishAuto || false,
|
|
40
|
+
...(args.includeDownloadLink ? {
|
|
41
|
+
downloadUrl: r.downloadUrl || null,
|
|
42
|
+
exportLinks: r.exportLinks || null,
|
|
43
|
+
} : {}),
|
|
44
|
+
};
|
|
45
|
+
return JSON.stringify(result, null, 2);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
log.error(`Error getting revision: ${error.message || error}`);
|
|
49
|
+
if (error.code === 404)
|
|
50
|
+
throw new UserError(`File or revision not found (fileId: ${args.fileId}, revisionId: ${args.revisionId}).`);
|
|
51
|
+
if (error.code === 403)
|
|
52
|
+
throw new UserError('Permission denied. Make sure you have access to this file.');
|
|
53
|
+
throw new UserError(`Failed to get revision: ${error.message || 'Unknown error'}`);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -19,6 +19,9 @@ import { register as listPermissions } from './listPermissions.js';
|
|
|
19
19
|
import { register as addPermission } from './addPermission.js';
|
|
20
20
|
import { register as removePermission } from './removePermission.js';
|
|
21
21
|
import { register as updatePermission } from './updatePermission.js';
|
|
22
|
+
import { register as listRevisions } from './listRevisions.js';
|
|
23
|
+
import { register as getRevision } from './getRevision.js';
|
|
24
|
+
import { register as updateRevision } from './updateRevision.js';
|
|
22
25
|
export function registerDriveTools(server) {
|
|
23
26
|
listDriveFiles(server);
|
|
24
27
|
searchGoogleDocs(server);
|
|
@@ -41,4 +44,7 @@ export function registerDriveTools(server) {
|
|
|
41
44
|
addPermission(server);
|
|
42
45
|
removePermission(server);
|
|
43
46
|
updatePermission(server);
|
|
47
|
+
listRevisions(server);
|
|
48
|
+
getRevision(server);
|
|
49
|
+
updateRevision(server);
|
|
44
50
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'listRevisions',
|
|
7
|
+
description: 'Lists all saved versions (revision history) of a Drive file such as a Google Doc, Sheet, or Slide. Returns each revision\'s ID, modifier, timestamp, and whether it is kept forever.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
fileId: z.string().describe('The ID of the Drive file to list revisions for.'),
|
|
10
|
+
}),
|
|
11
|
+
execute: async (args, { log }) => {
|
|
12
|
+
const drive = await getDriveClient();
|
|
13
|
+
log.info(`Listing revisions for file: ${args.fileId}`);
|
|
14
|
+
try {
|
|
15
|
+
const revisions = [];
|
|
16
|
+
let pageToken;
|
|
17
|
+
do {
|
|
18
|
+
const response = await drive.revisions.list({
|
|
19
|
+
fileId: args.fileId,
|
|
20
|
+
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser(displayName,emailAddress),keepForever,published,size)',
|
|
21
|
+
pageSize: 200,
|
|
22
|
+
...(pageToken ? { pageToken } : {}),
|
|
23
|
+
});
|
|
24
|
+
const page = response.data.revisions || [];
|
|
25
|
+
revisions.push(...page);
|
|
26
|
+
pageToken = response.data.nextPageToken;
|
|
27
|
+
} while (pageToken);
|
|
28
|
+
const result = revisions.map((r, i) => ({
|
|
29
|
+
revisionId: r.id,
|
|
30
|
+
index: i + 1,
|
|
31
|
+
modifiedTime: r.modifiedTime,
|
|
32
|
+
modifiedBy: r.lastModifyingUser?.displayName || null,
|
|
33
|
+
modifiedByEmail: r.lastModifyingUser?.emailAddress || null,
|
|
34
|
+
keepForever: r.keepForever || false,
|
|
35
|
+
published: r.published || false,
|
|
36
|
+
sizeBytes: r.size ? parseInt(r.size, 10) : null,
|
|
37
|
+
}));
|
|
38
|
+
return JSON.stringify({ total: result.length, revisions: result }, null, 2);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
log.error(`Error listing revisions: ${error.message || error}`);
|
|
42
|
+
if (error.code === 404)
|
|
43
|
+
throw new UserError(`File not found (ID: ${args.fileId}).`);
|
|
44
|
+
if (error.code === 403)
|
|
45
|
+
throw new UserError('Permission denied. Make sure you have access to this file.');
|
|
46
|
+
throw new UserError(`Failed to list revisions: ${error.message || 'Unknown error'}`);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'updateRevision',
|
|
7
|
+
description: 'Updates metadata for a specific revision of a Drive file. Use this to pin a revision so it is kept forever (never auto-deleted by Drive), or to control publishing settings for Google Docs/Slides.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
fileId: z.string().describe('The ID of the Drive file.'),
|
|
10
|
+
revisionId: z.string().describe('The revision ID to update (from listRevisions).'),
|
|
11
|
+
keepForever: z.boolean().optional().describe('If true, this revision is kept indefinitely and will not be automatically deleted. Only applies to files with a binary content type; Google Docs revisions are always kept.'),
|
|
12
|
+
published: z.boolean().optional().describe('Whether this revision is published. Only applies to Google Docs and Slides.'),
|
|
13
|
+
publishAuto: z.boolean().optional().describe('Whether subsequent revisions are automatically republished. Only applies to Google Docs and Slides.'),
|
|
14
|
+
}),
|
|
15
|
+
execute: async (args, { log }) => {
|
|
16
|
+
const drive = await getDriveClient();
|
|
17
|
+
log.info(`Updating revision ${args.revisionId} for file: ${args.fileId}`);
|
|
18
|
+
const body = {};
|
|
19
|
+
if (args.keepForever !== undefined) body.keepForever = args.keepForever;
|
|
20
|
+
if (args.published !== undefined) body.published = args.published;
|
|
21
|
+
if (args.publishAuto !== undefined) body.publishAuto = args.publishAuto;
|
|
22
|
+
if (Object.keys(body).length === 0) {
|
|
23
|
+
throw new UserError('No fields to update. Provide at least one of: keepForever, published, publishAuto.');
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const response = await drive.revisions.update({
|
|
27
|
+
fileId: args.fileId,
|
|
28
|
+
revisionId: args.revisionId,
|
|
29
|
+
fields: 'id,keepForever,published,publishAuto,modifiedTime',
|
|
30
|
+
requestBody: body,
|
|
31
|
+
});
|
|
32
|
+
const r = response.data;
|
|
33
|
+
return JSON.stringify({
|
|
34
|
+
revisionId: r.id,
|
|
35
|
+
modifiedTime: r.modifiedTime,
|
|
36
|
+
keepForever: r.keepForever || false,
|
|
37
|
+
published: r.published || false,
|
|
38
|
+
publishAuto: r.publishAuto || false,
|
|
39
|
+
}, null, 2);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
log.error(`Error updating revision: ${error.message || error}`);
|
|
43
|
+
if (error.code === 404)
|
|
44
|
+
throw new UserError(`File or revision not found (fileId: ${args.fileId}, revisionId: ${args.revisionId}).`);
|
|
45
|
+
if (error.code === 403)
|
|
46
|
+
throw new UserError('Permission denied. You need edit access to update revisions.');
|
|
47
|
+
throw new UserError(`Failed to update revision: ${error.message || 'Unknown error'}`);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -80,24 +80,4 @@ export function register(server) {
|
|
|
80
80
|
},
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
server.addTool({
|
|
84
|
-
name: 'update_label',
|
|
85
|
-
description: 'Update an existing label',
|
|
86
|
-
parameters: z.object({
|
|
87
|
-
id: z.string().describe("The ID of the label to update"),
|
|
88
|
-
name: z.string().optional().describe("The display name of the label"),
|
|
89
|
-
messageListVisibility: z.enum(['show', 'hide']).optional().describe("Visibility of messages with this label"),
|
|
90
|
-
labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"),
|
|
91
|
-
color: z.object({
|
|
92
|
-
textColor: z.string().describe("The text color as hex string"),
|
|
93
|
-
backgroundColor: z.string().describe("The background color as hex string"),
|
|
94
|
-
}).optional().describe("The color settings for the label"),
|
|
95
|
-
}),
|
|
96
|
-
execute: async (params) => {
|
|
97
|
-
const { id, ...labelData } = params;
|
|
98
|
-
const gmail = await getGmailClient();
|
|
99
|
-
const { data } = await gmail.users.labels.update({ userId: 'me', id, requestBody: labelData });
|
|
100
|
-
return JSON.stringify(data);
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
83
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Gmail Message tools
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getGmailClient } from '../../clients.js';
|
|
4
|
-
import { processMessagePart, constructRawMessage, constructRawMessageWithAttachments, findHeader, formatEmailList, getNestedHistory, getPlainTextBody, isHtmlBody, wrapTextBody } from '../../helpers.js';
|
|
4
|
+
import { processMessagePart, constructRawMessage, constructRawMessageWithAttachments, findHeader, formatEmailList, getNestedHistory, getPlainTextBody, isHtmlBody, wrapTextBody, formatMessageClean, formatMessageMetadata } from '../../helpers.js';
|
|
5
5
|
|
|
6
6
|
export function register(server) {
|
|
7
7
|
server.addTool({
|
|
@@ -279,42 +279,60 @@ export function register(server) {
|
|
|
279
279
|
|
|
280
280
|
server.addTool({
|
|
281
281
|
name: 'get_message',
|
|
282
|
-
description: 'Get a specific message by ID
|
|
282
|
+
description: 'Get a specific message by ID. format="clean" (default) returns from/to/subject/date/body as a flat object. format="metadata" returns headers only, no body. format="full" returns the raw MIME tree (current legacy behavior).',
|
|
283
283
|
parameters: z.object({
|
|
284
284
|
id: z.string().describe("The ID of the message to retrieve"),
|
|
285
|
-
|
|
285
|
+
format: z.enum(['full', 'clean', 'metadata']).optional().default('clean').describe("Response format: clean (default), metadata (headers only), or full (raw MIME tree)"),
|
|
286
|
+
maxBodyChars: z.number().optional().default(3000).describe("Max body characters in clean mode. 0 = unlimited. Truncated responses include bodyTruncated: true."),
|
|
287
|
+
includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
|
|
286
288
|
}),
|
|
287
289
|
execute: async (params) => {
|
|
288
290
|
const gmail = await getGmailClient();
|
|
289
291
|
const { data } = await gmail.users.messages.get({ userId: 'me', id: params.id, format: 'full' });
|
|
290
|
-
if (data.
|
|
291
|
-
|
|
292
|
-
|
|
292
|
+
if (params.format === 'clean') return JSON.stringify(formatMessageClean(data, params.maxBodyChars));
|
|
293
|
+
if (params.format === 'metadata') return JSON.stringify(formatMessageMetadata(data));
|
|
294
|
+
if (data.payload) data.payload = processMessagePart(data.payload, params.includeBodyHtml);
|
|
293
295
|
return JSON.stringify(data);
|
|
294
296
|
},
|
|
295
297
|
});
|
|
296
298
|
|
|
297
299
|
server.addTool({
|
|
298
300
|
name: 'list_messages',
|
|
299
|
-
description: 'List messages in the user\'s mailbox with optional filtering',
|
|
301
|
+
description: 'List messages in the user\'s mailbox with optional filtering. format="metadata" (default) auto-fetches message details for each result. format="clean" includes the message body. format="full" returns raw MIME data. Omit format to get bare IDs only.',
|
|
300
302
|
parameters: z.object({
|
|
301
303
|
maxResults: z.number().optional().describe("Maximum number of messages to return (1-500)"),
|
|
302
304
|
pageToken: z.string().optional().describe("Page token to retrieve a specific page of results"),
|
|
303
305
|
q: z.string().optional().describe("Only return messages matching the specified query (same format as Gmail search box)"),
|
|
304
306
|
labelIds: z.array(z.string()).optional().describe("Only return messages with labels that match all specified label IDs"),
|
|
305
307
|
includeSpamTrash: z.boolean().optional().describe("Include messages from SPAM and TRASH"),
|
|
306
|
-
|
|
308
|
+
format: z.enum(['full', 'clean', 'metadata']).optional().describe("When set, auto-fetches full message details. metadata=headers only (default when set), clean=with body, full=raw MIME tree."),
|
|
309
|
+
maxBodyChars: z.number().optional().default(3000).describe("Max body characters in clean mode. 0 = unlimited."),
|
|
310
|
+
includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
|
|
307
311
|
}),
|
|
308
312
|
execute: async (params) => {
|
|
309
313
|
const gmail = await getGmailClient();
|
|
310
|
-
const { data } = await gmail.users.messages.list({
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
314
|
+
const { data } = await gmail.users.messages.list({
|
|
315
|
+
userId: 'me',
|
|
316
|
+
maxResults: params.maxResults,
|
|
317
|
+
pageToken: params.pageToken,
|
|
318
|
+
q: params.q,
|
|
319
|
+
labelIds: params.labelIds,
|
|
320
|
+
includeSpamTrash: params.includeSpamTrash,
|
|
321
|
+
});
|
|
322
|
+
if (params.format && data.messages?.length) {
|
|
323
|
+
data.messages = await Promise.all(
|
|
324
|
+
data.messages.map(async ({ id }) => {
|
|
325
|
+
try {
|
|
326
|
+
const { data: msg } = await gmail.users.messages.get({ userId: 'me', id, format: 'full' });
|
|
327
|
+
if (params.format === 'clean') return formatMessageClean(msg, params.maxBodyChars);
|
|
328
|
+
if (params.format === 'metadata') return formatMessageMetadata(msg);
|
|
329
|
+
if (msg.payload) msg.payload = processMessagePart(msg.payload, params.includeBodyHtml);
|
|
330
|
+
return msg;
|
|
331
|
+
} catch (e) {
|
|
332
|
+
return { id, error: e.message || 'Failed to retrieve message' };
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
);
|
|
318
336
|
}
|
|
319
337
|
return JSON.stringify(data);
|
|
320
338
|
},
|
|
@@ -322,98 +340,61 @@ export function register(server) {
|
|
|
322
340
|
|
|
323
341
|
server.addTool({
|
|
324
342
|
name: 'modify_message',
|
|
325
|
-
description: 'Modify the labels on a
|
|
343
|
+
description: 'Modify the labels on one or more messages. Pass a single id or an array of ids.',
|
|
326
344
|
parameters: z.object({
|
|
327
|
-
|
|
345
|
+
ids: z.union([z.string(), z.array(z.string())]).describe("Message ID or array of message IDs to modify"),
|
|
328
346
|
addLabelIds: z.array(z.string()).optional().describe("Label IDs to add"),
|
|
329
347
|
removeLabelIds: z.array(z.string()).optional().describe("Label IDs to remove"),
|
|
330
348
|
}),
|
|
331
349
|
execute: async (params) => {
|
|
332
350
|
const gmail = await getGmailClient();
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
351
|
+
const ids = Array.isArray(params.ids) ? params.ids : [params.ids];
|
|
352
|
+
const { data } = await gmail.users.messages.batchModify({
|
|
353
|
+
userId: 'me',
|
|
354
|
+
requestBody: { ids, addLabelIds: params.addLabelIds, removeLabelIds: params.removeLabelIds },
|
|
336
355
|
});
|
|
337
|
-
return JSON.stringify(data);
|
|
356
|
+
return JSON.stringify(data || { success: true });
|
|
338
357
|
},
|
|
339
358
|
});
|
|
340
359
|
|
|
341
360
|
server.addTool({
|
|
342
361
|
name: 'delete_message',
|
|
343
|
-
description: 'Immediately and permanently delete a
|
|
362
|
+
description: 'Immediately and permanently delete one or more messages. Pass a single id or an array of ids.',
|
|
344
363
|
parameters: z.object({
|
|
345
|
-
|
|
364
|
+
ids: z.union([z.string(), z.array(z.string())]).describe("Message ID or array of message IDs to delete"),
|
|
346
365
|
}),
|
|
347
366
|
execute: async (params) => {
|
|
348
367
|
const gmail = await getGmailClient();
|
|
349
|
-
const
|
|
368
|
+
const ids = Array.isArray(params.ids) ? params.ids : [params.ids];
|
|
369
|
+
const { data } = await gmail.users.messages.batchDelete({ userId: 'me', requestBody: { ids } });
|
|
350
370
|
return JSON.stringify(data || { success: true });
|
|
351
371
|
},
|
|
352
372
|
});
|
|
353
373
|
|
|
354
374
|
server.addTool({
|
|
355
375
|
name: 'trash_message',
|
|
356
|
-
description: 'Move a message to the trash',
|
|
357
|
-
parameters: z.object({
|
|
358
|
-
id: z.string().describe("The ID of the message to move to trash"),
|
|
359
|
-
}),
|
|
360
|
-
execute: async (params) => {
|
|
361
|
-
const gmail = await getGmailClient();
|
|
362
|
-
const { data } = await gmail.users.messages.trash({ userId: 'me', id: params.id });
|
|
363
|
-
return JSON.stringify(data);
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
server.addTool({
|
|
368
|
-
name: 'untrash_message',
|
|
369
|
-
description: 'Remove a message from the trash',
|
|
376
|
+
description: 'Move a message to the trash or restore it. Use action="trash" to move to trash, action="untrash" to restore.',
|
|
370
377
|
parameters: z.object({
|
|
371
|
-
id: z.string().describe("The ID of the message
|
|
378
|
+
id: z.string().describe("The ID of the message"),
|
|
379
|
+
action: z.enum(['trash', 'untrash']).describe("'trash' to move to trash, 'untrash' to restore"),
|
|
372
380
|
}),
|
|
373
381
|
execute: async (params) => {
|
|
374
382
|
const gmail = await getGmailClient();
|
|
375
|
-
const
|
|
383
|
+
const fn = params.action === 'untrash' ? 'untrash' : 'trash';
|
|
384
|
+
const { data } = await gmail.users.messages[fn]({ userId: 'me', id: params.id });
|
|
376
385
|
return JSON.stringify(data);
|
|
377
386
|
},
|
|
378
387
|
});
|
|
379
388
|
|
|
380
|
-
server.addTool({
|
|
381
|
-
name: 'batch_delete_messages',
|
|
382
|
-
description: 'Delete multiple messages',
|
|
383
|
-
parameters: z.object({
|
|
384
|
-
ids: z.array(z.string()).describe("The IDs of the messages to delete"),
|
|
385
|
-
}),
|
|
386
|
-
execute: async (params) => {
|
|
387
|
-
const gmail = await getGmailClient();
|
|
388
|
-
const { data } = await gmail.users.messages.batchDelete({ userId: 'me', requestBody: { ids: params.ids } });
|
|
389
|
-
return JSON.stringify(data || { success: true });
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
server.addTool({
|
|
394
|
-
name: 'batch_modify_messages',
|
|
395
|
-
description: 'Modify the labels on multiple messages',
|
|
396
|
-
parameters: z.object({
|
|
397
|
-
ids: z.array(z.string()).describe("The IDs of the messages to modify"),
|
|
398
|
-
addLabelIds: z.array(z.string()).optional().describe("Label IDs to add"),
|
|
399
|
-
removeLabelIds: z.array(z.string()).optional().describe("Label IDs to remove"),
|
|
400
|
-
}),
|
|
401
|
-
execute: async (params) => {
|
|
402
|
-
const gmail = await getGmailClient();
|
|
403
|
-
const { data } = await gmail.users.messages.batchModify({
|
|
404
|
-
userId: 'me',
|
|
405
|
-
requestBody: { ids: params.ids, addLabelIds: params.addLabelIds, removeLabelIds: params.removeLabelIds }
|
|
406
|
-
});
|
|
407
|
-
return JSON.stringify(data || { success: true });
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
389
|
|
|
411
390
|
server.addTool({
|
|
412
391
|
name: 'batch_get_messages',
|
|
413
392
|
description: 'Get multiple messages by ID in parallel. More efficient than calling get_message multiple times.',
|
|
414
393
|
parameters: z.object({
|
|
415
394
|
ids: z.array(z.string()).describe("The IDs of the messages to retrieve"),
|
|
416
|
-
|
|
395
|
+
format: z.enum(['full', 'clean', 'metadata']).optional().default('clean').describe("Response format: clean (default), metadata (headers only), or full (raw MIME tree)"),
|
|
396
|
+
maxBodyChars: z.number().optional().default(3000).describe("Max body characters in clean mode. 0 = unlimited."),
|
|
397
|
+
includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
|
|
417
398
|
}),
|
|
418
399
|
execute: async (params) => {
|
|
419
400
|
const gmail = await getGmailClient();
|
|
@@ -421,9 +402,9 @@ export function register(server) {
|
|
|
421
402
|
params.ids.map(async (id) => {
|
|
422
403
|
try {
|
|
423
404
|
const { data } = await gmail.users.messages.get({ userId: 'me', id, format: 'full' });
|
|
424
|
-
if (data.
|
|
425
|
-
|
|
426
|
-
|
|
405
|
+
if (params.format === 'clean') return formatMessageClean(data, params.maxBodyChars);
|
|
406
|
+
if (params.format === 'metadata') return formatMessageMetadata(data);
|
|
407
|
+
if (data.payload) data.payload = processMessagePart(data.payload, params.includeBodyHtml);
|
|
427
408
|
return data;
|
|
428
409
|
} catch (error) {
|
|
429
410
|
return { id, error: error.message || 'Failed to retrieve message' };
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
// Gmail Thread tools
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getGmailClient } from '../../clients.js';
|
|
4
|
-
import { processMessagePart } from '../../helpers.js';
|
|
4
|
+
import { processMessagePart, formatMessageClean, formatMessageMetadata } from '../../helpers.js';
|
|
5
5
|
|
|
6
6
|
export function register(server) {
|
|
7
7
|
server.addTool({
|
|
8
8
|
name: 'get_thread',
|
|
9
|
-
description: 'Get a specific thread by ID',
|
|
9
|
+
description: 'Get a specific thread by ID. format="clean" (default) returns each message as from/to/subject/date/body. format="metadata" returns headers only. format="full" returns raw MIME trees.',
|
|
10
10
|
parameters: z.object({
|
|
11
11
|
id: z.string().describe("The ID of the thread to retrieve"),
|
|
12
|
-
|
|
12
|
+
format: z.enum(['full', 'clean', 'metadata']).optional().default('clean').describe("Response format for each message: clean (default), metadata (headers only), or full (raw MIME tree)"),
|
|
13
|
+
maxBodyChars: z.number().optional().default(3000).describe("Max body characters per message in clean mode. 0 = unlimited."),
|
|
14
|
+
includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
|
|
13
15
|
}),
|
|
14
16
|
execute: async (params) => {
|
|
15
17
|
const gmail = await getGmailClient();
|
|
16
18
|
const { data } = await gmail.users.threads.get({ userId: 'me', id: params.id, format: 'full' });
|
|
17
19
|
if (data.messages) {
|
|
18
20
|
data.messages = data.messages.map(message => {
|
|
19
|
-
if (message.
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
if (params.format === 'clean') return formatMessageClean(message, params.maxBodyChars);
|
|
22
|
+
if (params.format === 'metadata') return formatMessageMetadata(message);
|
|
23
|
+
if (message.payload) message.payload = processMessagePart(message.payload, params.includeBodyHtml);
|
|
22
24
|
return message;
|
|
23
25
|
});
|
|
24
26
|
}
|
|
@@ -28,30 +30,46 @@ export function register(server) {
|
|
|
28
30
|
|
|
29
31
|
server.addTool({
|
|
30
32
|
name: 'list_threads',
|
|
31
|
-
description: 'List threads in the user\'s mailbox',
|
|
33
|
+
description: 'List threads in the user\'s mailbox. format="metadata" (default) auto-fetches thread details with headers only. format="clean" includes message bodies. format="full" returns raw MIME data. Omit format to get bare thread stubs (id/snippet only).',
|
|
32
34
|
parameters: z.object({
|
|
33
35
|
maxResults: z.number().optional().describe("Maximum number of threads to return"),
|
|
34
36
|
pageToken: z.string().optional().describe("Page token to retrieve a specific page of results"),
|
|
35
37
|
q: z.string().optional().describe("Only return threads matching the specified query"),
|
|
36
38
|
labelIds: z.array(z.string()).optional().describe("Only return threads with labels that match all specified label IDs"),
|
|
37
39
|
includeSpamTrash: z.boolean().optional().describe("Include threads from SPAM and TRASH"),
|
|
38
|
-
|
|
40
|
+
format: z.enum(['full', 'clean', 'metadata']).optional().describe("When set, auto-fetches full thread details. metadata=headers only (default when set), clean=with bodies, full=raw MIME tree."),
|
|
41
|
+
maxBodyChars: z.number().optional().default(3000).describe("Max body characters per message in clean mode. 0 = unlimited."),
|
|
42
|
+
includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
|
|
39
43
|
}),
|
|
40
44
|
execute: async (params) => {
|
|
41
45
|
const gmail = await getGmailClient();
|
|
42
|
-
const { data } = await gmail.users.threads.list({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const { data } = await gmail.users.threads.list({
|
|
47
|
+
userId: 'me',
|
|
48
|
+
maxResults: params.maxResults,
|
|
49
|
+
pageToken: params.pageToken,
|
|
50
|
+
q: params.q,
|
|
51
|
+
labelIds: params.labelIds,
|
|
52
|
+
includeSpamTrash: params.includeSpamTrash,
|
|
53
|
+
});
|
|
54
|
+
if (params.format && data.threads?.length) {
|
|
55
|
+
data.threads = await Promise.all(
|
|
56
|
+
data.threads.map(async ({ id }) => {
|
|
57
|
+
try {
|
|
58
|
+
const { data: thread } = await gmail.users.threads.get({ userId: 'me', id, format: 'full' });
|
|
59
|
+
if (thread.messages) {
|
|
60
|
+
thread.messages = thread.messages.map(message => {
|
|
61
|
+
if (params.format === 'clean') return formatMessageClean(message, params.maxBodyChars);
|
|
62
|
+
if (params.format === 'metadata') return formatMessageMetadata(message);
|
|
63
|
+
if (message.payload) message.payload = processMessagePart(message.payload, params.includeBodyHtml);
|
|
64
|
+
return message;
|
|
65
|
+
});
|
|
49
66
|
}
|
|
50
|
-
return
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
return thread;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return { id, error: e.message || 'Failed to retrieve thread' };
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
);
|
|
55
73
|
}
|
|
56
74
|
return JSON.stringify(data);
|
|
57
75
|
},
|
|
@@ -62,7 +80,9 @@ export function register(server) {
|
|
|
62
80
|
description: 'Get multiple threads by ID in parallel. More efficient than calling get_thread multiple times.',
|
|
63
81
|
parameters: z.object({
|
|
64
82
|
ids: z.array(z.string()).describe("The IDs of the threads to retrieve"),
|
|
65
|
-
|
|
83
|
+
format: z.enum(['full', 'clean', 'metadata']).optional().default('clean').describe("Response format for each message: clean (default), metadata (headers only), or full (raw MIME tree)"),
|
|
84
|
+
maxBodyChars: z.number().optional().default(3000).describe("Max body characters per message in clean mode. 0 = unlimited."),
|
|
85
|
+
includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
|
|
66
86
|
}),
|
|
67
87
|
execute: async (params) => {
|
|
68
88
|
const gmail = await getGmailClient();
|
|
@@ -72,9 +92,9 @@ export function register(server) {
|
|
|
72
92
|
const { data } = await gmail.users.threads.get({ userId: 'me', id, format: 'full' });
|
|
73
93
|
if (data.messages) {
|
|
74
94
|
data.messages = data.messages.map(message => {
|
|
75
|
-
if (message.
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
if (params.format === 'clean') return formatMessageClean(message, params.maxBodyChars);
|
|
96
|
+
if (params.format === 'metadata') return formatMessageMetadata(message);
|
|
97
|
+
if (message.payload) message.payload = processMessagePart(message.payload, params.includeBodyHtml);
|
|
78
98
|
return message;
|
|
79
99
|
});
|
|
80
100
|
}
|
|
@@ -119,26 +139,15 @@ export function register(server) {
|
|
|
119
139
|
|
|
120
140
|
server.addTool({
|
|
121
141
|
name: 'trash_thread',
|
|
122
|
-
description: 'Move a thread to the trash',
|
|
123
|
-
parameters: z.object({
|
|
124
|
-
id: z.string().describe("The ID of the thread to move to trash"),
|
|
125
|
-
}),
|
|
126
|
-
execute: async (params) => {
|
|
127
|
-
const gmail = await getGmailClient();
|
|
128
|
-
const { data } = await gmail.users.threads.trash({ userId: 'me', id: params.id });
|
|
129
|
-
return JSON.stringify(data);
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
server.addTool({
|
|
134
|
-
name: 'untrash_thread',
|
|
135
|
-
description: 'Remove a thread from the trash',
|
|
142
|
+
description: 'Move a thread to the trash or restore it. Use action="trash" to move to trash, action="untrash" to restore.',
|
|
136
143
|
parameters: z.object({
|
|
137
|
-
id: z.string().describe("The ID of the thread
|
|
144
|
+
id: z.string().describe("The ID of the thread"),
|
|
145
|
+
action: z.enum(['trash', 'untrash']).describe("'trash' to move to trash, 'untrash' to restore"),
|
|
138
146
|
}),
|
|
139
147
|
execute: async (params) => {
|
|
140
148
|
const gmail = await getGmailClient();
|
|
141
|
-
const
|
|
149
|
+
const fn = params.action === 'untrash' ? 'untrash' : 'trash';
|
|
150
|
+
const { data } = await gmail.users.threads[fn]({ userId: 'me', id: params.id });
|
|
142
151
|
return JSON.stringify(data);
|
|
143
152
|
},
|
|
144
153
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-tools-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "The easiest MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, Calendar, and Forms. 153 tools with one-click browser auth. Read Word docs, PDFs, and spreadsheets straight from Drive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|