UNPKG

gmail-mcp-cli

Version:

Deploy Gmail MCP Server with 17 AI-powered email tools for Claude Desktop

1,334 lines (1,143 loc) โ€ข 81.9 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { google } from 'googleapis'; import { OAuth2Client } from 'google-auth-library'; import { authenticate } from '@google-cloud/local-auth'; import OpenAI from 'openai'; import * as fs from 'fs/promises'; import * as path from 'path'; import { z } from 'zod'; import * as dotenv from 'dotenv'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Load environment variables dotenv.config({ path: path.join(__dirname, '..', '.env') }); // Initialize OpenAI const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, }); // Check if API key is available if (!process.env.OPENAI_API_KEY) { console.error('Error: OPENAI_API_KEY environment variable is not set'); console.error('Please create a .env file in the project root with OPENAI_API_KEY=your-api-key'); process.exit(1); } // Gmail auth setup with full Gmail access for v3.0.0 const GMAIL_SCOPES = [ 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.modify', 'https://www.googleapis.com/auth/gmail.compose', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.labels', 'https://www.googleapis.com/auth/gmail.settings.basic', 'https://www.googleapis.com/auth/gmail.settings.sharing' ]; const GMAIL_TOKEN_PATH = path.join(__dirname, '..', 'token.json'); const GMAIL_CREDENTIALS_PATH = path.join(__dirname, '..', 'credentials.json'); // Gmail authentication functions async function loadSavedCredentialsIfExist() { try { const content = await fs.readFile(GMAIL_TOKEN_PATH, 'utf-8'); const credentials = JSON.parse(content); const { client_secret, client_id, refresh_token } = credentials; const client = new OAuth2Client(client_id, client_secret); client.setCredentials({ refresh_token }); return client; } catch (err) { return null; } } async function saveCredentials(client) { const content = await fs.readFile(GMAIL_CREDENTIALS_PATH, 'utf-8'); const keys = JSON.parse(content); const key = keys.installed || keys.web; const payload = JSON.stringify({ type: 'authorized_user', client_id: key.client_id, client_secret: key.client_secret, refresh_token: client.credentials?.refresh_token, }); await fs.writeFile(GMAIL_TOKEN_PATH, payload); } async function authorizeGmail() { let client = await loadSavedCredentialsIfExist(); if (client) { return client; } const newClient = await authenticate({ scopes: GMAIL_SCOPES, keyfilePath: GMAIL_CREDENTIALS_PATH, }); if (newClient.credentials) { await saveCredentials(newClient); } return newClient; } async function getGmailService() { const auth = await authorizeGmail(); return google.gmail({ version: 'v1', auth }); } // Enhanced email parsing utilities function extractEmailBody(payload) { if (payload.body?.data) { return Buffer.from(payload.body.data, 'base64').toString('utf-8'); } if (payload.parts) { for (const part of payload.parts) { if (part.mimeType === 'text/plain' || part.mimeType === 'text/html') { const text = extractEmailBody(part); if (text) return text; } } } return ''; } function parseEmailMetadata(email) { const headers = email.payload?.headers || []; const getHeader = (name) => headers.find((h) => h.name === name)?.value || ''; // Parse attachments with attachment IDs const attachments = []; function extractAttachments(payload) { if (payload.filename && payload.body?.size > 0) { attachments.push({ filename: payload.filename, mimeType: payload.mimeType, size: payload.body.size, attachmentId: payload.body.attachmentId }); } if (payload.parts) { payload.parts.forEach(extractAttachments); } } if (email.payload) { extractAttachments(email.payload); } // Determine category based on labels const labels = email.labelIds || []; let category = 'primary'; if (labels.includes('CATEGORY_PROMOTIONS')) category = 'promotions'; else if (labels.includes('CATEGORY_SOCIAL')) category = 'social'; else if (labels.includes('CATEGORY_UPDATES')) category = 'updates'; // Parse date for proper sorting const dateStr = getHeader('Date'); const dateTimestamp = dateStr ? new Date(dateStr).getTime() : 0; return { id: email.id, threadId: email.threadId, subject: getHeader('Subject') || 'No Subject', from: getHeader('From'), to: getHeader('To'), cc: getHeader('Cc'), bcc: getHeader('Bcc'), date: dateStr, dateTimestamp, body: extractEmailBody(email.payload || {}), snippet: email.snippet || '', isRead: !labels.includes('UNREAD'), isImportant: labels.includes('IMPORTANT'), isStarred: labels.includes('STARRED'), labels, category, attachments, internalDate: email.internalDate || '0', messageId: getHeader('Message-ID'), inReplyTo: getHeader('In-Reply-To'), references: getHeader('References') }; } // Tool schemas for complete Gmail functionality (existing + new JSON-RPC endpoints) const GetEmailsSchema = z.object({ count: z.number().min(1).max(100).default(10), query: z.string().optional(), category: z.enum(['primary', 'promotions', 'social', 'updates', 'all']).default('primary'), includeBody: z.boolean().default(false), orderBy: z.enum(['date_desc', 'date_asc', 'relevance']).default('date_desc') }); const AnalyzeEmailsSchema = z.object({ count: z.number().min(1).max(50).default(10), query: z.string().optional(), category: z.enum(['primary', 'promotions', 'social', 'updates', 'all']).default('primary'), analysisType: z.enum(['summary', 'priority', 'sentiment', 'comprehensive']).default('comprehensive') }); const SummarizeThreadSchema = z.object({ threadId: z.string().describe("Thread ID to summarize"), summaryType: z.enum(['brief', 'detailed', 'action_items']).default('detailed').describe("Type of summary") }); const ListActionItemsSchema = z.object({ folder: z.enum(['inbox', 'sent', 'drafts', 'all']).default('inbox').describe("Folder to analyze"), account: z.string().optional().describe("Account filter"), timeframe: z.enum(['today', 'week', 'month', 'all']).default('week').describe("Timeframe for action items"), priority: z.enum(['high', 'medium', 'low', 'all']).default('all').describe("Priority filter") }); const GenerateDraftSchema = z.object({ prompt: z.string().describe("AI prompt for draft content"), replyToID: z.string().optional().describe("Email ID to reply to (optional)"), tone: z.enum(['professional', 'friendly', 'formal', 'casual']).default('professional').describe("Email tone"), length: z.enum(['brief', 'medium', 'detailed']).default('medium').describe("Draft length") }); const SendNudgeSchema = z.object({ emailId: z.string().describe("Email ID to nudge about"), nudgeType: z.enum(['follow_up', 'deadline_reminder', 'meeting_reminder', 'response_needed']).describe("Type of nudge"), delay: z.string().default('3 days').describe("When to send nudge (e.g., '3 days', '1 week')"), message: z.string().optional().describe("Custom nudge message") }); const ExtractAttachmentsSummarySchema = z.object({ emailId: z.string().describe("Email ID to analyze attachments"), includeContent: z.boolean().default(false).describe("Whether to analyze attachment content"), summaryDepth: z.enum(['basic', 'detailed', 'comprehensive']).default('detailed').describe("Level of analysis") }); const ComposeEmailSchema = z.object({ to: z.string().describe("Recipient email address(es)"), subject: z.string().describe("Email subject"), body: z.string().describe("Email body content"), cc: z.string().optional().describe("CC recipients"), bcc: z.string().optional().describe("BCC recipients"), replyTo: z.string().optional().describe("Reply-to address"), isHtml: z.boolean().default(false).describe("Whether body is HTML"), send: z.boolean().default(false).describe("Send immediately or save as draft") }); const ReplyEmailSchema = z.object({ emailId: z.string().describe("Email ID to reply to"), body: z.string().describe("Reply body content"), replyAll: z.boolean().default(false).describe("Reply to all recipients"), isHtml: z.boolean().default(false).describe("Whether body is HTML"), send: z.boolean().default(false).describe("Send immediately or save as draft") }); const ManageSubscriptionsSchema = z.object({ action: z.enum(['list', 'unsubscribe', 'block_sender']).describe("Action to perform"), sender: z.string().optional().describe("Sender email (required for unsubscribe/block)"), category: z.enum(['all', 'promotions', 'social', 'updates']).default('all').describe("Category to analyze") }); const ManageLabelsSchema = z.object({ action: z.enum(['list', 'create', 'delete', 'update']).describe("Action to perform"), name: z.string().optional().describe("Label name"), newName: z.string().optional().describe("New label name (for update)"), color: z.string().optional().describe("Label color"), visibility: z.enum(['show', 'hide', 'show_if_unread']).optional().describe("Label visibility") }); const GetThreadSchema = z.object({ threadId: z.string().describe("Thread ID to retrieve"), includeBody: z.boolean().default(false).describe("Include full email bodies") }); // Create server const server = new Server( { name: 'complete-gmail-mcp-server', version: '3.1.0', }, { capabilities: { tools: {}, }, } ); // Register all 17 comprehensive tools (including JSON-RPC endpoints) server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // EXISTING CORE TOOLS (Enhanced from v2.1.0) { name: 'get_emails', description: '๐Ÿ“ง Get emails from Gmail with comprehensive metadata and category filtering (Primary tab by default)', inputSchema: { type: 'object', properties: { count: { type: 'number', default: 10, minimum: 1, maximum: 100 }, category: { type: 'string', enum: ['primary', 'promotions', 'social', 'updates', 'all'], default: 'primary' }, query: { type: 'string' }, includeBody: { type: 'boolean', default: false }, orderBy: { type: 'string', enum: ['date_desc', 'date_asc', 'relevance'], default: 'date_desc' } }, required: [] } }, { name: 'search_emails', description: '๐Ÿ” Advanced Gmail search with full search syntax support (searchEmails JSON-RPC endpoint)', inputSchema: { type: 'object', properties: { query: { type: 'string' }, maxResults: { type: 'number', default: 20, minimum: 1, maximum: 100 } }, required: ['query'] } }, { name: 'manage_email', description: '๐Ÿ›  Manage emails including applyLabel functionality (applyLabel JSON-RPC endpoint)', inputSchema: { type: 'object', properties: { emailId: { type: 'string' }, action: { type: 'string', enum: ['mark_read', 'mark_unread', 'add_label', 'remove_label', 'archive', 'delete', 'star', 'unstar'] }, labelName: { type: 'string' } }, required: ['emailId', 'action'] } }, { name: 'analyze_emails', description: '๐Ÿค– AI-powered analysis of emails with priorities and insights (Primary tab by default)', inputSchema: { type: 'object', properties: { count: { type: 'number', default: 10, minimum: 1, maximum: 50 }, category: { type: 'string', enum: ['primary', 'promotions', 'social', 'updates', 'all'], default: 'primary' }, analysisType: { type: 'string', enum: ['summary', 'priority', 'sentiment', 'comprehensive'], default: 'comprehensive' } }, required: [] } }, { name: 'get_email_details', description: '๐Ÿ”ฌ Get detailed information about a specific email', inputSchema: { type: 'object', properties: { emailId: { type: 'string' }, includeRaw: { type: 'boolean', default: false } }, required: ['emailId'] } }, { name: 'get_gmail_stats', description: '๐Ÿ“Š Get comprehensive Gmail statistics (inbox counts, categories, etc.)', inputSchema: { type: 'object', properties: { detailed: { type: 'boolean', default: false } }, required: [] } }, { name: 'get_special_emails', description: '๐Ÿ“ Get special emails (drafts, sent, snoozed, starred, important, trash, spam)', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['drafts', 'sent', 'snoozed', 'starred', 'important', 'trash', 'spam'] }, count: { type: 'number', default: 10, minimum: 1, maximum: 50 } }, required: ['type'] } }, // v3.0.0: EMAIL COMPOSITION & COMMUNICATION { name: 'compose_email', description: 'โœ๏ธ Compose new emails (send immediately or save as draft)', inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'Recipient email address(es)' }, subject: { type: 'string', description: 'Email subject' }, body: { type: 'string', description: 'Email body content' }, cc: { type: 'string', description: 'CC recipients' }, bcc: { type: 'string', description: 'BCC recipients' }, isHtml: { type: 'boolean', default: false, description: 'Whether body is HTML' }, send: { type: 'boolean', default: false, description: 'Send immediately or save as draft' } }, required: ['to', 'subject', 'body'] } }, { name: 'reply_email', description: 'โ†ฉ๏ธ Reply to emails (reply or reply-all)', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'Email ID to reply to' }, body: { type: 'string', description: 'Reply body content' }, replyAll: { type: 'boolean', default: false, description: 'Reply to all recipients' }, isHtml: { type: 'boolean', default: false }, send: { type: 'boolean', default: false, description: 'Send immediately or save as draft' } }, required: ['emailId', 'body'] } }, // v3.0.0: SUBSCRIPTION MANAGEMENT { name: 'manage_subscriptions', description: '๐Ÿ“ง Manage email subscriptions (list, unsubscribe, block senders) - SOLVES YOUR UNSUBSCRIBE NEEDS!', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'unsubscribe', 'block_sender'], description: 'list: show all subscriptions, unsubscribe: find unsubscribe links, block_sender: auto-delete future emails' }, sender: { type: 'string', description: 'Sender email address (required for unsubscribe/block actions)' }, category: { type: 'string', enum: ['all', 'promotions', 'social', 'updates'], default: 'all', description: 'Gmail category to analyze for subscriptions' } }, required: ['action'] } }, // v3.0.0: LABEL MANAGEMENT { name: 'manage_labels', description: '๐Ÿท๏ธ Manage Gmail labels (create, delete, update, list)', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'create', 'delete', 'update'], description: 'Action to perform on labels' }, name: { type: 'string', description: 'Label name (required for create/delete/update)' }, newName: { type: 'string', description: 'New label name (for update action)' }, visibility: { type: 'string', enum: ['show', 'hide', 'show_if_unread'], description: 'Label visibility setting' } }, required: ['action'] } }, // v3.0.0: THREAD MANAGEMENT { name: 'get_thread', description: '๐Ÿงต Get email thread/conversation with all messages', inputSchema: { type: 'object', properties: { threadId: { type: 'string', description: 'Thread ID to retrieve' }, includeBody: { type: 'boolean', default: false, description: 'Include full email bodies in thread' } }, required: ['threadId'] } }, // NEW v3.1.0: JSON-RPC ENDPOINTS { name: 'summarize_thread', description: '๐Ÿ“ AI-powered thread summarization (summarizeThread JSON-RPC endpoint)', inputSchema: { type: 'object', properties: { threadId: { type: 'string', description: 'Thread ID to summarize' }, summaryType: { type: 'string', enum: ['brief', 'detailed', 'action_items'], default: 'detailed', description: 'Type of summary to generate' } }, required: ['threadId'] } }, { name: 'list_action_items', description: '๐Ÿ“‹ Extract action items from emails by folder/account (listActionItems JSON-RPC endpoint)', inputSchema: { type: 'object', properties: { folder: { type: 'string', enum: ['inbox', 'sent', 'drafts', 'all'], default: 'inbox', description: 'Folder to analyze' }, account: { type: 'string', description: 'Account filter' }, timeframe: { type: 'string', enum: ['today', 'week', 'month', 'all'], default: 'week', description: 'Timeframe for analysis' }, priority: { type: 'string', enum: ['high', 'medium', 'low', 'all'], default: 'all', description: 'Priority filter' } }, required: [] } }, { name: 'generate_draft', description: '๐Ÿค– AI-powered draft generation from prompts (generateDraft JSON-RPC endpoint)', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'AI prompt for draft content' }, replyToID: { type: 'string', description: 'Email ID to reply to (optional)' }, tone: { type: 'string', enum: ['professional', 'friendly', 'formal', 'casual'], default: 'professional', description: 'Email tone' }, length: { type: 'string', enum: ['brief', 'medium', 'detailed'], default: 'medium', description: 'Draft length' } }, required: ['prompt'] } }, { name: 'send_nudge', description: 'โฐ Send follow-up nudges and reminders (sendNudge JSON-RPC endpoint)', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'Email ID to nudge about' }, nudgeType: { type: 'string', enum: ['follow_up', 'deadline_reminder', 'meeting_reminder', 'response_needed'], description: 'Type of nudge to send' }, delay: { type: 'string', default: '3 days', description: 'When to send nudge (e.g., "3 days", "1 week")' }, message: { type: 'string', description: 'Custom nudge message' } }, required: ['emailId', 'nudgeType'] } }, { name: 'extract_attachments_summary', description: '๐Ÿ“Ž AI-powered attachment analysis and summarization (extractAttachmentsSummary JSON-RPC endpoint)', inputSchema: { type: 'object', properties: { emailId: { type: 'string', description: 'Email ID to analyze attachments' }, includeContent: { type: 'boolean', default: false, description: 'Whether to analyze attachment content' }, summaryDepth: { type: 'string', enum: ['basic', 'detailed', 'comprehensive'], default: 'detailed', description: 'Level of analysis depth' } }, required: ['emailId'] } } ], }; }); // Handle tool calls with routing to appropriate handlers server.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; try { switch (toolName) { // Existing tools (keeping v3.0.0 implementations) case 'get_emails': return await handleGetEmails(request.params.arguments); case 'analyze_emails': return await handleAnalyzeEmails(request.params.arguments); case 'search_emails': return await handleSearchEmails(request.params.arguments); case 'get_email_details': return await handleGetEmailDetails(request.params.arguments); case 'manage_email': return await handleManageEmail(request.params.arguments); case 'get_gmail_stats': return await handleGetGmailStats(request.params.arguments); case 'get_special_emails': return await handleGetSpecialEmails(request.params.arguments); // v3.0.0 EMAIL COMPOSITION TOOLS case 'compose_email': return await handleComposeEmail(request.params.arguments); case 'reply_email': return await handleReplyEmail(request.params.arguments); // v3.0.0 SUBSCRIPTION MANAGEMENT (Your main request!) case 'manage_subscriptions': return await handleManageSubscriptions(request.params.arguments); // v3.0.0 LABEL MANAGEMENT case 'manage_labels': return await handleManageLabels(request.params.arguments); // v3.0.0 THREAD MANAGEMENT case 'get_thread': return await handleGetThread(request.params.arguments); // NEW v3.1.0: JSON-RPC ENDPOINTS case 'summarize_thread': return await handleSummarizeThread(request.params.arguments); case 'list_action_items': return await handleListActionItems(request.params.arguments); case 'generate_draft': return await handleGenerateDraft(request.params.arguments); case 'send_nudge': return await handleSendNudge(request.params.arguments); case 'extract_attachments_summary': return await handleExtractAttachmentsSummary(request.params.arguments); default: throw new Error(`Unknown tool: ${toolName}`); } } catch (error) { console.error(`Error in ${toolName}:`, error); return { content: [ { type: 'text', text: `Error executing ${toolName}: ${error.message}`, }, ], }; } }); // IMPLEMENTATION OF NEW v3.0.0 TOOLS async function handleComposeEmail(args) { const params = ComposeEmailSchema.parse(args); const gmail = await getGmailService(); // Build email message const messageParts = []; messageParts.push(`To: ${params.to}`); if (params.cc) messageParts.push(`Cc: ${params.cc}`); if (params.bcc) messageParts.push(`Bcc: ${params.bcc}`); if (params.replyTo) messageParts.push(`Reply-To: ${params.replyTo}`); messageParts.push(`Subject: ${params.subject}`); messageParts.push(`Content-Type: ${params.isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`); messageParts.push(''); messageParts.push(params.body); const message = messageParts.join('\n'); const encodedMessage = Buffer.from(message).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); if (params.send) { // Send email immediately const response = await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encodedMessage } }); return { content: [ { type: 'text', text: `โœ… **Email Sent Successfully!** ๐Ÿ“ง **To:** ${params.to} ๐Ÿ“ **Subject:** ${params.subject} ${params.cc ? `๐Ÿ“‹ **CC:** ${params.cc}\n` : ''}${params.bcc ? `๐Ÿ“‹ **BCC:** ${params.bcc}\n` : ''}๐Ÿ†” **Message ID:** ${response.data.id} Your email has been delivered!`, }, ], }; } else { // Save as draft const response = await gmail.users.drafts.create({ userId: 'me', requestBody: { message: { raw: encodedMessage } } }); return { content: [ { type: 'text', text: `๐Ÿ’พ **Email Saved as Draft!** ๐Ÿ“ง **To:** ${params.to} ๐Ÿ“ **Subject:** ${params.subject} ๐Ÿ†” **Draft ID:** ${response.data.id} You can send this later from your drafts folder.`, }, ], }; } } async function handleReplyEmail(args) { const params = ReplyEmailSchema.parse(args); const gmail = await getGmailService(); // Get original email for reply context const originalEmail = await gmail.users.messages.get({ userId: 'me', id: params.emailId, format: 'full' }); const headers = originalEmail.data.payload?.headers || []; const getHeader = (name) => headers.find((h) => h.name === name)?.value || ''; const originalFrom = getHeader('From'); const originalTo = getHeader('To'); const originalCc = getHeader('Cc'); const originalSubject = getHeader('Subject'); const messageId = getHeader('Message-ID'); // Build reply recipients let replyTo = originalFrom; let replyCc = ''; if (params.replyAll) { const allRecipients = [originalTo, originalCc].filter(Boolean).join(', '); replyCc = allRecipients; } // Build reply message const messageParts = []; messageParts.push(`To: ${replyTo}`); if (replyCc && params.replyAll) messageParts.push(`Cc: ${replyCc}`); messageParts.push(`Subject: Re: ${originalSubject.replace(/^Re:\s*/, '')}`); messageParts.push(`In-Reply-To: ${messageId}`); messageParts.push(`References: ${messageId}`); messageParts.push(`Content-Type: ${params.isHtml ? 'text/html' : 'text/plain'}; charset=utf-8`); messageParts.push(''); messageParts.push(params.body); const message = messageParts.join('\n'); const encodedMessage = Buffer.from(message).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); if (params.send) { const response = await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encodedMessage, threadId: originalEmail.data.threadId } }); return { content: [ { type: 'text', text: `โ†ฉ๏ธ **Reply Sent Successfully!** ๐Ÿ“ง **To:** ${replyTo} ${params.replyAll && replyCc ? `๐Ÿ“‹ **CC:** ${replyCc}\n` : ''}๐Ÿ“ **Subject:** Re: ${originalSubject} ๐Ÿ†” **Message ID:** ${response.data.id} Your reply has been sent in the conversation thread.`, }, ], }; } else { const response = await gmail.users.drafts.create({ userId: 'me', requestBody: { message: { raw: encodedMessage, threadId: originalEmail.data.threadId } } }); return { content: [ { type: 'text', text: `๐Ÿ’พ **Reply Saved as Draft!** ๐Ÿ“ง **To:** ${replyTo} ๐Ÿ“ **Subject:** Re: ${originalSubject} ๐Ÿ†” **Draft ID:** ${response.data.id} You can send this reply later from your drafts.`, }, ], }; } } async function handleManageSubscriptions(args) { const params = ManageSubscriptionsSchema.parse(args); const gmail = await getGmailService(); if (params.action === 'list') { // Analyze subscription emails from your specific categories let query = 'unsubscribe OR "manage subscription" OR "email preferences" OR "opt out"'; if (params.category !== 'all') { query = `category:${params.category} ${query}`; } const response = await gmail.users.messages.list({ userId: 'me', maxResults: 100, q: query }); const messages = response.data.messages || []; if (messages.length === 0) { return { content: [ { type: 'text', text: `๐Ÿ“ง **No subscription emails found in ${params.category} category.** Try searching a different category or check "all" categories.`, }, ], }; } // Group by sender to identify subscriptions (like your Gmail interface shows) const senderMap = new Map(); for (const message of messages.slice(0, 50)) { // Limit for performance const email = await gmail.users.messages.get({ userId: 'me', id: message.id, format: 'metadata', metadataHeaders: ['From', 'Date', 'Subject'] }); const headers = email.data.payload?.headers || []; const getHeader = (name) => headers.find((h) => h.name === name)?.value || ''; const from = getHeader('From'); const date = getHeader('Date'); if (from) { // Extract email address from "Name <email@domain.com>" format const emailMatch = from.match(/<(.+?)>/) || from.match(/([^\s<>]+@[^\s<>]+)/); const emailAddress = emailMatch ? emailMatch[1] || emailMatch[0] : from; if (senderMap.has(emailAddress)) { const existing = senderMap.get(emailAddress); existing.totalEmails++; if (new Date(date) > new Date(existing.lastEmail)) { existing.lastEmail = date; } } else { senderMap.set(emailAddress, { sender: from, email: emailAddress, frequency: 'Multiple emails recently', lastEmail: date, totalEmails: 1, category: params.category }); } } } const subscriptions = Array.from(senderMap.values()) .sort((a, b) => b.totalEmails - a.totalEmails) .slice(0, 20); const subscriptionList = subscriptions.map((sub, index) => `**${index + 1}. ${sub.sender}** ๐Ÿ“ง Email: ${sub.email} ๐Ÿ“Š Recent Emails: ${sub.totalEmails}+ ๐Ÿ“… Last Email: ${sub.lastEmail} ๐Ÿ“ Category: ${sub.category} ๐Ÿ”— **Unsubscribe:** \`manage_subscriptions\` action: "unsubscribe", sender: "${sub.email}" ๐Ÿšซ **Block Forever:** \`manage_subscriptions\` action: "block_sender", sender: "${sub.email}"` ).join('\n\n---\n\n'); return { content: [ { type: 'text', text: `๐Ÿ“ง **Email Subscriptions Analysis - ${params.category.toUpperCase()} Category** **Found ${subscriptions.length} active subscription senders:** ${subscriptionList} ๐Ÿ’ก **Quick Actions:** - **Unsubscribe**: Finds unsubscribe links in recent emails - **Block Sender**: Creates filter to auto-delete future emails ๐Ÿ“Š **Total analyzed**: ${messages.length} subscription-related emails`, }, ], }; } if (params.action === 'unsubscribe') { if (!params.sender) { throw new Error('Sender email is required for unsubscribe action'); } // Find recent emails from this sender with unsubscribe links const response = await gmail.users.messages.list({ userId: 'me', maxResults: 10, q: `from:${params.sender}` }); const messages = response.data.messages || []; if (messages.length === 0) { return { content: [ { type: 'text', text: `โŒ **No recent emails found from ${params.sender}** The sender might have been typed incorrectly or no recent emails exist.`, }, ], }; } // Get the most recent email and look for unsubscribe links const email = await gmail.users.messages.get({ userId: 'me', id: messages[0].id, format: 'full' }); const body = extractEmailBody(email.data.payload || {}); // Look for various unsubscribe patterns const unsubscribePatterns = [ /https?:\/\/[^\s]+unsubscribe[^\s]*/gi, /https?:\/\/[^\s]+opt[_-]?out[^\s]*/gi, /https?:\/\/[^\s]+email[_-]?preferences[^\s]*/gi, /https?:\/\/[^\s]+manage[_-]?subscription[^\s]*/gi ]; let unsubscribeLinks = []; unsubscribePatterns.forEach(pattern => { const matches = body.match(pattern) || []; unsubscribeLinks = [...unsubscribeLinks, ...matches]; }); // Remove duplicates unsubscribeLinks = [...new Set(unsubscribeLinks)]; if (unsubscribeLinks.length > 0) { return { content: [ { type: 'text', text: `๐Ÿ”— **Unsubscribe Options for ${params.sender}:** **Found ${unsubscribeLinks.length} unsubscribe link(s):** ${unsubscribeLinks.map((link, i) => `${i + 1}. ${link}`).join('\n')} **๐Ÿ“ฑ Next Steps:** 1. **Click on one of the unsubscribe links above** (safest method) 2. **Or use auto-block**: \`manage_subscriptions\` action: "block_sender", sender: "${params.sender}" โš ๏ธ **Note**: Clicking unsubscribe links is usually safe for legitimate senders, but for unknown senders, blocking might be safer.`, }, ], }; } else { return { content: [ { type: 'text', text: `โŒ **No unsubscribe links found in recent emails from ${params.sender}** **๐Ÿ”ง Alternative Options:** 1. **Auto-block sender**: \`manage_subscriptions\` action: "block_sender", sender: "${params.sender}" 2. **Mark as spam**: Use \`manage_email\` to move emails to spam 3. **Create custom filter**: Use Gmail's filter system to auto-organize **๐Ÿ›ก๏ธ Recommended**: Use "block_sender" to automatically delete future emails from this sender.`, }, ], }; } } if (params.action === 'block_sender') { if (!params.sender) { throw new Error('Sender email is required for block_sender action'); } try { // Create a filter to automatically delete emails from this sender await gmail.users.settings.filters.create({ userId: 'me', requestBody: { criteria: { from: params.sender }, action: { removeLabelIds: ['INBOX'], addLabelIds: ['TRASH'] } } }); return { content: [ { type: 'text', text: `๐Ÿšซ **Sender Blocked Successfully!** โœ… **Created automatic filter for**: ${params.sender} ๐Ÿ—‘๏ธ **Future emails will be**: Automatically moved to trash ๐Ÿ“ง **Existing emails**: Remain in your inbox (unaffected) **๐Ÿ“‹ Filter Details:** - **Criteria**: All emails from ${params.sender} - **Action**: Bypass inbox โ†’ Move to trash - **Status**: Active immediately **๐Ÿ› ๏ธ Management**: You can view/modify this filter using the \`manage_filters\` tool if available, or through Gmail settings. **โœจ You're now unsubscribed automatically!** No more emails from this sender will clutter your inbox.`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `โŒ **Failed to block sender**: ${error.message} This might be due to insufficient permissions or API limitations. **๐Ÿ”ง Alternative**: Manually create a filter in Gmail: 1. Go to Gmail Settings โ†’ Filters and Blocked Addresses 2. Create new filter with "From: ${params.sender}" 3. Choose "Delete it" action`, }, ], }; } } return { content: [ { type: 'text', text: `โŒ Unknown action: ${params.action}. Available actions: list, unsubscribe, block_sender`, }, ], }; } async function handleManageLabels(args) { const params = ManageLabelsSchema.parse(args); const gmail = await getGmailService(); if (params.action === 'list') { const response = await gmail.users.labels.list({ userId: 'me' }); const labels = response.data.labels || []; const userLabels = labels.filter(label => label.type === 'user'); const systemLabels = labels.filter(label => label.type === 'system'); const formatLabels = (labelList, title) => { if (labelList.length === 0) return ''; return `**${title}:**\n${labelList.map(label => `๐Ÿท๏ธ **${label.name}** (ID: ${label.id})\n ๐Ÿ“Š Messages: ${label.messagesTotal || 0} | Unread: ${label.messagesUnread || 0}` ).join('\n')}\n\n`; }; return { content: [ { type: 'text', text: `๐Ÿท๏ธ **Gmail Labels Management** ${formatLabels(userLabels, 'Your Custom Labels')}${formatLabels(systemLabels, 'System Labels')} **๐Ÿ› ๏ธ Available Actions:** - **Create**: \`manage_labels\` action: "create", name: "YourLabelName" - **Delete**: \`manage_labels\` action: "delete", name: "ExistingLabel" - **Update**: \`manage_labels\` action: "update", name: "OldName", newName: "NewName" **๐Ÿ’ก Usage Tips:** - Use labels to organize emails by project, priority, or category - System labels (like INBOX, SENT) cannot be deleted - Labels can be applied to emails using \`manage_email\` tool`, }, ], }; } if (params.action === 'create') { if (!params.name) { throw new Error('Label name is required for create action'); } try { const response = await gmail.users.labels.create({ userId: 'me', requestBody: { name: params.name, labelListVisibility: params.visibility === 'hide' ? 'labelHide' : 'labelShow', messageListVisibility: 'show' } }); return { content: [ { type: 'text', text: `โœ… **Label Created Successfully!** ๐Ÿท๏ธ **Name**: ${params.name} ๐Ÿ†” **ID**: ${response.data.id} ๐Ÿ‘๏ธ **Visibility**: ${params.visibility || 'show'} **๐Ÿš€ Next Steps:** - Apply to emails: Use \`manage_email\` with action "add_label" and labelName "${params.name}" - The label will appear in your Gmail sidebar - You can organize emails by dragging them to this label`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `โŒ **Failed to create label**: ${error.message} **Common issues:** - Label name already exists - Invalid characters in name - Insufficient permissions`, }, ], }; } } if (params.action === 'delete') { if (!params.name) { throw new Error('Label name is required for delete action'); } try { // Find label by name const labelsResponse = await gmail.users.labels.list({ userId: 'me' }); const label = labelsResponse.data.labels?.find(l => l.name === params.name); if (!label) { throw new Error(`Label "${params.name}" not found`); } if (label.type === 'system') { throw new Error(`Cannot delete system label "${params.name}"`); } await gmail.users.labels.delete({ userId: 'me', id: label.id }); return { content: [ { type: 'text', text: `๐Ÿ—‘๏ธ **Label Deleted Successfully!** **Deleted label**: ${params.name} **Impact**: All emails with this label are now unlabeled **Note**: This action cannot be undone The label has been removed from your Gmail account.`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `โŒ **Failed to delete label**: ${error.message}`, }, ], }; } } return { content: [ { type: 'text', text: `๐Ÿ”ง **Label action "${params.action}" not yet fully implemented** Available actions: list, create, delete Update functionality coming in future version.`, }, ], }; } async function handleGetThread(args) { const params = GetThreadSchema.parse(args); const gmail = await getGmailService(); try { const thread = await gmail.users.threads.get({ userId: 'me', id: params.threadId, format: 'full' }); const messages = thread.data.messages || []; const emails = messages.map(parseEmailMetadata); // Sort by date emails.sort((a, b) => a.dateTimestamp - b.dateTimestamp); const participants = [...new Set(emails.flatMap(e => [e.from, e.to].filter(Boolean)))]; const subjects = [...new Set(emails.map(e => e.subject).filter(Boolean))]; const threadSummary = `๐Ÿงต **Email Thread Conversation** **๐Ÿ“Š Thread Overview:** - **Thread ID**: ${params.threadId} - **Messages**: ${emails.length} - **Participants**: ${participants.join(', ')} - **Subject(s)**: ${subjects.join(', ')} **๐Ÿ’ฌ Conversation Flow:** ${emails.map((email, i) => ` **Message ${i + 1}** (${email.isRead ? '๐Ÿ“– Read' : '๐Ÿ“ง Unread'}) ๐Ÿ‘ค **From**: ${email.from} ๐Ÿ“… **Date**: ${email.date} ๐Ÿ“ **Subject**: ${email.subject} ${email.isStarred ? 'โญ Starred | ' : ''}${email.isImportant ? '๐Ÿ”ฅ Important | ' : ''}${email.attachments.length > 0 ? `๐Ÿ“Ž ${email.attachments.length} attachments` : ''} ${params.includeBody ? `\n๐Ÿ“„ **Content**: ${email.body.substring(0, 300)}${email.body.length > 300 ? '...' : ''}` : `\n๐Ÿ“‹ **Snippet**: ${email.snippet}`} ๐Ÿ†” **Email ID**: ${email.id} `).join('\n---\n')} **๐Ÿ› ๏ธ Actions Available:** - Reply to any message: \`reply_email\` with emailId from above - Manage any message: \`manage_email\` with actions like star, archive, etc. - Get full details: \`get_email_details\` with specific emailId`; return { content: [ { type: 'text', text: threadSummary, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `โŒ **Failed to retrieve thread**: ${error.message} **Possible issues:** - Invalid thread ID - Thread doesn't exist - Access permissions **๐Ÿ’ก To find thread IDs**: Use \`get_emails\` or \`search_emails\` - thread IDs are included in email metadata.`, }, ], }; } } // NEW v3.1.0: COMPLETE JSON-RPC ENDPOINT IMPLEMENTATIONS async function handleSummarizeThread(args) { const params = SummarizeThreadSchema.parse(args); const gmail = await getGmailService(); try { const thread = await gmail.users.threads.get({ userId: 'me', id: params.threadId, format: 'full' }); const messages = thread.data.messages || []; const emails = messages.map(parseEmailMetadata); emails.sort((a, b) => a.dateTimestamp - b.dateTimestamp); // Prepare conversation for AI analysis const conversationText = emails.map((email, i) => `Message ${i + 1} (${email.date}):\nFrom: ${email.from}\nTo: ${email.to}\nSubject: ${email.subject}\n\nContent: ${email.body.substring(0, 1000)}${email.body.length > 1000 ? '...' : ''}\n\n---\n` ).join(''); // Generate AI summary based on type let prompt = ''; if (params.summaryType === 'brief') { prompt = `Provide a brief 2-3 sentence summary of this email conversation:\n\n${conversationText}`; } else if (params.summaryType === 'action_items') { prompt = `Extract all action items, tasks, deadlines, and next steps from this email conversation. Format as a bullet list:\n\n${conversationText}`; } else { // detailed prompt = `Provide a detailed summary of this email conversation including key points, decisions made, participants, and any action items:\n\n${conversationText}`; } const completion = await openai.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: prompt }], max_tokens: 500, temperature: 0.3 }); const aiSummary = completion.choices[0]?.message?.content || 'Unable to generate summary'; return { content: [ { type: 'text', text: `๐Ÿ“ **Thread Summary (${params.summaryType.toUpperCase()})**\n\n**Thread ID**: ${params.threadId}\n**Messages**: ${emails.length}\n**Participants**: ${[...new Set(emails.map(e => e.from))].join(', ')}\n\n**๐Ÿค– AI Summary:**\n${aiSummary}\n\n**๐Ÿ“Š Thread Stats:**\n- **Date Range**: ${emails[0]?.date} โ†’ ${emails[emails.length - 1]?.date}\n- **Attachments**: ${emails.reduce((sum, e) => sum + e.attachments.length, 0)}\n- **Unread**: ${emails.filter(e => !e.isRead).length}`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `โŒ **Failed to summarize thread**: ${error.message}`, }, ], }; } } async function handleListActionItems(args) { const params = ListActionItemsSchema.parse(args); const gmail = await getGmailService(); try { // Build query based on folder and timeframe let query = ''; if (params.folder === 'inbox') query = 'in:inbox'; else if (params.folder === 'sent') query = 'in:sent'; else if (params.folder === 'drafts') query = 'in:drafts'; else query = 'in:inbox OR in:sent'; // Add timeframe filter if (params.timeframe === 'today') { query += ' newer_than:1d'; } else if (params.timeframe === 'week') { query += ' newer_than:7d'; } else if (params.timeframe === 'month') { query += ' newer_than:30d'; } // Add action-related keywords query += ' ("action required" OR "deadline" OR "due date" OR "task" OR "TODO" OR "follow up" OR "next steps" OR "please" OR "need to" OR "reminder")'; const response = await gmail.users.messages.list({ userId: 'me', maxResults: 50, q: query }); const messages = response.data.messages || []; if (messages.length === 0) { return { content: [ { type: 'text', text: `๐Ÿ“‹ **No Action Items Found**\n\n**Searched in**: ${params.folder}\n**Timeframe**: ${params.timeframe}\n\nNo emails with action-related keywords found in the specified timeframe.`, }, ], }; } // Analyze first 20 emails for action items const actionItems = []; for (const message of messages.slice(0, 20)) { const email = await gmail.users.messages.get({ userId: 'me', id: message.id, format: 'full' }); const emailData = parseEmailMetadata(email.data); // Use AI to extract action items const prompt = `Extract specific action items, tasks, deadlines, and next steps from this email. Format as bullet points with priority (High/Medium/Low):\n\nSubject: ${emailData.subject}\nFrom: ${emailData.from}\nBody: ${emailData.body.substring(0, 800)}`; try { const completion = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: prompt }], max_tokens: 200, temperature: 0.2 }); const extractedActions = completion.choices[0]?.message?.content || ''; if (extractedActions && !extractedActions.toLowerCase().includes('no action items')) { actionItems.push({ emailId: emailData.id, subject: emailData.subject, from: emailData.from, date: emailData.date, actions: extractedActions, isRead: emailData.isRead, priority: extractedActions.toLowerCase().includes('urgent') || extractedActions.toLowerCase().includes('high') ? 'High' : extractedActions.toLowerCase().includes('low') ? 'Low' : 'Medium' }); } } catch (aiError) { // Fallback: simple keyword detection const actionKeywords = ['deadline', 'due', 'task', 'action required', 'follow up', 'next steps']; const hasActionKeywords = actionKeywords.some(keyword => emailData.body.toLowerCase().includes(keyword) || emailData.subject.toLowerCase().includes(keyword) ); if (hasActionKeywords) { actionItems.push({ emailId: emailData.id, subject: emailData.subject, from: emailData.from, date: emailData.date, actions: 'Contains action-related keywords - manual review needed', isRead: emailData.isRead, priority: 'Medium' }); } } } // Filter by priority if specified const filteredItems = params.priority === 'all' ? actionItems : actionItems.filter(item => item.priority.toLowerCase() === params.priority); // Sort by priority and date const priorityOrder = { 'High': 3, 'Medium': 2, 'Low': 1 }; filteredItems.sort((a, b) => { const priorityDiff = (priorityOrder[b.priority] || 0) - (priorityOrder[a.priority] || 0); if (priorityDiff !== 0) return priorityDiff; return new Date(b.date).getTime() - new Date(a.date).getTime(); }); const actionItemsList = filteredItems.map((item, index) => `**${index + 1}. ${item.priority} Priority** ${item.isRead ? '๐Ÿ“–' : '๐Ÿ“ง'}\n๐Ÿ“ง **From**: ${item.from}\n๐Ÿ“ **Subject**: ${item.subject}\n๐Ÿ“… **Date**: ${item.date}\n๐ŸŽฏ **Actions**: ${item.actions}\n๐Ÿ†” **Email ID**: ${item.emailId}\n` ).join('\n---\n\n'); return { content: [ { type: 'text', text: `๐Ÿ“‹ **Action Items Analysis - ${params.folder.toUpperCase()} (${params.timeframe})**\n\n**Found ${filteredItems.length} action items** (${params.priority} priority)\n\n${actionItemsList}\n\n**๐Ÿ”ง Quick Actions:**\n- **View email**: \`get_email_details\` with emailId\n- **Reply**: \`reply_email\` with emailId\n- **Mark complete**: \`manage_email\` action: "add_label", labelName: "Completed"\n\n**๐Ÿ“Š Analysis**: Searched ${messages.length} emails in ${params.folder} folder from ${params.timeframe}`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `โŒ **Failed to analyze action items**: ${error.message}`, }, ], }; } } async function handleGenerateDraft(args) { const params = GenerateDraftSchema.parse(args); const gmail = await getGmailService(); try { let contextInfo = ''; let replyHeaders = ''; // If replying to an email, get context if (params.replyToID) { const originalEmail = await gmail.users.messages.get({ userId: 'me', id: params.replyToID, format: 'full' }); const originalData = parseEmailMetadata(originalEmail.data); contextInfo = `\n\nOriginal email context:\nFrom: ${originalData.from}\nSubject: ${originalData.subject}\nContent: ${originalData.body.substring(0, 500)}...`; replyHeaders = `Replying to: ${originalData.subject}\nTo: ${originalData.from}\n`; } // Generate AI draft const prompt = `Generate a ${params.tone} email draft with ${params.length} length. ${params.replyToID ? 'This is a reply.' : 'This is a new email.'}\n\nUser request: ${params.prompt}${contextInfo}\n\nPlease provide:\n1. Subject line\n2. Email body\n3. Appropriate greeting and closing for ${params.tone} tone`; const completion = await openai.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: prompt }], max_tokens: params.length === 'brief' ? 300 : params.length === 'medium' ? 600 : 1000, temperature: 0.7 }); const aiDraft =