Gmail MCP Server

import { gmail } from '../../config/auth.js'; import { gmail_v1 } from 'googleapis'; import { DraftEmailArgs, ListDraftsArgs, ReadDraftArgs, MessageResponse } from './types.js'; import { DeleteDraftArgs, UpdateDraftArgs } from '../../types/gmail.js'; function extractBody(message: gmail_v1.Schema$Message): string { if (message.payload?.body?.data) { return Buffer.from(message.payload.body.data, 'base64').toString('utf-8'); } const textPart = message.payload?.parts?.find(part => part.mimeType === 'text/plain' || part.mimeType === 'text/html' ); return textPart?.body?.data ? Buffer.from(textPart.body.data, 'base64').toString('utf-8') : ''; } function determineIfHtml(message: gmail_v1.Schema$Message): boolean { const contentType = message.payload?.headers ?.find(h => h.name?.toLowerCase() === 'content-type')?.value || ''; return contentType.includes('text/html'); } function createRawMessage(message: { to?: string; cc?: string; bcc?: string; subject?: string; body: string; isHtml?: boolean; }): string { const raw = Buffer.from( `${message.to ? `To: ${message.to}\n` : ''}` + `${message.cc ? `Cc: ${message.cc}\n` : ''}` + `${message.bcc ? `Bcc: ${message.bcc}\n` : ''}` + `${message.subject ? `Subject: ${message.subject}\n` : ''}` + `Content-Type: ${message.isHtml ? 'text/html' : 'text/plain'}; charset=utf-8\n\n` + `${message.body}` ).toString('base64'); return raw.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } export async function draftEmail({ to, cc, bcc, subject, body, isHtml }: DraftEmailArgs): Promise<MessageResponse> { try { const message = { to: to.join(','), ...(cc?.length && { cc: cc.join(',') }), ...(bcc?.length && { bcc: bcc.join(',') }), subject, ...(isHtml ? { html: body } : { text: body }) }; const encodedMessage = Buffer.from( `To: ${message.to}\n` + (message.cc ? `Cc: ${message.cc}\n` : '') + (message.bcc ? `Bcc: ${message.bcc}\n` : '') + `Subject: ${message.subject}\n` + `Content-Type: ${isHtml ? 'text/html' : 'text/plain'}; charset=utf-8\n\n` + `${body}` ).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const response = await gmail.users.drafts.create({ userId: 'me', requestBody: { message: { raw: encodedMessage } } }); return { content: [{ type: "text", text: `Draft created successfully. Draft ID: ${response.data.id}` }] }; } catch (error) { console.error('Draft email error:', error); throw error; } } export async function listDrafts({ maxResults = 10, query, verbose = false }: ListDraftsArgs): Promise<MessageResponse> { try { const response = await gmail.users.drafts.list({ userId: 'me', maxResults, ...(query && { q: query }) }); const drafts = response.data.drafts || []; return { content: [{ type: "text", text: drafts.map((draft, i: number) => `${i + 1}. Draft ID: ${draft.id}` ).join('\n') }] }; } catch (error) { console.error('List drafts error:', error); throw error; } } export async function readDraft({ draftId }: ReadDraftArgs): Promise<MessageResponse> { try { const response = await gmail.users.drafts.get({ userId: 'me', id: draftId, format: 'full' }); const draft = response.data; const message = draft.message; if (!message || !message.payload) { throw new Error('Draft message or payload not found'); } // Parse headers with proper type safety const headers = message.payload.headers || []; const subject = headers.find(h => h.name?.toLowerCase() === 'subject')?.value || '(no subject)'; const to = headers.find(h => h.name?.toLowerCase() === 'to')?.value || ''; const cc = headers.find(h => h.name?.toLowerCase() === 'cc')?.value || ''; const bcc = headers.find(h => h.name?.toLowerCase() === 'bcc')?.value || ''; // Get body content let body = ''; if (message.payload.body?.data) { body = Buffer.from(message.payload.body.data, 'base64').toString('utf-8'); } else if (message.payload.parts) { const textPart = message.payload.parts.find(part => part.mimeType === 'text/plain' || part.mimeType === 'text/html' ); if (textPart?.body?.data) { body = Buffer.from(textPart.body.data, 'base64').toString('utf-8'); } } return { content: [{ type: "text", text: `Draft ID: ${draft.id}\nSubject: ${subject}\nTo: ${to}\nCC: ${cc}\nBCC: ${bcc}\n\nBody:\n${body}` }] }; } catch (error) { console.error('Read draft error:', error); throw error; } } export async function updateDraft({ draftId, to, cc, bcc, subject, body, isHtml }: UpdateDraftArgs): Promise<MessageResponse> { try { // 1. Fetch existing draft const existingDraft = await gmail.users.drafts.get({ userId: 'me', id: draftId, format: 'full' }); if (!existingDraft.data.message || !existingDraft.data.message.payload) { throw new Error('Draft message or payload not found'); } const currentMessage = existingDraft.data.message; const headers = currentMessage.payload?.headers || []; // 2. Merge updates with existing content const updatedMessage = { to: to?.join(',') || headers.find(h => h.name?.toLowerCase() === 'to')?.value || undefined, cc: cc?.join(',') || headers.find(h => h.name?.toLowerCase() === 'cc')?.value || undefined, bcc: bcc?.join(',') || headers.find(h => h.name?.toLowerCase() === 'bcc')?.value || undefined, subject: subject || headers.find(h => h.name?.toLowerCase() === 'subject')?.value || undefined, body: body || extractBody(currentMessage) || '', isHtml: isHtml ?? determineIfHtml(currentMessage) }; // 3. Create new raw message const raw = createRawMessage(updatedMessage); // 4. Update draft await gmail.users.drafts.update({ userId: 'me', id: draftId, requestBody: { message: { raw } } }); return { content: [{ type: "text", text: `Draft ${draftId} updated successfully` }] }; } catch (error) { console.error('Update draft error:', error); throw error; } } export async function deleteDraft({ draftId }: DeleteDraftArgs): Promise<MessageResponse> { try { await gmail.users.drafts.delete({ userId: 'me', id: draftId }); return { content: [{ type: "text", text: `Draft ${draftId} deleted successfully` }] }; } catch (error) { console.error('Delete draft error:', error); throw error; } }