google-workspace-mcp 2.1.1 → 2.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/dist/accounts.d.ts.map +1 -1
- package/dist/accounts.js +1 -0
- package/dist/accounts.js.map +1 -1
- package/dist/excelHelpers.d.ts +108 -0
- package/dist/excelHelpers.d.ts.map +1 -0
- package/dist/excelHelpers.js +343 -0
- package/dist/excelHelpers.js.map +1 -0
- package/dist/securityHelpers.d.ts +118 -0
- package/dist/securityHelpers.d.ts.map +1 -0
- package/dist/securityHelpers.js +437 -0
- package/dist/securityHelpers.js.map +1 -0
- package/dist/server.js +22 -6
- package/dist/server.js.map +1 -1
- package/dist/serverWrapper.d.ts +9 -1
- package/dist/serverWrapper.d.ts.map +1 -1
- package/dist/serverWrapper.js +76 -7
- package/dist/serverWrapper.js.map +1 -1
- package/dist/tools/docs.tools.d.ts.map +1 -1
- package/dist/tools/docs.tools.js +29 -10
- package/dist/tools/docs.tools.js.map +1 -1
- package/dist/tools/drive.tools.d.ts.map +1 -1
- package/dist/tools/drive.tools.js +680 -6
- package/dist/tools/drive.tools.js.map +1 -1
- package/dist/tools/excel.tools.d.ts +3 -0
- package/dist/tools/excel.tools.d.ts.map +1 -0
- package/dist/tools/excel.tools.js +651 -0
- package/dist/tools/excel.tools.js.map +1 -0
- package/dist/tools/forms.tools.d.ts.map +1 -1
- package/dist/tools/forms.tools.js +13 -7
- package/dist/tools/forms.tools.js.map +1 -1
- package/dist/tools/gmail.tools.d.ts.map +1 -1
- package/dist/tools/gmail.tools.js +376 -37
- package/dist/tools/gmail.tools.js.map +1 -1
- package/dist/tools/sheets.tools.d.ts.map +1 -1
- package/dist/tools/sheets.tools.js +138 -4
- package/dist/tools/sheets.tools.js.map +1 -1
- package/dist/tools/slides.tools.d.ts.map +1 -1
- package/dist/tools/slides.tools.js +3 -1
- package/dist/tools/slides.tools.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -6
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// gmail.tools.ts - Gmail tool module
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
2
5
|
import { z } from 'zod';
|
|
3
6
|
import { formatToolError } from '../errorHelpers.js';
|
|
4
|
-
import { getGmailMessageUrl, getGmailDraftsUrl } from '../urlHelpers.js';
|
|
7
|
+
import { getGmailMessageUrl, getGmailDraftsUrl, getDriveFileUrl } from '../urlHelpers.js';
|
|
8
|
+
import { Readable } from 'stream';
|
|
9
|
+
import { validateWritePath, wrapEmailContent } from '../securityHelpers.js';
|
|
10
|
+
import { getServerConfig } from '../serverWrapper.js';
|
|
5
11
|
export function registerGmailTools(options) {
|
|
6
|
-
const { server, getGmailClient, getAccountEmail } = options;
|
|
12
|
+
const { server, getGmailClient, getDriveClient, getAccountEmail } = options;
|
|
7
13
|
server.addTool({
|
|
8
14
|
name: 'listGmailMessages',
|
|
9
15
|
description: 'List email messages from Gmail inbox or other labels. Returns message IDs and snippets.',
|
|
@@ -79,6 +85,10 @@ export function registerGmailTools(options) {
|
|
|
79
85
|
.optional()
|
|
80
86
|
.default('full')
|
|
81
87
|
.describe('Response format'),
|
|
88
|
+
maxLength: z
|
|
89
|
+
.number()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Maximum character length of the response. If the result exceeds this, it will be truncated with a notice.'),
|
|
82
92
|
}),
|
|
83
93
|
async execute(args, { log: _log }) {
|
|
84
94
|
try {
|
|
@@ -154,7 +164,13 @@ export function registerGmailTools(options) {
|
|
|
154
164
|
if (message.snippet) {
|
|
155
165
|
result += `**Snippet:** ${message.snippet}\n\n`;
|
|
156
166
|
}
|
|
157
|
-
|
|
167
|
+
// Wrap email body with security warnings to defend against prompt injection
|
|
168
|
+
const from = getHeader('From');
|
|
169
|
+
const subject = getHeader('Subject');
|
|
170
|
+
const wrappedBody = body
|
|
171
|
+
? wrapEmailContent(body, from || undefined, subject || undefined)
|
|
172
|
+
: '(empty)';
|
|
173
|
+
result += `**Body**\n${wrappedBody}\n\n`;
|
|
158
174
|
if (attachments.length > 0) {
|
|
159
175
|
result += `**Attachments (${attachments.length})**\n`;
|
|
160
176
|
attachments.forEach((att, i) => {
|
|
@@ -167,6 +183,11 @@ export function registerGmailTools(options) {
|
|
|
167
183
|
if (link) {
|
|
168
184
|
result += `View in Gmail: ${link}`;
|
|
169
185
|
}
|
|
186
|
+
// Apply maxLength truncation if specified
|
|
187
|
+
if (args.maxLength && result.length > args.maxLength) {
|
|
188
|
+
result = result.substring(0, args.maxLength);
|
|
189
|
+
result += `\n\n[...TRUNCATED at ${args.maxLength} chars. Full message has more content. Use a larger maxLength or omit it to see everything.]`;
|
|
190
|
+
}
|
|
170
191
|
return result;
|
|
171
192
|
}
|
|
172
193
|
catch (error) {
|
|
@@ -298,6 +319,78 @@ export function registerGmailTools(options) {
|
|
|
298
319
|
}
|
|
299
320
|
},
|
|
300
321
|
});
|
|
322
|
+
// --- Create Gmail Label ---
|
|
323
|
+
server.addTool({
|
|
324
|
+
name: 'createGmailLabel',
|
|
325
|
+
description: 'Create a new Gmail label (folder). Labels can be used to organize emails. After creating a label, use addGmailLabel to apply it to messages.',
|
|
326
|
+
annotations: {
|
|
327
|
+
title: 'Create Gmail Label',
|
|
328
|
+
readOnlyHint: false,
|
|
329
|
+
destructiveHint: false,
|
|
330
|
+
idempotentHint: false,
|
|
331
|
+
openWorldHint: true,
|
|
332
|
+
},
|
|
333
|
+
parameters: z.object({
|
|
334
|
+
account: z.string().describe('Account name to use'),
|
|
335
|
+
name: z
|
|
336
|
+
.string()
|
|
337
|
+
.describe('Name of the label to create. Use "/" for nested labels (e.g., "Work/Projects")'),
|
|
338
|
+
labelListVisibility: z
|
|
339
|
+
.enum(['labelShow', 'labelShowIfUnread', 'labelHide'])
|
|
340
|
+
.optional()
|
|
341
|
+
.default('labelShow')
|
|
342
|
+
.describe('Whether to show the label in the label list: labelShow (always), labelShowIfUnread (only when unread), labelHide (hidden)'),
|
|
343
|
+
messageListVisibility: z
|
|
344
|
+
.enum(['show', 'hide'])
|
|
345
|
+
.optional()
|
|
346
|
+
.default('show')
|
|
347
|
+
.describe('Whether to show the label in the message list'),
|
|
348
|
+
backgroundColor: z
|
|
349
|
+
.string()
|
|
350
|
+
.optional()
|
|
351
|
+
.describe('Background color in hex format (e.g., "#16a765")'),
|
|
352
|
+
textColor: z.string().optional().describe('Text color in hex format (e.g., "#ffffff")'),
|
|
353
|
+
}),
|
|
354
|
+
async execute(args, { log: _log }) {
|
|
355
|
+
try {
|
|
356
|
+
const gmail = await getGmailClient(args.account);
|
|
357
|
+
const labelColor = args.backgroundColor || args.textColor
|
|
358
|
+
? {
|
|
359
|
+
backgroundColor: args.backgroundColor,
|
|
360
|
+
textColor: args.textColor,
|
|
361
|
+
}
|
|
362
|
+
: undefined;
|
|
363
|
+
const response = await gmail.users.labels.create({
|
|
364
|
+
userId: 'me',
|
|
365
|
+
requestBody: {
|
|
366
|
+
name: args.name,
|
|
367
|
+
labelListVisibility: args.labelListVisibility,
|
|
368
|
+
messageListVisibility: args.messageListVisibility,
|
|
369
|
+
color: labelColor,
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
const label = response.data;
|
|
373
|
+
let result = `Successfully created label "${args.name}".\n\n`;
|
|
374
|
+
result += `Label ID: ${label.id}\n`;
|
|
375
|
+
result += `Name: ${label.name}\n`;
|
|
376
|
+
result += `Type: ${label.type}\n`;
|
|
377
|
+
if (label.labelListVisibility) {
|
|
378
|
+
result += `Label List Visibility: ${label.labelListVisibility}\n`;
|
|
379
|
+
}
|
|
380
|
+
if (label.messageListVisibility) {
|
|
381
|
+
result += `Message List Visibility: ${label.messageListVisibility}\n`;
|
|
382
|
+
}
|
|
383
|
+
if (label.color) {
|
|
384
|
+
result += `Color: ${label.color.backgroundColor || 'default'} / ${label.color.textColor || 'default'}\n`;
|
|
385
|
+
}
|
|
386
|
+
result += '\nUse this Label ID with addGmailLabel to apply it to messages.';
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
throw new Error(formatToolError('createGmailLabel', error));
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
});
|
|
301
394
|
// --- Add Gmail Label ---
|
|
302
395
|
server.addTool({
|
|
303
396
|
name: 'addGmailLabel',
|
|
@@ -463,26 +556,26 @@ export function registerGmailTools(options) {
|
|
|
463
556
|
emailContent += `In-Reply-To: ${inReplyTo}\r\n`;
|
|
464
557
|
if (references)
|
|
465
558
|
emailContent += `References: ${references}\r\n`;
|
|
466
|
-
emailContent +=
|
|
559
|
+
emailContent += 'MIME-Version: 1.0\r\n';
|
|
467
560
|
emailContent += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`;
|
|
468
|
-
emailContent +=
|
|
561
|
+
emailContent += '\r\n';
|
|
469
562
|
// Body part
|
|
470
563
|
emailContent += `--${boundary}\r\n`;
|
|
471
564
|
emailContent += `Content-Type: ${args.isHtml ? 'text/html' : 'text/plain'}; charset=utf-8\r\n`;
|
|
472
|
-
emailContent +=
|
|
473
|
-
emailContent +=
|
|
565
|
+
emailContent += 'Content-Transfer-Encoding: 7bit\r\n';
|
|
566
|
+
emailContent += '\r\n';
|
|
474
567
|
emailContent += `${args.body}\r\n`;
|
|
475
568
|
// Attachment parts
|
|
476
569
|
for (const attachment of args.attachments) {
|
|
477
570
|
emailContent += `--${boundary}\r\n`;
|
|
478
571
|
emailContent += `Content-Type: ${attachment.mimeType}; name="${attachment.filename}"\r\n`;
|
|
479
572
|
emailContent += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
|
|
480
|
-
emailContent +=
|
|
481
|
-
emailContent +=
|
|
573
|
+
emailContent += 'Content-Transfer-Encoding: base64\r\n';
|
|
574
|
+
emailContent += '\r\n';
|
|
482
575
|
// Split base64 data into 76-character lines per RFC 2045
|
|
483
576
|
const base64Lines = attachment.base64Data.match(/.{1,76}/g) || [];
|
|
484
577
|
emailContent += base64Lines.join('\r\n');
|
|
485
|
-
emailContent +=
|
|
578
|
+
emailContent += '\r\n';
|
|
486
579
|
}
|
|
487
580
|
emailContent += `--${boundary}--\r\n`;
|
|
488
581
|
}
|
|
@@ -768,26 +861,26 @@ export function registerGmailTools(options) {
|
|
|
768
861
|
if (newBcc)
|
|
769
862
|
emailContent += `Bcc: ${newBcc}\r\n`;
|
|
770
863
|
emailContent += `Subject: ${newSubject}\r\n`;
|
|
771
|
-
emailContent +=
|
|
864
|
+
emailContent += 'MIME-Version: 1.0\r\n';
|
|
772
865
|
emailContent += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`;
|
|
773
|
-
emailContent +=
|
|
866
|
+
emailContent += '\r\n';
|
|
774
867
|
// Body part
|
|
775
868
|
emailContent += `--${boundary}\r\n`;
|
|
776
869
|
emailContent += `Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8\r\n`;
|
|
777
|
-
emailContent +=
|
|
778
|
-
emailContent +=
|
|
870
|
+
emailContent += 'Content-Transfer-Encoding: 7bit\r\n';
|
|
871
|
+
emailContent += '\r\n';
|
|
779
872
|
emailContent += `${newBody}\r\n`;
|
|
780
873
|
// Attachment parts
|
|
781
874
|
for (const attachment of args.attachments) {
|
|
782
875
|
emailContent += `--${boundary}\r\n`;
|
|
783
876
|
emailContent += `Content-Type: ${attachment.mimeType}; name="${attachment.filename}"\r\n`;
|
|
784
877
|
emailContent += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
|
|
785
|
-
emailContent +=
|
|
786
|
-
emailContent +=
|
|
878
|
+
emailContent += 'Content-Transfer-Encoding: base64\r\n';
|
|
879
|
+
emailContent += '\r\n';
|
|
787
880
|
// Split base64 data into 76-character lines per RFC 2045
|
|
788
881
|
const base64Lines = attachment.base64Data.match(/.{1,76}/g) || [];
|
|
789
882
|
emailContent += base64Lines.join('\r\n');
|
|
790
|
-
emailContent +=
|
|
883
|
+
emailContent += '\r\n';
|
|
791
884
|
}
|
|
792
885
|
emailContent += `--${boundary}--\r\n`;
|
|
793
886
|
}
|
|
@@ -943,25 +1036,25 @@ export function registerGmailTools(options) {
|
|
|
943
1036
|
if (getCurrentHeader('Bcc'))
|
|
944
1037
|
emailContent += `Bcc: ${getCurrentHeader('Bcc')}\r\n`;
|
|
945
1038
|
emailContent += `Subject: ${getCurrentHeader('Subject')}\r\n`;
|
|
946
|
-
emailContent +=
|
|
1039
|
+
emailContent += 'MIME-Version: 1.0\r\n';
|
|
947
1040
|
emailContent += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`;
|
|
948
|
-
emailContent +=
|
|
1041
|
+
emailContent += '\r\n';
|
|
949
1042
|
// Body part
|
|
950
1043
|
emailContent += `--${boundary}\r\n`;
|
|
951
1044
|
emailContent += `Content-Type: ${bodyMimeType}; charset=utf-8\r\n`;
|
|
952
|
-
emailContent +=
|
|
953
|
-
emailContent +=
|
|
1045
|
+
emailContent += 'Content-Transfer-Encoding: 7bit\r\n';
|
|
1046
|
+
emailContent += '\r\n';
|
|
954
1047
|
emailContent += `${bodyContent}\r\n`;
|
|
955
1048
|
// Attachment parts
|
|
956
1049
|
for (const attachment of existingAttachments) {
|
|
957
1050
|
emailContent += `--${boundary}\r\n`;
|
|
958
1051
|
emailContent += `Content-Type: ${attachment.mimeType}; name="${attachment.filename}"\r\n`;
|
|
959
1052
|
emailContent += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
|
|
960
|
-
emailContent +=
|
|
961
|
-
emailContent +=
|
|
1053
|
+
emailContent += 'Content-Transfer-Encoding: base64\r\n';
|
|
1054
|
+
emailContent += '\r\n';
|
|
962
1055
|
const base64Lines = attachment.data.match(/.{1,76}/g) || [];
|
|
963
1056
|
emailContent += base64Lines.join('\r\n');
|
|
964
|
-
emailContent +=
|
|
1057
|
+
emailContent += '\r\n';
|
|
965
1058
|
}
|
|
966
1059
|
emailContent += `--${boundary}--\r\n`;
|
|
967
1060
|
const encodedEmail = Buffer.from(emailContent)
|
|
@@ -1088,25 +1181,25 @@ export function registerGmailTools(options) {
|
|
|
1088
1181
|
if (getCurrentHeader('Bcc'))
|
|
1089
1182
|
emailContent += `Bcc: ${getCurrentHeader('Bcc')}\r\n`;
|
|
1090
1183
|
emailContent += `Subject: ${getCurrentHeader('Subject')}\r\n`;
|
|
1091
|
-
emailContent +=
|
|
1184
|
+
emailContent += 'MIME-Version: 1.0\r\n';
|
|
1092
1185
|
emailContent += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`;
|
|
1093
|
-
emailContent +=
|
|
1186
|
+
emailContent += '\r\n';
|
|
1094
1187
|
// Body part
|
|
1095
1188
|
emailContent += `--${boundary}\r\n`;
|
|
1096
1189
|
emailContent += `Content-Type: ${bodyMimeType}; charset=utf-8\r\n`;
|
|
1097
|
-
emailContent +=
|
|
1098
|
-
emailContent +=
|
|
1190
|
+
emailContent += 'Content-Transfer-Encoding: 7bit\r\n';
|
|
1191
|
+
emailContent += '\r\n';
|
|
1099
1192
|
emailContent += `${bodyContent}\r\n`;
|
|
1100
1193
|
// Remaining attachment parts
|
|
1101
1194
|
for (const attachment of existingAttachments) {
|
|
1102
1195
|
emailContent += `--${boundary}\r\n`;
|
|
1103
1196
|
emailContent += `Content-Type: ${attachment.mimeType}; name="${attachment.filename}"\r\n`;
|
|
1104
1197
|
emailContent += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
|
|
1105
|
-
emailContent +=
|
|
1106
|
-
emailContent +=
|
|
1198
|
+
emailContent += 'Content-Transfer-Encoding: base64\r\n';
|
|
1199
|
+
emailContent += '\r\n';
|
|
1107
1200
|
const base64Lines = attachment.data.match(/.{1,76}/g) || [];
|
|
1108
1201
|
emailContent += base64Lines.join('\r\n');
|
|
1109
|
-
emailContent +=
|
|
1202
|
+
emailContent += '\r\n';
|
|
1110
1203
|
}
|
|
1111
1204
|
emailContent += `--${boundary}--\r\n`;
|
|
1112
1205
|
}
|
|
@@ -1256,7 +1349,7 @@ export function registerGmailTools(options) {
|
|
|
1256
1349
|
// --- Get Gmail Attachment ---
|
|
1257
1350
|
server.addTool({
|
|
1258
1351
|
name: 'getGmailAttachment',
|
|
1259
|
-
description: '
|
|
1352
|
+
description: 'Get metadata and a preview of a Gmail attachment. Returns truncated base64 data (first 500 chars). For full attachment data or saving to file, use downloadGmailAttachment instead.',
|
|
1260
1353
|
annotations: {
|
|
1261
1354
|
title: 'Get Gmail Attachment',
|
|
1262
1355
|
readOnlyHint: true,
|
|
@@ -1287,7 +1380,10 @@ export function registerGmailTools(options) {
|
|
|
1287
1380
|
// The data is already base64url encoded from Gmail API
|
|
1288
1381
|
result += `**Base64 Data (first 500 chars):**\n${attachment.data.substring(0, 500)}${attachment.data.length > 500 ? '...' : ''}\n\n`;
|
|
1289
1382
|
result += `**Full data length:** ${attachment.data.length} characters\n`;
|
|
1290
|
-
result +=
|
|
1383
|
+
result +=
|
|
1384
|
+
'\nNote: Data is base64url encoded. To decode, replace - with + and _ with /, then base64 decode.';
|
|
1385
|
+
result +=
|
|
1386
|
+
'\n\nTip: Use downloadGmailAttachment to get full data or save directly to a file.';
|
|
1291
1387
|
}
|
|
1292
1388
|
else {
|
|
1293
1389
|
result += 'No attachment data available.';
|
|
@@ -1299,6 +1395,234 @@ export function registerGmailTools(options) {
|
|
|
1299
1395
|
}
|
|
1300
1396
|
},
|
|
1301
1397
|
});
|
|
1398
|
+
// --- Download Gmail Attachment ---
|
|
1399
|
+
server.addTool({
|
|
1400
|
+
name: 'downloadGmailAttachment',
|
|
1401
|
+
description: 'Download a Gmail attachment with full data. Returns complete base64-encoded data, or saves directly to a file if savePath is provided. Use this instead of getGmailAttachment when you need the full attachment content.',
|
|
1402
|
+
annotations: {
|
|
1403
|
+
title: 'Download Gmail Attachment',
|
|
1404
|
+
readOnlyHint: true,
|
|
1405
|
+
openWorldHint: true,
|
|
1406
|
+
},
|
|
1407
|
+
parameters: z.object({
|
|
1408
|
+
account: z.string().describe('Account name to use'),
|
|
1409
|
+
messageId: z.string().describe('The ID of the message containing the attachment'),
|
|
1410
|
+
attachmentId: z
|
|
1411
|
+
.string()
|
|
1412
|
+
.describe('The attachment ID (from readGmailMessage attachment info)'),
|
|
1413
|
+
savePath: z
|
|
1414
|
+
.string()
|
|
1415
|
+
.optional()
|
|
1416
|
+
.describe('Optional file path to save the attachment to. If provided, the decoded attachment is written to this path. The path must be absolute.'),
|
|
1417
|
+
}),
|
|
1418
|
+
async execute(args, { log: _log }) {
|
|
1419
|
+
try {
|
|
1420
|
+
const gmail = await getGmailClient(args.account);
|
|
1421
|
+
// First, get attachment metadata from the message to find the filename
|
|
1422
|
+
const messageResponse = await gmail.users.messages.get({
|
|
1423
|
+
userId: 'me',
|
|
1424
|
+
id: args.messageId,
|
|
1425
|
+
format: 'full',
|
|
1426
|
+
});
|
|
1427
|
+
// Find attachment info in message parts
|
|
1428
|
+
let attachmentFilename = 'attachment';
|
|
1429
|
+
let attachmentMimeType = 'application/octet-stream';
|
|
1430
|
+
const findAttachmentInfo = (part) => {
|
|
1431
|
+
if (part.body?.attachmentId === args.attachmentId && part.filename) {
|
|
1432
|
+
attachmentFilename = part.filename;
|
|
1433
|
+
attachmentMimeType = part.mimeType || 'application/octet-stream';
|
|
1434
|
+
return true;
|
|
1435
|
+
}
|
|
1436
|
+
if (part.parts) {
|
|
1437
|
+
for (const subpart of part.parts) {
|
|
1438
|
+
if (findAttachmentInfo(subpart))
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return false;
|
|
1443
|
+
};
|
|
1444
|
+
if (messageResponse.data.payload) {
|
|
1445
|
+
findAttachmentInfo(messageResponse.data.payload);
|
|
1446
|
+
}
|
|
1447
|
+
// Get the attachment data
|
|
1448
|
+
const response = await gmail.users.messages.attachments.get({
|
|
1449
|
+
userId: 'me',
|
|
1450
|
+
messageId: args.messageId,
|
|
1451
|
+
id: args.attachmentId,
|
|
1452
|
+
});
|
|
1453
|
+
const attachment = response.data;
|
|
1454
|
+
const size = attachment.size || 0;
|
|
1455
|
+
if (!attachment.data) {
|
|
1456
|
+
throw new Error('No attachment data available');
|
|
1457
|
+
}
|
|
1458
|
+
// Convert base64url to standard base64
|
|
1459
|
+
const base64Data = attachment.data.replace(/-/g, '+').replace(/_/g, '/');
|
|
1460
|
+
let result = '**Attachment Downloaded**\n\n';
|
|
1461
|
+
result += `Filename: ${attachmentFilename}\n`;
|
|
1462
|
+
result += `MIME Type: ${attachmentMimeType}\n`;
|
|
1463
|
+
result += `Size: ${size} bytes (${(size / 1024).toFixed(2)} KB)\n`;
|
|
1464
|
+
result += `Message ID: ${args.messageId}\n`;
|
|
1465
|
+
result += `Attachment ID: ${args.attachmentId}\n\n`;
|
|
1466
|
+
if (args.savePath) {
|
|
1467
|
+
// Validate that savePath is absolute
|
|
1468
|
+
if (!path.isAbsolute(args.savePath)) {
|
|
1469
|
+
throw new Error(`savePath must be an absolute path. Received: ${args.savePath}`);
|
|
1470
|
+
}
|
|
1471
|
+
// Validate path for security
|
|
1472
|
+
const pathValidation = validateWritePath(args.savePath, getServerConfig().pathSecurity);
|
|
1473
|
+
if (!pathValidation.valid) {
|
|
1474
|
+
throw new Error(`Cannot save to this path: ${pathValidation.error}`);
|
|
1475
|
+
}
|
|
1476
|
+
// Decode and save to file
|
|
1477
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
1478
|
+
// Ensure parent directory exists
|
|
1479
|
+
const parentDir = path.dirname(pathValidation.resolvedPath);
|
|
1480
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
1481
|
+
await fs.writeFile(pathValidation.resolvedPath, buffer);
|
|
1482
|
+
result += `**Saved to:** ${pathValidation.resolvedPath}\n`;
|
|
1483
|
+
result += `File size on disk: ${buffer.length} bytes`;
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
// Auto-save large attachments to temp file to avoid blowing up LLM context
|
|
1487
|
+
const MAX_INLINE_BYTES = 1024; // 1KB threshold
|
|
1488
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
1489
|
+
if (buffer.length > MAX_INLINE_BYTES) {
|
|
1490
|
+
const tempDir = os.tmpdir();
|
|
1491
|
+
const safeName = attachmentFilename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1492
|
+
const tempPath = path.join(tempDir, `gmail-attachment-${Date.now()}-${safeName}`);
|
|
1493
|
+
await fs.writeFile(tempPath, buffer);
|
|
1494
|
+
result += `**Saved to:** ${tempPath}\n`;
|
|
1495
|
+
result += `File size on disk: ${buffer.length} bytes\n\n`;
|
|
1496
|
+
result += `⚠️ File was auto-saved to a temp path because it exceeds the ${MAX_INLINE_BYTES}-byte inline limit.\n`;
|
|
1497
|
+
result += `Use the savePath parameter to save to a specific location.`;
|
|
1498
|
+
}
|
|
1499
|
+
else {
|
|
1500
|
+
// Small enough to return inline
|
|
1501
|
+
result += `**Base64 Data (standard encoding):**\n${base64Data}`;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return result;
|
|
1505
|
+
}
|
|
1506
|
+
catch (error) {
|
|
1507
|
+
throw new Error(formatToolError('downloadGmailAttachment', error));
|
|
1508
|
+
}
|
|
1509
|
+
},
|
|
1510
|
+
});
|
|
1511
|
+
// --- Save Gmail Attachment to Drive ---
|
|
1512
|
+
server.addTool({
|
|
1513
|
+
name: 'saveAttachmentToDrive',
|
|
1514
|
+
description: 'Save a Gmail attachment directly to Google Drive. Uploads the attachment as a file to Drive without downloading locally first.',
|
|
1515
|
+
annotations: {
|
|
1516
|
+
title: 'Save Attachment to Drive',
|
|
1517
|
+
readOnlyHint: false,
|
|
1518
|
+
destructiveHint: false,
|
|
1519
|
+
idempotentHint: false,
|
|
1520
|
+
openWorldHint: true,
|
|
1521
|
+
},
|
|
1522
|
+
parameters: z.object({
|
|
1523
|
+
account: z.string().describe('Account name to use'),
|
|
1524
|
+
messageId: z.string().describe('The ID of the message containing the attachment'),
|
|
1525
|
+
attachmentId: z
|
|
1526
|
+
.string()
|
|
1527
|
+
.describe('The attachment ID (from readGmailMessage attachment info)'),
|
|
1528
|
+
fileName: z
|
|
1529
|
+
.string()
|
|
1530
|
+
.optional()
|
|
1531
|
+
.describe('Optional custom file name for the saved file. If not provided, uses the original attachment name.'),
|
|
1532
|
+
folderId: z
|
|
1533
|
+
.string()
|
|
1534
|
+
.optional()
|
|
1535
|
+
.describe('Optional Google Drive folder ID to save the file to. If not provided, saves to root of My Drive.'),
|
|
1536
|
+
}),
|
|
1537
|
+
async execute(args, { log: _log }) {
|
|
1538
|
+
try {
|
|
1539
|
+
const gmail = await getGmailClient(args.account);
|
|
1540
|
+
const drive = await getDriveClient(args.account);
|
|
1541
|
+
const accountEmail = await getAccountEmail(args.account);
|
|
1542
|
+
// First, get attachment metadata from the message to find the filename
|
|
1543
|
+
const messageResponse = await gmail.users.messages.get({
|
|
1544
|
+
userId: 'me',
|
|
1545
|
+
id: args.messageId,
|
|
1546
|
+
format: 'full',
|
|
1547
|
+
});
|
|
1548
|
+
// Find attachment info in message parts
|
|
1549
|
+
let attachmentFilename = 'attachment';
|
|
1550
|
+
let attachmentMimeType = 'application/octet-stream';
|
|
1551
|
+
const findAttachmentInfo = (part) => {
|
|
1552
|
+
if (part.body?.attachmentId === args.attachmentId && part.filename) {
|
|
1553
|
+
attachmentFilename = part.filename;
|
|
1554
|
+
attachmentMimeType = part.mimeType || 'application/octet-stream';
|
|
1555
|
+
return true;
|
|
1556
|
+
}
|
|
1557
|
+
if (part.parts) {
|
|
1558
|
+
for (const subpart of part.parts) {
|
|
1559
|
+
if (findAttachmentInfo(subpart))
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return false;
|
|
1564
|
+
};
|
|
1565
|
+
if (messageResponse.data.payload) {
|
|
1566
|
+
findAttachmentInfo(messageResponse.data.payload);
|
|
1567
|
+
}
|
|
1568
|
+
// Use custom filename if provided
|
|
1569
|
+
const finalFileName = args.fileName || attachmentFilename;
|
|
1570
|
+
// Get the attachment data
|
|
1571
|
+
const attachmentResponse = await gmail.users.messages.attachments.get({
|
|
1572
|
+
userId: 'me',
|
|
1573
|
+
messageId: args.messageId,
|
|
1574
|
+
id: args.attachmentId,
|
|
1575
|
+
});
|
|
1576
|
+
const attachment = attachmentResponse.data;
|
|
1577
|
+
const size = attachment.size || 0;
|
|
1578
|
+
if (!attachment.data) {
|
|
1579
|
+
throw new Error('No attachment data available');
|
|
1580
|
+
}
|
|
1581
|
+
// Convert base64url to standard base64, then to buffer
|
|
1582
|
+
const base64Data = attachment.data.replace(/-/g, '+').replace(/_/g, '/');
|
|
1583
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
1584
|
+
// Create a readable stream from the buffer for Drive upload
|
|
1585
|
+
const bufferStream = new Readable();
|
|
1586
|
+
bufferStream.push(buffer);
|
|
1587
|
+
bufferStream.push(null);
|
|
1588
|
+
// Upload to Google Drive
|
|
1589
|
+
const fileMetadata = {
|
|
1590
|
+
name: finalFileName,
|
|
1591
|
+
};
|
|
1592
|
+
if (args.folderId) {
|
|
1593
|
+
fileMetadata.parents = [args.folderId];
|
|
1594
|
+
}
|
|
1595
|
+
const driveResponse = await drive.files.create({
|
|
1596
|
+
requestBody: fileMetadata,
|
|
1597
|
+
media: {
|
|
1598
|
+
mimeType: attachmentMimeType,
|
|
1599
|
+
body: bufferStream,
|
|
1600
|
+
},
|
|
1601
|
+
fields: 'id,name,webViewLink,mimeType,size',
|
|
1602
|
+
});
|
|
1603
|
+
const driveFile = driveResponse.data;
|
|
1604
|
+
const fileId = driveFile.id;
|
|
1605
|
+
if (!fileId) {
|
|
1606
|
+
throw new Error('Failed to upload file to Drive - no file ID returned');
|
|
1607
|
+
}
|
|
1608
|
+
const driveLink = getDriveFileUrl(fileId, accountEmail);
|
|
1609
|
+
let result = '**Attachment Saved to Drive**\n\n';
|
|
1610
|
+
result += `File Name: ${driveFile.name}\n`;
|
|
1611
|
+
result += `File ID: ${fileId}\n`;
|
|
1612
|
+
result += `MIME Type: ${driveFile.mimeType || attachmentMimeType}\n`;
|
|
1613
|
+
result += `Size: ${size} bytes (${(size / 1024).toFixed(2)} KB)\n`;
|
|
1614
|
+
if (args.folderId) {
|
|
1615
|
+
result += `Folder ID: ${args.folderId}\n`;
|
|
1616
|
+
}
|
|
1617
|
+
result += `\nView in Drive: ${driveLink}\n`;
|
|
1618
|
+
result += `\nSource Message ID: ${args.messageId}`;
|
|
1619
|
+
return result;
|
|
1620
|
+
}
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
throw new Error(formatToolError('saveAttachmentToDrive', error));
|
|
1623
|
+
}
|
|
1624
|
+
},
|
|
1625
|
+
});
|
|
1302
1626
|
// --- Mark as Read ---
|
|
1303
1627
|
server.addTool({
|
|
1304
1628
|
name: 'markAsRead',
|
|
@@ -1458,6 +1782,10 @@ export function registerGmailTools(options) {
|
|
|
1458
1782
|
.optional()
|
|
1459
1783
|
.default('full')
|
|
1460
1784
|
.describe('Response format for messages in the thread'),
|
|
1785
|
+
maxLength: z
|
|
1786
|
+
.number()
|
|
1787
|
+
.optional()
|
|
1788
|
+
.describe('Maximum character length of the response. If the result exceeds this, it will be truncated with a notice.'),
|
|
1461
1789
|
}),
|
|
1462
1790
|
async execute(args, { log: _log }) {
|
|
1463
1791
|
try {
|
|
@@ -1494,7 +1822,7 @@ export function registerGmailTools(options) {
|
|
|
1494
1822
|
}
|
|
1495
1823
|
return '';
|
|
1496
1824
|
};
|
|
1497
|
-
let result =
|
|
1825
|
+
let result = '**Email Thread**\n\n';
|
|
1498
1826
|
result += `Thread ID: ${thread.id}\n`;
|
|
1499
1827
|
result += `Messages in thread: ${messages.length}\n`;
|
|
1500
1828
|
result += `History ID: ${thread.historyId}\n\n`;
|
|
@@ -1513,7 +1841,13 @@ export function registerGmailTools(options) {
|
|
|
1513
1841
|
result += `Labels: ${(message.labelIds ?? []).join(', ') || 'None'}\n\n`;
|
|
1514
1842
|
if (args.format === 'full' && message.payload) {
|
|
1515
1843
|
const body = extractBody(message.payload);
|
|
1516
|
-
|
|
1844
|
+
// Wrap email body with security warnings to defend against prompt injection
|
|
1845
|
+
const from = getHeader(headers, 'From');
|
|
1846
|
+
const subject = getHeader(headers, 'Subject');
|
|
1847
|
+
const wrappedBody = body
|
|
1848
|
+
? wrapEmailContent(body, from || undefined, subject || undefined)
|
|
1849
|
+
: '(empty)';
|
|
1850
|
+
result += `**Body:**\n${wrappedBody}\n`;
|
|
1517
1851
|
}
|
|
1518
1852
|
else if (message.snippet) {
|
|
1519
1853
|
result += `**Snippet:** ${message.snippet}\n`;
|
|
@@ -1526,6 +1860,11 @@ export function registerGmailTools(options) {
|
|
|
1526
1860
|
if (link) {
|
|
1527
1861
|
result += `\n\nView thread in Gmail: ${link}`;
|
|
1528
1862
|
}
|
|
1863
|
+
// Apply maxLength truncation if specified
|
|
1864
|
+
if (args.maxLength && result.length > args.maxLength) {
|
|
1865
|
+
result = result.substring(0, args.maxLength);
|
|
1866
|
+
result += `\n\n[...TRUNCATED at ${args.maxLength} chars. Thread has ${messages.length} messages. Use a larger maxLength or omit it to see everything.]`;
|
|
1867
|
+
}
|
|
1529
1868
|
return result;
|
|
1530
1869
|
}
|
|
1531
1870
|
catch (error) {
|
|
@@ -1664,7 +2003,7 @@ export function registerGmailTools(options) {
|
|
|
1664
2003
|
if (criteria.query)
|
|
1665
2004
|
result += ` - Query: ${criteria.query}\n`;
|
|
1666
2005
|
if (criteria.hasAttachment)
|
|
1667
|
-
result +=
|
|
2006
|
+
result += ' - Has attachment: yes\n';
|
|
1668
2007
|
if (criteria.size)
|
|
1669
2008
|
result += ` - Size: ${criteria.sizeComparison} ${criteria.size} bytes\n`;
|
|
1670
2009
|
result += ' Actions:\n';
|
|
@@ -1758,7 +2097,7 @@ export function registerGmailTools(options) {
|
|
|
1758
2097
|
if (args.query)
|
|
1759
2098
|
result += `- Query: ${args.query}\n`;
|
|
1760
2099
|
if (args.hasAttachment)
|
|
1761
|
-
result +=
|
|
2100
|
+
result += '- Has attachment: yes\n';
|
|
1762
2101
|
result += '\n**Actions:**\n';
|
|
1763
2102
|
if (args.addLabelIds?.length)
|
|
1764
2103
|
result += `- Add labels: ${args.addLabelIds.join(', ')}\n`;
|