Skip to main content
Glama

MCP Google Workspace Server

by j3k0
gmail.ts29.7 kB
import { Tool, TextContent, ImageContent, EmbeddedResource } from '@modelcontextprotocol/sdk/types.js'; import { GAuthService } from '../services/gauth.js'; import { google } from 'googleapis'; import { USER_ID_ARG } from '../types/tool-handler.js'; import { Buffer } from 'buffer'; import fs from 'fs'; function decodeBase64Data(fileData: string): Buffer { const standardBase64Data = fileData.replace(/-/g, '+').replace(/_/g, '/'); const padding = '='.repeat((4 - standardBase64Data.length % 4) % 4); return Buffer.from(standardBase64Data + padding, 'base64'); } export class GmailTools { private gmail: ReturnType<typeof google.gmail>; constructor(private gauth: GAuthService) { this.gmail = google.gmail({ version: 'v1', auth: this.gauth.getClient() }); } // Helper methods for email content extraction private decodeBase64UrlString(base64UrlString: string): string { try { const base64String = base64UrlString.replace(/-/g, '+').replace(/_/g, '/'); const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = base64String + padding; return Buffer.from(base64, 'base64').toString('utf-8'); } catch (error) { console.error('Error decoding base64 string:', error); return '[Error decoding content]'; } } private extractEmailText(payload: any): string { // For simple text emails if (payload.mimeType === 'text/plain' && payload.body?.data) { return this.decodeBase64UrlString(payload.body.data); } // For HTML-only emails, we'll still return the HTML content if (payload.mimeType === 'text/html' && payload.body?.data) { return this.decodeBase64UrlString(payload.body.data); } // For multipart emails, look for text/plain part first, then text/html if (payload.parts && Array.isArray(payload.parts)) { // First try to find text/plain part const textPart = payload.parts.find((part: any) => part.mimeType === 'text/plain'); if (textPart && textPart.body?.data) { return this.decodeBase64UrlString(textPart.body.data); } // If no text/plain, try text/html const htmlPart = payload.parts.find((part: any) => part.mimeType === 'text/html'); if (htmlPart && htmlPart.body?.data) { return this.decodeBase64UrlString(htmlPart.body.data); } // Recursively check nested multipart structures for (const part of payload.parts) { if (part.parts) { const nestedText = this.extractEmailText(part); if (nestedText) { return nestedText; } } } } return ''; } private extractEmailHeaders(headers: any[]): Record<string, string> { const result: Record<string, string> = {}; const importantHeaders = ['from', 'to', 'cc', 'bcc', 'subject', 'date', 'reply-to']; if (headers && Array.isArray(headers)) { headers.forEach(header => { if (header.name && header.value) { const headerName = header.name.toLowerCase(); if (importantHeaders.includes(headerName)) { result[headerName] = header.value; } } }); } return result; } getTools(): Tool[] { return ([ { name: 'gmail_list_accounts', description: 'Lists all configured Google accounts that can be used with the Gmail tools. This tool does not require a user_id as it lists available accounts before selection.', inputSchema: { type: 'object', properties: {}, additionalProperties: false, required: [] } } as Tool, { name: 'gmail_query_emails', description: `Query Gmail emails based on an optional search query. Returns emails in reverse chronological order (newest first). Returns metadata such as subject and also a short summary of the content.`, inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, query: { type: 'string', description: `Gmail search query (optional). Examples: - a $string: Search email body, subject, and sender information for $string - 'is:unread' for unread emails - 'from:example@gmail.com' for emails from a specific sender - 'newer_than:2d' for emails from last 2 days - 'has:attachment' for emails with attachments If not provided, returns recent emails without filtering.` }, max_results: { type: 'integer', description: 'Maximum number of emails to retrieve (1-500)', minimum: 1, maximum: 500, default: 100 } }, required: [USER_ID_ARG] } }, { name: 'gmail_get_email', description: 'Retrieves a complete Gmail email message by its ID, including the full message body and attachment IDs.', inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, email_id: { type: 'string', description: 'The ID of the Gmail message to retrieve' } }, required: ['email_id', USER_ID_ARG] } }, { name: 'gmail_bulk_get_emails', description: 'Retrieves multiple Gmail email messages by their IDs in a single request, including the full message bodies and attachment IDs.', inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, email_ids: { type: 'array', items: { type: 'string' }, description: 'List of Gmail message IDs to retrieve' } }, required: ['email_ids', USER_ID_ARG] } }, { name: 'gmail_create_draft', description: `Creates a draft email message from scratch in Gmail with specified recipient, subject, body, and optional CC recipients. Do NOT use this tool when you want to draft or send a REPLY to an existing message. This tool does NOT include any previous message content. Use the reply_gmail_email tool with send=false instead.`, inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, to: { type: 'string', description: 'Email address of the recipient' }, subject: { type: 'string', description: 'Subject line of the email' }, body: { type: 'string', description: 'Body content of the email' }, cc: { type: 'array', items: { type: 'string' }, description: 'Optional list of email addresses to CC' } }, required: ['to', 'subject', 'body', USER_ID_ARG] } }, { name: 'gmail_delete_draft', description: 'Deletes a Gmail draft message by its ID. This action cannot be undone.', inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, draft_id: { type: 'string', description: 'The ID of the draft to delete' } }, required: ['draft_id', USER_ID_ARG] } }, { name: 'gmail_reply', description: `Creates a reply to an existing Gmail email message and either sends it or saves as draft. Use this tool if you want to draft a reply. Use the 'cc' argument if you want to perform a "reply all".`, inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, original_message_id: { type: 'string', description: 'The ID of the Gmail message to reply to' }, reply_body: { type: 'string', description: 'The body content of your reply message' }, send: { type: 'boolean', description: 'If true, sends the reply immediately. If false, saves as draft.', default: false }, cc: { type: 'array', items: { type: 'string' }, description: 'Optional list of email addresses to CC on the reply' } }, required: ['original_message_id', 'reply_body', USER_ID_ARG] } }, { name: 'gmail_get_attachment', description: 'Retrieves a Gmail attachment by its ID.', inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, message_id: { type: 'string', description: 'The ID of the Gmail message containing the attachment' }, attachment_id: { type: 'string', description: 'The ID of the attachment to retrieve' }, mime_type: { type: 'string', description: 'The MIME type of the attachment' }, filename: { type: 'string', description: 'The filename of the attachment' }, save_to_disk: { type: 'string', description: 'The fullpath to save the attachment to disk. If not provided, the attachment is returned as a resource.' } }, required: ['message_id', 'attachment_id', 'mime_type', 'filename', USER_ID_ARG] } }, { name: 'gmail_bulk_save_attachments', description: 'Saves multiple Gmail attachments to disk by their message IDs and attachment IDs in a single request.', inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, attachments: { type: 'array', items: { type: 'object', properties: { message_id: { type: 'string', description: 'ID of the Gmail message containing the attachment' }, part_id: { type: 'string', description: 'ID of the part containing the attachment' }, save_path: { type: 'string', description: 'Path where the attachment should be saved' } }, required: ['message_id', 'part_id', 'save_path'] } } }, required: ['attachments', USER_ID_ARG] } }, { name: 'gmail_archive', description: 'Archives a Gmail message by removing it from the inbox.', inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, message_id: { type: 'string', description: 'The ID of the Gmail message to archive' } }, required: ['message_id', USER_ID_ARG] } }, { name: 'gmail_bulk_archive', description: 'Archives multiple Gmail messages by removing them from the inbox.', inputSchema: { type: 'object', properties: { [USER_ID_ARG]: { type: 'string', description: 'Email address of the user' }, message_ids: { type: 'array', items: { type: 'string' }, description: 'List of Gmail message IDs to archive' } }, required: ['message_ids', USER_ID_ARG] } } ] as Tool[]).filter(tool => ( (process.env.GMAIL_ALLOW_SENDING === 'true') ? true : (tool.name !== 'gmail_reply' && tool.name !== 'gmail_create_draft'))); } async handleTool(name: string, args: Record<string, any>): Promise<Array<TextContent | ImageContent | EmbeddedResource>> { switch (name) { case 'gmail_list_accounts': return this.listAccounts(); case 'gmail_query_emails': return this.queryEmails(args); case 'gmail_get_email': return this.getEmailById(args); case 'gmail_bulk_get_emails': return this.bulkGetEmails(args); case 'gmail_create_draft': return this.createDraft(args); case 'gmail_delete_draft': return this.deleteDraft(args); case 'gmail_reply': return this.reply(args); case 'gmail_get_attachment': return this.getAttachment(args); case 'gmail_bulk_save_attachments': return this.bulkSaveAttachments(args); case 'gmail_archive': return this.archive(args); case 'gmail_bulk_archive': return this.bulkArchive(args); // Add other tool handlers here... default: throw new Error(`Unknown tool: ${name}`); } } private async listAccounts(): Promise<Array<TextContent>> { try { const accounts = await this.gauth.getAccountInfo(); const accountList = accounts.map(account => ({ email: account.email, accountType: account.accountType, extraInfo: account.extraInfo, description: account.toDescription() })); if (accountList.length === 0) { return [{ type: 'text', text: JSON.stringify({ message: 'No accounts configured. Please check your .accounts.json file.', accounts: [] }, null, 2) }]; } return [{ type: 'text', text: JSON.stringify({ message: `Found ${accountList.length} configured account(s)`, accounts: accountList }, null, 2) }]; } catch (error) { console.error('Error listing accounts:', error); return [{ type: 'text', text: JSON.stringify({ error: `Failed to list accounts: ${(error as Error).message}`, accounts: [] }, null, 2) }]; } } private async queryEmails(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } try { const response = await this.gmail.users.messages.list({ userId, q: args.query, maxResults: args.max_results || 100 }); const messages = response.data.messages || []; const emails = await Promise.all( messages.map(async (msg) => { const email = await this.gmail.users.messages.get({ userId, id: msg.id!, format: 'metadata', metadataHeaders: ['From', 'To', 'Subject', 'Date'] }); // Extract headers into a more readable format const headers: Record<string, string> = {}; email.data.payload?.headers?.forEach(header => { if (header.name && header.value) { headers[header.name.toLowerCase()] = header.value; } }); return { id: email.data.id, threadId: email.data.threadId, labelIds: email.data.labelIds, snippet: email.data.snippet, internalDate: email.data.internalDate, headers }; }) ); return [{ type: 'text', text: JSON.stringify(emails, null, 2) }]; } catch (error) { console.error('Error querying emails:', error); throw error; } } private async getEmailById(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const emailId = args.email_id; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!emailId) { throw new Error('Missing required argument: email_id'); } try { const email = await this.gmail.users.messages.get({ userId, id: emailId, format: 'full' }); // Extract headers const headers = this.extractEmailHeaders(email.data.payload?.headers || []); // Extract text content const textContent = this.extractEmailText(email.data.payload || {}); // Get attachments if any const attachments: Record<string, any> = {}; if (email.data.payload?.parts) { for (const part of email.data.payload.parts) { if (part.body?.attachmentId) { attachments[part.partId!] = { filename: part.filename, mimeType: part.mimeType, attachmentId: part.body.attachmentId }; } } } // Create simplified email object const result = { id: email.data.id, threadId: email.data.threadId, labelIds: email.data.labelIds, headers, textContent, hasAttachments: Object.keys(attachments).length > 0, attachments: Object.keys(attachments).length > 0 ? attachments : undefined }; return [{ type: 'text', text: JSON.stringify(result, null, 2) }]; } catch (error) { console.error('Error getting email:', error); throw error; } } private async bulkGetEmails(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const emailIds = args.email_ids; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!emailIds || emailIds.length === 0) { throw new Error('Missing required argument: email_ids'); } try { const emails = await Promise.all( emailIds.map(async (emailId: string) => { const email = await this.gmail.users.messages.get({ userId, id: emailId, format: 'full' }); // Extract headers const headers = this.extractEmailHeaders(email.data.payload?.headers || []); // Extract text content const textContent = this.extractEmailText(email.data.payload || {}); // Get attachments if any const attachments: Record<string, any> = {}; if (email.data.payload?.parts) { for (const part of email.data.payload.parts) { if (part.body?.attachmentId) { attachments[part.partId!] = { filename: part.filename, mimeType: part.mimeType, attachmentId: part.body.attachmentId }; } } } // Create simplified email object return { id: email.data.id, threadId: email.data.threadId, labelIds: email.data.labelIds, headers, textContent, hasAttachments: Object.keys(attachments).length > 0, attachments: Object.keys(attachments).length > 0 ? attachments : undefined }; }) ); return [{ type: 'text', text: JSON.stringify(emails, null, 2) }]; } catch (error) { console.error('Error getting emails:', error); throw error; } } private async createDraft(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const to = args.to; const subject = args.subject; const body = args.body; const cc = args.cc || []; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!to) { throw new Error('Missing required argument: to'); } if (!subject) { throw new Error('Missing required argument: subject'); } if (!body) { throw new Error('Missing required argument: body'); } try { const message = { raw: Buffer.from( `To: ${to}\r\n` + `Subject: ${subject}\r\n` + `Cc: ${cc.join(', ')}\r\n` + `Content-Type: text/plain; charset="UTF-8"\r\n` + `\r\n` + `${body}` ).toString('base64url') }; const draft = await this.gmail.users.drafts.create({ userId, requestBody: { message } }); return [{ type: 'text', text: JSON.stringify(draft.data, null, 2) }]; } catch (error) { console.error('Error creating draft:', error); throw error; } } private async deleteDraft(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const draftId = args.draft_id; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!draftId) { throw new Error('Missing required argument: draft_id'); } try { await this.gmail.users.drafts.delete({ userId, id: draftId }); return [{ type: 'text', text: `Draft ${draftId} deleted successfully` }]; } catch (error) { console.error('Error deleting draft:', error); throw error; } } private async reply(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const originalMessageId = args.original_message_id; const replyBody = args.reply_body; // NEVER SEND EMAILS const send = false; // args.send || false; const cc = args.cc || []; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!originalMessageId) { throw new Error('Missing required argument: original_message_id'); } if (!replyBody) { throw new Error('Missing required argument: reply_body'); } try { // First get the original message to extract headers const originalMessage = await this.gmail.users.messages.get({ userId, id: originalMessageId }); const headers = originalMessage.data.payload?.headers?.reduce((acc: Record<string, string>, header) => { if (header.name && header.value) { acc[header.name.toLowerCase()] = header.value; } return acc; }, {}); if (!headers) { throw new Error('Could not extract headers from original message'); } // Get the threadId from the original message const threadId = originalMessage.data.threadId; if (!threadId) { throw new Error('Could not extract threadId from original message'); } const message = { raw: Buffer.from( `In-Reply-To: ${originalMessageId}\r\n` + `References: ${originalMessageId}\r\n` + `Subject: Re: ${headers.subject || ''}\r\n` + `To: ${headers.from || ''}\r\n` + `Cc: ${cc.join(', ')}\r\n` + `Content-Type: text/plain; charset="UTF-8"\r\n` + `\r\n` + `${replyBody}` ).toString('base64url'), threadId: threadId }; if (send) { await this.gmail.users.messages.send({ userId, requestBody: { raw: message.raw, threadId: message.threadId } }); return [{ type: 'text', text: 'Reply sent successfully' }]; } else { const draft = await this.gmail.users.drafts.create({ userId, requestBody: { message } }); return [{ type: 'text', text: JSON.stringify(draft.data, null, 2) }]; } } catch (error) { console.error('Error replying to email:', error); throw error; } } private async getAttachment(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const messageId = args.message_id; const attachmentId = args.attachment_id; const mimeType = args.mime_type; const filename = args.filename; const saveToDisk = args.save_to_disk; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!messageId) { throw new Error('Missing required argument: message_id'); } if (!attachmentId) { throw new Error('Missing required argument: attachment_id'); } if (!mimeType) { throw new Error('Missing required argument: mime_type'); } if (!filename) { throw new Error('Missing required argument: filename'); } try { const attachment = await this.gmail.users.messages.attachments.get({ userId, messageId, id: attachmentId }); const attachmentData = attachment.data.data; if (!attachmentData) { throw new Error('Attachment data not found'); } const decodedData = Buffer.from(attachmentData, 'base64').toString('utf-8'); const decodedContent = this.decodeBase64UrlString(decodedData); if (saveToDisk) { fs.writeFileSync(saveToDisk, decodedContent); return [{ type: 'text', text: `Attachment saved to ${saveToDisk}` }]; } else { return [{ type: 'text', text: decodedContent }]; } } catch (error) { console.error('Error getting attachment:', error); throw error; } } private async bulkSaveAttachments(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const attachments = args.attachments; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!attachments || attachments.length === 0) { throw new Error('Missing required argument: attachments'); } try { const results = await Promise.all( attachments.map(async (attachmentInfo: any) => { const messageId = attachmentInfo.message_id; const partId = attachmentInfo.part_id; const savePath = attachmentInfo.save_path; if (!messageId || !partId || !savePath) { throw new Error('Missing required arguments: message_id, part_id, or save_path'); } const attachmentData = await this.gmail.users.messages.attachments.get({ userId, messageId, id: partId }); const fileData = attachmentData.data.data; if (!fileData) { throw new Error('Attachment data not found'); } const decodedData = Buffer.from(fileData, 'base64').toString('utf-8'); const decodedContent = this.decodeBase64UrlString(decodedData); fs.writeFileSync(savePath, decodedContent); return { messageId, partId, savePath, status: 'success' }; }) ); return [{ type: 'text', text: JSON.stringify(results, null, 2) }]; } catch (error) { console.error('Error saving attachments:', error); throw error; } } private async archive(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const messageId = args.message_id; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!messageId) { throw new Error('Missing required argument: message_id'); } try { await this.gmail.users.messages.trash({ userId, id: messageId }); return [{ type: 'text', text: `Message ${messageId} archived successfully` }]; } catch (error) { console.error('Error archiving message:', error); throw error; } } private async bulkArchive(args: Record<string, any>): Promise<Array<TextContent>> { const userId = args[USER_ID_ARG]; const messageIds = args.message_ids; if (!userId) { throw new Error(`Missing required argument: ${USER_ID_ARG}`); } if (!messageIds || messageIds.length === 0) { throw new Error('Missing required argument: message_ids'); } try { const results = await Promise.all( messageIds.map(async (messageId: string) => { await this.gmail.users.messages.trash({ userId, id: messageId }); return { messageId, status: 'archived' }; }) ); return [{ type: 'text', text: JSON.stringify(results, null, 2) }]; } catch (error) { console.error('Error archiving messages:', error); throw error; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/j3k0/mcp-google-workspace'

If you have feedback or need assistance with the MCP directory API, please join our Discord server