google-tools-mcp 1.2.12 → 2.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.
Files changed (97) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +537 -276
  3. package/dist/cachedToolsList.js +52 -52
  4. package/dist/googleDocsApiHelpers.js +76 -12
  5. package/dist/helpers.js +572 -252
  6. package/dist/httpAuth.js +296 -0
  7. package/dist/index.js +162 -11
  8. package/dist/logger.js +87 -87
  9. package/dist/markdown-transformer/docsToMarkdown.js +128 -0
  10. package/dist/markdown-transformer/index.js +17 -7
  11. package/dist/markdown-transformer/markdownToDocs.js +185 -27
  12. package/dist/readTracker.js +136 -17
  13. package/dist/sessionContext.js +37 -0
  14. package/dist/setup.js +187 -9
  15. package/dist/tools/calendar/getBusy.js +64 -64
  16. package/dist/tools/calendar/getEvents.js +141 -141
  17. package/dist/tools/calendar/getFree.js +225 -225
  18. package/dist/tools/calendar/index.js +19 -19
  19. package/dist/tools/calendar/listCalendars.js +38 -38
  20. package/dist/tools/calendar/listRecurringInstances.js +83 -83
  21. package/dist/tools/calendar/manageCalendar.js +121 -121
  22. package/dist/tools/calendar/manageEvent.js +258 -258
  23. package/dist/tools/calendar/moveEvent.js +66 -66
  24. package/dist/tools/docs/addTab.js +10 -12
  25. package/dist/tools/docs/appendToGoogleDoc.js +9 -4
  26. package/dist/tools/docs/deleteRange.js +9 -4
  27. package/dist/tools/docs/findAndReplace.js +9 -4
  28. package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
  29. package/dist/tools/docs/formatting/getFormatting.js +171 -171
  30. package/dist/tools/docs/insertImage.js +15 -2
  31. package/dist/tools/docs/insertPageBreak.js +4 -1
  32. package/dist/tools/docs/insertTable.js +4 -1
  33. package/dist/tools/docs/insertTableWithData.js +4 -1
  34. package/dist/tools/docs/modifyText.js +14 -4
  35. package/dist/tools/docs/modifyText.test.js +84 -84
  36. package/dist/tools/docs/readGoogleDoc.js +71 -11
  37. package/dist/tools/docs/renameTab.js +12 -14
  38. package/dist/tools/drafts.js +165 -165
  39. package/dist/tools/drive/createDocument.js +12 -0
  40. package/dist/tools/drive/downloadFile.js +268 -268
  41. package/dist/tools/drive/getFileInfo.js +48 -48
  42. package/dist/tools/drive/getFilePath.js +50 -50
  43. package/dist/tools/drive/listDriveFiles.js +112 -112
  44. package/dist/tools/drive/listSharedDrives.js +52 -52
  45. package/dist/tools/drive/listSharedWithMe.js +97 -97
  46. package/dist/tools/drive/uploadFile.js +111 -111
  47. package/dist/tools/extras/index.js +9 -9
  48. package/dist/tools/extras/readDriveFile.js +197 -196
  49. package/dist/tools/extras/readFile.js +84 -84
  50. package/dist/tools/extras/searchFileContents.js +81 -81
  51. package/dist/tools/forms/batchUpdateForm.js +81 -81
  52. package/dist/tools/forms/createForm.js +54 -54
  53. package/dist/tools/forms/getForm.js +118 -118
  54. package/dist/tools/forms/getFormResponse.js +45 -45
  55. package/dist/tools/forms/index.js +15 -15
  56. package/dist/tools/forms/listFormResponses.js +56 -56
  57. package/dist/tools/forms/setPublishSettings.js +59 -59
  58. package/dist/tools/gmail/drafts.js +165 -165
  59. package/dist/tools/gmail/labels.js +54 -83
  60. package/dist/tools/gmail/messages.js +444 -441
  61. package/dist/tools/gmail/settings.js +358 -528
  62. package/dist/tools/gmail/threads.js +286 -164
  63. package/dist/tools/index.js +511 -473
  64. package/dist/tools/labels.js +103 -103
  65. package/dist/tools/legacyAliases.js +426 -0
  66. package/dist/tools/maps/directions.js +72 -0
  67. package/dist/tools/maps/geocode.js +16 -0
  68. package/dist/tools/maps/index.js +15 -0
  69. package/dist/tools/maps/mapsClient.js +83 -0
  70. package/dist/tools/maps/placeDetails.js +16 -0
  71. package/dist/tools/maps/reverseGeocode.js +19 -0
  72. package/dist/tools/maps/searchNearby.js +54 -0
  73. package/dist/tools/maps/searchPlaces.js +24 -0
  74. package/dist/tools/messages.js +448 -448
  75. package/dist/tools/settings.js +528 -528
  76. package/dist/tools/slides/createPresentation.js +104 -104
  77. package/dist/tools/slides/createShape.js +92 -92
  78. package/dist/tools/slides/createTextBox.js +82 -82
  79. package/dist/tools/slides/deleteSlide.js +30 -30
  80. package/dist/tools/slides/duplicateSlide.js +37 -37
  81. package/dist/tools/slides/exportThumbnail.js +42 -42
  82. package/dist/tools/slides/formatParagraph.js +72 -72
  83. package/dist/tools/slides/formatText.js +84 -84
  84. package/dist/tools/slides/getPresentation.js +87 -87
  85. package/dist/tools/slides/index.js +33 -33
  86. package/dist/tools/slides/reorderSlides.js +41 -41
  87. package/dist/tools/slides/replaceAllText.js +46 -46
  88. package/dist/tools/slides/setBackground.js +58 -58
  89. package/dist/tools/slides/speakerNotes.js +102 -102
  90. package/dist/tools/slides/styleShape.js +111 -111
  91. package/dist/tools/slides/updatePresentation.js +128 -128
  92. package/dist/tools/threads.js +145 -145
  93. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +30 -6
  94. package/dist/tools/utils/replaceDocumentWithMarkdown.js +71 -8
  95. package/dist/updateCheck.js +202 -0
  96. package/dist/workspace.js +122 -0
  97. package/package.json +82 -81
@@ -1,441 +1,444 @@
1
- // Gmail Message tools
2
- import { z } from 'zod';
3
- import { getGmailClient } from '../../clients.js';
4
- import { processMessagePart, constructRawMessage, constructRawMessageWithAttachments, findHeader, formatEmailList, getNestedHistory, getPlainTextBody, isHtmlBody, wrapTextBody, formatMessageClean, formatMessageMetadata } from '../../helpers.js';
5
-
6
- export function register(server) {
7
- server.addTool({
8
- name: 'send_message',
9
- description: 'Send an email message to specified recipients. Note the mechanics of the raw parameter.',
10
- parameters: z.object({
11
- raw: z.string().optional().describe("The entire email message in base64url encoded RFC 2822 format, ignores to, cc, bcc, subject, body if provided"),
12
- threadId: z.string().optional().describe("The thread ID to associate this message with"),
13
- to: z.array(z.string()).optional().describe("List of recipient email addresses"),
14
- cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
15
- bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
16
- subject: z.string().optional().describe("The subject of the email"),
17
- body: z.string().optional().describe("The body of the email. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted emails."),
18
- attachments: z.array(z.object({
19
- filename: z.string().describe("Attachment file name"),
20
- mimeType: z.string().describe("MIME type of the attachment"),
21
- base64Data: z.string().describe("Base64 encoded attachment data"),
22
- })).optional().describe("File attachments to include"),
23
- includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return for each body"),
24
- }),
25
- execute: async (params) => {
26
- const gmail = await getGmailClient();
27
- let raw = params.raw;
28
- if (!raw) {
29
- if (params.attachments?.length) {
30
- raw = await constructRawMessageWithAttachments(gmail, params);
31
- } else {
32
- raw = await constructRawMessage(gmail, params);
33
- }
34
- }
35
- const sendParams = { userId: 'me', requestBody: { raw } };
36
- if (params.threadId && sendParams.requestBody) {
37
- sendParams.requestBody.threadId = params.threadId;
38
- }
39
- const { data } = await gmail.users.messages.send(sendParams);
40
- if (data.payload) {
41
- data.payload = processMessagePart(data.payload, params.includeBodyHtml);
42
- }
43
- return JSON.stringify(data);
44
- },
45
- });
46
-
47
- server.addTool({
48
- name: 'reply_message',
49
- description: 'Reply to a message. Automatically handles To/Cc recipients, subject prefix, threading headers, and quoted content. Use replyAll to include all original recipients.',
50
- parameters: z.object({
51
- messageId: z.string().describe("The ID of the message to reply to"),
52
- body: z.string().describe("The reply body text. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted replies."),
53
- replyAll: z.boolean().optional().describe("If true, reply to all original recipients (To + Cc minus yourself). Default: false"),
54
- to: z.array(z.string()).optional().describe("Override recipient list (if omitted, replies to sender or Reply-To)"),
55
- cc: z.array(z.string()).optional().describe("Override CC list (if omitted and replyAll, uses original To + Cc minus yourself)"),
56
- bcc: z.array(z.string()).optional().describe("Optional BCC recipients"),
57
- attachments: z.array(z.object({
58
- filename: z.string().describe("Attachment file name"),
59
- mimeType: z.string().describe("MIME type of the attachment"),
60
- base64Data: z.string().describe("Base64 encoded attachment data"),
61
- })).optional().describe("Optional attachments to include in the reply"),
62
- includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return"),
63
- }),
64
- execute: async (params) => {
65
- const gmail = await getGmailClient();
66
- // Fetch original message
67
- const { data: original } = await gmail.users.messages.get({ userId: 'me', id: params.messageId, format: 'full' });
68
- const headers = original.payload?.headers || [];
69
- const threadId = original.threadId;
70
- // Get sender info for quoted content
71
- const fromHeader = findHeader(headers, 'reply-to') || findHeader(headers, 'from');
72
- const dateHeader = findHeader(headers, 'date');
73
- const originalSubject = findHeader(headers, 'subject') || '';
74
- const messageIdHeader = findHeader(headers, 'message-id');
75
- const referencesHeader = findHeader(headers, 'references');
76
- // Build subject
77
- let subject = originalSubject;
78
- if (!subject.toLowerCase().startsWith('re:')) {
79
- subject = `Re: ${subject}`;
80
- }
81
- // Build recipients
82
- let to = params.to;
83
- let cc = params.cc;
84
- if (!to) {
85
- to = fromHeader ? [fromHeader] : [];
86
- }
87
- if (!cc && params.replyAll) {
88
- // Get own email for exclusion
89
- const { data: profile } = await gmail.users.getProfile({ userId: 'me' });
90
- const myEmail = profile.emailAddress?.toLowerCase();
91
- const originalTo = formatEmailList(findHeader(headers, 'to'));
92
- const originalCc = formatEmailList(findHeader(headers, 'cc'));
93
- const allRecipients = [...originalTo, ...originalCc].filter(
94
- email => !email.toLowerCase().includes(myEmail)
95
- );
96
- // Remove the sender from CC since they're in To
97
- cc = allRecipients.filter(email => !to.some(t => email.toLowerCase().includes(t.toLowerCase())));
98
- }
99
- // Build threading headers
100
- const references = [];
101
- if (referencesHeader) references.push(...referencesHeader.split(' '));
102
- if (messageIdHeader) references.push(messageIdHeader);
103
- // Build quoted content from the original message
104
- let quotedContent = '';
105
- if (original.payload) {
106
- const originalBody = getNestedHistory(original.payload);
107
- if (originalBody && fromHeader && dateHeader) {
108
- quotedContent = `\n\nOn ${dateHeader} ${fromHeader} wrote:\n\n${originalBody}`;
109
- }
110
- }
111
- const fullBody = params.body + quotedContent;
112
- // Build raw message
113
- const msgHeaders = [];
114
- if (to?.length) msgHeaders.push(`To: ${to.join(', ')}`);
115
- if (cc?.length) msgHeaders.push(`Cc: ${cc.join(', ')}`);
116
- if (params.bcc?.length) msgHeaders.push(`Bcc: ${params.bcc.join(', ')}`);
117
- msgHeaders.push(`Subject: ${subject}`);
118
- if (messageIdHeader) msgHeaders.push(`In-Reply-To: ${messageIdHeader}`);
119
- if (references.length) msgHeaders.push(`References: ${references.join(' ')}`);
120
- const htmlMode = isHtmlBody(params.body);
121
- let raw;
122
- if (params.attachments?.length) {
123
- const boundary = `boundary_${Date.now()}_${Math.random().toString(36).slice(2)}`;
124
- msgHeaders.push('MIME-Version: 1.0');
125
- msgHeaders.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
126
- const parts = [];
127
- parts.push([
128
- `--${boundary}`,
129
- `Content-Type: ${htmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`,
130
- 'Content-Transfer-Encoding: base64',
131
- '',
132
- Buffer.from(fullBody).toString('base64'),
133
- ].join('\r\n'));
134
- for (const att of params.attachments) {
135
- parts.push([
136
- `--${boundary}`,
137
- `Content-Type: ${att.mimeType}; name="${att.filename}"`,
138
- 'Content-Transfer-Encoding: base64',
139
- `Content-Disposition: attachment; filename="${att.filename}"`,
140
- '',
141
- att.base64Data,
142
- ].join('\r\n'));
143
- }
144
- const rawStr = [msgHeaders.join('\r\n'), '', parts.join('\r\n'), `--${boundary}--`].join('\r\n');
145
- raw = Buffer.from(rawStr).toString('base64url').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
146
- } else {
147
- msgHeaders.push(`Content-Type: ${htmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
148
- msgHeaders.push('Content-Transfer-Encoding: quoted-printable');
149
- msgHeaders.push('MIME-Version: 1.0');
150
- const rawStr = [msgHeaders.join('\r\n'), '', htmlMode ? fullBody : wrapTextBody(fullBody)].join('\r\n');
151
- raw = Buffer.from(rawStr).toString('base64url').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
152
- }
153
- const { data } = await gmail.users.messages.send({
154
- userId: 'me',
155
- requestBody: { raw, threadId },
156
- });
157
- if (data.payload) {
158
- data.payload = processMessagePart(data.payload, params.includeBodyHtml);
159
- }
160
- return JSON.stringify(data);
161
- },
162
- });
163
-
164
- server.addTool({
165
- name: 'forward_message',
166
- description: 'Forward a message to new recipients. Includes the original message body as quoted content and re-attaches any original attachments.',
167
- parameters: z.object({
168
- messageId: z.string().describe("The ID of the message to forward"),
169
- to: z.array(z.string()).describe("Recipient email addresses to forward to"),
170
- cc: z.array(z.string()).optional().describe("CC recipient email addresses"),
171
- bcc: z.array(z.string()).optional().describe("BCC recipient email addresses"),
172
- body: z.string().optional().describe("Optional commentary to prepend above the forwarded content. Supports plain text or HTML (auto-detected)."),
173
- includeAttachments: z.boolean().optional().describe("Whether to include original attachments. Default: true"),
174
- includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return"),
175
- }),
176
- execute: async (params) => {
177
- const gmail = await getGmailClient();
178
- // Fetch original message
179
- const { data: original } = await gmail.users.messages.get({ userId: 'me', id: params.messageId, format: 'full' });
180
- const headers = original.payload?.headers || [];
181
- const originalSubject = findHeader(headers, 'subject') || '';
182
- const fromHeader = findHeader(headers, 'from') || '';
183
- const dateHeader = findHeader(headers, 'date') || '';
184
- const toHeader = findHeader(headers, 'to') || '';
185
- // Build subject
186
- let subject = originalSubject;
187
- if (!subject.toLowerCase().startsWith('fwd:')) {
188
- subject = `Fwd: ${subject}`;
189
- }
190
- // Build forwarded content
191
- const originalBody = original.payload ? getPlainTextBody(original.payload) : '';
192
- let forwardedContent = [
193
- '---------- Forwarded message ---------',
194
- `From: ${fromHeader}`,
195
- `Date: ${dateHeader}`,
196
- `Subject: ${originalSubject}`,
197
- `To: ${toHeader}`,
198
- '',
199
- originalBody,
200
- ].join('\n');
201
- const fullBody = (params.body ? params.body + '\n\n' : '') + forwardedContent;
202
- // Collect original attachments
203
- const attachments = [];
204
- const includeAttachments = params.includeAttachments !== false;
205
- if (includeAttachments && original.payload) {
206
- const collectAttachments = (part) => {
207
- if (part.filename && part.body?.attachmentId) {
208
- attachments.push({
209
- filename: part.filename,
210
- mimeType: part.mimeType || 'application/octet-stream',
211
- attachmentId: part.body.attachmentId,
212
- });
213
- }
214
- if (part.parts) part.parts.forEach(collectAttachments);
215
- };
216
- collectAttachments(original.payload);
217
- }
218
- // Fetch attachment data
219
- const attachmentParts = [];
220
- for (const att of attachments) {
221
- const { data: attData } = await gmail.users.messages.attachments.get({
222
- userId: 'me', messageId: params.messageId, id: att.attachmentId,
223
- });
224
- attachmentParts.push({
225
- filename: att.filename,
226
- mimeType: att.mimeType,
227
- base64Data: attData.data.replace(/-/g, '+').replace(/_/g, '/'),
228
- });
229
- }
230
- // Build raw message
231
- const msgHeaders = [];
232
- msgHeaders.push(`To: ${params.to.join(', ')}`);
233
- if (params.cc?.length) msgHeaders.push(`Cc: ${params.cc.join(', ')}`);
234
- if (params.bcc?.length) msgHeaders.push(`Bcc: ${params.bcc.join(', ')}`);
235
- msgHeaders.push(`Subject: ${subject}`);
236
- const fwdHtmlMode = params.body && isHtmlBody(params.body);
237
- let raw;
238
- if (attachmentParts.length) {
239
- const boundary = `boundary_${Date.now()}_${Math.random().toString(36).slice(2)}`;
240
- msgHeaders.push('MIME-Version: 1.0');
241
- msgHeaders.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
242
- const parts = [];
243
- parts.push([
244
- `--${boundary}`,
245
- `Content-Type: ${fwdHtmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`,
246
- 'Content-Transfer-Encoding: base64',
247
- '',
248
- Buffer.from(fullBody).toString('base64'),
249
- ].join('\r\n'));
250
- for (const att of attachmentParts) {
251
- parts.push([
252
- `--${boundary}`,
253
- `Content-Type: ${att.mimeType}; name="${att.filename}"`,
254
- 'Content-Transfer-Encoding: base64',
255
- `Content-Disposition: attachment; filename="${att.filename}"`,
256
- '',
257
- att.base64Data,
258
- ].join('\r\n'));
259
- }
260
- const rawStr = [msgHeaders.join('\r\n'), '', parts.join('\r\n'), `--${boundary}--`].join('\r\n');
261
- raw = Buffer.from(rawStr).toString('base64url').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
262
- } else {
263
- msgHeaders.push(`Content-Type: ${fwdHtmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
264
- msgHeaders.push('Content-Transfer-Encoding: quoted-printable');
265
- msgHeaders.push('MIME-Version: 1.0');
266
- const rawStr = [msgHeaders.join('\r\n'), '', fwdHtmlMode ? fullBody : wrapTextBody(fullBody)].join('\r\n');
267
- raw = Buffer.from(rawStr).toString('base64url').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
268
- }
269
- const { data } = await gmail.users.messages.send({
270
- userId: 'me',
271
- requestBody: { raw },
272
- });
273
- if (data.payload) {
274
- data.payload = processMessagePart(data.payload, params.includeBodyHtml);
275
- }
276
- return JSON.stringify(data);
277
- },
278
- });
279
-
280
- server.addTool({
281
- name: 'get_message',
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
- parameters: z.object({
284
- id: z.string().describe("The ID of the message to retrieve"),
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"),
288
- }),
289
- execute: async (params) => {
290
- const gmail = await getGmailClient();
291
- const { data } = await gmail.users.messages.get({ userId: 'me', id: params.id, format: 'full' });
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);
295
- return JSON.stringify(data);
296
- },
297
- });
298
-
299
- server.addTool({
300
- name: 'list_messages',
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.',
302
- parameters: z.object({
303
- maxResults: z.number().optional().describe("Maximum number of messages to return (1-500)"),
304
- pageToken: z.string().optional().describe("Page token to retrieve a specific page of results"),
305
- q: z.string().optional().describe("Only return messages matching the specified query (same format as Gmail search box)"),
306
- labelIds: z.array(z.string()).optional().describe("Only return messages with labels that match all specified label IDs"),
307
- includeSpamTrash: z.boolean().optional().describe("Include messages from SPAM and TRASH"),
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"),
311
- }),
312
- execute: async (params) => {
313
- const gmail = await getGmailClient();
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
- );
336
- }
337
- return JSON.stringify(data);
338
- },
339
- });
340
-
341
- server.addTool({
342
- name: 'modify_message',
343
- description: 'Modify the labels on one or more messages. Pass a single id or an array of ids.',
344
- parameters: z.object({
345
- ids: z.union([z.string(), z.array(z.string())]).describe("Message ID or array of message IDs to modify"),
346
- addLabelIds: z.array(z.string()).optional().describe("Label IDs to add"),
347
- removeLabelIds: z.array(z.string()).optional().describe("Label IDs to remove"),
348
- }),
349
- execute: async (params) => {
350
- const gmail = await getGmailClient();
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 },
355
- });
356
- return JSON.stringify(data || { success: true });
357
- },
358
- });
359
-
360
- server.addTool({
361
- name: 'delete_message',
362
- description: 'Immediately and permanently delete one or more messages. Pass a single id or an array of ids.',
363
- parameters: z.object({
364
- ids: z.union([z.string(), z.array(z.string())]).describe("Message ID or array of message IDs to delete"),
365
- }),
366
- execute: async (params) => {
367
- const gmail = await getGmailClient();
368
- const ids = Array.isArray(params.ids) ? params.ids : [params.ids];
369
- const { data } = await gmail.users.messages.batchDelete({ userId: 'me', requestBody: { ids } });
370
- return JSON.stringify(data || { success: true });
371
- },
372
- });
373
-
374
- server.addTool({
375
- name: 'trash_message',
376
- description: 'Move one or more messages to the trash or restore them. Pass a single id or an array of ids.',
377
- parameters: z.object({
378
- ids: z.union([z.string(), z.array(z.string())]).describe("Message ID or array of message IDs"),
379
- action: z.enum(['trash', 'untrash']).describe("'trash' to move to trash, 'untrash' to restore"),
380
- }),
381
- execute: async (params) => {
382
- const gmail = await getGmailClient();
383
- const ids = Array.isArray(params.ids) ? params.ids : [params.ids];
384
- const fn = params.action === 'untrash' ? 'untrash' : 'trash';
385
- const results = await Promise.all(
386
- ids.map(async (id) => {
387
- try {
388
- const { data } = await gmail.users.messages[fn]({ userId: 'me', id });
389
- return data;
390
- } catch (e) {
391
- return { id, error: e.message || `Failed to ${fn} message` };
392
- }
393
- })
394
- );
395
- return JSON.stringify(ids.length === 1 ? results[0] : results);
396
- },
397
- });
398
-
399
-
400
- server.addTool({
401
- name: 'batch_get_messages',
402
- description: 'Get multiple messages by ID in parallel. More efficient than calling get_message multiple times.',
403
- parameters: z.object({
404
- ids: z.array(z.string()).describe("The IDs of the messages to retrieve"),
405
- format: z.enum(['full', 'clean', 'metadata']).optional().default('clean').describe("Response format: clean (default), metadata (headers only), or full (raw MIME tree)"),
406
- maxBodyChars: z.number().optional().default(3000).describe("Max body characters in clean mode. 0 = unlimited."),
407
- includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
408
- }),
409
- execute: async (params) => {
410
- const gmail = await getGmailClient();
411
- const results = await Promise.all(
412
- params.ids.map(async (id) => {
413
- try {
414
- const { data } = await gmail.users.messages.get({ userId: 'me', id, format: 'full' });
415
- if (params.format === 'clean') return formatMessageClean(data, params.maxBodyChars);
416
- if (params.format === 'metadata') return formatMessageMetadata(data);
417
- if (data.payload) data.payload = processMessagePart(data.payload, params.includeBodyHtml);
418
- return data;
419
- } catch (error) {
420
- return { id, error: error.message || 'Failed to retrieve message' };
421
- }
422
- })
423
- );
424
- return JSON.stringify(results);
425
- },
426
- });
427
-
428
- server.addTool({
429
- name: 'get_attachment',
430
- description: 'Get a message attachment',
431
- parameters: z.object({
432
- messageId: z.string().describe("ID of the message containing the attachment"),
433
- id: z.string().describe("The ID of the attachment"),
434
- }),
435
- execute: async (params) => {
436
- const gmail = await getGmailClient();
437
- const { data } = await gmail.users.messages.attachments.get({ userId: 'me', messageId: params.messageId, id: params.id });
438
- return JSON.stringify(data);
439
- },
440
- });
441
- }
1
+ // Gmail Message tools
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ import { processMessagePart, constructRawMessage, constructRawMessageWithAttachments, findHeader, foldHeader, formatEmailList, getNestedHistory, getPlainTextBody, isHtmlBody, wrapTextBody, formatMessageClean, formatMessageMetadata } from '../../helpers.js';
5
+
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'sendMessage',
9
+ description: 'Send an email message to specified recipients. Note the mechanics of the raw parameter.',
10
+ parameters: z.object({
11
+ raw: z.string().optional().describe("The entire email message in base64url encoded RFC 2822 format, ignores to, cc, bcc, subject, body if provided"),
12
+ threadId: z.string().optional().describe("The thread ID to associate this message with"),
13
+ to: z.array(z.string()).optional().describe("List of recipient email addresses"),
14
+ cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
15
+ bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
16
+ subject: z.string().optional().describe("The subject of the email"),
17
+ body: z.string().optional().describe("The body of the email. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted emails."),
18
+ attachments: z.array(z.object({
19
+ filename: z.string().describe("Attachment file name"),
20
+ mimeType: z.string().describe("MIME type of the attachment"),
21
+ base64Data: z.string().describe("Base64 encoded attachment data"),
22
+ })).optional().describe("File attachments to include"),
23
+ includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return for each body"),
24
+ }),
25
+ execute: async (params) => {
26
+ const gmail = await getGmailClient();
27
+ let raw = params.raw;
28
+ if (!raw) {
29
+ if (params.attachments?.length) {
30
+ raw = await constructRawMessageWithAttachments(gmail, params);
31
+ } else {
32
+ raw = await constructRawMessage(gmail, params);
33
+ }
34
+ }
35
+ const sendParams = { userId: 'me', requestBody: { raw } };
36
+ if (params.threadId && sendParams.requestBody) {
37
+ sendParams.requestBody.threadId = params.threadId;
38
+ }
39
+ const { data } = await gmail.users.messages.send(sendParams);
40
+ if (data.payload) {
41
+ data.payload = processMessagePart(data.payload, params.includeBodyHtml);
42
+ }
43
+ return JSON.stringify(data);
44
+ },
45
+ });
46
+
47
+ server.addTool({
48
+ name: 'replyMessage',
49
+ description: 'Reply to a message. Automatically handles To/Cc recipients, subject prefix, threading headers, and quoted content. Use replyAll to include all original recipients.',
50
+ parameters: z.object({
51
+ messageId: z.string().describe("The ID of the message to reply to"),
52
+ body: z.string().describe("The reply body text. Supports plain text or HTML (auto-detected). Use HTML tags like <p>, <br>, <b> for formatted replies."),
53
+ replyAll: z.boolean().optional().describe("If true, reply to all original recipients (To + Cc minus yourself). Default: false"),
54
+ to: z.array(z.string()).optional().describe("Override recipient list (if omitted, replies to sender or Reply-To)"),
55
+ cc: z.array(z.string()).optional().describe("Override CC list (if omitted and replyAll, uses original To + Cc minus yourself)"),
56
+ bcc: z.array(z.string()).optional().describe("Optional BCC recipients"),
57
+ attachments: z.array(z.object({
58
+ filename: z.string().describe("Attachment file name"),
59
+ mimeType: z.string().describe("MIME type of the attachment"),
60
+ base64Data: z.string().describe("Base64 encoded attachment data"),
61
+ })).optional().describe("Optional attachments to include in the reply"),
62
+ includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return"),
63
+ }),
64
+ execute: async (params) => {
65
+ const gmail = await getGmailClient();
66
+ // Fetch original message
67
+ const { data: original } = await gmail.users.messages.get({ userId: 'me', id: params.messageId, format: 'full' });
68
+ const headers = original.payload?.headers || [];
69
+ const threadId = original.threadId;
70
+ // Get sender info for quoted content
71
+ const fromHeader = findHeader(headers, 'reply-to') || findHeader(headers, 'from');
72
+ const dateHeader = findHeader(headers, 'date');
73
+ const originalSubject = findHeader(headers, 'subject') || '';
74
+ const messageIdHeader = findHeader(headers, 'message-id');
75
+ const referencesHeader = findHeader(headers, 'references');
76
+ // Build subject
77
+ let subject = originalSubject;
78
+ if (!subject.toLowerCase().startsWith('re:')) {
79
+ subject = `Re: ${subject}`;
80
+ }
81
+ // Build recipients
82
+ let to = params.to;
83
+ let cc = params.cc;
84
+ if (!to) {
85
+ to = fromHeader ? [fromHeader] : [];
86
+ }
87
+ if (!cc && params.replyAll) {
88
+ // Get own email for exclusion
89
+ const { data: profile } = await gmail.users.getProfile({ userId: 'me' });
90
+ const myEmail = profile.emailAddress?.toLowerCase();
91
+ const originalTo = formatEmailList(findHeader(headers, 'to'));
92
+ const originalCc = formatEmailList(findHeader(headers, 'cc'));
93
+ const allRecipients = [...originalTo, ...originalCc].filter(
94
+ email => !email.toLowerCase().includes(myEmail)
95
+ );
96
+ // Remove the sender from CC since they're in To
97
+ cc = allRecipients.filter(email => !to.some(t => email.toLowerCase().includes(t.toLowerCase())));
98
+ }
99
+ // Build threading headers
100
+ const references = [];
101
+ if (referencesHeader) references.push(...referencesHeader.split(' '));
102
+ if (messageIdHeader) references.push(messageIdHeader);
103
+ // Build quoted content from the original message
104
+ let quotedContent = '';
105
+ if (original.payload) {
106
+ const originalBody = getNestedHistory(original.payload);
107
+ if (originalBody && fromHeader && dateHeader) {
108
+ quotedContent = `\n\nOn ${dateHeader} ${fromHeader} wrote:\n\n${originalBody}`;
109
+ }
110
+ }
111
+ const fullBody = params.body + quotedContent;
112
+ // Build raw message
113
+ const msgHeaders = [];
114
+ if (to?.length) msgHeaders.push(foldHeader('To', to.join(', ')));
115
+ if (cc?.length) msgHeaders.push(foldHeader('Cc', cc.join(', ')));
116
+ if (params.bcc?.length) msgHeaders.push(foldHeader('Bcc', params.bcc.join(', ')));
117
+ msgHeaders.push(foldHeader('Subject', subject));
118
+ if (messageIdHeader) msgHeaders.push(foldHeader('In-Reply-To', messageIdHeader));
119
+ if (references.length) msgHeaders.push(foldHeader('References', references.join(' ')));
120
+ const htmlMode = isHtmlBody(params.body);
121
+ let raw;
122
+ if (params.attachments?.length) {
123
+ const boundary = `boundary_${Date.now()}_${Math.random().toString(36).slice(2)}`;
124
+ msgHeaders.push('MIME-Version: 1.0');
125
+ msgHeaders.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
126
+ const parts = [];
127
+ parts.push([
128
+ `--${boundary}`,
129
+ `Content-Type: ${htmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`,
130
+ 'Content-Transfer-Encoding: base64',
131
+ '',
132
+ Buffer.from(fullBody).toString('base64'),
133
+ ].join('\r\n'));
134
+ for (const att of params.attachments) {
135
+ parts.push([
136
+ `--${boundary}`,
137
+ `Content-Type: ${att.mimeType}; name="${att.filename}"`,
138
+ 'Content-Transfer-Encoding: base64',
139
+ `Content-Disposition: attachment; filename="${att.filename}"`,
140
+ '',
141
+ att.base64Data,
142
+ ].join('\r\n'));
143
+ }
144
+ const rawStr = [msgHeaders.join('\r\n'), '', parts.join('\r\n'), `--${boundary}--`].join('\r\n');
145
+ raw = Buffer.from(rawStr).toString('base64url').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
146
+ } else {
147
+ msgHeaders.push(`Content-Type: ${htmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
148
+ msgHeaders.push('Content-Transfer-Encoding: quoted-printable');
149
+ msgHeaders.push('MIME-Version: 1.0');
150
+ const rawStr = [msgHeaders.join('\r\n'), '', htmlMode ? fullBody : wrapTextBody(fullBody)].join('\r\n');
151
+ raw = Buffer.from(rawStr).toString('base64url').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
152
+ }
153
+ const { data } = await gmail.users.messages.send({
154
+ userId: 'me',
155
+ requestBody: { raw, threadId },
156
+ });
157
+ if (data.payload) {
158
+ data.payload = processMessagePart(data.payload, params.includeBodyHtml);
159
+ }
160
+ return JSON.stringify(data);
161
+ },
162
+ });
163
+
164
+ server.addTool({
165
+ name: 'forwardMessage',
166
+ description: 'Forward a message to new recipients. Includes the original message body as quoted content and re-attaches any original attachments.',
167
+ parameters: z.object({
168
+ messageId: z.string().describe("The ID of the message to forward"),
169
+ to: z.array(z.string()).describe("Recipient email addresses to forward to"),
170
+ cc: z.array(z.string()).optional().describe("CC recipient email addresses"),
171
+ bcc: z.array(z.string()).optional().describe("BCC recipient email addresses"),
172
+ body: z.string().optional().describe("Optional commentary to prepend above the forwarded content. Supports plain text or HTML (auto-detected)."),
173
+ includeAttachments: z.boolean().optional().describe("Whether to include original attachments. Default: true"),
174
+ includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return"),
175
+ }),
176
+ execute: async (params) => {
177
+ const gmail = await getGmailClient();
178
+ // Fetch original message
179
+ const { data: original } = await gmail.users.messages.get({ userId: 'me', id: params.messageId, format: 'full' });
180
+ const headers = original.payload?.headers || [];
181
+ const originalSubject = findHeader(headers, 'subject') || '';
182
+ const fromHeader = findHeader(headers, 'from') || '';
183
+ const dateHeader = findHeader(headers, 'date') || '';
184
+ const toHeader = findHeader(headers, 'to') || '';
185
+ // Build subject
186
+ let subject = originalSubject;
187
+ if (!subject.toLowerCase().startsWith('fwd:')) {
188
+ subject = `Fwd: ${subject}`;
189
+ }
190
+ // Build forwarded content
191
+ const originalBody = original.payload ? getPlainTextBody(original.payload) : '';
192
+ let forwardedContent = [
193
+ '---------- Forwarded message ---------',
194
+ `From: ${fromHeader}`,
195
+ `Date: ${dateHeader}`,
196
+ `Subject: ${originalSubject}`,
197
+ `To: ${toHeader}`,
198
+ '',
199
+ originalBody,
200
+ ].join('\n');
201
+ const fullBody = (params.body ? params.body + '\n\n' : '') + forwardedContent;
202
+ // Collect original attachments
203
+ const attachments = [];
204
+ const includeAttachments = params.includeAttachments !== false;
205
+ if (includeAttachments && original.payload) {
206
+ const collectAttachments = (part) => {
207
+ if (part.filename && part.body?.attachmentId) {
208
+ attachments.push({
209
+ filename: part.filename,
210
+ mimeType: part.mimeType || 'application/octet-stream',
211
+ attachmentId: part.body.attachmentId,
212
+ });
213
+ }
214
+ if (part.parts) part.parts.forEach(collectAttachments);
215
+ };
216
+ collectAttachments(original.payload);
217
+ }
218
+ // Fetch attachment data
219
+ const attachmentParts = [];
220
+ for (const att of attachments) {
221
+ const { data: attData } = await gmail.users.messages.attachments.get({
222
+ userId: 'me', messageId: params.messageId, id: att.attachmentId,
223
+ });
224
+ attachmentParts.push({
225
+ filename: att.filename,
226
+ mimeType: att.mimeType,
227
+ base64Data: attData.data.replace(/-/g, '+').replace(/_/g, '/'),
228
+ });
229
+ }
230
+ // Build raw message
231
+ const msgHeaders = [];
232
+ msgHeaders.push(foldHeader('To', params.to.join(', ')));
233
+ if (params.cc?.length) msgHeaders.push(foldHeader('Cc', params.cc.join(', ')));
234
+ if (params.bcc?.length) msgHeaders.push(foldHeader('Bcc', params.bcc.join(', ')));
235
+ msgHeaders.push(foldHeader('Subject', subject));
236
+ const fwdHtmlMode = params.body && isHtmlBody(params.body);
237
+ let raw;
238
+ if (attachmentParts.length) {
239
+ const boundary = `boundary_${Date.now()}_${Math.random().toString(36).slice(2)}`;
240
+ msgHeaders.push('MIME-Version: 1.0');
241
+ msgHeaders.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
242
+ const parts = [];
243
+ parts.push([
244
+ `--${boundary}`,
245
+ `Content-Type: ${fwdHtmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`,
246
+ 'Content-Transfer-Encoding: base64',
247
+ '',
248
+ Buffer.from(fullBody).toString('base64'),
249
+ ].join('\r\n'));
250
+ for (const att of attachmentParts) {
251
+ parts.push([
252
+ `--${boundary}`,
253
+ `Content-Type: ${att.mimeType}; name="${att.filename}"`,
254
+ 'Content-Transfer-Encoding: base64',
255
+ `Content-Disposition: attachment; filename="${att.filename}"`,
256
+ '',
257
+ att.base64Data,
258
+ ].join('\r\n'));
259
+ }
260
+ const rawStr = [msgHeaders.join('\r\n'), '', parts.join('\r\n'), `--${boundary}--`].join('\r\n');
261
+ raw = Buffer.from(rawStr).toString('base64url').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
262
+ } else {
263
+ msgHeaders.push(`Content-Type: ${fwdHtmlMode ? 'text/html' : 'text/plain'}; charset="UTF-8"`);
264
+ msgHeaders.push('Content-Transfer-Encoding: quoted-printable');
265
+ msgHeaders.push('MIME-Version: 1.0');
266
+ const rawStr = [msgHeaders.join('\r\n'), '', fwdHtmlMode ? fullBody : wrapTextBody(fullBody)].join('\r\n');
267
+ raw = Buffer.from(rawStr).toString('base64url').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
268
+ }
269
+ const { data } = await gmail.users.messages.send({
270
+ userId: 'me',
271
+ requestBody: { raw },
272
+ });
273
+ if (data.payload) {
274
+ data.payload = processMessagePart(data.payload, params.includeBodyHtml);
275
+ }
276
+ return JSON.stringify(data);
277
+ },
278
+ });
279
+
280
+ server.addTool({
281
+ name: 'getMessage',
282
+ description: 'Get a specific message by ID. Clean mode removes quoted reply history by default. Full mode returns the raw MIME tree with decoded text bodies limited by maxBodyChars.',
283
+ parameters: z.object({
284
+ id: z.string().describe("The ID of the message to retrieve"),
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 decoded chars per text body the message body in clean mode, each MIME text part in full mode; not a whole-response cap. Truncated text carries bodyTruncated + totalChars; oversized undecoded parts (e.g. HTML) are omitted with a totalChars note. 0 = unlimited."),
287
+ includeQuoted: z.boolean().optional().default(false).describe("In clean mode: include quoted reply history. Default false."),
288
+ includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
289
+ }),
290
+ execute: async (params) => {
291
+ const gmail = await getGmailClient();
292
+ const { data } = await gmail.users.messages.get({ userId: 'me', id: params.id, format: 'full' });
293
+ if (params.format === 'clean') return JSON.stringify(formatMessageClean(data, params.maxBodyChars, params.includeQuoted));
294
+ if (params.format === 'metadata') return JSON.stringify(formatMessageMetadata(data));
295
+ if (data.payload) data.payload = processMessagePart(data.payload, params.includeBodyHtml, params.maxBodyChars);
296
+ return JSON.stringify(data);
297
+ },
298
+ });
299
+
300
+ server.addTool({
301
+ name: 'listMessages',
302
+ description: 'List messages with optional filtering. Clean mode removes quoted reply history by default. Full mode limits decoded text bodies with maxBodyChars. Omit format to get bare IDs only.',
303
+ parameters: z.object({
304
+ maxResults: z.number().optional().describe("Maximum number of messages to return (1-500)"),
305
+ pageToken: z.string().optional().describe("Page token to retrieve a specific page of results"),
306
+ q: z.string().optional().describe("Only return messages matching the specified query (same format as Gmail search box)"),
307
+ labelIds: z.array(z.string()).optional().describe("Only return messages with labels that match all specified label IDs"),
308
+ includeSpamTrash: z.boolean().optional().describe("Include messages from SPAM and TRASH"),
309
+ 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."),
310
+ maxBodyChars: z.number().optional().default(3000).describe("Max decoded chars per text body — per message in clean mode, per MIME text part in full mode; not a whole-response cap. Oversized undecoded parts (e.g. HTML) are omitted with a totalChars note. 0 = unlimited."),
311
+ includeQuoted: z.boolean().optional().default(false).describe("In clean mode: include quoted reply history. Default false."),
312
+ includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
313
+ }),
314
+ execute: async (params) => {
315
+ const gmail = await getGmailClient();
316
+ const { data } = await gmail.users.messages.list({
317
+ userId: 'me',
318
+ maxResults: params.maxResults,
319
+ pageToken: params.pageToken,
320
+ q: params.q,
321
+ labelIds: params.labelIds,
322
+ includeSpamTrash: params.includeSpamTrash,
323
+ });
324
+ if (params.format && data.messages?.length) {
325
+ data.messages = await Promise.all(
326
+ data.messages.map(async ({ id }) => {
327
+ try {
328
+ const { data: msg } = await gmail.users.messages.get({ userId: 'me', id, format: 'full' });
329
+ if (params.format === 'clean') return formatMessageClean(msg, params.maxBodyChars, params.includeQuoted);
330
+ if (params.format === 'metadata') return formatMessageMetadata(msg);
331
+ if (msg.payload) msg.payload = processMessagePart(msg.payload, params.includeBodyHtml, params.maxBodyChars);
332
+ return msg;
333
+ } catch (e) {
334
+ return { id, error: e.message || 'Failed to retrieve message' };
335
+ }
336
+ })
337
+ );
338
+ }
339
+ return JSON.stringify(data);
340
+ },
341
+ });
342
+
343
+ server.addTool({
344
+ name: 'modifyMessage',
345
+ description: 'Modify the labels on one or more messages. Pass a single id or an array of ids.',
346
+ parameters: z.object({
347
+ ids: z.union([z.string(), z.array(z.string())]).describe("Message ID or array of message IDs to modify"),
348
+ addLabelIds: z.array(z.string()).optional().describe("Label IDs to add"),
349
+ removeLabelIds: z.array(z.string()).optional().describe("Label IDs to remove"),
350
+ }),
351
+ execute: async (params) => {
352
+ const gmail = await getGmailClient();
353
+ const ids = Array.isArray(params.ids) ? params.ids : [params.ids];
354
+ const { data } = await gmail.users.messages.batchModify({
355
+ userId: 'me',
356
+ requestBody: { ids, addLabelIds: params.addLabelIds, removeLabelIds: params.removeLabelIds },
357
+ });
358
+ return JSON.stringify(data || { success: true });
359
+ },
360
+ });
361
+
362
+ server.addTool({
363
+ name: 'deleteMessage',
364
+ description: 'Immediately and permanently delete one or more messages. Pass a single id or an array of ids.',
365
+ parameters: z.object({
366
+ ids: z.union([z.string(), z.array(z.string())]).describe("Message ID or array of message IDs to delete"),
367
+ }),
368
+ execute: async (params) => {
369
+ const gmail = await getGmailClient();
370
+ const ids = Array.isArray(params.ids) ? params.ids : [params.ids];
371
+ const { data } = await gmail.users.messages.batchDelete({ userId: 'me', requestBody: { ids } });
372
+ return JSON.stringify(data || { success: true });
373
+ },
374
+ });
375
+
376
+ server.addTool({
377
+ name: 'trashMessage',
378
+ description: 'Move one or more messages to the trash or restore them. Pass a single id or an array of ids.',
379
+ parameters: z.object({
380
+ ids: z.union([z.string(), z.array(z.string())]).describe("Message ID or array of message IDs"),
381
+ action: z.enum(['trash', 'untrash']).describe("'trash' to move to trash, 'untrash' to restore"),
382
+ }),
383
+ execute: async (params) => {
384
+ const gmail = await getGmailClient();
385
+ const ids = Array.isArray(params.ids) ? params.ids : [params.ids];
386
+ const fn = params.action === 'untrash' ? 'untrash' : 'trash';
387
+ const results = await Promise.all(
388
+ ids.map(async (id) => {
389
+ try {
390
+ const { data } = await gmail.users.messages[fn]({ userId: 'me', id });
391
+ return data;
392
+ } catch (e) {
393
+ return { id, error: e.message || `Failed to ${fn} message` };
394
+ }
395
+ })
396
+ );
397
+ return JSON.stringify(ids.length === 1 ? results[0] : results);
398
+ },
399
+ });
400
+
401
+
402
+ server.addTool({
403
+ name: 'batchGetMessages',
404
+ description: 'Get multiple messages by ID in parallel. Clean mode removes quoted reply history; full mode limits decoded text bodies with maxBodyChars.',
405
+ parameters: z.object({
406
+ ids: z.array(z.string()).describe("The IDs of the messages to retrieve"),
407
+ format: z.enum(['full', 'clean', 'metadata']).optional().default('clean').describe("Response format: clean (default), metadata (headers only), or full (raw MIME tree)"),
408
+ maxBodyChars: z.number().optional().default(3000).describe("Max decoded chars per text body — per message in clean mode, per MIME text part in full mode; not a whole-response cap. Oversized undecoded parts (e.g. HTML) are omitted with a totalChars note. 0 = unlimited."),
409
+ includeQuoted: z.boolean().optional().default(false).describe("In clean mode: include quoted reply history. Default false."),
410
+ includeBodyHtml: z.boolean().optional().describe("In full mode only: whether to include parsed HTML body parts"),
411
+ }),
412
+ execute: async (params) => {
413
+ const gmail = await getGmailClient();
414
+ const results = await Promise.all(
415
+ params.ids.map(async (id) => {
416
+ try {
417
+ const { data } = await gmail.users.messages.get({ userId: 'me', id, format: 'full' });
418
+ if (params.format === 'clean') return formatMessageClean(data, params.maxBodyChars, params.includeQuoted);
419
+ if (params.format === 'metadata') return formatMessageMetadata(data);
420
+ if (data.payload) data.payload = processMessagePart(data.payload, params.includeBodyHtml, params.maxBodyChars);
421
+ return data;
422
+ } catch (error) {
423
+ return { id, error: error.message || 'Failed to retrieve message' };
424
+ }
425
+ })
426
+ );
427
+ return JSON.stringify(results);
428
+ },
429
+ });
430
+
431
+ server.addTool({
432
+ name: 'getAttachment',
433
+ description: 'Get a message attachment',
434
+ parameters: z.object({
435
+ messageId: z.string().describe("ID of the message containing the attachment"),
436
+ id: z.string().describe("The ID of the attachment"),
437
+ }),
438
+ execute: async (params) => {
439
+ const gmail = await getGmailClient();
440
+ const { data } = await gmail.users.messages.attachments.get({ userId: 'me', messageId: params.messageId, id: params.id });
441
+ return JSON.stringify(data);
442
+ },
443
+ });
444
+ }