google-tools-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.js +275 -0
- package/dist/cachedToolsList.js +52 -0
- package/dist/clients.js +97 -0
- package/dist/googleDocsApiHelpers.js +883 -0
- package/dist/googleSheetsApiHelpers.js +808 -0
- package/dist/helpers.js +210 -0
- package/dist/index.js +55 -0
- package/dist/logger.js +45 -0
- package/dist/markdown-transformer/docsToMarkdown.js +259 -0
- package/dist/markdown-transformer/index.js +126 -0
- package/dist/markdown-transformer/markdownToDocs.js +834 -0
- package/dist/tools/docs/addTab.js +92 -0
- package/dist/tools/docs/appendToGoogleDoc.js +81 -0
- package/dist/tools/docs/comments/addComment.js +83 -0
- package/dist/tools/docs/comments/deleteComment.js +30 -0
- package/dist/tools/docs/comments/getComment.js +45 -0
- package/dist/tools/docs/comments/index.js +14 -0
- package/dist/tools/docs/comments/listComments.js +43 -0
- package/dist/tools/docs/comments/replyToComment.js +35 -0
- package/dist/tools/docs/comments/resolveComment.js +55 -0
- package/dist/tools/docs/deleteRange.js +72 -0
- package/dist/tools/docs/findAndReplace.js +54 -0
- package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
- package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
- package/dist/tools/docs/formatting/index.js +6 -0
- package/dist/tools/docs/index.js +38 -0
- package/dist/tools/docs/insertImage.js +122 -0
- package/dist/tools/docs/insertPageBreak.js +58 -0
- package/dist/tools/docs/insertTable.js +53 -0
- package/dist/tools/docs/insertTableWithData.js +135 -0
- package/dist/tools/docs/insertText.js +61 -0
- package/dist/tools/docs/listDocumentTabs.js +60 -0
- package/dist/tools/docs/modifyText.js +158 -0
- package/dist/tools/docs/readGoogleDoc.js +165 -0
- package/dist/tools/docs/renameTab.js +61 -0
- package/dist/tools/drive/copyFile.js +63 -0
- package/dist/tools/drive/createDocument.js +89 -0
- package/dist/tools/drive/createFolder.js +48 -0
- package/dist/tools/drive/createFromTemplate.js +82 -0
- package/dist/tools/drive/deleteFile.js +72 -0
- package/dist/tools/drive/getDocumentInfo.js +48 -0
- package/dist/tools/drive/getFolderInfo.js +48 -0
- package/dist/tools/drive/index.js +30 -0
- package/dist/tools/drive/listFolderContents.js +82 -0
- package/dist/tools/drive/listGoogleDocs.js +67 -0
- package/dist/tools/drive/listSharedDrives.js +52 -0
- package/dist/tools/drive/listSharedWithMe.js +97 -0
- package/dist/tools/drive/moveFile.js +54 -0
- package/dist/tools/drive/renameFile.js +39 -0
- package/dist/tools/drive/searchGoogleDocs.js +98 -0
- package/dist/tools/extras/index.js +7 -0
- package/dist/tools/extras/readFile.js +82 -0
- package/dist/tools/extras/searchFileContents.js +81 -0
- package/dist/tools/gmail/drafts.js +165 -0
- package/dist/tools/gmail/labels.js +103 -0
- package/dist/tools/gmail/messages.js +448 -0
- package/dist/tools/gmail/settings.js +528 -0
- package/dist/tools/gmail/threads.js +145 -0
- package/dist/tools/index.js +163 -0
- package/dist/tools/sheets/addConditionalFormatting.js +143 -0
- package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
- package/dist/tools/sheets/appendSpreadsheetRows.js +43 -0
- package/dist/tools/sheets/appendTableRows.js +50 -0
- package/dist/tools/sheets/autoResizeColumns.js +67 -0
- package/dist/tools/sheets/batchWrite.js +59 -0
- package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
- package/dist/tools/sheets/copyFormatting.js +59 -0
- package/dist/tools/sheets/createSpreadsheet.js +71 -0
- package/dist/tools/sheets/createTable.js +120 -0
- package/dist/tools/sheets/deleteChart.js +41 -0
- package/dist/tools/sheets/deleteSheet.js +43 -0
- package/dist/tools/sheets/deleteTable.js +56 -0
- package/dist/tools/sheets/duplicateSheet.js +53 -0
- package/dist/tools/sheets/formatCells.js +106 -0
- package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
- package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
- package/dist/tools/sheets/getTable.js +48 -0
- package/dist/tools/sheets/groupRows.js +62 -0
- package/dist/tools/sheets/index.js +66 -0
- package/dist/tools/sheets/insertChart.js +225 -0
- package/dist/tools/sheets/listGoogleSheets.js +62 -0
- package/dist/tools/sheets/listTables.js +55 -0
- package/dist/tools/sheets/readCellFormat.js +143 -0
- package/dist/tools/sheets/readSpreadsheet.js +36 -0
- package/dist/tools/sheets/renameSheet.js +48 -0
- package/dist/tools/sheets/setColumnWidths.js +43 -0
- package/dist/tools/sheets/setDropdownValidation.js +51 -0
- package/dist/tools/sheets/ungroupAllRows.js +66 -0
- package/dist/tools/sheets/updateTableRange.js +51 -0
- package/dist/tools/sheets/writeSpreadsheet.js +43 -0
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +93 -0
- package/dist/tools/utils/index.js +6 -0
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +154 -0
- package/dist/types.js +186 -0
- package/package.json +30 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'renameFile',
|
|
7
|
+
description: 'Renames a file or folder in Google Drive. Returns the updated file info.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
fileId: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe('The file or folder ID from a Google Drive URL or a previous tool result.'),
|
|
12
|
+
newName: z.string().min(1).describe('New name for the file or folder.'),
|
|
13
|
+
}),
|
|
14
|
+
execute: async (args, { log }) => {
|
|
15
|
+
const drive = await getDriveClient();
|
|
16
|
+
log.info(`Renaming file ${args.fileId} to "${args.newName}"`);
|
|
17
|
+
try {
|
|
18
|
+
const response = await drive.files.update({
|
|
19
|
+
fileId: args.fileId,
|
|
20
|
+
requestBody: {
|
|
21
|
+
name: args.newName,
|
|
22
|
+
},
|
|
23
|
+
fields: 'id,name,webViewLink',
|
|
24
|
+
supportsAllDrives: true,
|
|
25
|
+
});
|
|
26
|
+
const file = response.data;
|
|
27
|
+
return `Successfully renamed to "${file.name}" (ID: ${file.id})\nLink: ${file.webViewLink}`;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
log.error(`Error renaming file: ${error.message || error}`);
|
|
31
|
+
if (error.code === 404)
|
|
32
|
+
throw new UserError('File not found. Check the file ID.');
|
|
33
|
+
if (error.code === 403)
|
|
34
|
+
throw new UserError('Permission denied. Make sure you have write access to this file.');
|
|
35
|
+
throw new UserError(`Failed to rename file: ${error.message || 'Unknown error'}`);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
export function register(server) {
|
|
5
|
+
server.addTool({
|
|
6
|
+
name: 'searchDocuments',
|
|
7
|
+
description: 'Searches for documents by name, content, or both. Finds Google Docs, Word (.docx), and PDF files. Use listDocuments for browsing and this tool for targeted queries.',
|
|
8
|
+
parameters: z.object({
|
|
9
|
+
query: z.string().min(1).describe('Search term to find in document names or content.'),
|
|
10
|
+
searchIn: z
|
|
11
|
+
.enum(['name', 'content', 'both'])
|
|
12
|
+
.optional()
|
|
13
|
+
.default('both')
|
|
14
|
+
.describe('Where to search: document names, content, or both.'),
|
|
15
|
+
fileType: z
|
|
16
|
+
.enum(['all', 'google-doc', 'docx', 'pdf'])
|
|
17
|
+
.optional()
|
|
18
|
+
.default('all')
|
|
19
|
+
.describe('Filter by file type: all document types, Google Docs only, Word (.docx) only, or PDF only.'),
|
|
20
|
+
maxResults: z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(50)
|
|
25
|
+
.optional()
|
|
26
|
+
.default(10)
|
|
27
|
+
.describe('Maximum number of results to return.'),
|
|
28
|
+
modifiedAfter: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Only return documents modified after this date (ISO 8601 format, e.g., "2024-01-01").'),
|
|
32
|
+
}),
|
|
33
|
+
execute: async (args, { log }) => {
|
|
34
|
+
const drive = await getDriveClient();
|
|
35
|
+
log.info(`Searching Google Docs for: "${args.query}" in ${args.searchIn}`);
|
|
36
|
+
try {
|
|
37
|
+
const mimeTypes = {
|
|
38
|
+
'google-doc': ["mimeType='application/vnd.google-apps.document'"],
|
|
39
|
+
'docx': ["mimeType='application/vnd.openxmlformats-officedocument.wordprocessingml.document'"],
|
|
40
|
+
'pdf': ["mimeType='application/pdf'"],
|
|
41
|
+
'all': [
|
|
42
|
+
"mimeType='application/vnd.google-apps.document'",
|
|
43
|
+
"mimeType='application/vnd.openxmlformats-officedocument.wordprocessingml.document'",
|
|
44
|
+
"mimeType='application/pdf'",
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
const selectedMimes = mimeTypes[args.fileType || 'all'];
|
|
48
|
+
const mimeFilter = selectedMimes.length === 1
|
|
49
|
+
? selectedMimes[0]
|
|
50
|
+
: `(${selectedMimes.join(' or ')})`;
|
|
51
|
+
let queryString = `${mimeFilter} and trashed=false`;
|
|
52
|
+
// Add search criteria
|
|
53
|
+
if (args.searchIn === 'name') {
|
|
54
|
+
queryString += ` and name contains '${args.query}'`;
|
|
55
|
+
}
|
|
56
|
+
else if (args.searchIn === 'content') {
|
|
57
|
+
queryString += ` and fullText contains '${args.query}'`;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
queryString += ` and (name contains '${args.query}' or fullText contains '${args.query}')`;
|
|
61
|
+
}
|
|
62
|
+
// Add date filter if provided
|
|
63
|
+
if (args.modifiedAfter) {
|
|
64
|
+
queryString += ` and modifiedTime > '${args.modifiedAfter}'`;
|
|
65
|
+
}
|
|
66
|
+
const response = await drive.files.list({
|
|
67
|
+
q: queryString,
|
|
68
|
+
pageSize: args.maxResults,
|
|
69
|
+
orderBy: 'modifiedTime desc',
|
|
70
|
+
fields: 'files(id,name,mimeType,modifiedTime,createdTime,webViewLink,owners(displayName),parents)',
|
|
71
|
+
supportsAllDrives: true,
|
|
72
|
+
includeItemsFromAllDrives: true,
|
|
73
|
+
});
|
|
74
|
+
const files = response.data.files || [];
|
|
75
|
+
const mimeToType = {
|
|
76
|
+
'application/vnd.google-apps.document': 'google-doc',
|
|
77
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
|
|
78
|
+
'application/pdf': 'pdf',
|
|
79
|
+
};
|
|
80
|
+
const documents = files.map((file) => ({
|
|
81
|
+
id: file.id,
|
|
82
|
+
name: file.name,
|
|
83
|
+
type: mimeToType[file.mimeType] || file.mimeType,
|
|
84
|
+
modifiedTime: file.modifiedTime,
|
|
85
|
+
owner: file.owners?.[0]?.displayName || null,
|
|
86
|
+
url: file.webViewLink,
|
|
87
|
+
}));
|
|
88
|
+
return JSON.stringify({ documents }, null, 2);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
log.error(`Error searching Google Docs: ${error.message || error}`);
|
|
92
|
+
if (error.code === 403)
|
|
93
|
+
throw new UserError('Permission denied. Make sure you have granted Google Drive access to the application.');
|
|
94
|
+
throw new UserError(`Failed to search documents: ${error.message || 'Unknown error'}`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
import mammoth from 'mammoth';
|
|
5
|
+
|
|
6
|
+
function isDocx(mimeType, name) {
|
|
7
|
+
return (
|
|
8
|
+
mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
|
|
9
|
+
(name && name.endsWith('.docx'))
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPdf(mimeType, name) {
|
|
14
|
+
return mimeType === 'application/pdf' || (name && name.endsWith('.pdf'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function downloadBuffer(drive, fileId) {
|
|
18
|
+
const response = await drive.files.get(
|
|
19
|
+
{ fileId, alt: 'media', supportsAllDrives: true },
|
|
20
|
+
{ responseType: 'arraybuffer' }
|
|
21
|
+
);
|
|
22
|
+
return Buffer.from(response.data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function extractText(buffer, mimeType, name) {
|
|
26
|
+
if (isDocx(mimeType, name)) {
|
|
27
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
28
|
+
return result.value;
|
|
29
|
+
}
|
|
30
|
+
if (isPdf(mimeType, name)) {
|
|
31
|
+
const { default: pdfParse } = await import('pdf-parse/lib/pdf-parse.js');
|
|
32
|
+
const result = await pdfParse(buffer);
|
|
33
|
+
return result.text;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { isDocx, isPdf, downloadBuffer };
|
|
39
|
+
|
|
40
|
+
export function register(server) {
|
|
41
|
+
server.addTool({
|
|
42
|
+
name: 'readFile',
|
|
43
|
+
description:
|
|
44
|
+
'Read the full text content of a .docx or .pdf file from Google Drive by file ID. ' +
|
|
45
|
+
'Use this for Word documents and PDFs that cannot be read with readDocument.',
|
|
46
|
+
parameters: z.object({
|
|
47
|
+
fileId: z.string().describe('The Google Drive file ID'),
|
|
48
|
+
}),
|
|
49
|
+
execute: async ({ fileId }, { log }) => {
|
|
50
|
+
const drive = await getDriveClient();
|
|
51
|
+
log.info(`Reading file ${fileId}`);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const meta = await drive.files.get({
|
|
55
|
+
fileId,
|
|
56
|
+
fields: 'name,mimeType',
|
|
57
|
+
supportsAllDrives: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { name, mimeType } = meta.data;
|
|
61
|
+
const buffer = await downloadBuffer(drive, fileId);
|
|
62
|
+
const text = await extractText(buffer, mimeType, name);
|
|
63
|
+
|
|
64
|
+
if (text === null) {
|
|
65
|
+
throw new UserError(
|
|
66
|
+
`Unsupported file type: ${mimeType} (${name}). Only .docx and .pdf are supported.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return `# ${name}\n\n${text}`;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error instanceof UserError) throw error;
|
|
73
|
+
log.error(`Error reading file: ${error.message}`);
|
|
74
|
+
if (error.code === 404)
|
|
75
|
+
throw new UserError(`File not found: ${fileId}`);
|
|
76
|
+
if (error.code === 403)
|
|
77
|
+
throw new UserError('Permission denied. Check that the file is shared with this account.');
|
|
78
|
+
throw new UserError(`Failed to read file: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { UserError } from 'fastmcp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriveClient } from '../../clients.js';
|
|
4
|
+
import { isDocx, isPdf, downloadBuffer, extractText } from './readFile.js';
|
|
5
|
+
|
|
6
|
+
export function register(server) {
|
|
7
|
+
server.addTool({
|
|
8
|
+
name: 'searchFileContents',
|
|
9
|
+
description:
|
|
10
|
+
'Search Google Drive for files whose content or name matches a query. ' +
|
|
11
|
+
'For .docx and .pdf files, extracts and returns matching text snippets. ' +
|
|
12
|
+
'Use this to find information inside Office documents and PDFs.',
|
|
13
|
+
parameters: z.object({
|
|
14
|
+
query: z.string().describe('Text to search for'),
|
|
15
|
+
maxResults: z
|
|
16
|
+
.number()
|
|
17
|
+
.optional()
|
|
18
|
+
.default(5)
|
|
19
|
+
.describe('Max number of files to search (default 5)'),
|
|
20
|
+
folderId: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Restrict search to a specific folder ID'),
|
|
24
|
+
}),
|
|
25
|
+
execute: async ({ query, maxResults = 5, folderId }, { log }) => {
|
|
26
|
+
const drive = await getDriveClient();
|
|
27
|
+
log.info(`Searching file contents for: "${query}"`);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const escaped = query.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
31
|
+
let q = `fullText contains '${escaped}' and trashed = false`;
|
|
32
|
+
if (folderId) q += ` and '${folderId}' in parents`;
|
|
33
|
+
|
|
34
|
+
const response = await drive.files.list({
|
|
35
|
+
q,
|
|
36
|
+
pageSize: maxResults,
|
|
37
|
+
fields: 'files(id,name,mimeType)',
|
|
38
|
+
supportsAllDrives: true,
|
|
39
|
+
includeItemsFromAllDrives: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const files = response.data.files || [];
|
|
43
|
+
if (files.length === 0) return 'No files found matching that query.';
|
|
44
|
+
|
|
45
|
+
const results = [];
|
|
46
|
+
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const canRead = isDocx(file.mimeType, file.name) || isPdf(file.mimeType, file.name);
|
|
49
|
+
|
|
50
|
+
if (canRead) {
|
|
51
|
+
try {
|
|
52
|
+
const buffer = await downloadBuffer(drive, file.id);
|
|
53
|
+
const text = await extractText(buffer, file.mimeType, file.name);
|
|
54
|
+
|
|
55
|
+
const lowerText = text.toLowerCase();
|
|
56
|
+
const lowerQuery = query.toLowerCase();
|
|
57
|
+
const idx = lowerText.indexOf(lowerQuery);
|
|
58
|
+
const snippet =
|
|
59
|
+
idx !== -1
|
|
60
|
+
? '...' + text.slice(Math.max(0, idx - 150), idx + 300).trim() + '...'
|
|
61
|
+
: text.slice(0, 400).trim() + '...';
|
|
62
|
+
|
|
63
|
+
results.push(`**${file.name}** (ID: \`${file.id}\`)\n\n${snippet}`);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
results.push(`**${file.name}** (ID: \`${file.id}\`)\n\n[Could not read: ${e.message}]`);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
results.push(`**${file.name}** (ID: \`${file.id}\`, type: ${file.mimeType})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return results.join('\n\n---\n\n');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
log.error(`Error searching files: ${error.message}`);
|
|
75
|
+
if (error.code === 403)
|
|
76
|
+
throw new UserError('Permission denied. Check Drive access.');
|
|
77
|
+
throw new UserError(`Failed to search files: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Gmail Draft tools
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { UserError } from 'fastmcp';
|
|
4
|
+
import { getGmailClient } from '../../clients.js';
|
|
5
|
+
import { processMessagePart, constructRawMessage, constructRawMessageWithAttachments } from '../../helpers.js';
|
|
6
|
+
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.addTool({
|
|
9
|
+
name: 'create_draft',
|
|
10
|
+
description: 'Create a draft email in Gmail. Note the mechanics of the raw parameter.',
|
|
11
|
+
parameters: z.object({
|
|
12
|
+
raw: z.string().optional().describe("The entire email message in base64url encoded RFC 2822 format, ignores params.to, cc, bcc, subject, body, includeBodyHtml if provided"),
|
|
13
|
+
threadId: z.string().optional().describe("The thread ID to associate this draft with"),
|
|
14
|
+
to: z.array(z.string()).optional().describe("List of recipient email addresses"),
|
|
15
|
+
cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
|
|
16
|
+
bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
|
|
17
|
+
subject: z.string().optional().describe("The subject of the email"),
|
|
18
|
+
body: z.string().optional().describe("The body of the email"),
|
|
19
|
+
attachments: z.array(z.object({
|
|
20
|
+
filename: z.string().describe("Attachment file name"),
|
|
21
|
+
mimeType: z.string().describe("MIME type of the attachment"),
|
|
22
|
+
base64Data: z.string().describe("Base64 encoded attachment data"),
|
|
23
|
+
})).optional().describe("File attachments to include"),
|
|
24
|
+
includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return for each body"),
|
|
25
|
+
}),
|
|
26
|
+
execute: async (params, { log }) => {
|
|
27
|
+
const gmail = await getGmailClient();
|
|
28
|
+
let raw = params.raw;
|
|
29
|
+
if (!raw) {
|
|
30
|
+
if (params.attachments?.length) {
|
|
31
|
+
raw = await constructRawMessageWithAttachments(gmail, params);
|
|
32
|
+
} else {
|
|
33
|
+
raw = await constructRawMessage(gmail, params);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const createParams = { userId: 'me', requestBody: { message: { raw } } };
|
|
37
|
+
if (params.threadId && createParams.requestBody?.message) {
|
|
38
|
+
createParams.requestBody.message.threadId = params.threadId;
|
|
39
|
+
}
|
|
40
|
+
const { data } = await gmail.users.drafts.create(createParams);
|
|
41
|
+
if (data.message?.payload) {
|
|
42
|
+
data.message.payload = processMessagePart(data.message.payload, params.includeBodyHtml);
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify(data);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
server.addTool({
|
|
49
|
+
name: 'update_draft',
|
|
50
|
+
description: 'Update an existing draft\'s content. Replaces the draft message with new content.',
|
|
51
|
+
parameters: z.object({
|
|
52
|
+
id: z.string().describe("The ID of the draft to update"),
|
|
53
|
+
raw: z.string().optional().describe("The entire email message in base64url encoded RFC 2822 format, ignores other params if provided"),
|
|
54
|
+
threadId: z.string().optional().describe("The thread ID to associate this draft with"),
|
|
55
|
+
to: z.array(z.string()).optional().describe("List of recipient email addresses"),
|
|
56
|
+
cc: z.array(z.string()).optional().describe("List of CC recipient email addresses"),
|
|
57
|
+
bcc: z.array(z.string()).optional().describe("List of BCC recipient email addresses"),
|
|
58
|
+
subject: z.string().optional().describe("The subject of the email"),
|
|
59
|
+
body: z.string().optional().describe("The body of the email"),
|
|
60
|
+
attachments: z.array(z.object({
|
|
61
|
+
filename: z.string().describe("Attachment file name"),
|
|
62
|
+
mimeType: z.string().describe("MIME type of the attachment"),
|
|
63
|
+
base64Data: z.string().describe("Base64 encoded attachment data"),
|
|
64
|
+
})).optional().describe("File attachments to include"),
|
|
65
|
+
includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return for each body"),
|
|
66
|
+
}),
|
|
67
|
+
execute: async (params) => {
|
|
68
|
+
const gmail = await getGmailClient();
|
|
69
|
+
let raw = params.raw;
|
|
70
|
+
if (!raw) {
|
|
71
|
+
if (params.attachments?.length) {
|
|
72
|
+
raw = await constructRawMessageWithAttachments(gmail, params);
|
|
73
|
+
} else {
|
|
74
|
+
raw = await constructRawMessage(gmail, params);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const updateParams = { userId: 'me', id: params.id, requestBody: { message: { raw } } };
|
|
78
|
+
if (params.threadId && updateParams.requestBody?.message) {
|
|
79
|
+
updateParams.requestBody.message.threadId = params.threadId;
|
|
80
|
+
}
|
|
81
|
+
const { data } = await gmail.users.drafts.update(updateParams);
|
|
82
|
+
if (data.message?.payload) {
|
|
83
|
+
data.message.payload = processMessagePart(data.message.payload, params.includeBodyHtml);
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify(data);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
server.addTool({
|
|
90
|
+
name: 'delete_draft',
|
|
91
|
+
description: 'Delete a draft',
|
|
92
|
+
parameters: z.object({
|
|
93
|
+
id: z.string().describe("The ID of the draft to delete"),
|
|
94
|
+
}),
|
|
95
|
+
execute: async (params) => {
|
|
96
|
+
const gmail = await getGmailClient();
|
|
97
|
+
const { data } = await gmail.users.drafts.delete({ userId: 'me', id: params.id });
|
|
98
|
+
return JSON.stringify(data || { success: true });
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
server.addTool({
|
|
103
|
+
name: 'get_draft',
|
|
104
|
+
description: 'Get a specific draft by ID',
|
|
105
|
+
parameters: z.object({
|
|
106
|
+
id: z.string().describe("The ID of the draft to retrieve"),
|
|
107
|
+
includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return for each body"),
|
|
108
|
+
}),
|
|
109
|
+
execute: async (params) => {
|
|
110
|
+
const gmail = await getGmailClient();
|
|
111
|
+
const { data } = await gmail.users.drafts.get({ userId: 'me', id: params.id, format: 'full' });
|
|
112
|
+
if (data.message?.payload) {
|
|
113
|
+
data.message.payload = processMessagePart(data.message.payload, params.includeBodyHtml);
|
|
114
|
+
}
|
|
115
|
+
return JSON.stringify(data);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
server.addTool({
|
|
120
|
+
name: 'list_drafts',
|
|
121
|
+
description: 'List drafts in the user\'s mailbox',
|
|
122
|
+
parameters: z.object({
|
|
123
|
+
maxResults: z.number().optional().describe("Maximum number of drafts to return (1-500)"),
|
|
124
|
+
q: z.string().optional().describe("Only return drafts matching the specified query"),
|
|
125
|
+
includeSpamTrash: z.boolean().optional().describe("Include drafts from SPAM and TRASH"),
|
|
126
|
+
includeBodyHtml: z.boolean().optional().describe("Whether to include the parsed HTML in the return for each body"),
|
|
127
|
+
}),
|
|
128
|
+
execute: async (params) => {
|
|
129
|
+
const gmail = await getGmailClient();
|
|
130
|
+
let drafts = [];
|
|
131
|
+
const { data } = await gmail.users.drafts.list({ userId: 'me', ...params });
|
|
132
|
+
drafts.push(...(data.drafts || []));
|
|
133
|
+
let pageToken = data.nextPageToken;
|
|
134
|
+
while (pageToken) {
|
|
135
|
+
const { data: nextData } = await gmail.users.drafts.list({ userId: 'me', ...params, pageToken });
|
|
136
|
+
drafts.push(...(nextData.drafts || []));
|
|
137
|
+
pageToken = nextData.nextPageToken;
|
|
138
|
+
}
|
|
139
|
+
drafts = drafts.map(draft => {
|
|
140
|
+
if (draft.message?.payload) {
|
|
141
|
+
draft.message.payload = processMessagePart(draft.message.payload, params.includeBodyHtml);
|
|
142
|
+
}
|
|
143
|
+
return draft;
|
|
144
|
+
});
|
|
145
|
+
return JSON.stringify(drafts);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
server.addTool({
|
|
150
|
+
name: 'send_draft',
|
|
151
|
+
description: 'Send an existing draft',
|
|
152
|
+
parameters: z.object({
|
|
153
|
+
id: z.string().describe("The ID of the draft to send"),
|
|
154
|
+
}),
|
|
155
|
+
execute: async (params) => {
|
|
156
|
+
const gmail = await getGmailClient();
|
|
157
|
+
try {
|
|
158
|
+
const { data } = await gmail.users.drafts.send({ userId: 'me', requestBody: { id: params.id } });
|
|
159
|
+
return JSON.stringify(data);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new UserError('Error sending draft, are you sure you have at least one recipient?');
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Gmail Label tools
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getGmailClient } from '../../clients.js';
|
|
4
|
+
|
|
5
|
+
export function register(server) {
|
|
6
|
+
server.addTool({
|
|
7
|
+
name: 'create_label',
|
|
8
|
+
description: 'Create a new label',
|
|
9
|
+
parameters: z.object({
|
|
10
|
+
name: z.string().describe("The display name of the label"),
|
|
11
|
+
messageListVisibility: z.enum(['show', 'hide']).optional().describe("Visibility of messages with this label in the message list"),
|
|
12
|
+
labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"),
|
|
13
|
+
color: z.object({
|
|
14
|
+
textColor: z.string().describe("The text color as hex string"),
|
|
15
|
+
backgroundColor: z.string().describe("The background color as hex string"),
|
|
16
|
+
}).optional().describe("The color settings for the label"),
|
|
17
|
+
}),
|
|
18
|
+
execute: async (params) => {
|
|
19
|
+
const gmail = await getGmailClient();
|
|
20
|
+
const { data } = await gmail.users.labels.create({ userId: 'me', requestBody: params });
|
|
21
|
+
return JSON.stringify(data);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
server.addTool({
|
|
26
|
+
name: 'delete_label',
|
|
27
|
+
description: 'Delete a label',
|
|
28
|
+
parameters: z.object({
|
|
29
|
+
id: z.string().describe("The ID of the label to delete"),
|
|
30
|
+
}),
|
|
31
|
+
execute: async (params) => {
|
|
32
|
+
const gmail = await getGmailClient();
|
|
33
|
+
const { data } = await gmail.users.labels.delete({ userId: 'me', id: params.id });
|
|
34
|
+
return JSON.stringify(data || { success: true });
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
server.addTool({
|
|
39
|
+
name: 'get_label',
|
|
40
|
+
description: 'Get a specific label by ID',
|
|
41
|
+
parameters: z.object({
|
|
42
|
+
id: z.string().describe("The ID of the label to retrieve"),
|
|
43
|
+
}),
|
|
44
|
+
execute: async (params) => {
|
|
45
|
+
const gmail = await getGmailClient();
|
|
46
|
+
const { data } = await gmail.users.labels.get({ userId: 'me', id: params.id });
|
|
47
|
+
return JSON.stringify(data);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
server.addTool({
|
|
52
|
+
name: 'list_labels',
|
|
53
|
+
description: 'List all labels in the user\'s mailbox',
|
|
54
|
+
parameters: z.object({}),
|
|
55
|
+
execute: async () => {
|
|
56
|
+
const gmail = await getGmailClient();
|
|
57
|
+
const { data } = await gmail.users.labels.list({ userId: 'me' });
|
|
58
|
+
return JSON.stringify(data);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
server.addTool({
|
|
63
|
+
name: 'patch_label',
|
|
64
|
+
description: 'Patch an existing label (partial update)',
|
|
65
|
+
parameters: z.object({
|
|
66
|
+
id: z.string().describe("The ID of the label to patch"),
|
|
67
|
+
name: z.string().optional().describe("The display name of the label"),
|
|
68
|
+
messageListVisibility: z.enum(['show', 'hide']).optional().describe("Visibility of messages with this label"),
|
|
69
|
+
labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"),
|
|
70
|
+
color: z.object({
|
|
71
|
+
textColor: z.string().describe("The text color as hex string"),
|
|
72
|
+
backgroundColor: z.string().describe("The background color as hex string"),
|
|
73
|
+
}).optional().describe("The color settings for the label"),
|
|
74
|
+
}),
|
|
75
|
+
execute: async (params) => {
|
|
76
|
+
const { id, ...labelData } = params;
|
|
77
|
+
const gmail = await getGmailClient();
|
|
78
|
+
const { data } = await gmail.users.labels.patch({ userId: 'me', id, requestBody: labelData });
|
|
79
|
+
return JSON.stringify(data);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
server.addTool({
|
|
84
|
+
name: 'update_label',
|
|
85
|
+
description: 'Update an existing label',
|
|
86
|
+
parameters: z.object({
|
|
87
|
+
id: z.string().describe("The ID of the label to update"),
|
|
88
|
+
name: z.string().optional().describe("The display name of the label"),
|
|
89
|
+
messageListVisibility: z.enum(['show', 'hide']).optional().describe("Visibility of messages with this label"),
|
|
90
|
+
labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"),
|
|
91
|
+
color: z.object({
|
|
92
|
+
textColor: z.string().describe("The text color as hex string"),
|
|
93
|
+
backgroundColor: z.string().describe("The background color as hex string"),
|
|
94
|
+
}).optional().describe("The color settings for the label"),
|
|
95
|
+
}),
|
|
96
|
+
execute: async (params) => {
|
|
97
|
+
const { id, ...labelData } = params;
|
|
98
|
+
const gmail = await getGmailClient();
|
|
99
|
+
const { data } = await gmail.users.labels.update({ userId: 'me', id, requestBody: labelData });
|
|
100
|
+
return JSON.stringify(data);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|