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/README.md +183 -0
- package/dist/auth-cli.js +82 -0
- package/dist/index.js +3391 -0
- package/dist/shared/chunk-n2k9eesp.js +169 -0
- package/package.json +63 -0
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
|
+
});
|