gmcp 0.1.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/dist/index.js ADDED
@@ -0,0 +1,3391 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createAuthenticatedClient,
4
+ getEnvConfig,
5
+ getHeader
6
+ } from "./shared/chunk-n2k9eesp.js";
7
+
8
+ // src/index.ts
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+
12
+ // src/calendar.ts
13
+ import { google } from "googleapis";
14
+ function parseCalendar(calendar) {
15
+ return {
16
+ id: calendar.id || "",
17
+ summary: calendar.summary || "",
18
+ description: calendar.description ?? undefined,
19
+ timeZone: calendar.timeZone ?? undefined,
20
+ primary: calendar.primary ?? undefined,
21
+ backgroundColor: calendar.backgroundColor ?? undefined,
22
+ foregroundColor: calendar.foregroundColor ?? undefined,
23
+ accessRole: calendar.accessRole ?? undefined
24
+ };
25
+ }
26
+ var ALL_DAY_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
27
+ function isAllDayDate(dateString) {
28
+ return ALL_DAY_DATE_REGEX.test(dateString);
29
+ }
30
+ function parseEventDateTime(datetime) {
31
+ return {
32
+ date: datetime?.date ?? undefined,
33
+ dateTime: datetime?.dateTime ?? undefined,
34
+ timeZone: datetime?.timeZone ?? undefined
35
+ };
36
+ }
37
+ function parseAttendee(attendee) {
38
+ return {
39
+ email: attendee.email || "",
40
+ displayName: attendee.displayName ?? undefined,
41
+ responseStatus: attendee.responseStatus,
42
+ optional: attendee.optional ?? undefined,
43
+ organizer: attendee.organizer ?? undefined,
44
+ self: attendee.self ?? undefined
45
+ };
46
+ }
47
+ function parseEventPerson(person) {
48
+ if (!person) {
49
+ return;
50
+ }
51
+ return {
52
+ email: person.email || "",
53
+ displayName: person.displayName ?? undefined
54
+ };
55
+ }
56
+ function parseEvent(event) {
57
+ return {
58
+ id: event.id || "",
59
+ summary: event.summary || "(No title)",
60
+ description: event.description ?? undefined,
61
+ location: event.location ?? undefined,
62
+ start: parseEventDateTime(event.start),
63
+ end: parseEventDateTime(event.end),
64
+ attendees: event.attendees?.map(parseAttendee),
65
+ creator: parseEventPerson(event.creator),
66
+ organizer: parseEventPerson(event.organizer),
67
+ status: event.status,
68
+ htmlLink: event.htmlLink ?? undefined,
69
+ hangoutLink: event.hangoutLink ?? undefined,
70
+ recurrence: event.recurrence ?? undefined,
71
+ recurringEventId: event.recurringEventId ?? undefined,
72
+ created: event.created ?? undefined,
73
+ updated: event.updated ?? undefined
74
+ };
75
+ }
76
+ function createCalendarClient(auth, logger) {
77
+ const calendar = google.calendar({ version: "v3", auth });
78
+ return {
79
+ async listCalendars(showHidden = false) {
80
+ const startTime = Date.now();
81
+ logger?.debug({ showHidden }, "listCalendars start");
82
+ try {
83
+ const response = await calendar.calendarList.list({
84
+ showHidden
85
+ });
86
+ const calendars = response.data.items || [];
87
+ const result = calendars.map((cal) => parseCalendar(cal));
88
+ logger?.info({ calendarCount: result.length, durationMs: Date.now() - startTime }, "listCalendars completed");
89
+ return result;
90
+ } catch (error) {
91
+ logger?.error({
92
+ error: error instanceof Error ? error.message : String(error),
93
+ stack: error instanceof Error ? error.stack : undefined
94
+ }, "listCalendars failed");
95
+ throw new Error(`Failed to list calendars: ${error}`);
96
+ }
97
+ },
98
+ async listEvents(calendarId = "primary", timeMin, timeMax, maxResults = 10, query, singleEvents = true, orderBy = "startTime") {
99
+ const startTime = Date.now();
100
+ logger?.debug({ calendarId, timeMin, timeMax, maxResults, query }, "listEvents start");
101
+ try {
102
+ const response = await calendar.events.list({
103
+ calendarId,
104
+ timeMin,
105
+ timeMax,
106
+ maxResults,
107
+ q: query,
108
+ singleEvents,
109
+ orderBy: singleEvents ? orderBy : undefined
110
+ });
111
+ const events = response.data.items || [];
112
+ const result = events.map((event) => parseEvent(event));
113
+ logger?.info({
114
+ calendarId,
115
+ eventCount: result.length,
116
+ durationMs: Date.now() - startTime
117
+ }, "listEvents completed");
118
+ return result;
119
+ } catch (error) {
120
+ logger?.error({
121
+ calendarId,
122
+ error: error instanceof Error ? error.message : String(error),
123
+ stack: error instanceof Error ? error.stack : undefined
124
+ }, "listEvents failed");
125
+ throw new Error(`Failed to list events from calendar ${calendarId}: ${error}`);
126
+ }
127
+ },
128
+ async getEvent(calendarId, eventId) {
129
+ const startTime = Date.now();
130
+ logger?.debug({ calendarId, eventId }, "getEvent start");
131
+ try {
132
+ const response = await calendar.events.get({
133
+ calendarId,
134
+ eventId
135
+ });
136
+ const result = parseEvent(response.data);
137
+ logger?.info({ calendarId, eventId, durationMs: Date.now() - startTime }, "getEvent completed");
138
+ return result;
139
+ } catch (error) {
140
+ logger?.error({
141
+ calendarId,
142
+ eventId,
143
+ error: error instanceof Error ? error.message : String(error),
144
+ stack: error instanceof Error ? error.stack : undefined
145
+ }, "getEvent failed");
146
+ throw new Error(`Failed to get event ${eventId} from calendar ${calendarId}: ${error}`);
147
+ }
148
+ },
149
+ async createEvent(calendarId, summary, start, end, description, location, attendees, timezone, recurrence, addMeet) {
150
+ const startTime = Date.now();
151
+ logger?.debug({ calendarId, summary, start, end, addMeet }, "createEvent start");
152
+ try {
153
+ const isAllDay = isAllDayDate(start);
154
+ const eventResource = {
155
+ summary,
156
+ description,
157
+ location,
158
+ start: isAllDay ? { date: start, timeZone: timezone } : { dateTime: start, timeZone: timezone },
159
+ end: isAllDay ? { date: end, timeZone: timezone } : { dateTime: end, timeZone: timezone },
160
+ attendees: attendees?.map((email) => ({ email })),
161
+ recurrence
162
+ };
163
+ if (addMeet) {
164
+ eventResource.conferenceData = {
165
+ createRequest: {
166
+ requestId: `${Date.now()}-${Math.random().toString(36).substring(7)}`,
167
+ conferenceSolutionKey: { type: "hangoutsMeet" }
168
+ }
169
+ };
170
+ }
171
+ const response = await calendar.events.insert({
172
+ calendarId,
173
+ requestBody: eventResource,
174
+ conferenceDataVersion: addMeet ? 1 : undefined
175
+ });
176
+ const result = parseEvent(response.data);
177
+ logger?.info({
178
+ calendarId,
179
+ summary,
180
+ eventId: result.id,
181
+ durationMs: Date.now() - startTime
182
+ }, "createEvent completed");
183
+ return result;
184
+ } catch (error) {
185
+ logger?.error({
186
+ calendarId,
187
+ summary,
188
+ error: error instanceof Error ? error.message : String(error),
189
+ stack: error instanceof Error ? error.stack : undefined
190
+ }, "createEvent failed");
191
+ throw new Error(`Failed to create event "${summary}" in calendar ${calendarId}: ${error}`);
192
+ }
193
+ }
194
+ };
195
+ }
196
+
197
+ // src/gmail.ts
198
+ import { google as google2 } from "googleapis";
199
+
200
+ // src/constants.ts
201
+ var EMAIL_FETCH_BATCH_SIZE = 10;
202
+
203
+ // src/gmail.ts
204
+ var BASE64_PADDING_REGEX = /=+$/;
205
+ function decodeBase64(data) {
206
+ try {
207
+ const base64 = data.replace(/-/g, "+").replace(/_/g, "/");
208
+ return Buffer.from(base64, "base64").toString("utf-8");
209
+ } catch (_error) {
210
+ return "(error decoding body)";
211
+ }
212
+ }
213
+ function getPartBody(part, mimeType) {
214
+ if (part.mimeType === mimeType && part.body?.data) {
215
+ return decodeBase64(part.body.data);
216
+ }
217
+ if (part.parts) {
218
+ for (const subPart of part.parts) {
219
+ const body = getPartBody(subPart, mimeType);
220
+ if (body) {
221
+ return body;
222
+ }
223
+ }
224
+ }
225
+ return "";
226
+ }
227
+ function extractBody(payload) {
228
+ if (!payload) {
229
+ return "";
230
+ }
231
+ let body = getPartBody(payload, "text/plain");
232
+ if (!body) {
233
+ body = getPartBody(payload, "text/html");
234
+ }
235
+ if (!body && payload.body?.data) {
236
+ body = decodeBase64(payload.body.data);
237
+ }
238
+ return body || "(no body)";
239
+ }
240
+ function parseMessage(message, includeBody) {
241
+ const headers = message.payload?.headers || [];
242
+ const subject = getHeader(headers, "Subject");
243
+ const from = getHeader(headers, "From");
244
+ const to = getHeader(headers, "To");
245
+ const date = getHeader(headers, "Date");
246
+ const email = {
247
+ id: message.id || "",
248
+ threadId: message.threadId || "",
249
+ subject: subject || "(no subject)",
250
+ from: from || "(unknown)",
251
+ to: to || "(unknown)",
252
+ date: date || "",
253
+ snippet: message.snippet || "",
254
+ labels: message.labelIds || undefined
255
+ };
256
+ if (includeBody) {
257
+ email.body = extractBody(message.payload);
258
+ }
259
+ return email;
260
+ }
261
+ function createMimeMessage(params) {
262
+ const lines = [];
263
+ lines.push(`To: ${params.to}`);
264
+ if (params.cc) {
265
+ lines.push(`Cc: ${params.cc}`);
266
+ }
267
+ if (params.bcc) {
268
+ lines.push(`Bcc: ${params.bcc}`);
269
+ }
270
+ lines.push(`Subject: ${params.subject}`);
271
+ if (params.inReplyTo) {
272
+ lines.push(`In-Reply-To: ${params.inReplyTo}`);
273
+ }
274
+ if (params.references) {
275
+ lines.push(`References: ${params.references}`);
276
+ }
277
+ lines.push(`Content-Type: ${params.contentType}; charset=utf-8`);
278
+ lines.push("");
279
+ lines.push(params.body);
280
+ return lines.join(`\r
281
+ `);
282
+ }
283
+ function encodeMessage(message) {
284
+ return Buffer.from(message).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(BASE64_PADDING_REGEX, "");
285
+ }
286
+ function parseLabel(label) {
287
+ return {
288
+ id: label.id || "",
289
+ name: label.name || "",
290
+ type: label.type === "system" ? "system" : "user",
291
+ messageListVisibility: label.messageListVisibility,
292
+ labelListVisibility: label.labelListVisibility,
293
+ messagesTotal: label.messagesTotal || undefined,
294
+ messagesUnread: label.messagesUnread || undefined,
295
+ color: label.color ? {
296
+ textColor: label.color.textColor || "",
297
+ backgroundColor: label.color.backgroundColor || ""
298
+ } : undefined
299
+ };
300
+ }
301
+ function createGmailClient(auth, logger) {
302
+ const gmail = google2.gmail({ version: "v1", auth });
303
+ return {
304
+ async searchEmails(query, maxResults = 10, includeBody = false, pageToken) {
305
+ const startTime = Date.now();
306
+ logger?.debug({ query, maxResults, includeBody, pageToken }, "searchEmails start");
307
+ try {
308
+ const listResponse = await gmail.users.messages.list({
309
+ userId: "me",
310
+ q: query,
311
+ maxResults,
312
+ pageToken
313
+ });
314
+ const messages = listResponse.data.messages || [];
315
+ const resultSizeEstimate = listResponse.data.resultSizeEstimate || 0;
316
+ const nextPageToken = listResponse.data.nextPageToken;
317
+ const validMessages = messages.filter((message) => message.id);
318
+ const emails = [];
319
+ for (let i = 0;i < validMessages.length; i += EMAIL_FETCH_BATCH_SIZE) {
320
+ const batchStartTime = Date.now();
321
+ const batch = validMessages.slice(i, i + EMAIL_FETCH_BATCH_SIZE);
322
+ const batchEmails = await Promise.all(batch.map(async (message) => {
323
+ const messageId = message.id ?? "";
324
+ const details = await gmail.users.messages.get({
325
+ userId: "me",
326
+ id: messageId,
327
+ format: includeBody ? "full" : "metadata",
328
+ metadataHeaders: includeBody ? undefined : ["From", "To", "Subject", "Date"]
329
+ });
330
+ return parseMessage(details.data, includeBody);
331
+ }));
332
+ emails.push(...batchEmails);
333
+ logger?.debug({
334
+ batchSize: batch.length,
335
+ durationMs: Date.now() - batchStartTime
336
+ }, "searchEmails batch completed");
337
+ }
338
+ const durationMs = Date.now() - startTime;
339
+ logger?.info({ query, resultCount: emails.length, durationMs }, "searchEmails completed");
340
+ return {
341
+ emails,
342
+ total_estimate: resultSizeEstimate,
343
+ has_more: !!nextPageToken,
344
+ next_page_token: nextPageToken || undefined
345
+ };
346
+ } catch (error) {
347
+ logger?.error({
348
+ query,
349
+ error: error instanceof Error ? error.message : String(error),
350
+ stack: error instanceof Error ? error.stack : undefined
351
+ }, "searchEmails failed");
352
+ throw new Error(`Failed to search emails with query "${query}": ${error}`);
353
+ }
354
+ },
355
+ async getMessage(messageId, includeBody = false) {
356
+ const startTime = Date.now();
357
+ logger?.debug({ messageId, includeBody }, "getMessage start");
358
+ try {
359
+ const response = await gmail.users.messages.get({
360
+ userId: "me",
361
+ id: messageId,
362
+ format: includeBody ? "full" : "metadata",
363
+ metadataHeaders: includeBody ? undefined : ["From", "To", "Subject", "Date"]
364
+ });
365
+ const result = parseMessage(response.data, includeBody);
366
+ logger?.info({ messageId, durationMs: Date.now() - startTime }, "getMessage completed");
367
+ return result;
368
+ } catch (error) {
369
+ logger?.error({
370
+ messageId,
371
+ error: error instanceof Error ? error.message : String(error),
372
+ stack: error instanceof Error ? error.stack : undefined
373
+ }, "getMessage failed");
374
+ throw new Error(`Failed to get message ${messageId}: ${error}`);
375
+ }
376
+ },
377
+ async getThread(threadId, includeBody = false) {
378
+ try {
379
+ const response = await gmail.users.threads.get({
380
+ userId: "me",
381
+ id: threadId,
382
+ format: includeBody ? "full" : "metadata",
383
+ metadataHeaders: includeBody ? undefined : ["From", "To", "Subject", "Date"]
384
+ });
385
+ const messages = response.data.messages || [];
386
+ return messages.map((message) => parseMessage(message, includeBody));
387
+ } catch (error) {
388
+ throw new Error(`Failed to get thread ${threadId}: ${error}`);
389
+ }
390
+ },
391
+ async listAttachments(messageId) {
392
+ try {
393
+ const response = await gmail.users.messages.get({
394
+ userId: "me",
395
+ id: messageId,
396
+ format: "full"
397
+ });
398
+ const attachments = [];
399
+ const extractAttachments = (part) => {
400
+ if (part.filename && part.body?.attachmentId) {
401
+ attachments.push({
402
+ filename: part.filename,
403
+ mimeType: part.mimeType || "application/octet-stream",
404
+ size: part.body.size || 0,
405
+ attachmentId: part.body.attachmentId
406
+ });
407
+ }
408
+ if (part.parts) {
409
+ for (const subPart of part.parts) {
410
+ extractAttachments(subPart);
411
+ }
412
+ }
413
+ };
414
+ if (response.data.payload) {
415
+ extractAttachments(response.data.payload);
416
+ }
417
+ return attachments;
418
+ } catch (error) {
419
+ throw new Error(`Failed to list attachments for ${messageId}: ${error}`);
420
+ }
421
+ },
422
+ async getAttachment(messageId, attachmentId) {
423
+ try {
424
+ const response = await gmail.users.messages.attachments.get({
425
+ userId: "me",
426
+ messageId,
427
+ id: attachmentId
428
+ });
429
+ if (!response.data.data) {
430
+ throw new Error("Attachment data not found");
431
+ }
432
+ return response.data.data;
433
+ } catch (error) {
434
+ throw new Error(`Failed to get attachment ${attachmentId} from message ${messageId}: ${error}`);
435
+ }
436
+ },
437
+ async modifyLabels(messageId, addLabelIds, removeLabelIds) {
438
+ try {
439
+ const response = await gmail.users.messages.modify({
440
+ userId: "me",
441
+ id: messageId,
442
+ requestBody: {
443
+ addLabelIds: addLabelIds || [],
444
+ removeLabelIds: removeLabelIds || []
445
+ }
446
+ });
447
+ return parseMessage(response.data, false);
448
+ } catch (error) {
449
+ throw new Error(`Failed to modify labels for ${messageId}: ${error}`);
450
+ }
451
+ },
452
+ async batchModifyLabels(messageIds, addLabelIds, removeLabelIds) {
453
+ const startTime = Date.now();
454
+ logger?.debug({
455
+ messageCount: messageIds.length,
456
+ addLabelCount: addLabelIds?.length || 0,
457
+ removeLabelCount: removeLabelIds?.length || 0
458
+ }, "batchModifyLabels start");
459
+ try {
460
+ await gmail.users.messages.batchModify({
461
+ userId: "me",
462
+ requestBody: {
463
+ ids: messageIds,
464
+ addLabelIds: addLabelIds || [],
465
+ removeLabelIds: removeLabelIds || []
466
+ }
467
+ });
468
+ logger?.info({
469
+ messageCount: messageIds.length,
470
+ durationMs: Date.now() - startTime
471
+ }, "batchModifyLabels completed");
472
+ } catch (error) {
473
+ logger?.error({
474
+ messageCount: messageIds.length,
475
+ error: error instanceof Error ? error.message : String(error),
476
+ stack: error instanceof Error ? error.stack : undefined
477
+ }, "batchModifyLabels failed");
478
+ throw new Error(`Failed to batch modify labels on ${messageIds.length} messages: ${error}`);
479
+ }
480
+ },
481
+ async sendEmail(to, subject, body, contentType = "text/plain", cc, bcc) {
482
+ const startTime = Date.now();
483
+ logger?.debug({ to, subject, contentType }, "sendEmail start");
484
+ try {
485
+ const mimeMessage = createMimeMessage({
486
+ to,
487
+ subject,
488
+ body,
489
+ contentType,
490
+ cc,
491
+ bcc
492
+ });
493
+ const encodedMessage = encodeMessage(mimeMessage);
494
+ const response = await gmail.users.messages.send({
495
+ userId: "me",
496
+ requestBody: {
497
+ raw: encodedMessage
498
+ }
499
+ });
500
+ const result = {
501
+ id: response.data.id || "",
502
+ threadId: response.data.threadId || "",
503
+ labelIds: response.data.labelIds || undefined
504
+ };
505
+ logger?.info({
506
+ to,
507
+ subject,
508
+ messageId: result.id,
509
+ durationMs: Date.now() - startTime
510
+ }, "sendEmail completed");
511
+ return result;
512
+ } catch (error) {
513
+ logger?.error({
514
+ to,
515
+ subject,
516
+ error: error instanceof Error ? error.message : String(error),
517
+ stack: error instanceof Error ? error.stack : undefined
518
+ }, "sendEmail failed");
519
+ throw new Error(`Failed to send email to ${to}: ${error}`);
520
+ }
521
+ },
522
+ async replyToEmail(to, subject, body, threadId, messageId, contentType = "text/plain", cc) {
523
+ try {
524
+ const replySubject = subject.startsWith("Re:") ? subject : `Re: ${subject}`;
525
+ const mimeMessage = createMimeMessage({
526
+ to,
527
+ subject: replySubject,
528
+ body,
529
+ contentType,
530
+ cc,
531
+ inReplyTo: `<${messageId}>`,
532
+ references: `<${messageId}>`
533
+ });
534
+ const encodedMessage = encodeMessage(mimeMessage);
535
+ const response = await gmail.users.messages.send({
536
+ userId: "me",
537
+ requestBody: {
538
+ raw: encodedMessage,
539
+ threadId
540
+ }
541
+ });
542
+ return {
543
+ id: response.data.id || "",
544
+ threadId: response.data.threadId || "",
545
+ labelIds: response.data.labelIds || undefined
546
+ };
547
+ } catch (error) {
548
+ throw new Error(`Failed to reply to message ${messageId} in thread ${threadId}: ${error}`);
549
+ }
550
+ },
551
+ async createDraft(to, subject, body, contentType = "text/plain", cc, bcc) {
552
+ try {
553
+ const mimeMessage = createMimeMessage({
554
+ to,
555
+ subject,
556
+ body,
557
+ contentType,
558
+ cc,
559
+ bcc
560
+ });
561
+ const encodedMessage = encodeMessage(mimeMessage);
562
+ const response = await gmail.users.drafts.create({
563
+ userId: "me",
564
+ requestBody: {
565
+ message: {
566
+ raw: encodedMessage
567
+ }
568
+ }
569
+ });
570
+ return {
571
+ id: response.data.id || "",
572
+ message: {
573
+ id: response.data.message?.id || "",
574
+ threadId: response.data.message?.threadId || ""
575
+ }
576
+ };
577
+ } catch (error) {
578
+ throw new Error(`Failed to create draft to ${to}: ${error}`);
579
+ }
580
+ },
581
+ async listLabels() {
582
+ try {
583
+ const response = await gmail.users.labels.list({
584
+ userId: "me"
585
+ });
586
+ const labels = response.data.labels || [];
587
+ return labels.map((label) => parseLabel(label));
588
+ } catch (error) {
589
+ throw new Error(`Failed to list labels: ${error}`);
590
+ }
591
+ },
592
+ async getLabel(labelId) {
593
+ try {
594
+ const response = await gmail.users.labels.get({
595
+ userId: "me",
596
+ id: labelId
597
+ });
598
+ return parseLabel(response.data);
599
+ } catch (error) {
600
+ throw new Error(`Failed to get label ${labelId}: ${error}`);
601
+ }
602
+ },
603
+ async createLabel(name, messageListVisibility, labelListVisibility, backgroundColor, textColor) {
604
+ try {
605
+ const response = await gmail.users.labels.create({
606
+ userId: "me",
607
+ requestBody: {
608
+ name,
609
+ messageListVisibility,
610
+ labelListVisibility,
611
+ color: backgroundColor && textColor ? { backgroundColor, textColor } : undefined
612
+ }
613
+ });
614
+ return parseLabel(response.data);
615
+ } catch (error) {
616
+ throw new Error(`Failed to create label "${name}": ${error}`);
617
+ }
618
+ },
619
+ async updateLabel(labelId, name, messageListVisibility, labelListVisibility, backgroundColor, textColor) {
620
+ try {
621
+ const response = await gmail.users.labels.update({
622
+ userId: "me",
623
+ id: labelId,
624
+ requestBody: {
625
+ id: labelId,
626
+ name,
627
+ messageListVisibility,
628
+ labelListVisibility,
629
+ color: backgroundColor && textColor ? { backgroundColor, textColor } : undefined
630
+ }
631
+ });
632
+ return parseLabel(response.data);
633
+ } catch (error) {
634
+ throw new Error(`Failed to update label ${labelId}: ${error}`);
635
+ }
636
+ },
637
+ async deleteLabel(labelId) {
638
+ try {
639
+ await gmail.users.labels.delete({
640
+ userId: "me",
641
+ id: labelId
642
+ });
643
+ } catch (error) {
644
+ throw new Error(`Failed to delete label ${labelId}: ${error}`);
645
+ }
646
+ }
647
+ };
648
+ }
649
+
650
+ // src/logger.ts
651
+ import pino from "pino";
652
+ function createLogger(options = {}) {
653
+ const level = options.level || process.env.LOG_LEVEL || "info";
654
+ const pretty = options.pretty ?? process.env.LOG_PRETTY === "true";
655
+ const pinoOptions = {
656
+ level,
657
+ name: options.name || "gmcp",
658
+ ...pretty && {
659
+ transport: {
660
+ target: "pino-pretty",
661
+ options: {
662
+ colorize: true,
663
+ translateTime: "HH:MM:ss Z",
664
+ ignore: "pid,hostname",
665
+ destination: 2
666
+ }
667
+ }
668
+ }
669
+ };
670
+ if (!pretty) {
671
+ return pino(pinoOptions, pino.destination(2));
672
+ }
673
+ return pino(pinoOptions);
674
+ }
675
+
676
+ // src/tool-registry.ts
677
+ var READ_ONLY_ANNOTATIONS = {
678
+ readOnlyHint: true,
679
+ destructiveHint: false,
680
+ idempotentHint: true,
681
+ openWorldHint: true
682
+ };
683
+ var MODIFY_ANNOTATIONS = {
684
+ readOnlyHint: false,
685
+ destructiveHint: false,
686
+ idempotentHint: true,
687
+ openWorldHint: false
688
+ };
689
+ var SEND_ANNOTATIONS = {
690
+ readOnlyHint: false,
691
+ destructiveHint: false,
692
+ idempotentHint: false,
693
+ openWorldHint: false
694
+ };
695
+ var DESTRUCTIVE_ANNOTATIONS = {
696
+ readOnlyHint: false,
697
+ destructiveHint: true,
698
+ idempotentHint: true,
699
+ openWorldHint: false
700
+ };
701
+ function registerTools(server, client, tools, logger) {
702
+ for (const tool of tools) {
703
+ server.registerTool(tool.name, {
704
+ title: tool.title,
705
+ description: tool.description,
706
+ inputSchema: tool.inputSchema,
707
+ annotations: tool.annotations
708
+ }, async (params) => {
709
+ const toolLogger = logger?.child({ tool: tool.name });
710
+ const startTime = Date.now();
711
+ toolLogger?.info({ params }, "Tool execution start");
712
+ try {
713
+ const result = await tool.handler(client, params);
714
+ toolLogger?.info({ durationMs: Date.now() - startTime, success: !result.isError }, "Tool execution completed");
715
+ return result;
716
+ } catch (error) {
717
+ toolLogger?.error({
718
+ durationMs: Date.now() - startTime,
719
+ error: error instanceof Error ? error.message : String(error),
720
+ stack: error instanceof Error ? error.stack : undefined
721
+ }, "Tool execution failed");
722
+ throw error;
723
+ }
724
+ });
725
+ }
726
+ }
727
+
728
+ // src/tools/batch-modify.ts
729
+ import json2md from "json2md";
730
+ import { z } from "zod";
731
+
732
+ // src/utils/tool-helpers.ts
733
+ function formatEmailForOutput(email) {
734
+ return {
735
+ id: email.id,
736
+ thread_id: email.threadId,
737
+ subject: email.subject,
738
+ from: email.from,
739
+ to: email.to,
740
+ date: email.date,
741
+ snippet: email.snippet,
742
+ ...email.body ? { body: email.body } : {},
743
+ ...email.labels ? { labels: email.labels } : {}
744
+ };
745
+ }
746
+ function createErrorResponse(context, error, logger) {
747
+ const errorMessage = error instanceof Error ? error.message : String(error);
748
+ logger?.error({
749
+ context,
750
+ error: errorMessage,
751
+ stack: error instanceof Error ? error.stack : undefined
752
+ }, "Tool error");
753
+ return {
754
+ content: [
755
+ {
756
+ type: "text",
757
+ text: `Error ${context}: ${errorMessage}`
758
+ }
759
+ ],
760
+ isError: true
761
+ };
762
+ }
763
+
764
+ // src/tools/batch-modify.ts
765
+ var BatchModifyInputSchema = z.object({
766
+ message_ids: z.array(z.string()).min(1, "Must provide at least one message ID").max(1000, "Maximum 1000 messages per batch").describe("Array of Gmail message IDs to modify (max 1000)"),
767
+ add_labels: z.array(z.string()).optional().describe("Label IDs to add to all messages (e.g., ['STARRED', 'IMPORTANT'])"),
768
+ remove_labels: z.array(z.string()).optional().describe("Label IDs to remove from all messages (e.g., ['UNREAD', 'INBOX'])"),
769
+ output_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
770
+ });
771
+ function batchModificationToMarkdown(messageCount, addedLabels, removedLabels) {
772
+ const sections = [
773
+ { h1: "Batch Label Modification Successful" },
774
+ { p: `**Modified Messages:** ${messageCount}` }
775
+ ];
776
+ if (addedLabels && addedLabels.length > 0) {
777
+ sections.push({ h2: "Added Labels" });
778
+ sections.push({ ul: addedLabels });
779
+ }
780
+ if (removedLabels && removedLabels.length > 0) {
781
+ sections.push({ h2: "Removed Labels" });
782
+ sections.push({ ul: removedLabels });
783
+ }
784
+ sections.push({
785
+ p: `All ${messageCount} messages have been updated successfully.`
786
+ });
787
+ return json2md(sections);
788
+ }
789
+ async function batchModifyTool(gmailClient, params) {
790
+ try {
791
+ if (!(params.add_labels || params.remove_labels)) {
792
+ return {
793
+ content: [
794
+ {
795
+ type: "text",
796
+ text: "Error: Must specify at least one of add_labels or remove_labels"
797
+ }
798
+ ],
799
+ isError: true
800
+ };
801
+ }
802
+ await gmailClient.batchModifyLabels(params.message_ids, params.add_labels, params.remove_labels);
803
+ const output = {
804
+ modified_count: params.message_ids.length,
805
+ message_ids: params.message_ids,
806
+ added_labels: params.add_labels || [],
807
+ removed_labels: params.remove_labels || [],
808
+ success: true
809
+ };
810
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : batchModificationToMarkdown(params.message_ids.length, params.add_labels, params.remove_labels);
811
+ return {
812
+ content: [{ type: "text", text: textContent }],
813
+ structuredContent: output
814
+ };
815
+ } catch (error) {
816
+ return createErrorResponse("batch modifying labels", error);
817
+ }
818
+ }
819
+ var BATCH_MODIFY_DESCRIPTION = `Batch modify Gmail labels on multiple messages at once.
820
+
821
+ This tool allows you to efficiently add or remove labels from multiple Gmail messages in a single operation. It's ideal for bulk email management tasks.
822
+
823
+ **Common Use Cases**:
824
+ - Archive multiple emails at once
825
+ - Mark multiple emails as read
826
+ - Star a group of important emails
827
+ - Apply custom labels to multiple messages
828
+ - Clean up inbox by bulk archiving
829
+
830
+ **System Labels**:
831
+ - \`INBOX\` - Inbox (remove to archive)
832
+ - \`UNREAD\` - Unread status (remove to mark as read)
833
+ - \`STARRED\` - Starred/important
834
+ - \`SPAM\` - Spam folder
835
+ - \`TRASH\` - Trash folder
836
+ - \`IMPORTANT\` - Important marker
837
+
838
+ **Parameters**:
839
+ - \`message_ids\` (array, required): Array of message IDs to modify (max 1000)
840
+ - \`add_labels\` (array, optional): Label IDs to add to all messages
841
+ - \`remove_labels\` (array, optional): Label IDs to remove from all messages
842
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
843
+
844
+ **Returns**:
845
+ - \`modified_count\`: Number of messages modified
846
+ - \`message_ids\`: Array of message IDs that were modified
847
+ - \`added_labels\`: Labels that were added
848
+ - \`removed_labels\`: Labels that were removed
849
+ - \`success\`: Always true on success
850
+
851
+ **Examples**:
852
+ - Archive multiple emails: \`{ "message_ids": ["id1", "id2", "id3"], "remove_labels": ["INBOX"] }\`
853
+ - Bulk mark as read: \`{ "message_ids": ["id1", "id2"], "remove_labels": ["UNREAD"] }\`
854
+ - Star multiple emails: \`{ "message_ids": ["id1", "id2"], "add_labels": ["STARRED"] }\`
855
+ - Archive and mark read: \`{ "message_ids": ["id1", "id2"], "remove_labels": ["INBOX", "UNREAD"] }\`
856
+
857
+ **Limits**:
858
+ - Maximum 1000 messages per batch operation
859
+ - For larger operations, split into multiple batches
860
+
861
+ **Error Handling**:
862
+ - Returns error if any message ID is invalid
863
+ - Returns error if label ID is invalid
864
+ - Returns error if authentication lacks modify permissions
865
+ - Requires at least one of add_labels or remove_labels
866
+ - Operation is atomic: all messages are modified or none are
867
+
868
+ **Workflow**:
869
+ 1. Use \`search_emails\` to find messages matching criteria
870
+ 2. Extract message IDs from search results
871
+ 3. Use this tool to batch modify labels on all found messages
872
+
873
+ **Performance**:
874
+ - Much faster than modifying messages individually
875
+ - Single API call regardless of message count
876
+ - Recommended for operations affecting more than 5 messages`;
877
+
878
+ // src/tools/calendar-create.ts
879
+ import { z as z2 } from "zod";
880
+
881
+ // src/utils/markdown.ts
882
+ import json2md2 from "json2md";
883
+ function emailToJson2md(email) {
884
+ const elements = [
885
+ { h2: email.subject },
886
+ {
887
+ ul: [
888
+ `**From**: ${email.from}`,
889
+ `**To**: ${email.to}`,
890
+ `**Date**: ${email.date}`,
891
+ `**ID**: ${email.id}`,
892
+ `**Thread ID**: ${email.thread_id}`,
893
+ ...email.labels && email.labels.length > 0 ? [`**Labels**: ${email.labels.join(", ")}`] : []
894
+ ]
895
+ },
896
+ { p: `**Snippet**: ${email.snippet}` }
897
+ ];
898
+ if (email.body) {
899
+ const bodyPreview = email.body.length > 500 ? `${email.body.substring(0, 500)}...` : email.body;
900
+ elements.push({ p: "**Body**:" }, { code: { content: bodyPreview } });
901
+ }
902
+ elements.push({ hr: "" });
903
+ return elements;
904
+ }
905
+ function searchResultsToMarkdown(query, data) {
906
+ const elements = [
907
+ { h1: `Gmail Search Results: "${query}"` },
908
+ {
909
+ p: `Found approximately ${data.total_estimate} emails (showing ${data.count})`
910
+ }
911
+ ];
912
+ if (data.emails.length === 0) {
913
+ elements.push({ p: "No emails found matching the query." });
914
+ } else {
915
+ for (const email of data.emails) {
916
+ elements.push(...emailToJson2md(email));
917
+ }
918
+ if (data.has_more && data.next_page_token) {
919
+ elements.push({
920
+ p: `**Note**: More results available. Use page_token: "${data.next_page_token}" to fetch the next page.`
921
+ });
922
+ }
923
+ }
924
+ return json2md2(elements);
925
+ }
926
+ function calendarListToMarkdown(data) {
927
+ const sections = [
928
+ { h1: "Google Calendars" },
929
+ { p: `**Total Calendars:** ${data.count}` }
930
+ ];
931
+ if (data.calendars.length === 0) {
932
+ sections.push({ p: "No calendars found." });
933
+ } else {
934
+ for (const calendar of data.calendars) {
935
+ sections.push({ h2: calendar.summary });
936
+ const details = [
937
+ `**ID:** ${calendar.id}`,
938
+ `**Timezone:** ${calendar.timezone || "Not set"}`,
939
+ `**Access Role:** ${calendar.access_role || "Unknown"}`
940
+ ];
941
+ if (calendar.primary) {
942
+ details.push("**Primary:** Yes");
943
+ }
944
+ if (calendar.description) {
945
+ details.push(`**Description:** ${calendar.description}`);
946
+ }
947
+ sections.push({ ul: details });
948
+ }
949
+ }
950
+ return json2md2(sections);
951
+ }
952
+ function formatEventTime(time) {
953
+ if (time.date) {
954
+ return `\uD83D\uDCC5 ${time.date} (All day)`;
955
+ }
956
+ if (time.dateTime) {
957
+ const tz = time.timeZone ? ` (${time.timeZone})` : "";
958
+ return `\uD83D\uDD50 ${time.dateTime}${tz}`;
959
+ }
960
+ return "Not set";
961
+ }
962
+ function buildEventDetails(event) {
963
+ const details = [
964
+ `**Event ID:** ${event.id}`,
965
+ `**Start:** ${formatEventTime(event.start)}`,
966
+ `**End:** ${formatEventTime(event.end)}`
967
+ ];
968
+ if (event.location) {
969
+ details.push(`**Location:** ${event.location}`);
970
+ }
971
+ if (event.status) {
972
+ details.push(`**Status:** ${event.status}`);
973
+ }
974
+ if (event.hangout_link) {
975
+ details.push(`**Google Meet:** ${event.hangout_link}`);
976
+ }
977
+ if (event.html_link) {
978
+ details.push(`**View in Calendar:** ${event.html_link}`);
979
+ }
980
+ return details;
981
+ }
982
+ function formatAttendee(att) {
983
+ const name = att.display_name || att.email;
984
+ const status = att.response_status ? ` (${att.response_status})` : "";
985
+ const optional = att.optional ? " [Optional]" : "";
986
+ return `${name}${status}${optional}`;
987
+ }
988
+ function addEventSummary(sections, event) {
989
+ sections.push({ h2: event.summary });
990
+ sections.push({ ul: buildEventDetails(event) });
991
+ if (event.description) {
992
+ sections.push({ p: `**Description:** ${event.description}` });
993
+ }
994
+ if (event.attendees && event.attendees.length > 0) {
995
+ sections.push({ h3: "Attendees" });
996
+ sections.push({ ul: event.attendees.map(formatAttendee) });
997
+ }
998
+ if (event.recurrence && event.recurrence.length > 0) {
999
+ sections.push({ p: `**Recurrence:** ${event.recurrence.join(", ")}` });
1000
+ }
1001
+ }
1002
+ function eventListToMarkdown(data) {
1003
+ const sections = [
1004
+ { h1: "Calendar Events" },
1005
+ {
1006
+ p: `**Calendar:** ${data.calendar_id} | **Events Found:** ${data.count}`
1007
+ }
1008
+ ];
1009
+ if (data.events.length === 0) {
1010
+ sections.push({ p: "No events found matching the criteria." });
1011
+ } else {
1012
+ for (const event of data.events) {
1013
+ addEventSummary(sections, event);
1014
+ sections.push({ hr: "" });
1015
+ }
1016
+ }
1017
+ return json2md2(sections);
1018
+ }
1019
+ function formatAttendeeDetailed(att) {
1020
+ const name = att.display_name || att.email;
1021
+ const status = att.response_status ? ` - ${att.response_status}` : "";
1022
+ const optional = att.optional ? " [Optional]" : "";
1023
+ return `**${name}**${status}${optional}`;
1024
+ }
1025
+ function addEventMetadata(sections, event) {
1026
+ if (event.creator) {
1027
+ sections.push({
1028
+ p: `**Created by:** ${event.creator.display_name || event.creator.email}`
1029
+ });
1030
+ }
1031
+ if (event.organizer) {
1032
+ sections.push({
1033
+ p: `**Organized by:** ${event.organizer.display_name || event.organizer.email}`
1034
+ });
1035
+ }
1036
+ if (event.recurrence && event.recurrence.length > 0) {
1037
+ sections.push({ h2: "Recurrence" });
1038
+ sections.push({ ul: event.recurrence });
1039
+ }
1040
+ if (event.created || event.updated) {
1041
+ const metadata = [];
1042
+ if (event.created) {
1043
+ metadata.push(`**Created:** ${event.created}`);
1044
+ }
1045
+ if (event.updated) {
1046
+ metadata.push(`**Last Updated:** ${event.updated}`);
1047
+ }
1048
+ sections.push({ p: metadata.join(" | ") });
1049
+ }
1050
+ }
1051
+ function eventToMarkdown(event, successMessage) {
1052
+ const sections = [];
1053
+ sections.push({ h1: successMessage || "Calendar Event" });
1054
+ sections.push({ h2: event.summary });
1055
+ sections.push({ ul: buildEventDetails(event) });
1056
+ if (event.description) {
1057
+ sections.push({ h2: "Description" });
1058
+ sections.push({ p: event.description });
1059
+ }
1060
+ if (event.attendees && event.attendees.length > 0) {
1061
+ sections.push({ h2: "Attendees" });
1062
+ sections.push({ ul: event.attendees.map(formatAttendeeDetailed) });
1063
+ }
1064
+ addEventMetadata(sections, event);
1065
+ return json2md2(sections);
1066
+ }
1067
+
1068
+ // src/tools/calendar-create.ts
1069
+ var CalendarCreateEventInputSchema = z2.object({
1070
+ calendar_id: z2.string().default("primary").describe('Calendar ID to create event in (default: "primary")'),
1071
+ summary: z2.string().min(1, "Event title is required").describe("Event title/summary (required)"),
1072
+ start: z2.string().min(1, "Start time is required").describe('Start time: RFC3339 (e.g., "2024-01-15T09:00:00-08:00") or date for all-day (e.g., "2024-01-15")'),
1073
+ end: z2.string().min(1, "End time is required").describe('End time: RFC3339 (e.g., "2024-01-15T10:00:00-08:00") or date for all-day (e.g., "2024-01-15")'),
1074
+ description: z2.string().optional().describe("Event description (optional)"),
1075
+ location: z2.string().optional().describe("Event location (optional)"),
1076
+ attendees: z2.array(z2.string().email()).optional().describe("Array of attendee email addresses (optional)"),
1077
+ timezone: z2.string().optional().describe('IANA timezone (e.g., "America/Los_Angeles", optional)'),
1078
+ recurrence: z2.array(z2.string()).optional().describe('Recurrence rules as RRULE strings (e.g., ["RRULE:FREQ=WEEKLY;COUNT=10"], optional)'),
1079
+ add_meet: z2.boolean().default(false).describe("Auto-create Google Meet conference link (default: false)"),
1080
+ confirm: z2.boolean().default(false).describe("Must be true to create the event (safety check)"),
1081
+ output_format: z2.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
1082
+ });
1083
+ async function calendarCreateEventTool(calendarClient, params) {
1084
+ if (!params.confirm) {
1085
+ return {
1086
+ content: [
1087
+ {
1088
+ type: "text",
1089
+ text: "❌ Event creation requires explicit confirmation. Please set `confirm: true` in the parameters to proceed.\n\nThis safety measure prevents accidental event creation."
1090
+ }
1091
+ ],
1092
+ structuredContent: {
1093
+ error: "Confirmation required",
1094
+ message: "Set confirm: true to create the event. This prevents accidental creation."
1095
+ }
1096
+ };
1097
+ }
1098
+ try {
1099
+ const event = await calendarClient.createEvent(params.calendar_id, params.summary, params.start, params.end, params.description, params.location, params.attendees, params.timezone, params.recurrence, params.add_meet);
1100
+ const output = {
1101
+ id: event.id,
1102
+ summary: event.summary,
1103
+ description: event.description,
1104
+ location: event.location,
1105
+ start: event.start,
1106
+ end: event.end,
1107
+ status: event.status,
1108
+ html_link: event.htmlLink,
1109
+ hangout_link: event.hangoutLink,
1110
+ attendees: event.attendees?.map((attendee) => ({
1111
+ email: attendee.email,
1112
+ display_name: attendee.displayName,
1113
+ response_status: attendee.responseStatus
1114
+ })),
1115
+ organizer: event.organizer,
1116
+ recurrence: event.recurrence,
1117
+ created: event.created
1118
+ };
1119
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : eventToMarkdown(output, "Event created successfully!");
1120
+ return {
1121
+ content: [{ type: "text", text: textContent }],
1122
+ structuredContent: output
1123
+ };
1124
+ } catch (error) {
1125
+ return createErrorResponse(`creating event "${params.summary}" in calendar ${params.calendar_id}`, error);
1126
+ }
1127
+ }
1128
+ var CALENDAR_CREATE_EVENT_DESCRIPTION = `Create a new event in Google Calendar.
1129
+
1130
+ This tool creates a new calendar event with support for attendees, recurring events, Google Meet links, and more.
1131
+
1132
+ **IMPORTANT**: You must set \`confirm: true\` to create the event. This prevents accidental event creation.
1133
+
1134
+ **Parameters**:
1135
+ - \`calendar_id\` (string, optional): Calendar ID (default: "primary")
1136
+ - \`summary\` (string, required): Event title
1137
+ - \`start\` (string, required): Start time (see formats below)
1138
+ - \`end\` (string, required): End time (see formats below)
1139
+ - \`description\` (string, optional): Event description
1140
+ - \`location\` (string, optional): Event location
1141
+ - \`attendees\` (array, optional): Array of attendee email addresses
1142
+ - \`timezone\` (string, optional): IANA timezone (e.g., "America/Los_Angeles")
1143
+ - \`recurrence\` (array, optional): RRULE strings for recurring events
1144
+ - \`add_meet\` (boolean, optional): Auto-create Google Meet link (default: false)
1145
+ - \`confirm\` (boolean, required): Must be true to create event (safety check)
1146
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
1147
+
1148
+ **Time Formats**:
1149
+ - **Timed event**: RFC3339 with timezone
1150
+ - \`"2024-01-15T09:00:00-08:00"\` (Pacific Time)
1151
+ - \`"2024-01-15T17:00:00Z"\` (UTC)
1152
+ - **All-day event**: Date only (YYYY-MM-DD)
1153
+ - \`"2024-01-15"\` (all day)
1154
+
1155
+ **Recurrence Examples**:
1156
+ - Weekly for 10 weeks: \`["RRULE:FREQ=WEEKLY;COUNT=10"]\`
1157
+ - Daily on weekdays: \`["RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"]\`
1158
+ - Monthly on 15th: \`["RRULE:FREQ=MONTHLY;BYMONTHDAY=15"]\`
1159
+
1160
+ **Returns**:
1161
+ Created event object containing:
1162
+ - \`id\`: Event ID (use for updates/deletes)
1163
+ - \`summary\`: Event title
1164
+ - \`start\`/\`end\`: Event times
1165
+ - \`html_link\`: Link to view in Google Calendar
1166
+ - \`hangout_link\`: Google Meet link (if add_meet was true)
1167
+ - \`attendees\`: List of attendees
1168
+ - All other event details
1169
+
1170
+ **Examples**:
1171
+ - Simple event:
1172
+ \`\`\`json
1173
+ {
1174
+ "summary": "Team Meeting",
1175
+ "start": "2024-01-15T14:00:00-08:00",
1176
+ "end": "2024-01-15T15:00:00-08:00",
1177
+ "confirm": true
1178
+ }
1179
+ \`\`\`
1180
+
1181
+ - All-day event:
1182
+ \`\`\`json
1183
+ {
1184
+ "summary": "Conference",
1185
+ "start": "2024-01-20",
1186
+ "end": "2024-01-21",
1187
+ "location": "San Francisco",
1188
+ "confirm": true
1189
+ }
1190
+ \`\`\`
1191
+
1192
+ - Meeting with attendees and Google Meet:
1193
+ \`\`\`json
1194
+ {
1195
+ "summary": "Sprint Planning",
1196
+ "start": "2024-01-15T10:00:00-08:00",
1197
+ "end": "2024-01-15T11:30:00-08:00",
1198
+ "description": "Q1 sprint planning session",
1199
+ "attendees": ["alice@example.com", "bob@example.com"],
1200
+ "add_meet": true,
1201
+ "confirm": true
1202
+ }
1203
+ \`\`\`
1204
+
1205
+ - Recurring weekly meeting:
1206
+ \`\`\`json
1207
+ {
1208
+ "summary": "Weekly Standup",
1209
+ "start": "2024-01-15T09:00:00-08:00",
1210
+ "end": "2024-01-15T09:30:00-08:00",
1211
+ "recurrence": ["RRULE:FREQ=WEEKLY;COUNT=10"],
1212
+ "add_meet": true,
1213
+ "confirm": true
1214
+ }
1215
+ \`\`\`
1216
+
1217
+ **Common Use Cases**:
1218
+ - Create meetings with automatic Google Meet links
1219
+ - Schedule recurring events (daily standups, weekly meetings)
1220
+ - Create all-day events for conferences or holidays
1221
+ - Add attendees to events
1222
+ - Schedule events in shared calendars
1223
+
1224
+ **Error Handling**:
1225
+ - Returns error if confirm is not true
1226
+ - Returns error if authentication fails
1227
+ - Returns error if calendar not accessible
1228
+ - Returns error if time format is invalid
1229
+ - Returns error if Calendar API request fails`;
1230
+
1231
+ // src/tools/calendar-events.ts
1232
+ import { z as z3 } from "zod";
1233
+ var CalendarEventsInputSchema = z3.object({
1234
+ calendar_id: z3.string().default("primary").describe('Calendar ID to list events from (default: "primary")'),
1235
+ time_min: z3.string().optional().describe("Lower bound for event start time (RFC3339, e.g., 2024-01-01T00:00:00Z)"),
1236
+ time_max: z3.string().optional().describe("Upper bound for event start time (RFC3339, e.g., 2024-12-31T23:59:59Z)"),
1237
+ max_results: z3.number().int().min(1).max(250).default(10).describe("Maximum number of events to return (default: 10, max: 250)"),
1238
+ query: z3.string().optional().describe("Free text search query to filter events"),
1239
+ single_events: z3.boolean().default(true).describe("Expand recurring events into individual instances (default: true)"),
1240
+ order_by: z3.enum(["startTime", "updated"]).default("startTime").describe('Sort order: "startTime" (default) or "updated"'),
1241
+ output_format: z3.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
1242
+ });
1243
+ async function calendarEventsTool(calendarClient, params) {
1244
+ try {
1245
+ const events = await calendarClient.listEvents(params.calendar_id, params.time_min, params.time_max, params.max_results, params.query, params.single_events, params.order_by);
1246
+ const output = {
1247
+ calendar_id: params.calendar_id,
1248
+ count: events.length,
1249
+ events: events.map((event) => ({
1250
+ id: event.id,
1251
+ summary: event.summary,
1252
+ description: event.description,
1253
+ location: event.location,
1254
+ start: event.start,
1255
+ end: event.end,
1256
+ status: event.status,
1257
+ html_link: event.htmlLink,
1258
+ hangout_link: event.hangoutLink,
1259
+ attendees: event.attendees?.map((attendee) => ({
1260
+ email: attendee.email,
1261
+ display_name: attendee.displayName,
1262
+ response_status: attendee.responseStatus,
1263
+ optional: attendee.optional
1264
+ })),
1265
+ creator: event.creator,
1266
+ organizer: event.organizer,
1267
+ recurrence: event.recurrence,
1268
+ recurring_event_id: event.recurringEventId
1269
+ }))
1270
+ };
1271
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : eventListToMarkdown(output);
1272
+ return {
1273
+ content: [{ type: "text", text: textContent }],
1274
+ structuredContent: output
1275
+ };
1276
+ } catch (error) {
1277
+ return createErrorResponse("listing calendar events", error);
1278
+ }
1279
+ }
1280
+ var CALENDAR_EVENTS_DESCRIPTION = `List events from a Google Calendar with flexible filtering options.
1281
+
1282
+ This tool retrieves events from a specified calendar with support for time ranges, text search, and various filtering options.
1283
+
1284
+ **Parameters**:
1285
+ - \`calendar_id\` (string, optional): Calendar ID (default: "primary" for main calendar)
1286
+ - \`time_min\` (string, optional): Lower bound for event start time (RFC3339 timestamp)
1287
+ - \`time_max\` (string, optional): Upper bound for event start time (RFC3339 timestamp)
1288
+ - \`max_results\` (number, optional): Max events to return (1-250, default: 10)
1289
+ - \`query\` (string, optional): Free text search to filter events
1290
+ - \`single_events\` (boolean, optional): Expand recurring events (default: true)
1291
+ - \`order_by\` (string, optional): Sort by "startTime" (default) or "updated"
1292
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
1293
+
1294
+ **Returns**:
1295
+ - \`calendar_id\`: The calendar ID used for the query
1296
+ - \`count\`: Number of events returned
1297
+ - \`events\`: Array of event objects containing:
1298
+ - \`id\`: Event ID
1299
+ - \`summary\`: Event title
1300
+ - \`description\`: Event description
1301
+ - \`location\`: Event location
1302
+ - \`start\`: Start time (date or dateTime with timezone)
1303
+ - \`end\`: End time (date or dateTime with timezone)
1304
+ - \`status\`: Event status (confirmed, tentative, cancelled)
1305
+ - \`html_link\`: Link to view event in Google Calendar
1306
+ - \`hangout_link\`: Google Meet link if available
1307
+ - \`attendees\`: List of attendees with response status
1308
+ - \`creator\`: Event creator information
1309
+ - \`organizer\`: Event organizer information
1310
+ - \`recurrence\`: RRULE array for recurring events
1311
+ - \`recurring_event_id\`: ID of recurring event series
1312
+
1313
+ **RFC3339 Timestamp Format**:
1314
+ - Full datetime: \`2024-01-15T09:00:00-08:00\` or \`2024-01-15T17:00:00Z\`
1315
+ - Note: Use timezone offset or Z for UTC
1316
+
1317
+ **Examples**:
1318
+ - Next 10 events: \`{ "calendar_id": "primary" }\`
1319
+ - Events this week: \`{ "time_min": "2024-01-15T00:00:00Z", "time_max": "2024-01-22T23:59:59Z" }\`
1320
+ - Search meetings: \`{ "query": "standup" }\`
1321
+ - Recent updates: \`{ "order_by": "updated", "max_results": 20 }\`
1322
+ - From shared calendar: \`{ "calendar_id": "team@example.com" }\`
1323
+ - JSON output: \`{ "output_format": "json" }\`
1324
+
1325
+ **Common Use Cases**:
1326
+ - View upcoming events for today/week/month
1327
+ - Search for specific meetings or topics
1328
+ - Get event IDs for modification
1329
+ - Check availability and schedules
1330
+ - Monitor recently updated events
1331
+
1332
+ **Error Handling**:
1333
+ - Returns error if authentication fails
1334
+ - Returns error if calendar doesn't exist or not accessible
1335
+ - Returns error if Calendar API request fails
1336
+ - Returns empty array if no events match filters`;
1337
+
1338
+ // src/tools/calendar-get-event.ts
1339
+ import { z as z4 } from "zod";
1340
+ var CalendarGetEventInputSchema = z4.object({
1341
+ calendar_id: z4.string().default("primary").describe('Calendar ID (default: "primary")'),
1342
+ event_id: z4.string().min(1, "Event ID is required").describe("Event ID to retrieve"),
1343
+ output_format: z4.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
1344
+ });
1345
+ async function calendarGetEventTool(calendarClient, params) {
1346
+ try {
1347
+ const event = await calendarClient.getEvent(params.calendar_id, params.event_id);
1348
+ const output = {
1349
+ id: event.id,
1350
+ summary: event.summary,
1351
+ description: event.description,
1352
+ location: event.location,
1353
+ start: event.start,
1354
+ end: event.end,
1355
+ status: event.status,
1356
+ html_link: event.htmlLink,
1357
+ hangout_link: event.hangoutLink,
1358
+ attendees: event.attendees?.map((attendee) => ({
1359
+ email: attendee.email,
1360
+ display_name: attendee.displayName,
1361
+ response_status: attendee.responseStatus,
1362
+ optional: attendee.optional,
1363
+ organizer: attendee.organizer,
1364
+ self: attendee.self
1365
+ })),
1366
+ creator: event.creator,
1367
+ organizer: event.organizer,
1368
+ recurrence: event.recurrence,
1369
+ recurring_event_id: event.recurringEventId,
1370
+ created: event.created,
1371
+ updated: event.updated
1372
+ };
1373
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : eventToMarkdown(output);
1374
+ return {
1375
+ content: [{ type: "text", text: textContent }],
1376
+ structuredContent: output
1377
+ };
1378
+ } catch (error) {
1379
+ return createErrorResponse(`getting event ${params.event_id} from calendar ${params.calendar_id}`, error);
1380
+ }
1381
+ }
1382
+ var CALENDAR_GET_EVENT_DESCRIPTION = `Get detailed information about a specific calendar event by ID.
1383
+
1384
+ This tool retrieves complete details for a single event, including all attendees, recurrence rules, and metadata.
1385
+
1386
+ **Parameters**:
1387
+ - \`calendar_id\` (string, optional): Calendar ID (default: "primary")
1388
+ - \`event_id\` (string, required): Event ID to retrieve
1389
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
1390
+
1391
+ **Returns**:
1392
+ Complete event object containing:
1393
+ - \`id\`: Event ID
1394
+ - \`summary\`: Event title
1395
+ - \`description\`: Full event description
1396
+ - \`location\`: Event location
1397
+ - \`start\`: Start time (date or dateTime with timezone)
1398
+ - \`end\`: End time (date or dateTime with timezone)
1399
+ - \`status\`: Event status (confirmed, tentative, cancelled)
1400
+ - \`html_link\`: Link to view/edit event in Google Calendar
1401
+ - \`hangout_link\`: Google Meet link if available
1402
+ - \`attendees\`: Complete attendee list with:
1403
+ - \`email\`: Attendee email address
1404
+ - \`display_name\`: Attendee name
1405
+ - \`response_status\`: needsAction, declined, tentative, or accepted
1406
+ - \`optional\`: Whether attendance is optional
1407
+ - \`organizer\`: Whether this attendee is the organizer
1408
+ - \`self\`: Whether this is the authenticated user
1409
+ - \`creator\`: Event creator (email and display name)
1410
+ - \`organizer\`: Event organizer (email and display name)
1411
+ - \`recurrence\`: RRULE array for recurring events
1412
+ - \`recurring_event_id\`: Parent event ID if this is a recurring instance
1413
+ - \`created\`: Event creation timestamp
1414
+ - \`updated\`: Last update timestamp
1415
+
1416
+ **Examples**:
1417
+ - Get event from primary calendar: \`{ "event_id": "abc123xyz" }\`
1418
+ - Get from shared calendar: \`{ "calendar_id": "team@example.com", "event_id": "abc123xyz" }\`
1419
+ - JSON output: \`{ "event_id": "abc123xyz", "output_format": "json" }\`
1420
+
1421
+ **Common Use Cases**:
1422
+ - View full event details including description
1423
+ - Check attendee responses
1424
+ - Get Google Meet link for a meeting
1425
+ - Retrieve recurrence rules
1426
+ - Check event metadata (created/updated times)
1427
+
1428
+ **Error Handling**:
1429
+ - Returns error if authentication fails
1430
+ - Returns error if event doesn't exist
1431
+ - Returns error if calendar not accessible
1432
+ - Returns error if Calendar API request fails`;
1433
+
1434
+ // src/tools/calendar-list.ts
1435
+ import { z as z5 } from "zod";
1436
+ var CalendarListInputSchema = z5.object({
1437
+ show_hidden: z5.boolean().default(false).describe("Include hidden calendars in the results (default: false)"),
1438
+ output_format: z5.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
1439
+ });
1440
+ async function calendarListTool(calendarClient, params) {
1441
+ try {
1442
+ const calendars = await calendarClient.listCalendars(params.show_hidden);
1443
+ const output = {
1444
+ count: calendars.length,
1445
+ calendars: calendars.map((cal) => ({
1446
+ id: cal.id,
1447
+ summary: cal.summary,
1448
+ description: cal.description,
1449
+ timezone: cal.timeZone,
1450
+ primary: cal.primary,
1451
+ access_role: cal.accessRole,
1452
+ background_color: cal.backgroundColor,
1453
+ foreground_color: cal.foregroundColor
1454
+ }))
1455
+ };
1456
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : calendarListToMarkdown(output);
1457
+ return {
1458
+ content: [{ type: "text", text: textContent }],
1459
+ structuredContent: output
1460
+ };
1461
+ } catch (error) {
1462
+ return createErrorResponse("listing calendars", error);
1463
+ }
1464
+ }
1465
+ var CALENDAR_LIST_DESCRIPTION = `List all calendars for the authenticated Google account.
1466
+
1467
+ This tool retrieves all calendars accessible by the authenticated user, including their primary calendar, shared calendars, and subscribed calendars.
1468
+
1469
+ **Parameters**:
1470
+ - \`show_hidden\` (boolean, optional): Include hidden calendars (default: false)
1471
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
1472
+
1473
+ **Returns**:
1474
+ - \`count\`: Number of calendars found
1475
+ - \`calendars\`: Array of calendar objects containing:
1476
+ - \`id\`: Calendar ID (use this for other calendar operations)
1477
+ - \`summary\`: Calendar name/title
1478
+ - \`description\`: Calendar description
1479
+ - \`timezone\`: Calendar timezone (IANA format)
1480
+ - \`primary\`: Whether this is the user's primary calendar
1481
+ - \`access_role\`: User's access level (owner, writer, reader, freeBusyReader)
1482
+ - \`background_color\`: Calendar background color (hex)
1483
+ - \`foreground_color\`: Calendar foreground color (hex)
1484
+
1485
+ **Examples**:
1486
+ - List visible calendars: \`{ "show_hidden": false }\`
1487
+ - List all calendars including hidden: \`{ "show_hidden": true }\`
1488
+ - JSON output: \`{ "output_format": "json" }\`
1489
+
1490
+ **Common Use Cases**:
1491
+ - Get calendar IDs for use with other calendar tools
1492
+ - Discover shared calendars
1493
+ - Verify calendar access permissions
1494
+
1495
+ **Error Handling**:
1496
+ - Returns error if authentication fails
1497
+ - Returns error if Calendar API request fails`;
1498
+
1499
+ // src/tools/create-draft.ts
1500
+ import json2md3 from "json2md";
1501
+ import { z as z6 } from "zod";
1502
+ var CreateDraftInputSchema = z6.object({
1503
+ to: z6.email("Must be a valid email address").describe("Recipient email address"),
1504
+ subject: z6.string().min(1, "Subject cannot be empty").describe("Email subject"),
1505
+ body: z6.string().min(1, "Body cannot be empty").describe("Email body content"),
1506
+ content_type: z6.enum(["text/plain", "text/html"]).default("text/plain").describe("Content type: text/plain (default) or text/html for HTML emails"),
1507
+ cc: z6.email("CC must be a valid email address").optional().describe("CC (carbon copy) email address"),
1508
+ bcc: z6.email("BCC must be a valid email address").optional().describe("BCC (blind carbon copy) email address"),
1509
+ output_format: z6.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
1510
+ });
1511
+ function draftCreatedToMarkdown(params, result) {
1512
+ const sections = [
1513
+ { h1: "✅ Draft Created Successfully" },
1514
+ { h2: "Draft Details" },
1515
+ {
1516
+ ul: [
1517
+ `**To:** ${params.to}`,
1518
+ ...params.cc ? [`**CC:** ${params.cc}`] : [],
1519
+ ...params.bcc ? [`**BCC:** ${params.bcc}`] : [],
1520
+ `**Subject:** ${params.subject}`,
1521
+ `**Draft ID:** ${result.id}`,
1522
+ `**Message ID:** ${result.message.id}`
1523
+ ]
1524
+ },
1525
+ {
1526
+ p: "The draft has been saved and will appear in your Drafts folder. You can edit and send it later from Gmail."
1527
+ }
1528
+ ];
1529
+ return json2md3(sections);
1530
+ }
1531
+ async function createDraftTool(gmailClient, params) {
1532
+ try {
1533
+ const result = await gmailClient.createDraft(params.to, params.subject, params.body, params.content_type, params.cc, params.bcc);
1534
+ const output = {
1535
+ created: true,
1536
+ draft_id: result.id,
1537
+ message_id: result.message.id,
1538
+ thread_id: result.message.threadId,
1539
+ to: params.to,
1540
+ cc: params.cc,
1541
+ bcc: params.bcc,
1542
+ subject: params.subject,
1543
+ content_type: params.content_type
1544
+ };
1545
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : draftCreatedToMarkdown(params, result);
1546
+ return {
1547
+ content: [{ type: "text", text: textContent }],
1548
+ structuredContent: output
1549
+ };
1550
+ } catch (error) {
1551
+ return createErrorResponse("creating draft", error);
1552
+ }
1553
+ }
1554
+ var CREATE_DRAFT_DESCRIPTION = `Create a Gmail draft email that can be edited and sent later.
1555
+
1556
+ This tool creates a draft email in your Gmail Drafts folder. Unlike sending an email directly, drafts are saved but not sent, allowing you to review, edit, or send them later through the Gmail interface.
1557
+
1558
+ **Use Case**:
1559
+ - Compose emails for later review
1560
+ - Prepare emails that need approval before sending
1561
+ - Create email templates
1562
+ - Save work-in-progress emails
1563
+
1564
+ **Parameters**:
1565
+ - \`to\` (string, required): Recipient email address
1566
+ - \`subject\` (string, required): Email subject line
1567
+ - \`body\` (string, required): Email body content
1568
+ - \`content_type\` (string, optional): "text/plain" (default) or "text/html" for HTML emails
1569
+ - \`cc\` (string, optional): CC email address
1570
+ - \`bcc\` (string, optional): BCC email address
1571
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
1572
+
1573
+ **Returns**:
1574
+ - \`created\`: Always true on success
1575
+ - \`draft_id\`: Draft ID (for future operations)
1576
+ - \`message_id\`: Associated message ID
1577
+ - \`thread_id\`: Thread ID
1578
+ - \`to\`, \`cc\`, \`bcc\`: Recipients
1579
+ - \`subject\`: Email subject
1580
+ - \`content_type\`: Content type used
1581
+
1582
+ **Examples**:
1583
+ - Simple draft: \`{ "to": "user@example.com", "subject": "Follow up", "body": "Hi there!" }\`
1584
+ - HTML draft: \`{ "to": "user@example.com", "subject": "Newsletter", "body": "<h1>News</h1>", "content_type": "text/html" }\`
1585
+ - With CC/BCC: \`{ "to": "user@example.com", "cc": "manager@example.com", "bcc": "archive@example.com", "subject": "Report", "body": "Quarterly report attached" }\`
1586
+
1587
+ **HTML Emails**:
1588
+ - Set \`content_type: "text/html"\`
1589
+ - Body should contain valid HTML
1590
+ - Supported tags: h1-h6, p, a, strong, em, ul, ol, li, br, img
1591
+
1592
+ **Comparison with send_email**:
1593
+ - \`create_draft\`: Saves to Drafts folder, does not send
1594
+ - \`send_email\`: Sends immediately (with confirmation)
1595
+
1596
+ **Draft Management**:
1597
+ - Created drafts appear in Gmail's Drafts folder
1598
+ - Can be edited in Gmail web or mobile app
1599
+ - Can be sent later using Gmail interface
1600
+ - Draft ID can be used with other draft tools (update_draft, delete_draft)
1601
+
1602
+ **Error Handling**:
1603
+ - Returns error if email addresses are invalid
1604
+ - Returns error if authentication lacks compose permissions
1605
+ - Returns error if Gmail API fails
1606
+
1607
+ **Permissions**:
1608
+ - Requires \`gmail.compose\` or \`gmail.modify\` scope
1609
+
1610
+ **Workflow**:
1611
+ 1. Create draft with email content
1612
+ 2. Draft is saved to Drafts folder
1613
+ 3. Review/edit in Gmail if needed
1614
+ 4. Send from Gmail when ready
1615
+
1616
+ **Benefits**:
1617
+ - Safe: No risk of accidentally sending
1618
+ - Flexible: Can be edited before sending
1619
+ - Convenient: Pre-compose emails for later
1620
+ - Collaborative: Others with access can review drafts`;
1621
+
1622
+ // src/tools/create-label.ts
1623
+ import json2md4 from "json2md";
1624
+ import { z as z7 } from "zod";
1625
+ var CreateLabelInputSchema = z7.object({
1626
+ name: z7.string().min(1, "Label name cannot be empty").describe("The label name (e.g., 'Work', 'Personal/Family'). Use '/' for nested labels."),
1627
+ message_list_visibility: z7.enum(["show", "hide"]).optional().describe("How label appears in message list. Default: 'show'. Use 'hide' to hide messages with this label from message list."),
1628
+ label_list_visibility: z7.enum(["labelShow", "labelShowIfUnread", "labelHide"]).optional().describe("How label appears in label list. Default: 'labelShow'. Options: 'labelShow' (always visible), 'labelShowIfUnread' (only when unread), 'labelHide' (hidden)."),
1629
+ background_color: z7.string().optional().describe("Background color in hex format (e.g., '#ff0000'). Must provide both background and text color together."),
1630
+ text_color: z7.string().optional().describe("Text color in hex format (e.g., '#ffffff'). Must provide both background and text color together."),
1631
+ output_format: z7.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
1632
+ });
1633
+ function createdLabelToMarkdown(label) {
1634
+ const sections = [
1635
+ { h1: "Label Created Successfully" },
1636
+ { h2: "Details" },
1637
+ {
1638
+ ul: [
1639
+ `**Name:** ${label.name}`,
1640
+ `**ID:** ${label.id}`,
1641
+ "**Type:** Custom Label"
1642
+ ]
1643
+ }
1644
+ ];
1645
+ if (label.messageListVisibility || label.labelListVisibility) {
1646
+ sections.push({ h2: "Visibility Settings" });
1647
+ const visibilityItems = [];
1648
+ if (label.messageListVisibility) {
1649
+ visibilityItems.push(`**Message List:** ${label.messageListVisibility}`);
1650
+ }
1651
+ if (label.labelListVisibility) {
1652
+ visibilityItems.push(`**Label List:** ${label.labelListVisibility}`);
1653
+ }
1654
+ sections.push({ ul: visibilityItems });
1655
+ }
1656
+ if (label.color) {
1657
+ sections.push({ h2: "Color" });
1658
+ sections.push({
1659
+ ul: [
1660
+ `**Text Color:** ${label.color.textColor}`,
1661
+ `**Background Color:** ${label.color.backgroundColor}`
1662
+ ]
1663
+ });
1664
+ }
1665
+ sections.push({
1666
+ p: `You can now use this label ID (${label.id}) with other tools like modify_labels or batch_modify.`
1667
+ });
1668
+ return json2md4(sections);
1669
+ }
1670
+ async function createLabelTool(gmailClient, params) {
1671
+ try {
1672
+ if (params.background_color && !params.text_color || !params.background_color && params.text_color) {
1673
+ return {
1674
+ content: [
1675
+ {
1676
+ type: "text",
1677
+ text: "Error: Both background_color and text_color must be provided together"
1678
+ }
1679
+ ],
1680
+ isError: true
1681
+ };
1682
+ }
1683
+ const label = await gmailClient.createLabel(params.name, params.message_list_visibility, params.label_list_visibility, params.background_color, params.text_color);
1684
+ const output = {
1685
+ id: label.id,
1686
+ name: label.name,
1687
+ type: label.type,
1688
+ message_list_visibility: label.messageListVisibility || null,
1689
+ label_list_visibility: label.labelListVisibility || null,
1690
+ color: label.color || null,
1691
+ created: true
1692
+ };
1693
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : createdLabelToMarkdown(label);
1694
+ return {
1695
+ content: [{ type: "text", text: textContent }],
1696
+ structuredContent: output
1697
+ };
1698
+ } catch (error) {
1699
+ return createErrorResponse("creating label", error);
1700
+ }
1701
+ }
1702
+ var CREATE_LABEL_DESCRIPTION = `Create a new custom Gmail label for organizing emails.
1703
+
1704
+ This tool creates a new label with customizable visibility and color settings. Labels are used to organize and categorize emails in Gmail.
1705
+
1706
+ **Parameters**:
1707
+ - \`name\` (string, required): The label name. Can include "/" for nested labels (e.g., "Work/Projects")
1708
+ - \`message_list_visibility\` (string, optional): How label appears in message list
1709
+ - "show" (default): Messages with this label appear in message list
1710
+ - "hide": Messages with this label are hidden from message list
1711
+ - \`label_list_visibility\` (string, optional): How label appears in label list (sidebar)
1712
+ - "labelShow" (default): Always visible in label list
1713
+ - "labelShowIfUnread": Only visible when there are unread messages
1714
+ - "labelHide": Hidden from label list
1715
+ - \`background_color\` (string, optional): Background color in hex format (e.g., "#ff0000")
1716
+ - \`text_color\` (string, optional): Text color in hex format (e.g., "#ffffff")
1717
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
1718
+
1719
+ **Color Settings**:
1720
+ - Both \`background_color\` and \`text_color\` must be provided together
1721
+ - Colors should be in hex format (e.g., "#ff0000" for red)
1722
+ - Gmail API will validate colors and may reject invalid combinations
1723
+
1724
+ **Nested Labels**:
1725
+ - Use "/" in label name to create nested labels (e.g., "Work/Projects/2024")
1726
+ - Gmail automatically creates parent labels if they don't exist
1727
+ - Nested labels appear hierarchically in the label list
1728
+
1729
+ **Returns**:
1730
+ - \`id\`: The newly created label ID (e.g., "Label_123")
1731
+ - \`name\`: The label name
1732
+ - \`type\`: Always "user" for custom labels
1733
+ - \`message_list_visibility\`: Applied message list visibility
1734
+ - \`label_list_visibility\`: Applied label list visibility
1735
+ - \`color\`: Applied color settings (if provided)
1736
+ - \`created\`: Always true on success
1737
+
1738
+ **Use Cases**:
1739
+ - Create organizational labels (Work, Personal, Projects, etc.)
1740
+ - Create nested label hierarchies for detailed organization
1741
+ - Set up labels with custom colors for visual organization
1742
+ - Create labels to use with filters and automation
1743
+
1744
+ **Examples**:
1745
+ - Simple label: \`{ "name": "Important" }\`
1746
+ - Nested label: \`{ "name": "Work/Projects/Q1-2024" }\`
1747
+ - Colored label: \`{ "name": "Urgent", "background_color": "#ff0000", "text_color": "#ffffff" }\`
1748
+ - Hidden label: \`{ "name": "Archive", "label_list_visibility": "labelHide" }\`
1749
+
1750
+ **Error Handling**:
1751
+ - Returns error if label name already exists
1752
+ - Returns error if colors are invalid or only one color is provided
1753
+ - Returns error if authentication lacks gmail.labels scope
1754
+ - Requires \`gmail.labels\` scope for label creation
1755
+
1756
+ **Scope Requirements**:
1757
+ - Requires \`gmail.labels\` or \`gmail.modify\` scope`;
1758
+
1759
+ // src/tools/delete-label.ts
1760
+ import json2md5 from "json2md";
1761
+ import { z as z8 } from "zod";
1762
+ var SYSTEM_LABELS = [
1763
+ "INBOX",
1764
+ "SENT",
1765
+ "DRAFT",
1766
+ "TRASH",
1767
+ "SPAM",
1768
+ "STARRED",
1769
+ "IMPORTANT",
1770
+ "UNREAD",
1771
+ "CATEGORY_PERSONAL",
1772
+ "CATEGORY_SOCIAL",
1773
+ "CATEGORY_PROMOTIONS",
1774
+ "CATEGORY_UPDATES",
1775
+ "CATEGORY_FORUMS"
1776
+ ];
1777
+ var DeleteLabelInputSchema = z8.object({
1778
+ label_id: z8.string().min(1, "Label ID cannot be empty").describe("The label ID to delete (e.g., 'Label_123'). Cannot delete system labels."),
1779
+ output_format: z8.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
1780
+ });
1781
+ function deletionToMarkdown(labelId) {
1782
+ const sections = [
1783
+ { h1: "Label Deleted Successfully" },
1784
+ {
1785
+ p: `Label with ID **${labelId}** has been permanently deleted from your Gmail account.`
1786
+ },
1787
+ { h2: "Important Notes" },
1788
+ {
1789
+ ul: [
1790
+ "The label has been removed from all messages",
1791
+ "Messages are not deleted, only the label is removed",
1792
+ "This action cannot be undone",
1793
+ "Label will no longer appear in label lists or on messages"
1794
+ ]
1795
+ }
1796
+ ];
1797
+ return json2md5(sections);
1798
+ }
1799
+ async function deleteLabelTool(gmailClient, params) {
1800
+ try {
1801
+ if (SYSTEM_LABELS.includes(params.label_id)) {
1802
+ return {
1803
+ content: [
1804
+ {
1805
+ type: "text",
1806
+ text: `Error: Cannot delete system label ${params.label_id}. Only user-created labels can be deleted.`
1807
+ }
1808
+ ],
1809
+ isError: true
1810
+ };
1811
+ }
1812
+ if (params.label_id.startsWith("CATEGORY_") || params.label_id === params.label_id.toUpperCase()) {
1813
+ return {
1814
+ content: [
1815
+ {
1816
+ type: "text",
1817
+ text: `Error: Cannot delete system label ${params.label_id}. Only user-created labels can be deleted.`
1818
+ }
1819
+ ],
1820
+ isError: true
1821
+ };
1822
+ }
1823
+ await gmailClient.deleteLabel(params.label_id);
1824
+ const output = {
1825
+ label_id: params.label_id,
1826
+ deleted: true,
1827
+ message: "Label deleted successfully. The label has been removed from all messages."
1828
+ };
1829
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : deletionToMarkdown(params.label_id);
1830
+ return {
1831
+ content: [{ type: "text", text: textContent }],
1832
+ structuredContent: output
1833
+ };
1834
+ } catch (error) {
1835
+ const errorMessage = error instanceof Error ? error.message : String(error);
1836
+ if (errorMessage.includes("cannot") || errorMessage.includes("system") || errorMessage.includes("permission")) {
1837
+ return {
1838
+ content: [
1839
+ {
1840
+ type: "text",
1841
+ text: `Error: Cannot delete label ${params.label_id}. This appears to be a system label. Only user-created labels can be deleted.`
1842
+ }
1843
+ ],
1844
+ isError: true
1845
+ };
1846
+ }
1847
+ return {
1848
+ content: [
1849
+ {
1850
+ type: "text",
1851
+ text: `Error deleting label: ${errorMessage}`
1852
+ }
1853
+ ],
1854
+ isError: true
1855
+ };
1856
+ }
1857
+ }
1858
+ var DELETE_LABEL_DESCRIPTION = `Delete a custom Gmail label permanently.
1859
+
1860
+ This tool permanently deletes a user-created label from your Gmail account. System labels cannot be deleted. The label is removed from all messages, but the messages themselves are not deleted.
1861
+
1862
+ **IMPORTANT: This is a destructive operation that cannot be undone.**
1863
+
1864
+ **Parameters**:
1865
+ - \`label_id\` (string, required): The label ID to delete (e.g., "Label_123")
1866
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
1867
+
1868
+ **System Label Protection**:
1869
+ - System labels (INBOX, SENT, TRASH, STARRED, etc.) **cannot** be deleted
1870
+ - Category labels (CATEGORY_PERSONAL, CATEGORY_SOCIAL, etc.) **cannot** be deleted
1871
+ - Only user-created custom labels can be deleted
1872
+ - Attempting to delete a system label returns a clear error message
1873
+
1874
+ **What Happens When You Delete a Label**:
1875
+ 1. The label is permanently removed from your Gmail account
1876
+ 2. The label is removed from all messages that had it
1877
+ 3. Messages are **not** deleted - only the label is removed
1878
+ 4. This action **cannot be undone**
1879
+ 5. The label will no longer appear in:
1880
+ - Label lists
1881
+ - Message labels
1882
+ - Filters or automation rules
1883
+
1884
+ **Returns**:
1885
+ - \`label_id\`: The deleted label ID
1886
+ - \`deleted\`: Always true on success
1887
+ - \`message\`: Confirmation message
1888
+
1889
+ **Use Cases**:
1890
+ - Remove unused custom labels
1891
+ - Clean up label organization
1892
+ - Delete obsolete or incorrectly created labels
1893
+ - Remove labels that are no longer needed
1894
+
1895
+ **Examples**:
1896
+ - Delete custom label: \`{ "label_id": "Label_123" }\`
1897
+ - Delete with JSON output: \`{ "label_id": "Label_456", "output_format": "json" }\`
1898
+
1899
+ **Error Handling**:
1900
+ - Returns error if label ID doesn't exist
1901
+ - Returns error if trying to delete system labels
1902
+ - Returns error if authentication lacks gmail.labels scope
1903
+ - Returns clear error message explaining why deletion failed
1904
+
1905
+ **Safety Considerations**:
1906
+ - This is a destructive operation marked with \`destructiveHint: true\`
1907
+ - Consider using \`get_label\` first to verify which label you're deleting
1908
+ - Use \`list_labels\` to see all labels before deletion
1909
+ - There is no "undo" - once deleted, the label is gone permanently
1910
+ - Messages with the label are not affected, only the label itself is removed
1911
+
1912
+ **Scope Requirements**:
1913
+ - Requires \`gmail.labels\` or \`gmail.modify\` scope
1914
+
1915
+ **Alternative to Deletion**:
1916
+ If you want to hide a label instead of deleting it, consider using the \`update_label\` tool to set \`label_list_visibility\` to "labelHide" instead.`;
1917
+
1918
+ // src/tools/get-attachment.ts
1919
+ import { z as z9 } from "zod";
1920
+ var GetAttachmentInputSchema = z9.object({
1921
+ message_id: z9.string().min(1, "Message ID cannot be empty").describe("The Gmail message ID containing the attachment"),
1922
+ attachment_id: z9.string().min(1, "Attachment ID cannot be empty").describe("The attachment ID to download (from list_attachments)"),
1923
+ output_format: z9.enum(["base64", "json"]).default("base64").describe("Output format: base64 (default, returns raw base64url string) or json (returns structured object)")
1924
+ });
1925
+ async function getAttachmentTool(gmailClient, params) {
1926
+ try {
1927
+ const attachmentData = await gmailClient.getAttachment(params.message_id, params.attachment_id);
1928
+ const output = {
1929
+ message_id: params.message_id,
1930
+ attachment_id: params.attachment_id,
1931
+ data: attachmentData,
1932
+ encoding: "base64url",
1933
+ note: "Data is base64url encoded. To decode: replace - with +, _ with /, then base64 decode."
1934
+ };
1935
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : attachmentData;
1936
+ return {
1937
+ content: [{ type: "text", text: textContent }],
1938
+ structuredContent: output
1939
+ };
1940
+ } catch (error) {
1941
+ return createErrorResponse("getting attachment", error);
1942
+ }
1943
+ }
1944
+ var GET_ATTACHMENT_DESCRIPTION = `Download a Gmail attachment by its attachment ID.
1945
+
1946
+ This tool downloads the content of a specific attachment from a Gmail message. The attachment is returned as base64url-encoded data.
1947
+
1948
+ **Parameters**:
1949
+ - \`message_id\` (string, required): The Gmail message ID containing the attachment
1950
+ - \`attachment_id\` (string, required): The attachment ID (obtained from list_attachments tool)
1951
+ - \`output_format\` (string, optional): Output format: "base64" (default) or "json"
1952
+
1953
+ **Returns**:
1954
+ - \`message_id\`: The message ID
1955
+ - \`attachment_id\`: The attachment ID
1956
+ - \`data\`: Base64url-encoded attachment data
1957
+ - \`encoding\`: Always "base64url"
1958
+ - \`note\`: Decoding instructions
1959
+
1960
+ **Base64url Encoding**:
1961
+ Gmail uses base64url encoding (URL-safe variant):
1962
+ - Characters \`-\` and \`_\` replace \`+\` and \`/\`
1963
+ - No padding with \`=\`
1964
+
1965
+ To decode:
1966
+ 1. Replace \`-\` with \`+\`
1967
+ 2. Replace \`_\` with \`/\`
1968
+ 3. Base64 decode the result
1969
+
1970
+ **Examples**:
1971
+ - Get attachment: \`{ "message_id": "18f3c5d4e8a2b1c0", "attachment_id": "ANGjdJ8..." }\`
1972
+ - JSON output: \`{ "message_id": "18f3c5d4e8a2b1c0", "attachment_id": "ANGjdJ8...", "output_format": "json" }\`
1973
+
1974
+ **Error Handling**:
1975
+ - Returns error if message ID doesn't exist
1976
+ - Returns error if attachment ID doesn't exist or is invalid
1977
+ - Returns error if authentication fails
1978
+
1979
+ **Use Cases**:
1980
+ - Download email attachments programmatically
1981
+ - Extract attachment data for processing
1982
+ - Save attachments to disk after decoding
1983
+
1984
+ **Workflow**:
1985
+ 1. Use \`list_attachments\` to get attachment IDs and metadata
1986
+ 2. Use this tool to download specific attachments by ID
1987
+ 3. Decode base64url data to get original file content`;
1988
+
1989
+ // src/tools/get-email.ts
1990
+ import json2md6 from "json2md";
1991
+ import { z as z10 } from "zod";
1992
+ var GetEmailInputSchema = z10.object({
1993
+ message_id: z10.string().min(1, "Message ID cannot be empty").describe("The Gmail message ID to retrieve"),
1994
+ include_body: z10.boolean().default(true).describe("Whether to include full email body in results (default: true)"),
1995
+ output_format: z10.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
1996
+ });
1997
+ function emailToMarkdown(email) {
1998
+ const sections = [
1999
+ { h1: email.subject },
2000
+ {
2001
+ ul: [
2002
+ `**From:** ${email.from}`,
2003
+ `**To:** ${email.to}`,
2004
+ `**Date:** ${email.date}`,
2005
+ `**Message ID:** ${email.id}`,
2006
+ `**Thread ID:** ${email.thread_id}`
2007
+ ]
2008
+ }
2009
+ ];
2010
+ if (email.labels && email.labels.length > 0) {
2011
+ sections.push({
2012
+ p: `**Labels:** ${email.labels.join(", ")}`
2013
+ });
2014
+ }
2015
+ if (email.body) {
2016
+ sections.push({ h2: "Body" });
2017
+ sections.push({ p: email.body });
2018
+ } else {
2019
+ sections.push({ h2: "Snippet" });
2020
+ sections.push({ p: email.snippet });
2021
+ }
2022
+ return json2md6(sections);
2023
+ }
2024
+ async function getEmailTool(gmailClient, params) {
2025
+ try {
2026
+ const email = await gmailClient.getMessage(params.message_id, params.include_body);
2027
+ const output = formatEmailForOutput(email);
2028
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : emailToMarkdown(output);
2029
+ return {
2030
+ content: [{ type: "text", text: textContent }],
2031
+ structuredContent: output
2032
+ };
2033
+ } catch (error) {
2034
+ return createErrorResponse("getting email", error);
2035
+ }
2036
+ }
2037
+ var GET_EMAIL_DESCRIPTION = `Get a single Gmail email by its message ID with full details.
2038
+
2039
+ This tool retrieves a specific email message from Gmail using its unique message ID. It returns detailed information including headers, body content, labels, and thread information.
2040
+
2041
+ **Parameters**:
2042
+ - \`message_id\` (string, required): The Gmail message ID to retrieve
2043
+ - \`include_body\` (boolean, optional): Include full email body (default: true)
2044
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2045
+
2046
+ **Returns**:
2047
+ - \`id\`: Message ID
2048
+ - \`thread_id\`: Thread ID (for conversation grouping)
2049
+ - \`subject\`: Email subject line
2050
+ - \`from\`: Sender email address
2051
+ - \`to\`: Recipient email address(es)
2052
+ - \`date\`: Email date
2053
+ - \`snippet\`: Short preview of email content
2054
+ - \`body\`: Full email body (if include_body is true)
2055
+ - \`labels\`: Array of Gmail labels applied to the email
2056
+
2057
+ **Examples**:
2058
+ - Get email with body: \`{ "message_id": "18f3c5d4e8a2b1c0" }\`
2059
+ - Get email metadata only: \`{ "message_id": "18f3c5d4e8a2b1c0", "include_body": false }\`
2060
+ - JSON output: \`{ "message_id": "18f3c5d4e8a2b1c0", "output_format": "json" }\`
2061
+
2062
+ **Error Handling**:
2063
+ - Returns error if message ID doesn't exist
2064
+ - Returns error if authentication fails
2065
+ - Returns error if Gmail API request fails
2066
+
2067
+ **Use Cases**:
2068
+ - View full content of a specific email
2069
+ - Get detailed email headers and metadata
2070
+ - Retrieve email for further processing or analysis`;
2071
+
2072
+ // src/tools/get-label.ts
2073
+ import json2md7 from "json2md";
2074
+ import { z as z11 } from "zod";
2075
+ var GetLabelInputSchema = z11.object({
2076
+ label_id: z11.string().min(1, "Label ID cannot be empty").describe("The label ID to retrieve (e.g., 'INBOX', 'Label_123')"),
2077
+ output_format: z11.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
2078
+ });
2079
+ function labelToMarkdown(label) {
2080
+ const sections = [
2081
+ { h1: `Label: ${label.name}` },
2082
+ { h2: "Details" },
2083
+ {
2084
+ ul: [
2085
+ `**ID:** ${label.id}`,
2086
+ `**Type:** ${label.type === "system" ? "System Label" : "Custom Label"}`,
2087
+ `**Total Messages:** ${label.messagesTotal || 0}`,
2088
+ `**Unread Messages:** ${label.messagesUnread || 0}`
2089
+ ]
2090
+ }
2091
+ ];
2092
+ if (label.messageListVisibility || label.labelListVisibility) {
2093
+ sections.push({ h2: "Visibility Settings" });
2094
+ const visibilityItems = [];
2095
+ if (label.messageListVisibility) {
2096
+ visibilityItems.push(`**Message List:** ${label.messageListVisibility}`);
2097
+ }
2098
+ if (label.labelListVisibility) {
2099
+ visibilityItems.push(`**Label List:** ${label.labelListVisibility}`);
2100
+ }
2101
+ sections.push({ ul: visibilityItems });
2102
+ }
2103
+ if (label.color) {
2104
+ sections.push({ h2: "Color" });
2105
+ sections.push({
2106
+ ul: [
2107
+ `**Text Color:** ${label.color.textColor}`,
2108
+ `**Background Color:** ${label.color.backgroundColor}`
2109
+ ]
2110
+ });
2111
+ }
2112
+ return json2md7(sections);
2113
+ }
2114
+ async function getLabelTool(gmailClient, params) {
2115
+ try {
2116
+ const label = await gmailClient.getLabel(params.label_id);
2117
+ const output = {
2118
+ id: label.id,
2119
+ name: label.name,
2120
+ type: label.type,
2121
+ message_list_visibility: label.messageListVisibility || null,
2122
+ label_list_visibility: label.labelListVisibility || null,
2123
+ messages_total: label.messagesTotal || 0,
2124
+ messages_unread: label.messagesUnread || 0,
2125
+ color: label.color || null
2126
+ };
2127
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : labelToMarkdown(label);
2128
+ return {
2129
+ content: [{ type: "text", text: textContent }],
2130
+ structuredContent: output
2131
+ };
2132
+ } catch (error) {
2133
+ return createErrorResponse("getting label", error);
2134
+ }
2135
+ }
2136
+ var GET_LABEL_DESCRIPTION = `Get detailed information about a specific Gmail label.
2137
+
2138
+ This tool retrieves comprehensive details about a label, including its name, type, visibility settings, message counts, and color configuration.
2139
+
2140
+ **Parameters**:
2141
+ - \`label_id\` (string, required): The label ID to retrieve (e.g., "INBOX", "Label_123")
2142
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2143
+
2144
+ **Returns**:
2145
+ - \`id\`: Label ID
2146
+ - \`name\`: Label name
2147
+ - \`type\`: "system" or "user"
2148
+ - \`message_list_visibility\`: How label appears in message list ("show" or "hide")
2149
+ - \`label_list_visibility\`: How label appears in label list ("labelShow", "labelShowIfUnread", or "labelHide")
2150
+ - \`messages_total\`: Total number of messages with this label
2151
+ - \`messages_unread\`: Number of unread messages with this label
2152
+ - \`color\`: Label color configuration (textColor and backgroundColor)
2153
+
2154
+ **Label Types**:
2155
+ - **System Labels**: Predefined Gmail labels like INBOX, SENT, TRASH, STARRED, etc.
2156
+ - **Custom Labels**: User-created labels with IDs like "Label_123"
2157
+
2158
+ **Visibility Settings**:
2159
+ - \`messageListVisibility\`: Controls if messages with this label show in message list
2160
+ - \`labelListVisibility\`: Controls how label appears in left sidebar label list
2161
+
2162
+ **Use Cases**:
2163
+ - Check message counts for a specific label
2164
+ - View label configuration and settings
2165
+ - Verify label exists before using in other operations
2166
+ - Check color settings for custom labels
2167
+
2168
+ **Examples**:
2169
+ - Get inbox details: \`{ "label_id": "INBOX" }\`
2170
+ - Get custom label: \`{ "label_id": "Label_123" }\`
2171
+ - Get starred label info: \`{ "label_id": "STARRED" }\`
2172
+
2173
+ **Error Handling**:
2174
+ - Returns error if label ID doesn't exist
2175
+ - Returns error if authentication fails
2176
+ - System labels and custom labels are both supported`;
2177
+
2178
+ // src/tools/get-thread.ts
2179
+ import json2md8 from "json2md";
2180
+ import { z as z12 } from "zod";
2181
+ var GetThreadInputSchema = z12.object({
2182
+ thread_id: z12.string().min(1, "Thread ID cannot be empty").describe("The Gmail thread ID to retrieve"),
2183
+ include_body: z12.boolean().default(false).describe("Whether to include full email body for all messages (default: false)"),
2184
+ output_format: z12.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
2185
+ });
2186
+ function threadToMarkdown(threadId, messages) {
2187
+ const sections = [
2188
+ { h1: `Thread: ${messages[0]?.subject || "Conversation"}` },
2189
+ {
2190
+ p: `**Thread ID:** ${threadId} | **Messages:** ${messages.length}`
2191
+ }
2192
+ ];
2193
+ for (const [index, message] of messages.entries()) {
2194
+ sections.push({ h2: `Message ${index + 1}` });
2195
+ sections.push({
2196
+ ul: [
2197
+ `**From:** ${message.from}`,
2198
+ `**To:** ${message.to}`,
2199
+ `**Date:** ${message.date}`,
2200
+ `**Message ID:** ${message.id}`
2201
+ ]
2202
+ });
2203
+ if (message.labels && message.labels.length > 0) {
2204
+ sections.push({
2205
+ p: `**Labels:** ${message.labels.join(", ")}`
2206
+ });
2207
+ }
2208
+ if (message.body) {
2209
+ sections.push({ h3: "Body" });
2210
+ sections.push({ p: message.body });
2211
+ } else {
2212
+ sections.push({ h3: "Snippet" });
2213
+ sections.push({ p: message.snippet });
2214
+ }
2215
+ if (index < messages.length - 1) {
2216
+ sections.push({ p: "---" });
2217
+ }
2218
+ }
2219
+ return json2md8(sections);
2220
+ }
2221
+ async function getThreadTool(gmailClient, params) {
2222
+ try {
2223
+ const messages = await gmailClient.getThread(params.thread_id, params.include_body);
2224
+ const output = {
2225
+ thread_id: params.thread_id,
2226
+ message_count: messages.length,
2227
+ messages: messages.map(formatEmailForOutput)
2228
+ };
2229
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : threadToMarkdown(params.thread_id, output.messages);
2230
+ return {
2231
+ content: [{ type: "text", text: textContent }],
2232
+ structuredContent: output
2233
+ };
2234
+ } catch (error) {
2235
+ return createErrorResponse("getting thread", error);
2236
+ }
2237
+ }
2238
+ var GET_THREAD_DESCRIPTION = `Get an entire Gmail conversation thread by its thread ID.
2239
+
2240
+ This tool retrieves all messages in a Gmail conversation thread (email chain). Threads group related messages together, including original emails and all replies.
2241
+
2242
+ **Parameters**:
2243
+ - \`thread_id\` (string, required): The Gmail thread ID to retrieve
2244
+ - \`include_body\` (boolean, optional): Include full email body for all messages (default: false, only snippets)
2245
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2246
+
2247
+ **Returns**:
2248
+ - \`thread_id\`: The thread ID
2249
+ - \`message_count\`: Number of messages in the thread
2250
+ - \`messages\`: Array of email messages in chronological order, each containing:
2251
+ - \`id\`: Message ID
2252
+ - \`thread_id\`: Thread ID
2253
+ - \`subject\`: Email subject
2254
+ - \`from\`: Sender email address
2255
+ - \`to\`: Recipient email address(es)
2256
+ - \`date\`: Email date
2257
+ - \`snippet\`: Short preview
2258
+ - \`body\`: Full body (if include_body is true)
2259
+ - \`labels\`: Gmail labels
2260
+
2261
+ **Examples**:
2262
+ - Get thread with snippets: \`{ "thread_id": "18f3c5d4e8a2b1c0" }\`
2263
+ - Get thread with full bodies: \`{ "thread_id": "18f3c5d4e8a2b1c0", "include_body": true }\`
2264
+ - JSON output: \`{ "thread_id": "18f3c5d4e8a2b1c0", "output_format": "json" }\`
2265
+
2266
+ **Error Handling**:
2267
+ - Returns error if thread ID doesn't exist
2268
+ - Returns error if authentication fails
2269
+ - Returns error if Gmail API request fails
2270
+
2271
+ **Use Cases**:
2272
+ - View complete email conversation history
2273
+ - Analyze email chains and reply patterns
2274
+ - Extract all messages from a discussion thread`;
2275
+
2276
+ // src/tools/list-attachments.ts
2277
+ import json2md9 from "json2md";
2278
+ import { z as z13 } from "zod";
2279
+ var ListAttachmentsInputSchema = z13.object({
2280
+ message_id: z13.string().min(1, "Message ID cannot be empty").describe("The Gmail message ID to list attachments from"),
2281
+ output_format: z13.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
2282
+ });
2283
+ function formatBytes(bytes) {
2284
+ if (bytes === 0) {
2285
+ return "0 B";
2286
+ }
2287
+ const k = 1024;
2288
+ const sizes = ["B", "KB", "MB", "GB"];
2289
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2290
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
2291
+ }
2292
+ function attachmentsToMarkdown(messageId, attachments) {
2293
+ const sections = [
2294
+ { h1: "Email Attachments" },
2295
+ {
2296
+ p: `**Message ID:** ${messageId} | **Total Attachments:** ${attachments.length}`
2297
+ }
2298
+ ];
2299
+ if (attachments.length === 0) {
2300
+ sections.push({ p: "*No attachments found in this email.*" });
2301
+ } else {
2302
+ for (const [index, attachment] of attachments.entries()) {
2303
+ sections.push({ h2: `${index + 1}. ${attachment.filename}` });
2304
+ sections.push({
2305
+ ul: [
2306
+ `**Type:** ${attachment.mimeType}`,
2307
+ `**Size:** ${formatBytes(attachment.size)}`,
2308
+ `**Attachment ID:** ${attachment.attachmentId}`
2309
+ ]
2310
+ });
2311
+ }
2312
+ }
2313
+ return json2md9(sections);
2314
+ }
2315
+ async function listAttachmentsTool(gmailClient, params) {
2316
+ try {
2317
+ const attachments = await gmailClient.listAttachments(params.message_id);
2318
+ const output = {
2319
+ message_id: params.message_id,
2320
+ attachment_count: attachments.length,
2321
+ attachments: attachments.map((att) => ({
2322
+ filename: att.filename,
2323
+ mime_type: att.mimeType,
2324
+ size: att.size,
2325
+ size_formatted: formatBytes(att.size),
2326
+ attachment_id: att.attachmentId
2327
+ }))
2328
+ };
2329
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : attachmentsToMarkdown(params.message_id, attachments);
2330
+ return {
2331
+ content: [{ type: "text", text: textContent }],
2332
+ structuredContent: output
2333
+ };
2334
+ } catch (error) {
2335
+ return createErrorResponse("listing attachments", error);
2336
+ }
2337
+ }
2338
+ var LIST_ATTACHMENTS_DESCRIPTION = `List all attachments for a Gmail email message.
2339
+
2340
+ This tool retrieves metadata about all attachments in a specific email message, including filenames, MIME types, sizes, and attachment IDs needed to download them.
2341
+
2342
+ **Parameters**:
2343
+ - \`message_id\` (string, required): The Gmail message ID to list attachments from
2344
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2345
+
2346
+ **Returns**:
2347
+ - \`message_id\`: The message ID
2348
+ - \`attachment_count\`: Number of attachments found
2349
+ - \`attachments\`: Array of attachment metadata objects:
2350
+ - \`filename\`: Original filename
2351
+ - \`mime_type\`: MIME type (e.g., "application/pdf", "image/jpeg")
2352
+ - \`size\`: Size in bytes
2353
+ - \`size_formatted\`: Human-readable size (e.g., "2.5 MB")
2354
+ - \`attachment_id\`: ID needed to download the attachment
2355
+
2356
+ **Examples**:
2357
+ - List attachments: \`{ "message_id": "18f3c5d4e8a2b1c0" }\`
2358
+ - JSON output: \`{ "message_id": "18f3c5d4e8a2b1c0", "output_format": "json" }\`
2359
+
2360
+ **Error Handling**:
2361
+ - Returns error if message ID doesn't exist
2362
+ - Returns error if authentication fails
2363
+ - Returns empty array if message has no attachments
2364
+
2365
+ **Use Cases**:
2366
+ - Check if an email has attachments before downloading
2367
+ - Get attachment metadata for processing decisions
2368
+ - List attachment IDs for use with get_attachment tool`;
2369
+
2370
+ // src/tools/list-labels.ts
2371
+ import json2md10 from "json2md";
2372
+ import { z as z14 } from "zod";
2373
+ var ListLabelsInputSchema = z14.object({
2374
+ output_format: z14.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
2375
+ });
2376
+ function labelsToMarkdown(labels) {
2377
+ const sections = [{ h1: "Gmail Labels" }];
2378
+ if (labels.system.length > 0) {
2379
+ sections.push({ h2: "System Labels" });
2380
+ const systemLabels = labels.system.map((label) => `**${label.name}** (${label.id}) - ${label.total} total, ${label.unread} unread`);
2381
+ sections.push({ ul: systemLabels });
2382
+ }
2383
+ if (labels.user.length > 0) {
2384
+ sections.push({ h2: "Custom Labels" });
2385
+ const userLabels = labels.user.map((label) => `**${label.name}** (${label.id}) - ${label.total} total, ${label.unread} unread`);
2386
+ sections.push({ ul: userLabels });
2387
+ }
2388
+ if (labels.system.length === 0 && labels.user.length === 0) {
2389
+ sections.push({ p: "*No labels found*" });
2390
+ }
2391
+ return json2md10(sections);
2392
+ }
2393
+ async function listLabelsTool(gmailClient, params) {
2394
+ try {
2395
+ const labels = await gmailClient.listLabels();
2396
+ const systemLabels = labels.filter((label) => label.type === "system").map((label) => ({
2397
+ id: label.id,
2398
+ name: label.name,
2399
+ total: label.messagesTotal || 0,
2400
+ unread: label.messagesUnread || 0
2401
+ }));
2402
+ const userLabels = labels.filter((label) => label.type === "user").map((label) => ({
2403
+ id: label.id,
2404
+ name: label.name,
2405
+ total: label.messagesTotal || 0,
2406
+ unread: label.messagesUnread || 0
2407
+ }));
2408
+ const output = {
2409
+ total_count: labels.length,
2410
+ system_count: systemLabels.length,
2411
+ user_count: userLabels.length,
2412
+ system: systemLabels,
2413
+ user: userLabels
2414
+ };
2415
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : labelsToMarkdown({ system: systemLabels, user: userLabels });
2416
+ return {
2417
+ content: [{ type: "text", text: textContent }],
2418
+ structuredContent: output
2419
+ };
2420
+ } catch (error) {
2421
+ return createErrorResponse("listing labels", error);
2422
+ }
2423
+ }
2424
+ var LIST_LABELS_DESCRIPTION = `List all Gmail labels (both system and custom labels).
2425
+
2426
+ This tool retrieves all labels in your Gmail account, including system labels (INBOX, SENT, etc.) and user-created custom labels. Each label includes message counts (total and unread).
2427
+
2428
+ **Returns**:
2429
+ - \`total_count\`: Total number of labels
2430
+ - \`system_count\`: Number of system labels
2431
+ - \`user_count\`: Number of custom user labels
2432
+ - \`system\`: Array of system labels with id, name, total, and unread counts
2433
+ - \`user\`: Array of custom labels with id, name, total, and unread counts
2434
+
2435
+ **System Labels**:
2436
+ System labels are predefined by Gmail and include:
2437
+ - \`INBOX\` - Inbox
2438
+ - \`SENT\` - Sent mail
2439
+ - \`DRAFT\` - Drafts
2440
+ - \`TRASH\` - Trash
2441
+ - \`SPAM\` - Spam
2442
+ - \`STARRED\` - Starred messages
2443
+ - \`IMPORTANT\` - Important marker
2444
+ - \`UNREAD\` - Unread status
2445
+ - \`CATEGORY_*\` - Category labels (PERSONAL, SOCIAL, PROMOTIONS, etc.)
2446
+
2447
+ **Custom Labels**:
2448
+ Custom labels are created by users for organizing emails. They have IDs like \`Label_123\` and can have any name, including nested labels using "/" (e.g., "Work/Projects").
2449
+
2450
+ **Parameters**:
2451
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2452
+
2453
+ **Use Cases**:
2454
+ - Discover available labels for organizing emails
2455
+ - Find label IDs for use with modify_labels or other tools
2456
+ - Check message counts per label
2457
+ - List all custom organizational labels
2458
+
2459
+ **Error Handling**:
2460
+ - Returns error if authentication fails
2461
+ - Returns error if Gmail API is unreachable`;
2462
+
2463
+ // src/tools/modify-labels.ts
2464
+ import json2md11 from "json2md";
2465
+ import { z as z15 } from "zod";
2466
+ var ModifyLabelsInputSchema = z15.object({
2467
+ message_id: z15.string().min(1, "Message ID cannot be empty").describe("The Gmail message ID to modify labels for"),
2468
+ add_labels: z15.array(z15.string()).optional().describe("Label IDs to add (e.g., ['STARRED', 'INBOX', 'UNREAD'] or custom label IDs)"),
2469
+ remove_labels: z15.array(z15.string()).optional().describe("Label IDs to remove (e.g., ['UNREAD', 'INBOX'] to mark read and archive)"),
2470
+ output_format: z15.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
2471
+ });
2472
+ function labelModificationToMarkdown(email, addedLabels, removedLabels) {
2473
+ const sections = [
2474
+ { h1: "Label Modification Successful" },
2475
+ { h2: "Message Details" },
2476
+ {
2477
+ ul: [
2478
+ `**Subject:** ${email.subject}`,
2479
+ `**From:** ${email.from}`,
2480
+ `**Message ID:** ${email.id}`
2481
+ ]
2482
+ }
2483
+ ];
2484
+ if (addedLabels && addedLabels.length > 0) {
2485
+ sections.push({ h2: "Added Labels" });
2486
+ sections.push({ ul: addedLabels });
2487
+ }
2488
+ if (removedLabels && removedLabels.length > 0) {
2489
+ sections.push({ h2: "Removed Labels" });
2490
+ sections.push({ ul: removedLabels });
2491
+ }
2492
+ sections.push({ h2: "Current Labels" });
2493
+ sections.push({
2494
+ p: email.labels.length > 0 ? email.labels.join(", ") : "*No labels on this message*"
2495
+ });
2496
+ return json2md11(sections);
2497
+ }
2498
+ async function modifyLabelsTool(gmailClient, params) {
2499
+ try {
2500
+ if (!(params.add_labels || params.remove_labels)) {
2501
+ return {
2502
+ content: [
2503
+ {
2504
+ type: "text",
2505
+ text: "Error: Must specify at least one of add_labels or remove_labels"
2506
+ }
2507
+ ],
2508
+ isError: true
2509
+ };
2510
+ }
2511
+ const email = await gmailClient.modifyLabels(params.message_id, params.add_labels, params.remove_labels);
2512
+ const output = {
2513
+ message_id: params.message_id,
2514
+ subject: email.subject,
2515
+ modified: true,
2516
+ added_labels: params.add_labels || [],
2517
+ removed_labels: params.remove_labels || [],
2518
+ current_labels: email.labels || []
2519
+ };
2520
+ const formattedEmail = formatEmailForOutput(email);
2521
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : labelModificationToMarkdown({
2522
+ ...formattedEmail,
2523
+ labels: email.labels || []
2524
+ }, params.add_labels, params.remove_labels);
2525
+ return {
2526
+ content: [{ type: "text", text: textContent }],
2527
+ structuredContent: output
2528
+ };
2529
+ } catch (error) {
2530
+ return createErrorResponse("modifying labels", error);
2531
+ }
2532
+ }
2533
+ var MODIFY_LABELS_DESCRIPTION = `Modify Gmail labels on a single message (add or remove labels).
2534
+
2535
+ This tool allows you to add or remove labels from a Gmail message. Labels are used for organizing emails, marking read/unread status, starring, archiving, and more.
2536
+
2537
+ **Common Gmail System Labels**:
2538
+ - \`INBOX\` - Inbox (remove to archive)
2539
+ - \`UNREAD\` - Unread status (remove to mark as read)
2540
+ - \`STARRED\` - Starred/important
2541
+ - \`SPAM\` - Spam folder
2542
+ - \`TRASH\` - Trash folder
2543
+ - \`SENT\` - Sent mail
2544
+ - \`DRAFT\` - Draft messages
2545
+ - \`IMPORTANT\` - Important marker
2546
+ - \`CATEGORY_PERSONAL\`, \`CATEGORY_SOCIAL\`, \`CATEGORY_PROMOTIONS\`, etc.
2547
+
2548
+ **Custom Labels**:
2549
+ - User-created labels have IDs like \`Label_123\`
2550
+ - Use the list_labels tool to discover custom label IDs
2551
+
2552
+ **Parameters**:
2553
+ - \`message_id\` (string, required): The Gmail message ID to modify
2554
+ - \`add_labels\` (array, optional): Label IDs to add
2555
+ - \`remove_labels\` (array, optional): Label IDs to remove
2556
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2557
+
2558
+ **Returns**:
2559
+ - \`message_id\`: The message ID
2560
+ - \`subject\`: Email subject
2561
+ - \`modified\`: Always true on success
2562
+ - \`added_labels\`: Labels that were added
2563
+ - \`removed_labels\`: Labels that were removed
2564
+ - \`current_labels\`: All current labels on the message
2565
+
2566
+ **Examples**:
2567
+ - Mark as read and archive: \`{ "message_id": "18f3c5d4e8a2b1c0", "remove_labels": ["UNREAD", "INBOX"] }\`
2568
+ - Star an email: \`{ "message_id": "18f3c5d4e8a2b1c0", "add_labels": ["STARRED"] }\`
2569
+ - Mark as unread: \`{ "message_id": "18f3c5d4e8a2b1c0", "add_labels": ["UNREAD"] }\`
2570
+ - Add to inbox: \`{ "message_id": "18f3c5d4e8a2b1c0", "add_labels": ["INBOX"] }\`
2571
+ - Apply custom label: \`{ "message_id": "18f3c5d4e8a2b1c0", "add_labels": ["Label_123"] }\`
2572
+
2573
+ **Error Handling**:
2574
+ - Returns error if message ID doesn't exist
2575
+ - Returns error if label ID is invalid
2576
+ - Returns error if authentication lacks modify permissions
2577
+ - Requires at least one of add_labels or remove_labels
2578
+
2579
+ **Use Cases**:
2580
+ - Mark emails as read or unread
2581
+ - Archive emails (remove INBOX label)
2582
+ - Star important messages
2583
+ - Apply custom labels for organization
2584
+ - Move messages to trash or spam`;
2585
+
2586
+ // src/tools/reply.ts
2587
+ import json2md12 from "json2md";
2588
+ import { z as z16 } from "zod";
2589
+ var ReplyInputSchema = z16.object({
2590
+ message_id: z16.string().min(1, "Message ID cannot be empty").describe("The Gmail message ID to reply to"),
2591
+ body: z16.string().min(1, "Reply body cannot be empty").describe("Reply message body"),
2592
+ content_type: z16.enum(["text/plain", "text/html"]).default("text/plain").describe("Content type: text/plain (default) or text/html for HTML replies"),
2593
+ cc: z16.email("CC must be a valid email address").optional().describe("CC (carbon copy) email address"),
2594
+ confirm: z16.boolean().default(false).describe("Set to true to confirm and send the reply. If false, returns preview only."),
2595
+ output_format: z16.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
2596
+ });
2597
+ function replyPreviewToMarkdown(originalEmail, replyBody, contentType, cc) {
2598
+ const sections = [
2599
+ { h1: "Reply Preview - NOT SENT" },
2600
+ {
2601
+ p: "⚠️ **This reply has not been sent yet.** Set `confirm: true` to send."
2602
+ },
2603
+ { h2: "Original Message" },
2604
+ {
2605
+ ul: [
2606
+ `**From:** ${originalEmail.from}`,
2607
+ `**Subject:** ${originalEmail.subject}`,
2608
+ `**Date:** ${originalEmail.date}`
2609
+ ]
2610
+ },
2611
+ { h2: "Your Reply" },
2612
+ {
2613
+ ul: [
2614
+ `**To:** ${originalEmail.from}`,
2615
+ ...cc ? [`**CC:** ${cc}`] : [],
2616
+ `**Subject:** Re: ${originalEmail.subject}`,
2617
+ `**Content Type:** ${contentType}`
2618
+ ]
2619
+ },
2620
+ { h2: "Reply Body" },
2621
+ { p: replyBody }
2622
+ ];
2623
+ return json2md12(sections);
2624
+ }
2625
+ function replySentToMarkdown(originalEmail, result, cc) {
2626
+ const sections = [
2627
+ { h1: "✅ Reply Sent Successfully" },
2628
+ { h2: "Reply Details" },
2629
+ {
2630
+ ul: [
2631
+ `**To:** ${originalEmail.from}`,
2632
+ ...cc ? [`**CC:** ${cc}`] : [],
2633
+ `**Subject:** Re: ${originalEmail.subject}`,
2634
+ `**Message ID:** ${result.id}`,
2635
+ `**Thread ID:** ${result.threadId}`
2636
+ ]
2637
+ },
2638
+ {
2639
+ p: "Your reply has been sent and added to the conversation thread."
2640
+ }
2641
+ ];
2642
+ return json2md12(sections);
2643
+ }
2644
+ async function replyTool(gmailClient, params) {
2645
+ try {
2646
+ const originalMessage = await gmailClient.getMessage(params.message_id);
2647
+ if (!params.confirm) {
2648
+ const output2 = {
2649
+ status: "preview",
2650
+ sent: false,
2651
+ original_message: {
2652
+ id: originalMessage.id,
2653
+ thread_id: originalMessage.threadId,
2654
+ subject: originalMessage.subject,
2655
+ from: originalMessage.from,
2656
+ date: originalMessage.date
2657
+ },
2658
+ reply: {
2659
+ to: originalMessage.from,
2660
+ cc: params.cc,
2661
+ subject: `Re: ${originalMessage.subject}`,
2662
+ body: params.body,
2663
+ content_type: params.content_type
2664
+ },
2665
+ warning: "Reply NOT sent. Set confirm:true to actually send this reply."
2666
+ };
2667
+ const textContent2 = params.output_format === "json" ? JSON.stringify(output2, null, 2) : replyPreviewToMarkdown(originalMessage, params.body, params.content_type, params.cc);
2668
+ return {
2669
+ content: [{ type: "text", text: textContent2 }],
2670
+ structuredContent: output2
2671
+ };
2672
+ }
2673
+ const result = await gmailClient.replyToEmail(originalMessage.from, originalMessage.subject, params.body, originalMessage.threadId, originalMessage.id, params.content_type, params.cc);
2674
+ const output = {
2675
+ status: "sent",
2676
+ sent: true,
2677
+ message_id: result.id,
2678
+ thread_id: result.threadId,
2679
+ original_message_id: params.message_id,
2680
+ to: originalMessage.from,
2681
+ cc: params.cc,
2682
+ subject: `Re: ${originalMessage.subject}`,
2683
+ label_ids: result.labelIds
2684
+ };
2685
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : replySentToMarkdown(originalMessage, result, params.cc);
2686
+ return {
2687
+ content: [{ type: "text", text: textContent }],
2688
+ structuredContent: output
2689
+ };
2690
+ } catch (error) {
2691
+ return createErrorResponse("sending reply", error);
2692
+ }
2693
+ }
2694
+ var REPLY_DESCRIPTION = `Reply to an existing Gmail email in its conversation thread.
2695
+
2696
+ This tool sends a reply to an existing email, automatically threading it in the conversation. It includes a safety feature: by default it shows a preview without sending. You must explicitly set \`confirm: true\` to actually send the reply.
2697
+
2698
+ **Safety Feature**:
2699
+ - Default behavior: Shows preview only, does NOT send
2700
+ - To actually send: Set \`confirm: true\`
2701
+ - This prevents accidental sends
2702
+
2703
+ **Automatic Threading**:
2704
+ - Reply is automatically added to the conversation thread
2705
+ - Subject line gets "Re:" prefix
2706
+ - Recipient is the sender of the original message
2707
+ - Thread ID is preserved for conversation continuity
2708
+
2709
+ **Parameters**:
2710
+ - \`message_id\` (string, required): The message ID to reply to
2711
+ - \`body\` (string, required): Your reply message body
2712
+ - \`content_type\` (string, optional): "text/plain" (default) or "text/html"
2713
+ - \`cc\` (string, optional): CC email address
2714
+ - \`confirm\` (boolean, required): Must be true to actually send (default: false)
2715
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2716
+
2717
+ **Returns** (preview mode):
2718
+ - \`status\`: "preview"
2719
+ - \`sent\`: false
2720
+ - \`original_message\`: Details of message being replied to
2721
+ - \`reply\`: Preview of your reply
2722
+ - Warning message
2723
+
2724
+ **Returns** (sent mode):
2725
+ - \`status\`: "sent"
2726
+ - \`sent\`: true
2727
+ - \`message_id\`: Reply message ID
2728
+ - \`thread_id\`: Conversation thread ID
2729
+ - \`original_message_id\`: Original message ID
2730
+ - \`label_ids\`: Applied labels
2731
+
2732
+ **Examples**:
2733
+ - Preview: \`{ "message_id": "18f3c5d4e8a2b1c0", "body": "Thanks for your email!" }\`
2734
+ - Send reply: \`{ "message_id": "18f3c5d4e8a2b1c0", "body": "Thanks!", "confirm": true }\`
2735
+ - HTML reply: \`{ "message_id": "18f3c5d4e8a2b1c0", "body": "<p>Thanks!</p>", "content_type": "text/html", "confirm": true }\`
2736
+ - With CC: \`{ "message_id": "18f3c5d4e8a2b1c0", "body": "Noted", "cc": "boss@example.com", "confirm": true }\`
2737
+
2738
+ **How It Works**:
2739
+ 1. Fetches original message to get sender, subject, and thread ID
2740
+ 2. Constructs reply with proper headers (In-Reply-To, References)
2741
+ 3. Sends reply in the same thread
2742
+ 4. Reply appears in Gmail as part of the conversation
2743
+
2744
+ **Error Handling**:
2745
+ - Returns error if message ID doesn't exist
2746
+ - Returns error if authentication lacks send permissions
2747
+ - Returns error if Gmail API fails
2748
+
2749
+ **Permissions**:
2750
+ - Requires \`gmail.send\` or \`gmail.compose\` scope
2751
+ - Requires \`gmail.readonly\` to fetch original message
2752
+
2753
+ **Use Cases**:
2754
+ - Respond to customer inquiries
2755
+ - Continue email conversations
2756
+ - Send acknowledgments
2757
+ - Reply to notifications
2758
+
2759
+ **Workflow**:
2760
+ 1. Use \`search_emails\` or \`get_email\` to find message
2761
+ 2. Call without \`confirm\` to preview reply
2762
+ 3. Review the preview
2763
+ 4. Call with \`confirm: true\` to send`;
2764
+
2765
+ // src/tools/search.ts
2766
+ import { z as z17 } from "zod";
2767
+ var SearchEmailsInputSchema = z17.object({
2768
+ query: z17.string().min(1, "Query cannot be empty").describe('Gmail search query using Gmail search syntax (e.g., "from:user@example.com subject:test is:unread")'),
2769
+ max_results: z17.number().int().min(1).max(100).default(10).describe("Maximum number of emails to return (default: 10, max: 100)"),
2770
+ include_body: z17.boolean().default(false).describe("Whether to include full email body in results (default: false)"),
2771
+ page_token: z17.string().optional().describe("Token for pagination to fetch next page of results"),
2772
+ output_format: z17.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
2773
+ });
2774
+ async function searchEmailsTool(gmailClient, params) {
2775
+ try {
2776
+ const result = await gmailClient.searchEmails(params.query, params.max_results, params.include_body, params.page_token);
2777
+ const output = {
2778
+ total_estimate: result.total_estimate,
2779
+ count: result.emails.length,
2780
+ has_more: result.has_more,
2781
+ ...result.next_page_token ? { next_page_token: result.next_page_token } : {},
2782
+ emails: result.emails.map(formatEmailForOutput)
2783
+ };
2784
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : searchResultsToMarkdown(params.query, output);
2785
+ return {
2786
+ content: [{ type: "text", text: textContent }],
2787
+ structuredContent: output
2788
+ };
2789
+ } catch (error) {
2790
+ return createErrorResponse("searching emails", error);
2791
+ }
2792
+ }
2793
+ var SEARCH_EMAILS_DESCRIPTION = `Search for emails in Gmail using Gmail's search syntax.
2794
+
2795
+ This tool allows you to search through your Gmail messages using the same query syntax available in Gmail's web interface. It supports various search operators and returns structured results with email metadata.
2796
+
2797
+ **Gmail Search Operators**:
2798
+ - \`from:user@example.com\` - emails from specific sender
2799
+ - \`to:user@example.com\` - emails to specific recipient
2800
+ - \`subject:keyword\` - emails with keyword in subject
2801
+ - \`is:unread\` - unread emails
2802
+ - \`is:starred\` - starred emails
2803
+ - \`has:attachment\` - emails with attachments
2804
+ - \`after:2024/01/01\` - emails after date
2805
+ - \`before:2024/12/31\` - emails before date
2806
+ - \`label:labelname\` - emails with specific label
2807
+ - \`filename:pdf\` - emails with specific file type
2808
+ - \`larger:5M\` - emails larger than size
2809
+ - \`newer_than:7d\` - emails newer than duration
2810
+
2811
+ **Combining Operators**:
2812
+ - Use \`AND\`, \`OR\`, and \`-\` (NOT) to combine operators
2813
+ - Example: \`from:boss@example.com is:unread -label:archive\`
2814
+
2815
+ **Parameters**:
2816
+ - \`query\` (string, required): Gmail search query
2817
+ - \`max_results\` (number, optional): Max emails to return (1-100, default: 10)
2818
+ - \`include_body\` (boolean, optional): Include full email body (default: false, only snippet)
2819
+ - \`page_token\` (string, optional): Pagination token for next page
2820
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2821
+
2822
+ **Returns**:
2823
+ - \`total_estimate\`: Approximate total matching emails
2824
+ - \`count\`: Number of emails in this response
2825
+ - \`has_more\`: Whether more results are available
2826
+ - \`next_page_token\`: Token for fetching next page (if has_more is true)
2827
+ - \`emails\`: Array of email objects with id, thread_id, subject, from, to, date, snippet, and optionally body
2828
+
2829
+ **Examples**:
2830
+ - Search unread emails: \`{ "query": "is:unread" }\`
2831
+ - Search by sender: \`{ "query": "from:alerts@service.com" }\`
2832
+ - Recent attachments: \`{ "query": "has:attachment newer_than:7d" }\`
2833
+ - With body: \`{ "query": "subject:invoice", "include_body": true }\`
2834
+ - JSON output: \`{ "query": "is:unread", "output_format": "json" }\`
2835
+
2836
+ **Error Handling**:
2837
+ - Returns error message if authentication fails
2838
+ - Returns error if Gmail API request fails
2839
+ - Empty results if no emails match the query`;
2840
+
2841
+ // src/tools/send-email.ts
2842
+ import json2md13 from "json2md";
2843
+ import { z as z18 } from "zod";
2844
+ var SendEmailInputSchema = z18.object({
2845
+ to: z18.email("Must be a valid email address").describe("Recipient email address"),
2846
+ subject: z18.string().min(1, "Subject cannot be empty").describe("Email subject"),
2847
+ body: z18.string().min(1, "Body cannot be empty").describe("Email body content"),
2848
+ content_type: z18.enum(["text/plain", "text/html"]).default("text/plain").describe("Content type: text/plain (default) or text/html for HTML emails"),
2849
+ cc: z18.email("CC must be a valid email address").optional().describe("CC (carbon copy) email address"),
2850
+ bcc: z18.email("BCC must be a valid email address").optional().describe("BCC (blind carbon copy) email address"),
2851
+ confirm: z18.boolean().default(false).describe("Set to true to confirm and send the email. If false, returns preview only."),
2852
+ output_format: z18.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
2853
+ });
2854
+ function emailPreviewToMarkdown(params) {
2855
+ const sections = [
2856
+ { h1: "Email Preview - NOT SENT" },
2857
+ {
2858
+ p: "⚠️ **This email has not been sent yet.** Set `confirm: true` to send."
2859
+ },
2860
+ { h2: "Email Details" },
2861
+ {
2862
+ ul: [
2863
+ `**To:** ${params.to}`,
2864
+ ...params.cc ? [`**CC:** ${params.cc}`] : [],
2865
+ ...params.bcc ? [`**BCC:** ${params.bcc}`] : [],
2866
+ `**Subject:** ${params.subject}`,
2867
+ `**Content Type:** ${params.content_type}`
2868
+ ]
2869
+ },
2870
+ { h2: "Body" },
2871
+ { p: params.body }
2872
+ ];
2873
+ return json2md13(sections);
2874
+ }
2875
+ function emailSentToMarkdown(params, result) {
2876
+ const sections = [
2877
+ { h1: "✅ Email Sent Successfully" },
2878
+ { h2: "Message Details" },
2879
+ {
2880
+ ul: [
2881
+ `**To:** ${params.to}`,
2882
+ ...params.cc ? [`**CC:** ${params.cc}`] : [],
2883
+ ...params.bcc ? [`**BCC:** ${params.bcc}`] : [],
2884
+ `**Subject:** ${params.subject}`,
2885
+ `**Message ID:** ${result.id}`,
2886
+ `**Thread ID:** ${result.threadId}`
2887
+ ]
2888
+ },
2889
+ {
2890
+ p: "The email has been sent and will appear in your Sent folder."
2891
+ }
2892
+ ];
2893
+ return json2md13(sections);
2894
+ }
2895
+ async function sendEmailTool(gmailClient, params) {
2896
+ try {
2897
+ if (!params.confirm) {
2898
+ const output2 = {
2899
+ status: "preview",
2900
+ sent: false,
2901
+ to: params.to,
2902
+ cc: params.cc,
2903
+ bcc: params.bcc,
2904
+ subject: params.subject,
2905
+ body: params.body,
2906
+ content_type: params.content_type,
2907
+ warning: "Email NOT sent. Set confirm:true to actually send this email."
2908
+ };
2909
+ const textContent2 = params.output_format === "json" ? JSON.stringify(output2, null, 2) : emailPreviewToMarkdown(params);
2910
+ return {
2911
+ content: [{ type: "text", text: textContent2 }],
2912
+ structuredContent: output2
2913
+ };
2914
+ }
2915
+ const result = await gmailClient.sendEmail(params.to, params.subject, params.body, params.content_type, params.cc, params.bcc);
2916
+ const output = {
2917
+ status: "sent",
2918
+ sent: true,
2919
+ message_id: result.id,
2920
+ thread_id: result.threadId,
2921
+ to: params.to,
2922
+ cc: params.cc,
2923
+ bcc: params.bcc,
2924
+ subject: params.subject,
2925
+ label_ids: result.labelIds
2926
+ };
2927
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : emailSentToMarkdown(params, result);
2928
+ return {
2929
+ content: [{ type: "text", text: textContent }],
2930
+ structuredContent: output
2931
+ };
2932
+ } catch (error) {
2933
+ return createErrorResponse("sending email", error);
2934
+ }
2935
+ }
2936
+ var SEND_EMAIL_DESCRIPTION = `Send a new Gmail email with optional CC/BCC and HTML support.
2937
+
2938
+ This tool sends email through your Gmail account. It includes a safety feature: by default it shows a preview without sending. You must explicitly set \`confirm: true\` to actually send the email.
2939
+
2940
+ **Safety Feature**:
2941
+ - Default behavior: Shows preview only, does NOT send
2942
+ - To actually send: Set \`confirm: true\`
2943
+ - This prevents accidental sends
2944
+
2945
+ **Parameters**:
2946
+ - \`to\` (string, required): Recipient email address
2947
+ - \`subject\` (string, required): Email subject line
2948
+ - \`body\` (string, required): Email body content
2949
+ - \`content_type\` (string, optional): "text/plain" (default) or "text/html" for HTML emails
2950
+ - \`cc\` (string, optional): CC email address
2951
+ - \`bcc\` (string, optional): BCC email address
2952
+ - \`confirm\` (boolean, required): Must be true to actually send (default: false)
2953
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
2954
+
2955
+ **Returns** (preview mode):
2956
+ - \`status\`: "preview"
2957
+ - \`sent\`: false
2958
+ - Email details for review
2959
+ - Warning message
2960
+
2961
+ **Returns** (sent mode):
2962
+ - \`status\`: "sent"
2963
+ - \`sent\`: true
2964
+ - \`message_id\`: Sent message ID
2965
+ - \`thread_id\`: Thread ID
2966
+ - \`label_ids\`: Applied labels
2967
+
2968
+ **Examples**:
2969
+ - Preview: \`{ "to": "user@example.com", "subject": "Hello", "body": "Hi there!" }\`
2970
+ - Send plain text: \`{ "to": "user@example.com", "subject": "Hello", "body": "Hi!", "confirm": true }\`
2971
+ - Send HTML: \`{ "to": "user@example.com", "subject": "Newsletter", "body": "<h1>Hello</h1>", "content_type": "text/html", "confirm": true }\`
2972
+ - With CC: \`{ "to": "user@example.com", "cc": "boss@example.com", "subject": "Report", "body": "See attached", "confirm": true }\`
2973
+
2974
+ **HTML Emails**:
2975
+ - Set \`content_type: "text/html"\`
2976
+ - Body should contain valid HTML
2977
+ - Basic HTML tags supported: h1-h6, p, a, strong, em, ul, ol, li, br
2978
+
2979
+ **Error Handling**:
2980
+ - Returns error if email addresses are invalid
2981
+ - Returns error if authentication lacks send permissions
2982
+ - Returns error if Gmail API fails
2983
+
2984
+ **Permissions**:
2985
+ - Requires \`gmail.send\` or \`gmail.compose\` scope
2986
+
2987
+ **Use Cases**:
2988
+ - Send automated email notifications
2989
+ - Reply to inquiries
2990
+ - Send reports or summaries
2991
+ - Compose and send HTML newsletters
2992
+
2993
+ **Workflow**:
2994
+ 1. First call without \`confirm\` to preview
2995
+ 2. Review the preview output
2996
+ 3. Call again with \`confirm: true\` to send`;
2997
+
2998
+ // src/tools/update-label.ts
2999
+ import json2md14 from "json2md";
3000
+ import { z as z19 } from "zod";
3001
+ var SYSTEM_LABELS2 = [
3002
+ "INBOX",
3003
+ "SENT",
3004
+ "DRAFT",
3005
+ "TRASH",
3006
+ "SPAM",
3007
+ "STARRED",
3008
+ "IMPORTANT",
3009
+ "UNREAD"
3010
+ ];
3011
+ var UpdateLabelInputSchema = z19.object({
3012
+ label_id: z19.string().min(1, "Label ID cannot be empty").describe("The label ID to update (e.g., 'Label_123' or 'INBOX')"),
3013
+ name: z19.string().optional().describe("New label name. Cannot rename system labels. Use '/' for nested labels."),
3014
+ message_list_visibility: z19.enum(["show", "hide"]).optional().describe("How label appears in message list"),
3015
+ label_list_visibility: z19.enum(["labelShow", "labelShowIfUnread", "labelHide"]).optional().describe("How label appears in label list (sidebar)"),
3016
+ background_color: z19.string().optional().describe("Background color in hex format (e.g., '#ff0000'). Must provide both background and text color together."),
3017
+ text_color: z19.string().optional().describe("Text color in hex format (e.g., '#ffffff'). Must provide both background and text color together."),
3018
+ output_format: z19.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (default) or json")
3019
+ });
3020
+ function updatedLabelToMarkdown(label, updates) {
3021
+ const sections = [
3022
+ { h1: "Label Updated Successfully" },
3023
+ { h2: "Details" },
3024
+ {
3025
+ ul: [
3026
+ `**Name:** ${label.name}`,
3027
+ `**ID:** ${label.id}`,
3028
+ `**Type:** ${label.type === "system" ? "System Label" : "Custom Label"}`
3029
+ ]
3030
+ }
3031
+ ];
3032
+ const changedItems = [];
3033
+ if (updates.name) {
3034
+ changedItems.push("Name");
3035
+ }
3036
+ if (updates.visibility) {
3037
+ changedItems.push("Visibility settings");
3038
+ }
3039
+ if (updates.color) {
3040
+ changedItems.push("Color");
3041
+ }
3042
+ if (changedItems.length > 0) {
3043
+ sections.push({ h2: "Changes Applied" });
3044
+ sections.push({ ul: changedItems });
3045
+ }
3046
+ if (label.messageListVisibility || label.labelListVisibility) {
3047
+ sections.push({ h2: "Current Visibility Settings" });
3048
+ const visibilityItems = [];
3049
+ if (label.messageListVisibility) {
3050
+ visibilityItems.push(`**Message List:** ${label.messageListVisibility}`);
3051
+ }
3052
+ if (label.labelListVisibility) {
3053
+ visibilityItems.push(`**Label List:** ${label.labelListVisibility}`);
3054
+ }
3055
+ sections.push({ ul: visibilityItems });
3056
+ }
3057
+ if (label.color) {
3058
+ sections.push({ h2: "Current Color" });
3059
+ sections.push({
3060
+ ul: [
3061
+ `**Text Color:** ${label.color.textColor}`,
3062
+ `**Background Color:** ${label.color.backgroundColor}`
3063
+ ]
3064
+ });
3065
+ }
3066
+ return json2md14(sections);
3067
+ }
3068
+ async function updateLabelTool(gmailClient, params) {
3069
+ try {
3070
+ if (params.name && SYSTEM_LABELS2.includes(params.label_id)) {
3071
+ return {
3072
+ content: [
3073
+ {
3074
+ type: "text",
3075
+ text: `Error: Cannot rename system label ${params.label_id}. System labels can only have visibility settings updated, not renamed.`
3076
+ }
3077
+ ],
3078
+ isError: true
3079
+ };
3080
+ }
3081
+ if (params.background_color && !params.text_color || !params.background_color && params.text_color) {
3082
+ return {
3083
+ content: [
3084
+ {
3085
+ type: "text",
3086
+ text: "Error: Both background_color and text_color must be provided together"
3087
+ }
3088
+ ],
3089
+ isError: true
3090
+ };
3091
+ }
3092
+ if (!(params.name || params.message_list_visibility || params.label_list_visibility || params.background_color || params.text_color)) {
3093
+ return {
3094
+ content: [
3095
+ {
3096
+ type: "text",
3097
+ text: "Error: At least one field to update must be provided (name, visibility, or color)"
3098
+ }
3099
+ ],
3100
+ isError: true
3101
+ };
3102
+ }
3103
+ const label = await gmailClient.updateLabel(params.label_id, params.name, params.message_list_visibility, params.label_list_visibility, params.background_color, params.text_color);
3104
+ const updates = {
3105
+ name: !!params.name,
3106
+ visibility: !!(params.message_list_visibility || params.label_list_visibility),
3107
+ color: !!(params.background_color && params.text_color)
3108
+ };
3109
+ const output = {
3110
+ id: label.id,
3111
+ name: label.name,
3112
+ type: label.type,
3113
+ message_list_visibility: label.messageListVisibility || null,
3114
+ label_list_visibility: label.labelListVisibility || null,
3115
+ color: label.color || null,
3116
+ updated: true,
3117
+ changes: updates
3118
+ };
3119
+ const textContent = params.output_format === "json" ? JSON.stringify(output, null, 2) : updatedLabelToMarkdown(label, updates);
3120
+ return {
3121
+ content: [{ type: "text", text: textContent }],
3122
+ structuredContent: output
3123
+ };
3124
+ } catch (error) {
3125
+ return createErrorResponse("updating label", error);
3126
+ }
3127
+ }
3128
+ var UPDATE_LABEL_DESCRIPTION = `Update an existing Gmail label's name, visibility, or color settings.
3129
+
3130
+ This tool modifies label properties. System labels (INBOX, SENT, etc.) can only have visibility updated, not renamed. Custom labels can have all properties updated.
3131
+
3132
+ **Parameters**:
3133
+ - \`label_id\` (string, required): The label ID to update (e.g., "Label_123" or "INBOX")
3134
+ - \`name\` (string, optional): New label name. Use "/" for nested labels. Cannot rename system labels.
3135
+ - \`message_list_visibility\` (string, optional): How label appears in message list
3136
+ - "show": Messages with this label appear in message list
3137
+ - "hide": Messages with this label are hidden from message list
3138
+ - \`label_list_visibility\` (string, optional): How label appears in label list (sidebar)
3139
+ - "labelShow": Always visible in label list
3140
+ - "labelShowIfUnread": Only visible when there are unread messages
3141
+ - "labelHide": Hidden from label list
3142
+ - \`background_color\` (string, optional): Background color in hex format (e.g., "#ff0000")
3143
+ - \`text_color\` (string, optional): Text color in hex format (e.g., "#ffffff")
3144
+ - \`output_format\` (string, optional): Output format: "markdown" (default) or "json"
3145
+
3146
+ **System Label Restrictions**:
3147
+ - System labels (INBOX, SENT, DRAFT, TRASH, SPAM, STARRED, IMPORTANT, UNREAD) cannot be renamed
3148
+ - System labels can have visibility settings updated
3149
+ - Some system labels may reject certain visibility changes based on Gmail's rules
3150
+ - Attempting to rename a system label returns an error
3151
+
3152
+ **Custom Label Updates**:
3153
+ - Custom labels can be renamed, including changing nested structure
3154
+ - All visibility and color properties can be updated
3155
+ - Renaming to include "/" creates nested label hierarchy
3156
+
3157
+ **Color Settings**:
3158
+ - Both \`background_color\` and \`text_color\` must be provided together
3159
+ - Colors should be in hex format (e.g., "#ff0000" for red)
3160
+ - To remove colors, omit both color parameters (not currently supported by Gmail API)
3161
+
3162
+ **Returns**:
3163
+ - \`id\`: The label ID
3164
+ - \`name\`: Updated label name
3165
+ - \`type\`: "system" or "user"
3166
+ - \`message_list_visibility\`: Current message list visibility
3167
+ - \`label_list_visibility\`: Current label list visibility
3168
+ - \`color\`: Current color settings
3169
+ - \`updated\`: Always true on success
3170
+ - \`changes\`: Object indicating which properties were updated
3171
+
3172
+ **Use Cases**:
3173
+ - Rename custom labels for better organization
3174
+ - Change label colors for visual distinction
3175
+ - Update visibility settings to hide/show labels
3176
+ - Restructure labels by changing nested hierarchy
3177
+
3178
+ **Examples**:
3179
+ - Rename label: \`{ "label_id": "Label_123", "name": "Work/Important" }\`
3180
+ - Change color: \`{ "label_id": "Label_123", "background_color": "#ff0000", "text_color": "#ffffff" }\`
3181
+ - Hide from sidebar: \`{ "label_id": "Label_123", "label_list_visibility": "labelHide" }\`
3182
+ - Update system label visibility: \`{ "label_id": "INBOX", "label_list_visibility": "labelShowIfUnread" }\`
3183
+
3184
+ **Error Handling**:
3185
+ - Returns error if trying to rename system labels
3186
+ - Returns error if label ID doesn't exist
3187
+ - Returns error if colors are invalid or only one color is provided
3188
+ - Returns error if no update parameters are provided
3189
+ - Returns error if authentication lacks gmail.labels scope
3190
+ - Requires at least one field to update
3191
+
3192
+ **Scope Requirements**:
3193
+ - Requires \`gmail.labels\` or \`gmail.modify\` scope`;
3194
+
3195
+ // src/index.ts
3196
+ async function main() {
3197
+ const logger = createLogger();
3198
+ logger.info({ version: "1.0.0" }, "GMCP Server starting");
3199
+ const { credentialsPath, tokenPath, scopes } = getEnvConfig();
3200
+ logger.info({
3201
+ credentialsPath,
3202
+ tokenPath,
3203
+ scopeCount: scopes.length,
3204
+ scopes
3205
+ }, "Environment configuration loaded");
3206
+ logger.info("Authenticating with Google APIs");
3207
+ const oauth2Client = await createAuthenticatedClient(credentialsPath, tokenPath, logger);
3208
+ const gmailClient = createGmailClient(oauth2Client, logger);
3209
+ const calendarClient = createCalendarClient(oauth2Client, logger);
3210
+ logger.info("Gmail and Calendar clients initialized");
3211
+ const server = new McpServer({
3212
+ name: "gmcp-server",
3213
+ version: "1.0.0"
3214
+ });
3215
+ const tools = [
3216
+ {
3217
+ name: "gmcp_gmail_search_emails",
3218
+ title: "Search Gmail Emails",
3219
+ description: SEARCH_EMAILS_DESCRIPTION,
3220
+ inputSchema: SearchEmailsInputSchema,
3221
+ annotations: READ_ONLY_ANNOTATIONS,
3222
+ handler: searchEmailsTool
3223
+ },
3224
+ {
3225
+ name: "gmcp_gmail_get_email",
3226
+ title: "Get Gmail Email",
3227
+ description: GET_EMAIL_DESCRIPTION,
3228
+ inputSchema: GetEmailInputSchema,
3229
+ annotations: READ_ONLY_ANNOTATIONS,
3230
+ handler: getEmailTool
3231
+ },
3232
+ {
3233
+ name: "gmcp_gmail_get_thread",
3234
+ title: "Get Gmail Thread",
3235
+ description: GET_THREAD_DESCRIPTION,
3236
+ inputSchema: GetThreadInputSchema,
3237
+ annotations: READ_ONLY_ANNOTATIONS,
3238
+ handler: getThreadTool
3239
+ },
3240
+ {
3241
+ name: "gmcp_gmail_list_attachments",
3242
+ title: "List Gmail Attachments",
3243
+ description: LIST_ATTACHMENTS_DESCRIPTION,
3244
+ inputSchema: ListAttachmentsInputSchema,
3245
+ annotations: READ_ONLY_ANNOTATIONS,
3246
+ handler: listAttachmentsTool
3247
+ },
3248
+ {
3249
+ name: "gmcp_gmail_get_attachment",
3250
+ title: "Get Gmail Attachment",
3251
+ description: GET_ATTACHMENT_DESCRIPTION,
3252
+ inputSchema: GetAttachmentInputSchema,
3253
+ annotations: READ_ONLY_ANNOTATIONS,
3254
+ handler: getAttachmentTool
3255
+ },
3256
+ {
3257
+ name: "gmcp_gmail_modify_labels",
3258
+ title: "Modify Gmail Labels",
3259
+ description: MODIFY_LABELS_DESCRIPTION,
3260
+ inputSchema: ModifyLabelsInputSchema,
3261
+ annotations: MODIFY_ANNOTATIONS,
3262
+ handler: modifyLabelsTool
3263
+ },
3264
+ {
3265
+ name: "gmcp_gmail_batch_modify",
3266
+ title: "Batch Modify Gmail Labels",
3267
+ description: BATCH_MODIFY_DESCRIPTION,
3268
+ inputSchema: BatchModifyInputSchema,
3269
+ annotations: MODIFY_ANNOTATIONS,
3270
+ handler: batchModifyTool
3271
+ },
3272
+ {
3273
+ name: "gmcp_gmail_send_email",
3274
+ title: "Send Gmail Email",
3275
+ description: SEND_EMAIL_DESCRIPTION,
3276
+ inputSchema: SendEmailInputSchema,
3277
+ annotations: SEND_ANNOTATIONS,
3278
+ handler: sendEmailTool
3279
+ },
3280
+ {
3281
+ name: "gmcp_gmail_reply",
3282
+ title: "Reply to Gmail Email",
3283
+ description: REPLY_DESCRIPTION,
3284
+ inputSchema: ReplyInputSchema,
3285
+ annotations: SEND_ANNOTATIONS,
3286
+ handler: replyTool
3287
+ },
3288
+ {
3289
+ name: "gmcp_gmail_create_draft",
3290
+ title: "Create Gmail Draft",
3291
+ description: CREATE_DRAFT_DESCRIPTION,
3292
+ inputSchema: CreateDraftInputSchema,
3293
+ annotations: SEND_ANNOTATIONS,
3294
+ handler: createDraftTool
3295
+ },
3296
+ {
3297
+ name: "gmcp_gmail_list_labels",
3298
+ title: "List Gmail Labels",
3299
+ description: LIST_LABELS_DESCRIPTION,
3300
+ inputSchema: ListLabelsInputSchema,
3301
+ annotations: READ_ONLY_ANNOTATIONS,
3302
+ handler: listLabelsTool
3303
+ },
3304
+ {
3305
+ name: "gmcp_gmail_get_label",
3306
+ title: "Get Gmail Label",
3307
+ description: GET_LABEL_DESCRIPTION,
3308
+ inputSchema: GetLabelInputSchema,
3309
+ annotations: READ_ONLY_ANNOTATIONS,
3310
+ handler: getLabelTool
3311
+ },
3312
+ {
3313
+ name: "gmcp_gmail_create_label",
3314
+ title: "Create Gmail Label",
3315
+ description: CREATE_LABEL_DESCRIPTION,
3316
+ inputSchema: CreateLabelInputSchema,
3317
+ annotations: MODIFY_ANNOTATIONS,
3318
+ handler: createLabelTool
3319
+ },
3320
+ {
3321
+ name: "gmcp_gmail_update_label",
3322
+ title: "Update Gmail Label",
3323
+ description: UPDATE_LABEL_DESCRIPTION,
3324
+ inputSchema: UpdateLabelInputSchema,
3325
+ annotations: MODIFY_ANNOTATIONS,
3326
+ handler: updateLabelTool
3327
+ },
3328
+ {
3329
+ name: "gmcp_gmail_delete_label",
3330
+ title: "Delete Gmail Label",
3331
+ description: DELETE_LABEL_DESCRIPTION,
3332
+ inputSchema: DeleteLabelInputSchema,
3333
+ annotations: DESTRUCTIVE_ANNOTATIONS,
3334
+ handler: deleteLabelTool
3335
+ }
3336
+ ];
3337
+ const calendarTools = [
3338
+ {
3339
+ name: "gmcp_calendar_list_calendars",
3340
+ title: "List Google Calendars",
3341
+ description: CALENDAR_LIST_DESCRIPTION,
3342
+ inputSchema: CalendarListInputSchema,
3343
+ annotations: READ_ONLY_ANNOTATIONS,
3344
+ handler: calendarListTool
3345
+ },
3346
+ {
3347
+ name: "gmcp_calendar_list_events",
3348
+ title: "List Calendar Events",
3349
+ description: CALENDAR_EVENTS_DESCRIPTION,
3350
+ inputSchema: CalendarEventsInputSchema,
3351
+ annotations: READ_ONLY_ANNOTATIONS,
3352
+ handler: calendarEventsTool
3353
+ },
3354
+ {
3355
+ name: "gmcp_calendar_get_event",
3356
+ title: "Get Calendar Event",
3357
+ description: CALENDAR_GET_EVENT_DESCRIPTION,
3358
+ inputSchema: CalendarGetEventInputSchema,
3359
+ annotations: READ_ONLY_ANNOTATIONS,
3360
+ handler: calendarGetEventTool
3361
+ },
3362
+ {
3363
+ name: "gmcp_calendar_create_event",
3364
+ title: "Create Calendar Event",
3365
+ description: CALENDAR_CREATE_EVENT_DESCRIPTION,
3366
+ inputSchema: CalendarCreateEventInputSchema,
3367
+ annotations: SEND_ANNOTATIONS,
3368
+ handler: calendarCreateEventTool
3369
+ }
3370
+ ];
3371
+ registerTools(server, gmailClient, tools, logger);
3372
+ registerTools(server, calendarClient, calendarTools, logger);
3373
+ const totalTools = tools.length + calendarTools.length;
3374
+ logger.info({
3375
+ gmailTools: tools.length,
3376
+ calendarTools: calendarTools.length,
3377
+ totalTools
3378
+ }, "Tools registered");
3379
+ const transport = new StdioServerTransport;
3380
+ await server.connect(transport);
3381
+ logger.info("MCP server connected via stdio");
3382
+ logger.info("Ready to accept requests");
3383
+ }
3384
+ main().catch((error) => {
3385
+ const logger = createLogger();
3386
+ logger.error({
3387
+ error: error instanceof Error ? error.message : String(error),
3388
+ stack: error instanceof Error ? error.stack : undefined
3389
+ }, "Fatal error");
3390
+ process.exit(1);
3391
+ });