UNPKG

@infograb/gmail-mcp-server

Version:

Gmail MCP 서버 - Claude Desktop에서 Gmail을 직접 관리

950 lines (949 loc) 48.9 kB
#!/usr/bin/env node 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 { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { OAuth2Client } from 'google-auth-library'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { createEmailMessage, createEmailWithNodemailer } from "./utl.js"; import { createLabel, updateLabel, deleteLabel, listLabels, getOrCreateLabel } from "./label-manager.js"; import { createFilter, listFilters, getFilter, deleteFilter, filterTemplates } from "./filter-manager.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Configuration from environment or command line const ACCESS_TOKEN = process.env.GMAIL_ACCESS_TOKEN || process.argv[2]; // OAuth2 configuration let oauth2Client; /** * Recursively extract email body content from MIME message parts * Handles complex email structures with nested parts */ function extractEmailContent(messagePart) { // Initialize containers for different content types let textContent = ''; let htmlContent = ''; // If the part has a body with data, process it based on MIME type if (messagePart.body && messagePart.body.data) { const content = Buffer.from(messagePart.body.data, 'base64').toString('utf8'); // Store content based on its MIME type if (messagePart.mimeType === 'text/plain') { textContent = content; } else if (messagePart.mimeType === 'text/html') { htmlContent = content; } } // If the part has nested parts, recursively process them if (messagePart.parts && messagePart.parts.length > 0) { for (const part of messagePart.parts) { const { text, html } = extractEmailContent(part); if (text) textContent += text; if (html) htmlContent += html; } } // Return both plain text and HTML content return { text: textContent, html: htmlContent }; } async function initializeAuth() { try { if (!ACCESS_TOKEN) { console.error('Error: Access token is required. Please provide it as a command-line argument or set GMAIL_ACCESS_TOKEN environment variable.'); console.error('Usage: npx @gongrzhe/server-gmail-autoauth-mcp <ACCESS_TOKEN>'); console.error(' or: GMAIL_ACCESS_TOKEN=<token> npx @gongrzhe/server-gmail-autoauth-mcp'); process.exit(1); } // Create OAuth2 client with just the access token oauth2Client = new OAuth2Client(); oauth2Client.setCredentials({ access_token: ACCESS_TOKEN }); } catch (error) { console.error('Error initializing authentication:', error); process.exit(1); } } // Remove the authenticate function as we no longer need OAuth flow // Schema definitions const SendEmailSchema = z.object({ to: z.array(z.string()).describe("List of recipient email addresses"), subject: z.string().describe("Email subject"), body: z.string().describe("Email body content (used for text/plain or when htmlBody not provided)"), htmlBody: z.string().optional().describe("HTML version of the email body"), mimeType: z.enum(['text/plain', 'text/html', 'multipart/alternative']).optional().default('text/plain').describe("Email content type"), cc: z.array(z.string()).optional().describe("List of CC recipients"), bcc: z.array(z.string()).optional().describe("List of BCC recipients"), threadId: z.string().optional().describe("Thread ID to reply to"), inReplyTo: z.string().optional().describe("Message ID being replied to"), attachments: z.array(z.string()).optional().describe("List of file paths to attach to the email"), }); const ReadEmailSchema = z.object({ messageId: z.string().describe("ID of the email message to retrieve"), }); const SearchEmailsSchema = z.object({ query: z.string().describe("Gmail search query (e.g., 'from:example@gmail.com')"), maxResults: z.number().optional().describe("Maximum number of results to return"), }); // Updated schema to include removeLabelIds const ModifyEmailSchema = z.object({ messageId: z.string().describe("ID of the email message to modify"), labelIds: z.array(z.string()).optional().describe("List of label IDs to apply"), addLabelIds: z.array(z.string()).optional().describe("List of label IDs to add to the message"), removeLabelIds: z.array(z.string()).optional().describe("List of label IDs to remove from the message"), }); const DeleteEmailSchema = z.object({ messageId: z.string().describe("ID of the email message to delete"), }); // New schema for listing email labels const ListEmailLabelsSchema = z.object({}).describe("Retrieves all available Gmail labels"); // Label management schemas const CreateLabelSchema = z.object({ name: z.string().describe("Name for the new label"), messageListVisibility: z.enum(['show', 'hide']).optional().describe("Whether to show or hide the label in the message list"), labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"), }).describe("Creates a new Gmail label"); const UpdateLabelSchema = z.object({ id: z.string().describe("ID of the label to update"), name: z.string().optional().describe("New name for the label"), messageListVisibility: z.enum(['show', 'hide']).optional().describe("Whether to show or hide the label in the message list"), labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"), }).describe("Updates an existing Gmail label"); const DeleteLabelSchema = z.object({ id: z.string().describe("ID of the label to delete"), }).describe("Deletes a Gmail label"); const GetOrCreateLabelSchema = z.object({ name: z.string().describe("Name of the label to get or create"), messageListVisibility: z.enum(['show', 'hide']).optional().describe("Whether to show or hide the label in the message list"), labelListVisibility: z.enum(['labelShow', 'labelShowIfUnread', 'labelHide']).optional().describe("Visibility of the label in the label list"), }).describe("Gets an existing label by name or creates it if it doesn't exist"); // Schemas for batch operations const BatchModifyEmailsSchema = z.object({ messageIds: z.array(z.string()).describe("List of message IDs to modify"), addLabelIds: z.array(z.string()).optional().describe("List of label IDs to add to all messages"), removeLabelIds: z.array(z.string()).optional().describe("List of label IDs to remove from all messages"), batchSize: z.number().optional().default(50).describe("Number of messages to process in each batch (default: 50)"), }); const BatchDeleteEmailsSchema = z.object({ messageIds: z.array(z.string()).describe("List of message IDs to delete"), batchSize: z.number().optional().default(50).describe("Number of messages to process in each batch (default: 50)"), }); // Filter management schemas const CreateFilterSchema = z.object({ criteria: z.object({ from: z.string().optional().describe("Sender email address to match"), to: z.string().optional().describe("Recipient email address to match"), subject: z.string().optional().describe("Subject text to match"), query: z.string().optional().describe("Gmail search query (e.g., 'has:attachment')"), negatedQuery: z.string().optional().describe("Text that must NOT be present"), hasAttachment: z.boolean().optional().describe("Whether to match emails with attachments"), excludeChats: z.boolean().optional().describe("Whether to exclude chat messages"), size: z.number().optional().describe("Email size in bytes"), sizeComparison: z.enum(['unspecified', 'smaller', 'larger']).optional().describe("Size comparison operator") }).describe("Criteria for matching emails"), action: z.object({ addLabelIds: z.array(z.string()).optional().describe("Label IDs to add to matching emails"), removeLabelIds: z.array(z.string()).optional().describe("Label IDs to remove from matching emails"), forward: z.string().optional().describe("Email address to forward matching emails to") }).describe("Actions to perform on matching emails") }).describe("Creates a new Gmail filter"); const ListFiltersSchema = z.object({}).describe("Retrieves all Gmail filters"); const GetFilterSchema = z.object({ filterId: z.string().describe("ID of the filter to retrieve") }).describe("Gets details of a specific Gmail filter"); const DeleteFilterSchema = z.object({ filterId: z.string().describe("ID of the filter to delete") }).describe("Deletes a Gmail filter"); const CreateFilterFromTemplateSchema = z.object({ template: z.enum(['fromSender', 'withSubject', 'withAttachments', 'largeEmails', 'containingText', 'mailingList']).describe("Pre-defined filter template to use"), parameters: z.object({ senderEmail: z.string().optional().describe("Sender email (for fromSender template)"), subjectText: z.string().optional().describe("Subject text (for withSubject template)"), searchText: z.string().optional().describe("Text to search for (for containingText template)"), listIdentifier: z.string().optional().describe("Mailing list identifier (for mailingList template)"), sizeInBytes: z.number().optional().describe("Size threshold in bytes (for largeEmails template)"), labelIds: z.array(z.string()).optional().describe("Label IDs to apply"), archive: z.boolean().optional().describe("Whether to archive (skip inbox)"), markAsRead: z.boolean().optional().describe("Whether to mark as read"), markImportant: z.boolean().optional().describe("Whether to mark as important") }).describe("Template-specific parameters") }).describe("Creates a filter using a pre-defined template"); const DownloadAttachmentSchema = z.object({ messageId: z.string().describe("ID of the email message containing the attachment"), attachmentId: z.string().describe("ID of the attachment to download"), filename: z.string().optional().describe("Filename to save the attachment as (if not provided, uses original filename)"), savePath: z.string().optional().describe("Directory path to save the attachment (defaults to current directory)"), }); // Main function async function main() { // Initialize authentication with provided token await initializeAuth(); // Initialize Gmail API const gmail = google.gmail({ version: 'v1', auth: oauth2Client }); // Server implementation const server = new Server({ name: "gmail", version: "1.0.0", capabilities: { tools: {}, }, }); // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "send_email", description: "Sends a new email", inputSchema: zodToJsonSchema(SendEmailSchema), }, { name: "draft_email", description: "Draft a new email", inputSchema: zodToJsonSchema(SendEmailSchema), }, { name: "read_email", description: "Retrieves the content of a specific email", inputSchema: zodToJsonSchema(ReadEmailSchema), }, { name: "search_emails", description: "Searches for emails using Gmail search syntax", inputSchema: zodToJsonSchema(SearchEmailsSchema), }, { name: "modify_email", description: "Modifies email labels (move to different folders)", inputSchema: zodToJsonSchema(ModifyEmailSchema), }, { name: "delete_email", description: "Permanently deletes an email", inputSchema: zodToJsonSchema(DeleteEmailSchema), }, { name: "list_email_labels", description: "Retrieves all available Gmail labels", inputSchema: zodToJsonSchema(ListEmailLabelsSchema), }, { name: "batch_modify_emails", description: "Modifies labels for multiple emails in batches", inputSchema: zodToJsonSchema(BatchModifyEmailsSchema), }, { name: "batch_delete_emails", description: "Permanently deletes multiple emails in batches", inputSchema: zodToJsonSchema(BatchDeleteEmailsSchema), }, { name: "create_label", description: "Creates a new Gmail label", inputSchema: zodToJsonSchema(CreateLabelSchema), }, { name: "update_label", description: "Updates an existing Gmail label", inputSchema: zodToJsonSchema(UpdateLabelSchema), }, { name: "delete_label", description: "Deletes a Gmail label", inputSchema: zodToJsonSchema(DeleteLabelSchema), }, { name: "get_or_create_label", description: "Gets an existing label by name or creates it if it doesn't exist", inputSchema: zodToJsonSchema(GetOrCreateLabelSchema), }, { name: "create_filter", description: "Creates a new Gmail filter with custom criteria and actions", inputSchema: zodToJsonSchema(CreateFilterSchema), }, { name: "list_filters", description: "Retrieves all Gmail filters", inputSchema: zodToJsonSchema(ListFiltersSchema), }, { name: "get_filter", description: "Gets details of a specific Gmail filter", inputSchema: zodToJsonSchema(GetFilterSchema), }, { name: "delete_filter", description: "Deletes a Gmail filter", inputSchema: zodToJsonSchema(DeleteFilterSchema), }, { name: "create_filter_from_template", description: "Creates a filter using a pre-defined template for common scenarios", inputSchema: zodToJsonSchema(CreateFilterFromTemplateSchema), }, { name: "download_attachment", description: "Downloads an email attachment to a specified location", inputSchema: zodToJsonSchema(DownloadAttachmentSchema), }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; async function handleEmailAction(action, validatedArgs) { let message; try { // Check if we have attachments if (validatedArgs.attachments && validatedArgs.attachments.length > 0) { // Use Nodemailer to create properly formatted RFC822 message message = await createEmailWithNodemailer(validatedArgs); if (action === "send") { const encodedMessage = Buffer.from(message).toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); const result = await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encodedMessage, ...(validatedArgs.threadId && { threadId: validatedArgs.threadId }) } }); return { content: [ { type: "text", text: `Email sent successfully with ID: ${result.data.id}`, }, ], }; } else { // For drafts with attachments, use the raw message const encodedMessage = Buffer.from(message).toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); const messageRequest = { raw: encodedMessage, ...(validatedArgs.threadId && { threadId: validatedArgs.threadId }) }; const response = await gmail.users.drafts.create({ userId: 'me', requestBody: { message: messageRequest, }, }); return { content: [ { type: "text", text: `Email draft created successfully with ID: ${response.data.id}`, }, ], }; } } else { // For emails without attachments, use the existing simple method message = createEmailMessage(validatedArgs); const encodedMessage = Buffer.from(message).toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); const messageRequest = { raw: encodedMessage, }; // Add threadId if specified if (validatedArgs.threadId) { messageRequest.threadId = validatedArgs.threadId; } if (action === "send") { const response = await gmail.users.messages.send({ userId: 'me', requestBody: messageRequest, }); return { content: [ { type: "text", text: `Email sent successfully with ID: ${response.data.id}`, }, ], }; } else { const response = await gmail.users.drafts.create({ userId: 'me', requestBody: { message: messageRequest, }, }); return { content: [ { type: "text", text: `Email draft created successfully with ID: ${response.data.id}`, }, ], }; } } } catch (error) { // Log attachment-related errors for debugging if (validatedArgs.attachments && validatedArgs.attachments.length > 0) { console.error(`Failed to send email with ${validatedArgs.attachments.length} attachments:`, error.message); } throw error; } } // Helper function to process operations in batches async function processBatches(items, batchSize, processFn) { const successes = []; const failures = []; // Process in batches for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); try { const results = await processFn(batch); successes.push(...results); } catch (error) { // If batch fails, try individual items for (const item of batch) { try { const result = await processFn([item]); successes.push(...result); } catch (itemError) { failures.push({ item, error: itemError }); } } } } return { successes, failures }; } try { switch (name) { case "send_email": case "draft_email": { const validatedArgs = SendEmailSchema.parse(args); const action = name === "send_email" ? "send" : "draft"; return await handleEmailAction(action, validatedArgs); } case "read_email": { const validatedArgs = ReadEmailSchema.parse(args); const response = await gmail.users.messages.get({ userId: 'me', id: validatedArgs.messageId, format: 'full', }); const headers = response.data.payload?.headers || []; const subject = headers.find(h => h.name?.toLowerCase() === 'subject')?.value || ''; const from = headers.find(h => h.name?.toLowerCase() === 'from')?.value || ''; const to = headers.find(h => h.name?.toLowerCase() === 'to')?.value || ''; const date = headers.find(h => h.name?.toLowerCase() === 'date')?.value || ''; const threadId = response.data.threadId || ''; // Extract email content using the recursive function const { text, html } = extractEmailContent(response.data.payload || {}); // Use plain text content if available, otherwise use HTML content // (optionally, you could implement HTML-to-text conversion here) let body = text || html || ''; // If we only have HTML content, add a note for the user const contentTypeNote = !text && html ? '[Note: This email is HTML-formatted. Plain text version not available.]\n\n' : ''; // Get attachment information const attachments = []; const processAttachmentParts = (part, path = '') => { if (part.body && part.body.attachmentId) { const filename = part.filename || `attachment-${part.body.attachmentId}`; attachments.push({ id: part.body.attachmentId, filename: filename, mimeType: part.mimeType || 'application/octet-stream', size: part.body.size || 0 }); } if (part.parts) { part.parts.forEach((subpart) => processAttachmentParts(subpart, `${path}/parts`)); } }; if (response.data.payload) { processAttachmentParts(response.data.payload); } // Add attachment info to output if any are present const attachmentInfo = attachments.length > 0 ? `\n\nAttachments (${attachments.length}):\n` + attachments.map(a => `- ${a.filename} (${a.mimeType}, ${Math.round(a.size / 1024)} KB, ID: ${a.id})`).join('\n') : ''; return { content: [ { type: "text", text: `Thread ID: ${threadId}\nSubject: ${subject}\nFrom: ${from}\nTo: ${to}\nDate: ${date}\n\n${contentTypeNote}${body}${attachmentInfo}`, }, ], }; } case "search_emails": { const validatedArgs = SearchEmailsSchema.parse(args); const response = await gmail.users.messages.list({ userId: 'me', q: validatedArgs.query, maxResults: validatedArgs.maxResults || 10, }); const messages = response.data.messages || []; const results = await Promise.all(messages.map(async (msg) => { const detail = await gmail.users.messages.get({ userId: 'me', id: msg.id, format: 'metadata', metadataHeaders: ['Subject', 'From', 'Date'], }); const headers = detail.data.payload?.headers || []; return { id: msg.id, subject: headers.find(h => h.name === 'Subject')?.value || '', from: headers.find(h => h.name === 'From')?.value || '', date: headers.find(h => h.name === 'Date')?.value || '', }; })); return { content: [ { type: "text", text: results.map(r => `ID: ${r.id}\nSubject: ${r.subject}\nFrom: ${r.from}\nDate: ${r.date}\n`).join('\n'), }, ], }; } // Updated implementation for the modify_email handler case "modify_email": { const validatedArgs = ModifyEmailSchema.parse(args); // Prepare request body const requestBody = {}; if (validatedArgs.labelIds) { requestBody.addLabelIds = validatedArgs.labelIds; } if (validatedArgs.addLabelIds) { requestBody.addLabelIds = validatedArgs.addLabelIds; } if (validatedArgs.removeLabelIds) { requestBody.removeLabelIds = validatedArgs.removeLabelIds; } await gmail.users.messages.modify({ userId: 'me', id: validatedArgs.messageId, requestBody: requestBody, }); return { content: [ { type: "text", text: `Email ${validatedArgs.messageId} labels updated successfully`, }, ], }; } case "delete_email": { const validatedArgs = DeleteEmailSchema.parse(args); await gmail.users.messages.delete({ userId: 'me', id: validatedArgs.messageId, }); return { content: [ { type: "text", text: `Email ${validatedArgs.messageId} deleted successfully`, }, ], }; } case "list_email_labels": { const labelResults = await listLabels(gmail); const systemLabels = labelResults.system; const userLabels = labelResults.user; return { content: [ { type: "text", text: `Found ${labelResults.count.total} labels (${labelResults.count.system} system, ${labelResults.count.user} user):\n\n` + "System Labels:\n" + systemLabels.map((l) => `ID: ${l.id}\nName: ${l.name}\n`).join('\n') + "\nUser Labels:\n" + userLabels.map((l) => `ID: ${l.id}\nName: ${l.name}\n`).join('\n') }, ], }; } case "batch_modify_emails": { const validatedArgs = BatchModifyEmailsSchema.parse(args); const messageIds = validatedArgs.messageIds; const batchSize = validatedArgs.batchSize || 50; // Prepare request body const requestBody = {}; if (validatedArgs.addLabelIds) { requestBody.addLabelIds = validatedArgs.addLabelIds; } if (validatedArgs.removeLabelIds) { requestBody.removeLabelIds = validatedArgs.removeLabelIds; } // Process messages in batches const { successes, failures } = await processBatches(messageIds, batchSize, async (batch) => { const results = await Promise.all(batch.map(async (messageId) => { const result = await gmail.users.messages.modify({ userId: 'me', id: messageId, requestBody: requestBody, }); return { messageId, success: true }; })); return results; }); // Generate summary of the operation const successCount = successes.length; const failureCount = failures.length; let resultText = `Batch label modification complete.\n`; resultText += `Successfully processed: ${successCount} messages\n`; if (failureCount > 0) { resultText += `Failed to process: ${failureCount} messages\n\n`; resultText += `Failed message IDs:\n`; resultText += failures.map(f => `- ${f.item.substring(0, 16)}... (${f.error.message})`).join('\n'); } return { content: [ { type: "text", text: resultText, }, ], }; } case "batch_delete_emails": { const validatedArgs = BatchDeleteEmailsSchema.parse(args); const messageIds = validatedArgs.messageIds; const batchSize = validatedArgs.batchSize || 50; // Process messages in batches const { successes, failures } = await processBatches(messageIds, batchSize, async (batch) => { const results = await Promise.all(batch.map(async (messageId) => { await gmail.users.messages.delete({ userId: 'me', id: messageId, }); return { messageId, success: true }; })); return results; }); // Generate summary of the operation const successCount = successes.length; const failureCount = failures.length; let resultText = `Batch delete operation complete.\n`; resultText += `Successfully deleted: ${successCount} messages\n`; if (failureCount > 0) { resultText += `Failed to delete: ${failureCount} messages\n\n`; resultText += `Failed message IDs:\n`; resultText += failures.map(f => `- ${f.item.substring(0, 16)}... (${f.error.message})`).join('\n'); } return { content: [ { type: "text", text: resultText, }, ], }; } // New label management handlers case "create_label": { const validatedArgs = CreateLabelSchema.parse(args); const result = await createLabel(gmail, validatedArgs.name, { messageListVisibility: validatedArgs.messageListVisibility, labelListVisibility: validatedArgs.labelListVisibility, }); return { content: [ { type: "text", text: `Label created successfully:\nID: ${result.id}\nName: ${result.name}\nType: ${result.type}`, }, ], }; } case "update_label": { const validatedArgs = UpdateLabelSchema.parse(args); // Prepare request body with only the fields that were provided const updates = {}; if (validatedArgs.name) updates.name = validatedArgs.name; if (validatedArgs.messageListVisibility) updates.messageListVisibility = validatedArgs.messageListVisibility; if (validatedArgs.labelListVisibility) updates.labelListVisibility = validatedArgs.labelListVisibility; const result = await updateLabel(gmail, validatedArgs.id, updates); return { content: [ { type: "text", text: `Label updated successfully:\nID: ${result.id}\nName: ${result.name}\nType: ${result.type}`, }, ], }; } case "delete_label": { const validatedArgs = DeleteLabelSchema.parse(args); const result = await deleteLabel(gmail, validatedArgs.id); return { content: [ { type: "text", text: result.message, }, ], }; } case "get_or_create_label": { const validatedArgs = GetOrCreateLabelSchema.parse(args); const result = await getOrCreateLabel(gmail, validatedArgs.name, { messageListVisibility: validatedArgs.messageListVisibility, labelListVisibility: validatedArgs.labelListVisibility, }); const action = result.type === 'user' && result.name === validatedArgs.name ? 'found existing' : 'created new'; return { content: [ { type: "text", text: `Successfully ${action} label:\nID: ${result.id}\nName: ${result.name}\nType: ${result.type}`, }, ], }; } // Filter management handlers case "create_filter": { const validatedArgs = CreateFilterSchema.parse(args); const result = await createFilter(gmail, validatedArgs.criteria, validatedArgs.action); // Format criteria for display const criteriaText = Object.entries(validatedArgs.criteria) .filter(([_, value]) => value !== undefined) .map(([key, value]) => `${key}: ${value}`) .join(', '); // Format actions for display const actionText = Object.entries(validatedArgs.action) .filter(([_, value]) => value !== undefined && (Array.isArray(value) ? value.length > 0 : true)) .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(', ') : value}`) .join(', '); return { content: [ { type: "text", text: `Filter created successfully:\nID: ${result.id}\nCriteria: ${criteriaText}\nActions: ${actionText}`, }, ], }; } case "list_filters": { const result = await listFilters(gmail); const filters = result.filters; if (filters.length === 0) { return { content: [ { type: "text", text: "No filters found.", }, ], }; } const filtersText = filters.map((filter) => { const criteriaEntries = Object.entries(filter.criteria || {}) .filter(([_, value]) => value !== undefined) .map(([key, value]) => `${key}: ${value}`) .join(', '); const actionEntries = Object.entries(filter.action || {}) .filter(([_, value]) => value !== undefined && (Array.isArray(value) ? value.length > 0 : true)) .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(', ') : value}`) .join(', '); return `ID: ${filter.id}\nCriteria: ${criteriaEntries}\nActions: ${actionEntries}\n`; }).join('\n'); return { content: [ { type: "text", text: `Found ${result.count} filters:\n\n${filtersText}`, }, ], }; } case "get_filter": { const validatedArgs = GetFilterSchema.parse(args); const result = await getFilter(gmail, validatedArgs.filterId); const criteriaText = Object.entries(result.criteria || {}) .filter(([_, value]) => value !== undefined) .map(([key, value]) => `${key}: ${value}`) .join(', '); const actionText = Object.entries(result.action || {}) .filter(([_, value]) => value !== undefined && (Array.isArray(value) ? value.length > 0 : true)) .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(', ') : value}`) .join(', '); return { content: [ { type: "text", text: `Filter details:\nID: ${result.id}\nCriteria: ${criteriaText}\nActions: ${actionText}`, }, ], }; } case "delete_filter": { const validatedArgs = DeleteFilterSchema.parse(args); const result = await deleteFilter(gmail, validatedArgs.filterId); return { content: [ { type: "text", text: result.message, }, ], }; } case "create_filter_from_template": { const validatedArgs = CreateFilterFromTemplateSchema.parse(args); const template = validatedArgs.template; const params = validatedArgs.parameters; let filterConfig; switch (template) { case 'fromSender': if (!params.senderEmail) throw new Error("senderEmail is required for fromSender template"); filterConfig = filterTemplates.fromSender(params.senderEmail, params.labelIds, params.archive); break; case 'withSubject': if (!params.subjectText) throw new Error("subjectText is required for withSubject template"); filterConfig = filterTemplates.withSubject(params.subjectText, params.labelIds, params.markAsRead); break; case 'withAttachments': filterConfig = filterTemplates.withAttachments(params.labelIds); break; case 'largeEmails': if (!params.sizeInBytes) throw new Error("sizeInBytes is required for largeEmails template"); filterConfig = filterTemplates.largeEmails(params.sizeInBytes, params.labelIds); break; case 'containingText': if (!params.searchText) throw new Error("searchText is required for containingText template"); filterConfig = filterTemplates.containingText(params.searchText, params.labelIds, params.markImportant); break; case 'mailingList': if (!params.listIdentifier) throw new Error("listIdentifier is required for mailingList template"); filterConfig = filterTemplates.mailingList(params.listIdentifier, params.labelIds, params.archive); break; default: throw new Error(`Unknown template: ${template}`); } const result = await createFilter(gmail, filterConfig.criteria, filterConfig.action); return { content: [ { type: "text", text: `Filter created from template '${template}':\nID: ${result.id}\nTemplate used: ${template}`, }, ], }; } case "download_attachment": { const validatedArgs = DownloadAttachmentSchema.parse(args); try { // Get the attachment data from Gmail API const attachmentResponse = await gmail.users.messages.attachments.get({ userId: 'me', messageId: validatedArgs.messageId, id: validatedArgs.attachmentId, }); if (!attachmentResponse.data.data) { throw new Error('No attachment data received'); } // Decode the base64 data const data = attachmentResponse.data.data; const buffer = Buffer.from(data, 'base64url'); // Determine save path and filename const savePath = validatedArgs.savePath || process.cwd(); let filename = validatedArgs.filename; if (!filename) { // Get original filename from message if not provided const messageResponse = await gmail.users.messages.get({ userId: 'me', id: validatedArgs.messageId, format: 'full', }); // Find the attachment part to get original filename const findAttachment = (part) => { if (part.body && part.body.attachmentId === validatedArgs.attachmentId) { return part.filename || `attachment-${validatedArgs.attachmentId}`; } if (part.parts) { for (const subpart of part.parts) { const found = findAttachment(subpart); if (found) return found; } } return null; }; filename = findAttachment(messageResponse.data.payload) || `attachment-${validatedArgs.attachmentId}`; } // Ensure save directory exists if (!fs.existsSync(savePath)) { fs.mkdirSync(savePath, { recursive: true }); } // Write file const fullPath = path.join(savePath, filename); fs.writeFileSync(fullPath, buffer); return { content: [ { type: "text", text: `Attachment downloaded successfully:\nFile: ${filename}\nSize: ${buffer.length} bytes\nSaved to: ${fullPath}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Failed to download attachment: ${error.message}`, }, ], }; } } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${error.message}`, }, ], }; } }); const transport = new StdioServerTransport(); server.connect(transport); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });