@triffon/google-docs-mcp 1.11.3-triffon

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +567 -0
  3. package/dist/auth.js +327 -0
  4. package/dist/cachedToolsList.js +38 -0
  5. package/dist/clients.js +180 -0
  6. package/dist/config.js +9 -0
  7. package/dist/downloadProxy.js +82 -0
  8. package/dist/driveQueryUtils.js +7 -0
  9. package/dist/firestoreTokenStorage.js +32 -0
  10. package/dist/googleDocsApiHelpers.js +1257 -0
  11. package/dist/googleSheetsApiHelpers.js +849 -0
  12. package/dist/index.js +240 -0
  13. package/dist/landingPage.js +103 -0
  14. package/dist/logger.js +58 -0
  15. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  16. package/dist/markdown-transformer/index.js +128 -0
  17. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  18. package/dist/remoteWrapper.js +72 -0
  19. package/dist/tools/calendar/createEvent.js +89 -0
  20. package/dist/tools/calendar/deleteEvent.js +49 -0
  21. package/dist/tools/calendar/helpers.js +19 -0
  22. package/dist/tools/calendar/index.js +12 -0
  23. package/dist/tools/calendar/listEvents.js +84 -0
  24. package/dist/tools/calendar/quickAddEvent.js +55 -0
  25. package/dist/tools/calendar/updateEvent.js +84 -0
  26. package/dist/tools/docs/addTab.js +84 -0
  27. package/dist/tools/docs/appendTableRows.js +89 -0
  28. package/dist/tools/docs/appendToGoogleDoc.js +85 -0
  29. package/dist/tools/docs/cloneTable.js +159 -0
  30. package/dist/tools/docs/comments/addComment.js +83 -0
  31. package/dist/tools/docs/comments/deleteComment.js +30 -0
  32. package/dist/tools/docs/comments/getComment.js +45 -0
  33. package/dist/tools/docs/comments/index.js +14 -0
  34. package/dist/tools/docs/comments/listComments.js +43 -0
  35. package/dist/tools/docs/comments/replyToComment.js +35 -0
  36. package/dist/tools/docs/comments/resolveComment.js +55 -0
  37. package/dist/tools/docs/deleteRange.js +61 -0
  38. package/dist/tools/docs/deleteTableRows.js +62 -0
  39. package/dist/tools/docs/findAndReplace.js +54 -0
  40. package/dist/tools/docs/findElement.js +43 -0
  41. package/dist/tools/docs/findSectionsByHeading.js +46 -0
  42. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  43. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  44. package/dist/tools/docs/formatting/batchApplyTextStyle.js +86 -0
  45. package/dist/tools/docs/formatting/index.js +16 -0
  46. package/dist/tools/docs/formatting/updateTableBorders.js +81 -0
  47. package/dist/tools/docs/formatting/updateTableCellStyle.js +85 -0
  48. package/dist/tools/docs/formatting/updateTableColumnWidth.js +51 -0
  49. package/dist/tools/docs/formatting/updateTableRowStyle.js +74 -0
  50. package/dist/tools/docs/getTableStructure.js +48 -0
  51. package/dist/tools/docs/index.js +66 -0
  52. package/dist/tools/docs/insertDateChip.js +82 -0
  53. package/dist/tools/docs/insertImage.js +112 -0
  54. package/dist/tools/docs/insertPageBreak.js +47 -0
  55. package/dist/tools/docs/insertPerson.js +53 -0
  56. package/dist/tools/docs/insertRichLink.js +58 -0
  57. package/dist/tools/docs/insertSectionBreak.js +60 -0
  58. package/dist/tools/docs/insertTable.js +42 -0
  59. package/dist/tools/docs/insertTableWithData.js +125 -0
  60. package/dist/tools/docs/insertText.js +49 -0
  61. package/dist/tools/docs/listDocumentTables.js +47 -0
  62. package/dist/tools/docs/listDocumentTabs.js +59 -0
  63. package/dist/tools/docs/listSmartChips.js +41 -0
  64. package/dist/tools/docs/modifyText.js +147 -0
  65. package/dist/tools/docs/readGoogleDoc.js +164 -0
  66. package/dist/tools/docs/renameTab.js +47 -0
  67. package/dist/tools/docs/replaceTableRowData.js +55 -0
  68. package/dist/tools/docs/smartChipHelpers.js +71 -0
  69. package/dist/tools/docs/structureHelpers.js +250 -0
  70. package/dist/tools/docs/tabFieldMasks.js +48 -0
  71. package/dist/tools/docs/tableRowDataHelpers.js +53 -0
  72. package/dist/tools/docs/updateSectionStyle.js +148 -0
  73. package/dist/tools/drive/copyFile.js +63 -0
  74. package/dist/tools/drive/createDocument.js +105 -0
  75. package/dist/tools/drive/createFolder.js +48 -0
  76. package/dist/tools/drive/createFromTemplate.js +82 -0
  77. package/dist/tools/drive/deleteFile.js +72 -0
  78. package/dist/tools/drive/downloadFile.js +266 -0
  79. package/dist/tools/drive/getDocumentInfo.js +48 -0
  80. package/dist/tools/drive/getFolderInfo.js +48 -0
  81. package/dist/tools/drive/index.js +34 -0
  82. package/dist/tools/drive/listDriveFiles.js +129 -0
  83. package/dist/tools/drive/listFolderContents.js +83 -0
  84. package/dist/tools/drive/listGoogleDocs.js +70 -0
  85. package/dist/tools/drive/moveFile.js +54 -0
  86. package/dist/tools/drive/renameFile.js +39 -0
  87. package/dist/tools/drive/savePathGuard.js +86 -0
  88. package/dist/tools/drive/searchDriveFiles.js +148 -0
  89. package/dist/tools/drive/searchGoogleDocs.js +77 -0
  90. package/dist/tools/drive/setFilePermission.js +68 -0
  91. package/dist/tools/gmail/createDraft.js +58 -0
  92. package/dist/tools/gmail/deleteDraft.js +37 -0
  93. package/dist/tools/gmail/getDraft.js +52 -0
  94. package/dist/tools/gmail/getMessage.js +92 -0
  95. package/dist/tools/gmail/helpers.js +113 -0
  96. package/dist/tools/gmail/index.js +28 -0
  97. package/dist/tools/gmail/listDrafts.js +74 -0
  98. package/dist/tools/gmail/listLabels.js +31 -0
  99. package/dist/tools/gmail/listMessages.js +87 -0
  100. package/dist/tools/gmail/modifyMessageLabels.js +54 -0
  101. package/dist/tools/gmail/sendDraft.js +42 -0
  102. package/dist/tools/gmail/sendEmail.js +56 -0
  103. package/dist/tools/gmail/trashMessage.js +37 -0
  104. package/dist/tools/gmail/triageInbox.js +147 -0
  105. package/dist/tools/gmail/updateDraft.js +60 -0
  106. package/dist/tools/index.js +64 -0
  107. package/dist/tools/script/appsScriptShared.js +96 -0
  108. package/dist/tools/script/createAppsScriptProject.js +74 -0
  109. package/dist/tools/script/getAppsScriptContent.js +56 -0
  110. package/dist/tools/script/index.js +14 -0
  111. package/dist/tools/script/updateAppsScriptContent.js +56 -0
  112. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  113. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  114. package/dist/tools/sheets/appendSpreadsheetRows.js +44 -0
  115. package/dist/tools/sheets/appendTableRows.js +51 -0
  116. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  117. package/dist/tools/sheets/autoResizeRows.js +63 -0
  118. package/dist/tools/sheets/batchWrite.js +61 -0
  119. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  120. package/dist/tools/sheets/comments/commentAnchor.js +95 -0
  121. package/dist/tools/sheets/comments/createSheetsCellNote.js +33 -0
  122. package/dist/tools/sheets/comments/createSheetsComment.js +168 -0
  123. package/dist/tools/sheets/comments/deleteSheetsComment.js +32 -0
  124. package/dist/tools/sheets/comments/getSheetsComment.js +51 -0
  125. package/dist/tools/sheets/comments/index.js +16 -0
  126. package/dist/tools/sheets/comments/listSheetsComments.js +177 -0
  127. package/dist/tools/sheets/comments/replyToSheetsComment.js +37 -0
  128. package/dist/tools/sheets/comments/resolveSheetsComment.js +53 -0
  129. package/dist/tools/sheets/copyFormatting.js +59 -0
  130. package/dist/tools/sheets/copySheetTo.js +36 -0
  131. package/dist/tools/sheets/createSpreadsheet.js +72 -0
  132. package/dist/tools/sheets/createTable.js +120 -0
  133. package/dist/tools/sheets/deleteChart.js +41 -0
  134. package/dist/tools/sheets/deleteConditionalFormatting.js +46 -0
  135. package/dist/tools/sheets/deleteSheet.js +43 -0
  136. package/dist/tools/sheets/deleteTable.js +56 -0
  137. package/dist/tools/sheets/duplicateSheet.js +53 -0
  138. package/dist/tools/sheets/formatCells.js +122 -0
  139. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  140. package/dist/tools/sheets/getConditionalFormatting.js +98 -0
  141. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  142. package/dist/tools/sheets/getTable.js +48 -0
  143. package/dist/tools/sheets/groupRows.js +62 -0
  144. package/dist/tools/sheets/index.js +84 -0
  145. package/dist/tools/sheets/insertChart.js +225 -0
  146. package/dist/tools/sheets/listGoogleSheets.js +66 -0
  147. package/dist/tools/sheets/listTables.js +55 -0
  148. package/dist/tools/sheets/protectRange.js +59 -0
  149. package/dist/tools/sheets/readCellFormat.js +143 -0
  150. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  151. package/dist/tools/sheets/renameSheet.js +48 -0
  152. package/dist/tools/sheets/setCellBorders.js +94 -0
  153. package/dist/tools/sheets/setColumnWidths.js +43 -0
  154. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  155. package/dist/tools/sheets/setRowHeights.js +63 -0
  156. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  157. package/dist/tools/sheets/updateTableRange.js +51 -0
  158. package/dist/tools/sheets/writeSpreadsheet.js +45 -0
  159. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +95 -0
  160. package/dist/tools/utils/index.js +8 -0
  161. package/dist/tools/utils/replaceDocumentWithMarkdown.js +161 -0
  162. package/dist/tools/utils/replaceRangeWithMarkdown.js +72 -0
  163. package/dist/types.js +208 -0
  164. package/dist/upstreamAuth.js +62 -0
  165. package/package.json +51 -0
@@ -0,0 +1,52 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ import { findHeaderValue, extractMessageBody } from './helpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'getDraft',
8
+ description: 'Fetches a single Gmail draft by ID with full headers and body. Use listDrafts to discover draft IDs.',
9
+ parameters: z.strictObject({
10
+ draftId: z.string().describe('The Gmail draft ID, typically from listDrafts results.'),
11
+ }),
12
+ execute: async (args, { log }) => {
13
+ const gmail = await getGmailClient();
14
+ log.info(`Getting Gmail draft ${args.draftId}`);
15
+ try {
16
+ const response = await gmail.users.drafts.get({
17
+ userId: 'me',
18
+ id: args.draftId,
19
+ format: 'full',
20
+ });
21
+ const draft = response.data;
22
+ const msg = draft.message;
23
+ const headers = msg?.payload?.headers;
24
+ const { text, html } = extractMessageBody(msg?.payload);
25
+ return JSON.stringify({
26
+ draftId: draft.id,
27
+ messageId: msg?.id,
28
+ threadId: msg?.threadId,
29
+ labelIds: msg?.labelIds ?? [],
30
+ snippet: msg?.snippet ?? '',
31
+ headers: {
32
+ from: findHeaderValue(headers, 'From'),
33
+ to: findHeaderValue(headers, 'To'),
34
+ cc: findHeaderValue(headers, 'Cc'),
35
+ bcc: findHeaderValue(headers, 'Bcc'),
36
+ subject: findHeaderValue(headers, 'Subject'),
37
+ date: findHeaderValue(headers, 'Date'),
38
+ },
39
+ body: { text, html },
40
+ }, null, 2);
41
+ }
42
+ catch (error) {
43
+ log.error(`Error getting draft: ${error.message || error}`);
44
+ if (error.code === 404)
45
+ throw new UserError(`Draft not found (ID: ${args.draftId}).`);
46
+ if (error.code === 403)
47
+ throw new UserError('Permission denied. Confirm the gmail.modify scope was granted.');
48
+ throw new UserError(`Failed to get draft: ${error.message || 'Unknown error'}`);
49
+ }
50
+ },
51
+ });
52
+ }
@@ -0,0 +1,92 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ import { findHeaderValue, extractMessageBody } from './helpers.js';
5
+ function collectAttachments(payload) {
6
+ const out = [];
7
+ if (!payload)
8
+ return out;
9
+ const walk = (part) => {
10
+ if (part.filename && part.body?.attachmentId) {
11
+ out.push({
12
+ partId: part.partId ?? null,
13
+ filename: part.filename,
14
+ mimeType: part.mimeType ?? 'application/octet-stream',
15
+ size: part.body.size ?? 0,
16
+ attachmentId: part.body.attachmentId ?? null,
17
+ });
18
+ }
19
+ if (part.parts)
20
+ for (const sub of part.parts)
21
+ walk(sub);
22
+ };
23
+ walk(payload);
24
+ return out;
25
+ }
26
+ export function register(server) {
27
+ server.addTool({
28
+ name: 'getMessage',
29
+ description: 'Fetches a single Gmail message by ID with headers, decoded plain-text body, HTML body, and a list of attachments (metadata only). Use listMessages to discover message IDs.',
30
+ parameters: z.strictObject({
31
+ messageId: z.string().describe('The Gmail message ID, typically from listMessages results.'),
32
+ format: z
33
+ .enum(['full', 'metadata', 'minimal'])
34
+ .optional()
35
+ .default('full')
36
+ .describe('"full" returns headers + body + attachments; "metadata" returns headers only; "minimal" returns just labels/snippet.'),
37
+ }),
38
+ execute: async (args, { log }) => {
39
+ const gmail = await getGmailClient();
40
+ log.info(`Getting Gmail message ${args.messageId} (format=${args.format})`);
41
+ try {
42
+ const response = await gmail.users.messages.get({
43
+ userId: 'me',
44
+ id: args.messageId,
45
+ format: args.format,
46
+ });
47
+ const msg = response.data;
48
+ const headers = msg.payload?.headers;
49
+ const base = {
50
+ id: msg.id,
51
+ threadId: msg.threadId,
52
+ labelIds: msg.labelIds ?? [],
53
+ snippet: msg.snippet ?? '',
54
+ historyId: msg.historyId,
55
+ internalDate: msg.internalDate,
56
+ sizeEstimate: msg.sizeEstimate,
57
+ };
58
+ if (args.format === 'minimal') {
59
+ return JSON.stringify(base, null, 2);
60
+ }
61
+ const headerSummary = {
62
+ from: findHeaderValue(headers, 'From'),
63
+ to: findHeaderValue(headers, 'To'),
64
+ cc: findHeaderValue(headers, 'Cc'),
65
+ bcc: findHeaderValue(headers, 'Bcc'),
66
+ subject: findHeaderValue(headers, 'Subject'),
67
+ date: findHeaderValue(headers, 'Date'),
68
+ messageIdHeader: findHeaderValue(headers, 'Message-Id'),
69
+ };
70
+ if (args.format === 'metadata') {
71
+ return JSON.stringify({ ...base, headers: headerSummary }, null, 2);
72
+ }
73
+ const { text, html } = extractMessageBody(msg.payload);
74
+ const attachments = collectAttachments(msg.payload);
75
+ return JSON.stringify({
76
+ ...base,
77
+ headers: headerSummary,
78
+ body: { text, html },
79
+ attachments,
80
+ }, null, 2);
81
+ }
82
+ catch (error) {
83
+ log.error(`Error getting Gmail message: ${error.message || error}`);
84
+ if (error.code === 404)
85
+ throw new UserError(`Gmail message not found (ID: ${args.messageId}).`);
86
+ if (error.code === 403)
87
+ throw new UserError('Permission denied. Confirm the gmail.modify scope was granted.');
88
+ throw new UserError(`Failed to get Gmail message: ${error.message || 'Unknown error'}`);
89
+ }
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,113 @@
1
+ export function findHeaderValue(headers, name) {
2
+ if (!headers)
3
+ return null;
4
+ return headers.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ?? null;
5
+ }
6
+ export function encodeHeader(value) {
7
+ // RFC 2047 encoded-word for any non-ASCII content in headers.
8
+ if (/^[\x00-\x7F]*$/.test(value))
9
+ return value;
10
+ return `=?UTF-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=`;
11
+ }
12
+ export function buildMimeMessage(opts) {
13
+ const lines = [];
14
+ lines.push(`To: ${opts.to.join(', ')}`);
15
+ if (opts.cc && opts.cc.length > 0)
16
+ lines.push(`Cc: ${opts.cc.join(', ')}`);
17
+ if (opts.bcc && opts.bcc.length > 0)
18
+ lines.push(`Bcc: ${opts.bcc.join(', ')}`);
19
+ lines.push(`Subject: ${encodeHeader(opts.subject)}`);
20
+ lines.push('MIME-Version: 1.0');
21
+ lines.push('Content-Type: text/plain; charset="UTF-8"');
22
+ lines.push('Content-Transfer-Encoding: 8bit');
23
+ if (opts.inReplyTo)
24
+ lines.push(`In-Reply-To: ${opts.inReplyTo}`);
25
+ if (opts.references)
26
+ lines.push(`References: ${opts.references}`);
27
+ lines.push('');
28
+ lines.push(opts.body);
29
+ return lines.join('\r\n');
30
+ }
31
+ export function encodeRawMessage(mime) {
32
+ return Buffer.from(mime, 'utf-8').toString('base64url');
33
+ }
34
+ /**
35
+ * Fetches the original message and returns the headers needed to build a
36
+ * threaded reply (Message-Id for In-Reply-To, References chain, threadId).
37
+ */
38
+ export async function getReplyContext(gmail, messageId) {
39
+ const original = await gmail.users.messages.get({
40
+ userId: 'me',
41
+ id: messageId,
42
+ format: 'metadata',
43
+ metadataHeaders: ['Message-Id', 'References', 'Subject'],
44
+ });
45
+ const threadId = original.data.threadId ?? undefined;
46
+ const origHeaders = original.data.payload?.headers;
47
+ const inReplyTo = findHeaderValue(origHeaders, 'Message-Id');
48
+ const origRefs = findHeaderValue(origHeaders, 'References');
49
+ const references = [origRefs, inReplyTo].filter(Boolean).join(' ') || null;
50
+ return { threadId, inReplyTo, references };
51
+ }
52
+ /**
53
+ * Resolves reply threading context (if any), builds the MIME message, and
54
+ * returns the base64url-encoded raw + threadId ready for messages.send,
55
+ * drafts.create, or drafts.update. Centralizes the compose pipeline so all
56
+ * three call sites stay in sync on threading and MIME formatting.
57
+ */
58
+ export async function prepareMimeRequest(gmail, args) {
59
+ const toList = Array.isArray(args.to) ? args.to : [args.to];
60
+ let threadId;
61
+ let inReplyTo = null;
62
+ let references = null;
63
+ if (args.replyToMessageId) {
64
+ const ctx = await getReplyContext(gmail, args.replyToMessageId);
65
+ threadId = ctx.threadId;
66
+ inReplyTo = ctx.inReplyTo;
67
+ references = ctx.references;
68
+ }
69
+ const raw = encodeRawMessage(buildMimeMessage({
70
+ to: toList,
71
+ cc: args.cc,
72
+ bcc: args.bcc,
73
+ subject: args.subject,
74
+ body: args.body,
75
+ inReplyTo,
76
+ references,
77
+ }));
78
+ return { raw, threadId, toList };
79
+ }
80
+ export function decodeBase64Url(data) {
81
+ if (!data)
82
+ return '';
83
+ return Buffer.from(data, 'base64url').toString('utf-8');
84
+ }
85
+ /**
86
+ * Walks a Gmail message payload tree and accumulates all text/plain and
87
+ * text/html parts. Returns both representations so callers can pick the
88
+ * one they want (text for processing, html for display).
89
+ */
90
+ export function extractMessageBody(payload) {
91
+ let text = '';
92
+ let html = '';
93
+ if (!payload)
94
+ return { text, html };
95
+ const walk = (part) => {
96
+ const mime = part.mimeType ?? '';
97
+ if (mime === 'text/plain' && part.body?.data)
98
+ text += decodeBase64Url(part.body.data);
99
+ else if (mime === 'text/html' && part.body?.data)
100
+ html += decodeBase64Url(part.body.data);
101
+ if (part.parts)
102
+ for (const sub of part.parts)
103
+ walk(sub);
104
+ };
105
+ walk(payload);
106
+ return { text, html };
107
+ }
108
+ export function extractDomain(fromHeader) {
109
+ if (!fromHeader)
110
+ return null;
111
+ const match = fromHeader.match(/<?([^@<>\s]+)@([^>\s]+)>?/);
112
+ return match ? match[2].toLowerCase() : null;
113
+ }
@@ -0,0 +1,28 @@
1
+ import { register as listMessages } from './listMessages.js';
2
+ import { register as getMessage } from './getMessage.js';
3
+ import { register as sendEmail } from './sendEmail.js';
4
+ import { register as trashMessage } from './trashMessage.js';
5
+ import { register as modifyMessageLabels } from './modifyMessageLabels.js';
6
+ import { register as listLabels } from './listLabels.js';
7
+ import { register as createDraft } from './createDraft.js';
8
+ import { register as listDrafts } from './listDrafts.js';
9
+ import { register as getDraft } from './getDraft.js';
10
+ import { register as updateDraft } from './updateDraft.js';
11
+ import { register as sendDraft } from './sendDraft.js';
12
+ import { register as deleteDraft } from './deleteDraft.js';
13
+ import { register as triageInbox } from './triageInbox.js';
14
+ export function registerGmailTools(server) {
15
+ listMessages(server);
16
+ getMessage(server);
17
+ sendEmail(server);
18
+ trashMessage(server);
19
+ modifyMessageLabels(server);
20
+ listLabels(server);
21
+ createDraft(server);
22
+ listDrafts(server);
23
+ getDraft(server);
24
+ updateDraft(server);
25
+ sendDraft(server);
26
+ deleteDraft(server);
27
+ triageInbox(server);
28
+ }
@@ -0,0 +1,74 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ import { findHeaderValue } from './helpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'listDrafts',
8
+ description: 'Lists Gmail drafts for the authenticated user. Returns draft IDs along with the recipient, subject, snippet, and date for each. Use sendDraft, updateDraft, or deleteDraft to act on a returned draft.',
9
+ parameters: z.strictObject({
10
+ maxResults: z
11
+ .number()
12
+ .int()
13
+ .min(1)
14
+ .max(100)
15
+ .optional()
16
+ .default(25)
17
+ .describe('Maximum number of drafts to return (1-100). Defaults to 25.'),
18
+ q: z
19
+ .string()
20
+ .optional()
21
+ .describe('Optional Gmail search query to filter drafts (e.g. "subject:proposal").'),
22
+ }),
23
+ execute: async (args, { log }) => {
24
+ const gmail = await getGmailClient();
25
+ log.info(`Listing Gmail drafts (max=${args.maxResults})`);
26
+ try {
27
+ const listResponse = await gmail.users.drafts.list({
28
+ userId: 'me',
29
+ maxResults: args.maxResults,
30
+ q: args.q,
31
+ });
32
+ const draftRefs = listResponse.data.drafts ?? [];
33
+ if (draftRefs.length === 0) {
34
+ return JSON.stringify({
35
+ drafts: [],
36
+ resultSizeEstimate: listResponse.data.resultSizeEstimate ?? 0,
37
+ nextPageToken: listResponse.data.nextPageToken ?? null,
38
+ }, null, 2);
39
+ }
40
+ const detailed = await Promise.all(draftRefs.map((ref) => gmail.users.drafts.get({
41
+ userId: 'me',
42
+ id: ref.id,
43
+ format: 'metadata',
44
+ })));
45
+ const drafts = detailed.map((response) => {
46
+ const draft = response.data;
47
+ const msg = draft.message;
48
+ const headers = msg?.payload?.headers;
49
+ return {
50
+ draftId: draft.id,
51
+ messageId: msg?.id,
52
+ threadId: msg?.threadId,
53
+ snippet: msg?.snippet ?? '',
54
+ to: findHeaderValue(headers, 'To'),
55
+ cc: findHeaderValue(headers, 'Cc'),
56
+ subject: findHeaderValue(headers, 'Subject'),
57
+ date: findHeaderValue(headers, 'Date'),
58
+ };
59
+ });
60
+ return JSON.stringify({
61
+ drafts,
62
+ resultSizeEstimate: listResponse.data.resultSizeEstimate ?? drafts.length,
63
+ nextPageToken: listResponse.data.nextPageToken ?? null,
64
+ }, null, 2);
65
+ }
66
+ catch (error) {
67
+ log.error(`Error listing drafts: ${error.message || error}`);
68
+ if (error.code === 403)
69
+ throw new UserError('Permission denied. Confirm the gmail.modify scope was granted.');
70
+ throw new UserError(`Failed to list drafts: ${error.message || 'Unknown error'}`);
71
+ }
72
+ },
73
+ });
74
+ }
@@ -0,0 +1,31 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'listLabels',
7
+ description: 'Lists all Gmail labels for the authenticated user, including system labels (INBOX, SENT, STARRED, UNREAD, etc.) and custom user-created labels. Use the returned IDs with modifyMessageLabels or listMessages labelIds filter.',
8
+ parameters: z.strictObject({}),
9
+ execute: async (_args, { log }) => {
10
+ const gmail = await getGmailClient();
11
+ log.info('Listing Gmail labels');
12
+ try {
13
+ const response = await gmail.users.labels.list({ userId: 'me' });
14
+ const labels = (response.data.labels ?? []).map((label) => ({
15
+ id: label.id,
16
+ name: label.name,
17
+ type: label.type,
18
+ messageListVisibility: label.messageListVisibility,
19
+ labelListVisibility: label.labelListVisibility,
20
+ }));
21
+ return JSON.stringify({ labels, count: labels.length }, null, 2);
22
+ }
23
+ catch (error) {
24
+ log.error(`Error listing Gmail labels: ${error.message || error}`);
25
+ if (error.code === 403)
26
+ throw new UserError('Permission denied. Confirm the gmail.modify scope was granted.');
27
+ throw new UserError(`Failed to list Gmail labels: ${error.message || 'Unknown error'}`);
28
+ }
29
+ },
30
+ });
31
+ }
@@ -0,0 +1,87 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ import { findHeaderValue } from './helpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'listMessages',
8
+ description: 'Lists Gmail messages for the authenticated user. Supports the full Gmail search syntax via the q parameter (e.g. "is:unread", "from:alice@example.com", "subject:invoice newer_than:7d"). Returns message IDs with sender, subject, date, and snippet for each result.',
9
+ parameters: z.strictObject({
10
+ maxResults: z
11
+ .number()
12
+ .int()
13
+ .min(1)
14
+ .max(100)
15
+ .optional()
16
+ .default(10)
17
+ .describe('Maximum number of messages to return (1-100). Defaults to 10.'),
18
+ q: z
19
+ .string()
20
+ .optional()
21
+ .describe('Gmail search query using the same syntax as the Gmail search box. Examples: "is:unread", "from:boss@acme.com", "has:attachment newer_than:3d".'),
22
+ labelIds: z
23
+ .array(z.string())
24
+ .optional()
25
+ .describe('Only return messages with these label IDs (e.g. ["INBOX"], ["STARRED"]). Use listLabels to discover custom label IDs.'),
26
+ includeSpamTrash: z
27
+ .boolean()
28
+ .optional()
29
+ .default(false)
30
+ .describe('If true, also include messages from SPAM and TRASH.'),
31
+ }),
32
+ execute: async (args, { log }) => {
33
+ const gmail = await getGmailClient();
34
+ log.info(`Listing Gmail messages (max=${args.maxResults}, q=${args.q ?? 'none'}, labels=${args.labelIds?.join(',') ?? 'none'})`);
35
+ try {
36
+ const listResponse = await gmail.users.messages.list({
37
+ userId: 'me',
38
+ maxResults: args.maxResults,
39
+ q: args.q,
40
+ labelIds: args.labelIds,
41
+ includeSpamTrash: args.includeSpamTrash,
42
+ });
43
+ const messageRefs = listResponse.data.messages ?? [];
44
+ if (messageRefs.length === 0) {
45
+ return JSON.stringify({
46
+ messages: [],
47
+ resultSizeEstimate: listResponse.data.resultSizeEstimate ?? 0,
48
+ nextPageToken: listResponse.data.nextPageToken ?? null,
49
+ }, null, 2);
50
+ }
51
+ const detailed = await Promise.all(messageRefs.map((ref) => gmail.users.messages.get({
52
+ userId: 'me',
53
+ id: ref.id,
54
+ format: 'metadata',
55
+ metadataHeaders: ['From', 'To', 'Subject', 'Date'],
56
+ })));
57
+ const messages = detailed.map((response) => {
58
+ const msg = response.data;
59
+ const headers = msg.payload?.headers;
60
+ return {
61
+ id: msg.id,
62
+ threadId: msg.threadId,
63
+ labelIds: msg.labelIds ?? [],
64
+ snippet: msg.snippet ?? '',
65
+ from: findHeaderValue(headers, 'From'),
66
+ to: findHeaderValue(headers, 'To'),
67
+ subject: findHeaderValue(headers, 'Subject'),
68
+ date: findHeaderValue(headers, 'Date'),
69
+ };
70
+ });
71
+ return JSON.stringify({
72
+ messages,
73
+ resultSizeEstimate: listResponse.data.resultSizeEstimate ?? messages.length,
74
+ nextPageToken: listResponse.data.nextPageToken ?? null,
75
+ }, null, 2);
76
+ }
77
+ catch (error) {
78
+ log.error(`Error listing Gmail messages: ${error.message || error}`);
79
+ if (error.code === 401)
80
+ throw new UserError('Gmail authorization failed. Re-authorize the MCP server (scopes may have changed).');
81
+ if (error.code === 403)
82
+ throw new UserError('Permission denied. Confirm the Gmail API is enabled and the gmail.modify scope was granted during consent.');
83
+ throw new UserError(`Failed to list Gmail messages: ${error.message || 'Unknown error'}`);
84
+ }
85
+ },
86
+ });
87
+ }
@@ -0,0 +1,54 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'modifyMessageLabels',
7
+ description: 'Adds and/or removes labels on a Gmail message. Use this to star (add STARRED), archive (remove INBOX), mark read (remove UNREAD), or apply custom labels. Discover label IDs with listLabels. At least one of addLabelIds or removeLabelIds must be provided.',
8
+ parameters: z
9
+ .object({
10
+ messageId: z.string().describe('The Gmail message ID to modify.'),
11
+ addLabelIds: z
12
+ .array(z.string())
13
+ .optional()
14
+ .describe('Label IDs to add (e.g. ["STARRED"], ["IMPORTANT"], or a custom label ID from listLabels).'),
15
+ removeLabelIds: z
16
+ .array(z.string())
17
+ .optional()
18
+ .describe('Label IDs to remove (e.g. ["INBOX"] to archive, ["UNREAD"] to mark as read).'),
19
+ })
20
+ .refine((v) => (v.addLabelIds && v.addLabelIds.length > 0) ||
21
+ (v.removeLabelIds && v.removeLabelIds.length > 0), { message: 'Provide at least one of addLabelIds or removeLabelIds.' }),
22
+ execute: async (args, { log }) => {
23
+ const gmail = await getGmailClient();
24
+ log.info(`Modifying labels on ${args.messageId} (add=${args.addLabelIds?.join(',') ?? 'none'}, remove=${args.removeLabelIds?.join(',') ?? 'none'})`);
25
+ try {
26
+ const response = await gmail.users.messages.modify({
27
+ userId: 'me',
28
+ id: args.messageId,
29
+ requestBody: {
30
+ addLabelIds: args.addLabelIds,
31
+ removeLabelIds: args.removeLabelIds,
32
+ },
33
+ });
34
+ return JSON.stringify({
35
+ success: true,
36
+ id: response.data.id,
37
+ threadId: response.data.threadId,
38
+ labelIds: response.data.labelIds ?? [],
39
+ message: `Labels updated on message ${args.messageId}.`,
40
+ }, null, 2);
41
+ }
42
+ catch (error) {
43
+ log.error(`Error modifying labels: ${error.message || error}`);
44
+ if (error.code === 404)
45
+ throw new UserError(`Gmail message or label not found. Verify messageId and label IDs (use listLabels).`);
46
+ if (error.code === 400)
47
+ throw new UserError(`Gmail rejected the label change: ${error.message || 'Bad request'}. Some system labels (e.g. DRAFT, SENT) cannot be applied manually.`);
48
+ if (error.code === 403)
49
+ throw new UserError('Permission denied. Confirm the gmail.modify scope was granted.');
50
+ throw new UserError(`Failed to modify labels: ${error.message || 'Unknown error'}`);
51
+ }
52
+ },
53
+ });
54
+ }
@@ -0,0 +1,42 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ export function register(server) {
5
+ server.addTool({
6
+ name: 'sendDraft',
7
+ description: 'Sends an existing Gmail draft. After sending, the draft is removed and the message appears in Sent. This is the second half of the compose-review-send flow that pairs with createDraft.',
8
+ parameters: z.strictObject({
9
+ draftId: z.string().describe('The Gmail draft ID to send (from createDraft or listDrafts).'),
10
+ }),
11
+ execute: async (args, { log }) => {
12
+ const gmail = await getGmailClient();
13
+ log.info(`Sending Gmail draft ${args.draftId}`);
14
+ try {
15
+ const response = await gmail.users.drafts.send({
16
+ userId: 'me',
17
+ requestBody: {
18
+ id: args.draftId,
19
+ },
20
+ });
21
+ return JSON.stringify({
22
+ success: true,
23
+ draftId: args.draftId,
24
+ messageId: response.data.id,
25
+ threadId: response.data.threadId,
26
+ labelIds: response.data.labelIds ?? [],
27
+ message: `Draft ${args.draftId} sent. Message ID: ${response.data.id}.`,
28
+ }, null, 2);
29
+ }
30
+ catch (error) {
31
+ log.error(`Error sending draft: ${error.message || error}`);
32
+ if (error.code === 404)
33
+ throw new UserError(`Draft not found (ID: ${args.draftId}).`);
34
+ if (error.code === 403)
35
+ throw new UserError('Permission denied. The account does not have permission to send mail via this OAuth client.');
36
+ if (error.code === 400)
37
+ throw new UserError(`Gmail rejected the send: ${error.message || 'Bad request'}. Verify the draft has at least one recipient and a subject.`);
38
+ throw new UserError(`Failed to send draft: ${error.message || 'Unknown error'}`);
39
+ }
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,56 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getGmailClient } from '../../clients.js';
4
+ import { prepareMimeRequest } from './helpers.js';
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'sendEmail',
8
+ description: 'Sends a plain-text email from the authenticated Gmail account. Supports cc/bcc and optional threading by passing replyToMessageId (which copies threadId and sets In-Reply-To/References so the reply lands in the same thread).',
9
+ parameters: z.strictObject({
10
+ to: z
11
+ .union([z.string(), z.array(z.string()).min(1)])
12
+ .describe('Recipient email address, or an array of recipient email addresses.'),
13
+ subject: z.string().describe('Email subject line.'),
14
+ body: z.string().describe('Plain-text body of the email.'),
15
+ cc: z.array(z.string()).optional().describe('Optional list of Cc recipients.'),
16
+ bcc: z.array(z.string()).optional().describe('Optional list of Bcc recipients.'),
17
+ replyToMessageId: z
18
+ .string()
19
+ .optional()
20
+ .describe('Optional Gmail message ID to reply to. When set, the new email is threaded with the original and uses In-Reply-To/References headers.'),
21
+ }),
22
+ execute: async (args, { log }) => {
23
+ const gmail = await getGmailClient();
24
+ try {
25
+ const { raw, threadId, toList } = await prepareMimeRequest(gmail, args);
26
+ log.info(`Sending Gmail message to ${toList.join(', ')}${args.replyToMessageId ? ` (reply to ${args.replyToMessageId})` : ''}`);
27
+ const send = await gmail.users.messages.send({
28
+ userId: 'me',
29
+ requestBody: {
30
+ raw,
31
+ ...(threadId ? { threadId } : {}),
32
+ },
33
+ });
34
+ return JSON.stringify({
35
+ success: true,
36
+ id: send.data.id,
37
+ threadId: send.data.threadId,
38
+ labelIds: send.data.labelIds ?? [],
39
+ to: toList,
40
+ subject: args.subject,
41
+ message: `Email sent to ${toList.join(', ')}.`,
42
+ }, null, 2);
43
+ }
44
+ catch (error) {
45
+ log.error(`Error sending Gmail message: ${error.message || error}`);
46
+ if (error.code === 401)
47
+ throw new UserError('Gmail authorization failed. Re-authorize to grant the gmail.modify scope.');
48
+ if (error.code === 403)
49
+ throw new UserError('Permission denied. The account does not have permission to send mail via this OAuth client.');
50
+ if (error.code === 400)
51
+ throw new UserError(`Gmail rejected the message: ${error.message || 'Bad request'}`);
52
+ throw new UserError(`Failed to send email: ${error.message || 'Unknown error'}`);
53
+ }
54
+ },
55
+ });
56
+ }