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
JavaScript
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 =