google-tools-mcp 1.2.11 → 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.
- package/LICENSE +21 -21
- package/README.md +555 -276
- package/dist/cachedToolsList.js +52 -52
- package/dist/googleDocsApiHelpers.js +76 -12
- package/dist/helpers.js +572 -252
- package/dist/httpAuth.js +296 -0
- package/dist/index.js +162 -11
- package/dist/logger.js +87 -87
- package/dist/markdown-transformer/docsToMarkdown.js +222 -21
- package/dist/markdown-transformer/index.js +19 -9
- package/dist/markdown-transformer/markdownToDocs.js +458 -28
- package/dist/readTracker.js +136 -17
- package/dist/sessionContext.js +37 -0
- package/dist/setup.js +187 -9
- package/dist/tools/calendar/getBusy.js +64 -64
- package/dist/tools/calendar/getEvents.js +141 -141
- package/dist/tools/calendar/getFree.js +225 -225
- package/dist/tools/calendar/index.js +19 -19
- package/dist/tools/calendar/listCalendars.js +38 -38
- package/dist/tools/calendar/listRecurringInstances.js +83 -83
- package/dist/tools/calendar/manageCalendar.js +121 -121
- package/dist/tools/calendar/manageEvent.js +258 -258
- package/dist/tools/calendar/moveEvent.js +66 -66
- package/dist/tools/docs/addTab.js +10 -12
- package/dist/tools/docs/appendToGoogleDoc.js +9 -4
- package/dist/tools/docs/deleteRange.js +9 -4
- package/dist/tools/docs/findAndReplace.js +9 -4
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
- package/dist/tools/docs/formatting/getFormatting.js +171 -171
- package/dist/tools/docs/insertImage.js +15 -2
- package/dist/tools/docs/insertPageBreak.js +4 -1
- package/dist/tools/docs/insertTable.js +4 -1
- package/dist/tools/docs/insertTableWithData.js +4 -1
- package/dist/tools/docs/modifyText.js +14 -4
- package/dist/tools/docs/modifyText.test.js +84 -84
- package/dist/tools/docs/readGoogleDoc.js +71 -11
- package/dist/tools/docs/renameTab.js +12 -14
- package/dist/tools/drafts.js +165 -165
- package/dist/tools/drive/createDocument.js +13 -1
- package/dist/tools/drive/downloadFile.js +268 -268
- package/dist/tools/drive/getFileInfo.js +48 -48
- package/dist/tools/drive/getFilePath.js +50 -50
- package/dist/tools/drive/listDriveFiles.js +112 -112
- package/dist/tools/drive/listSharedDrives.js +52 -52
- package/dist/tools/drive/listSharedWithMe.js +97 -97
- package/dist/tools/drive/uploadFile.js +111 -111
- package/dist/tools/extras/index.js +9 -9
- package/dist/tools/extras/readDriveFile.js +212 -206
- package/dist/tools/extras/readFile.js +84 -84
- package/dist/tools/extras/searchFileContents.js +81 -81
- package/dist/tools/forms/batchUpdateForm.js +81 -81
- package/dist/tools/forms/createForm.js +54 -54
- package/dist/tools/forms/getForm.js +118 -118
- package/dist/tools/forms/getFormResponse.js +45 -45
- package/dist/tools/forms/index.js +15 -15
- package/dist/tools/forms/listFormResponses.js +56 -56
- package/dist/tools/forms/setPublishSettings.js +59 -59
- package/dist/tools/gmail/drafts.js +165 -165
- package/dist/tools/gmail/labels.js +54 -83
- package/dist/tools/gmail/messages.js +444 -441
- package/dist/tools/gmail/settings.js +358 -528
- package/dist/tools/gmail/threads.js +286 -164
- package/dist/tools/index.js +511 -473
- package/dist/tools/labels.js +103 -103
- package/dist/tools/legacyAliases.js +426 -0
- package/dist/tools/maps/directions.js +72 -0
- package/dist/tools/maps/geocode.js +16 -0
- package/dist/tools/maps/index.js +15 -0
- package/dist/tools/maps/mapsClient.js +83 -0
- package/dist/tools/maps/placeDetails.js +16 -0
- package/dist/tools/maps/reverseGeocode.js +19 -0
- package/dist/tools/maps/searchNearby.js +54 -0
- package/dist/tools/maps/searchPlaces.js +24 -0
- package/dist/tools/messages.js +448 -448
- package/dist/tools/settings.js +528 -528
- package/dist/tools/slides/createPresentation.js +104 -104
- package/dist/tools/slides/createShape.js +92 -92
- package/dist/tools/slides/createTextBox.js +82 -82
- package/dist/tools/slides/deleteSlide.js +30 -30
- package/dist/tools/slides/duplicateSlide.js +37 -37
- package/dist/tools/slides/exportThumbnail.js +42 -42
- package/dist/tools/slides/formatParagraph.js +72 -72
- package/dist/tools/slides/formatText.js +84 -84
- package/dist/tools/slides/getPresentation.js +87 -87
- package/dist/tools/slides/index.js +33 -33
- package/dist/tools/slides/reorderSlides.js +41 -41
- package/dist/tools/slides/replaceAllText.js +46 -46
- package/dist/tools/slides/setBackground.js +58 -58
- package/dist/tools/slides/speakerNotes.js +102 -102
- package/dist/tools/slides/styleShape.js +111 -111
- package/dist/tools/slides/updatePresentation.js +128 -128
- package/dist/tools/threads.js +145 -145
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +31 -7
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +72 -9
- package/dist/updateCheck.js +202 -0
- package/dist/workspace.js +122 -0
- package/package.json +82 -80
|
@@ -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: '
|
|
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: '
|
|
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(
|
|
115
|
-
if (cc?.length) msgHeaders.push(
|
|
116
|
-
if (params.bcc?.length) msgHeaders.push(
|
|
117
|
-
msgHeaders.push(
|
|
118
|
-
if (messageIdHeader) msgHeaders.push(
|
|
119
|
-
if (references.length) msgHeaders.push(
|
|
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: '
|
|
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(
|
|
233
|
-
if (params.cc?.length) msgHeaders.push(
|
|
234
|
-
if (params.bcc?.length) msgHeaders.push(
|
|
235
|
-
msgHeaders.push(
|
|
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: '
|
|
282
|
-
description: 'Get a specific message by ID.
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
if (params.format === '
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
return msg;
|
|
331
|
-
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
return data;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
}
|