UNPKG

msexchange-mcp

Version:

MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API

201 lines (197 loc) 9.87 kB
import { z } from 'zod'; import accountsConfig from '../config/accountsLoader.js'; import { UnifiedSearchService } from '../services/unifiedSearch.js'; import { MailService } from '../services/mail.js'; export const findConversationTool = { name: 'find_conversation', description: `Find all emails between you and another person, with intelligent name resolution. Features: - Finds both sent and received emails - Smart name recognition using alias system - Optional topic filtering - Date range support - Handles partial names and nicknames Parameters: - with_person: Name or email (e.g., "Florian", "john@example.com") - about: Optional topic filter (e.g., "insurance", "project X") - aliases: Custom name mappings (e.g., {"Doss": ["Mads", "Mads Schwartz"]}) - date_range: Filter by date range - include_preview: Include email preview (default: true) - limit: Max results per direction (default: 50) Examples: - All emails with John: {"with_person": "John"} - Emails about project with Sarah: {"with_person": "Sarah", "about": "mobile app"} - Using aliases: {"with_person": "Doss", "aliases": {"Doss": ["Mads Schwartz"]}} - Last month's conversation: {"with_person": "client@company.com", "date_range": {"start": "2024-12-01"}} Best practices: - Use first names for known contacts (alias system will resolve) - Add topic filter to narrow results - Set reasonable limits to avoid token overload - Use aliases parameter for custom nicknames`, parameters: z.object({ user_id: z.string().describe('The user ID or email address'), with_person: z.string().describe('Name or email of the person (e.g., "Florian", "john@example.com")'), about: z.string().optional().describe('Optional topic filter (e.g., "insurance", "project X")'), aliases: z.record(z.array(z.string())).optional().describe('Name aliases (e.g., {"Doss": ["Mads", "Mads Schwartz"]})'), date_range: z.object({ start: z.string().optional().describe('Start date in ISO format'), end: z.string().optional().describe('End date in ISO format') }).optional().describe('Date range filter'), limit: z.number().optional().default(50).describe('Maximum results per direction (sent/received)'), include_preview: z.boolean().optional().default(true).describe('Include email preview'), }), execute: async (params) => { try { const account = accountsConfig.accounts.find(acc => acc.id === params.user_id || acc.email === params.user_id); if (!account) { throw new Error(`User not found: ${params.user_id}`); } const mailService = MailService.getInstance(); const unifiedSearch = new UnifiedSearchService(mailService); const results = { sent: [], received: [], conversations: new Map() }; console.log(`🔍 Finding conversations with: ${params.with_person}`); if (params.about) { console.log(` About: ${params.about}`); } // Search for emails FROM the person try { // If the person name contains spaces, quote it for exact match const personQuery = params.with_person.includes(' ') ? `from:"${params.with_person}"` : `from:${params.with_person}`; const searchQuery = personQuery + (params.about ? ` ${params.about}` : ''); const receivedResult = await unifiedSearch.search(searchQuery, { accountId: account.id, limit: params.limit || 50, includeBody: false, orderBy: 'receivedDateTime', orderDirection: 'desc', exactMatch: true // Use exact match to avoid false positives }); results.received = receivedResult.emails; console.log(`📥 Found ${receivedResult.emails.length} received emails from ${params.with_person}`); } catch (error) { console.error(`Error searching received emails: ${error}`); } // Search for emails TO the person (in sent folder) try { // If the person name contains spaces, quote it for exact match const personQuery = params.with_person.includes(' ') ? `to:"${params.with_person}"` : `to:${params.with_person}`; const sentSearchQuery = personQuery + (params.about ? ` ${params.about}` : ''); const sentResult = await unifiedSearch.search(sentSearchQuery, { accountId: account.id, limit: params.limit || 50, includeBody: false, orderBy: 'receivedDateTime', orderDirection: 'desc', exactMatch: true // Use exact match to avoid false positives }); results.sent = sentResult.emails; console.log(`📤 Found ${sentResult.emails.length} sent emails to ${params.with_person}`); } catch (error) { console.error(`Error searching sent emails: ${error}`); } // Group by conversation ID const allEmails = [...results.received, ...results.sent]; allEmails.forEach(email => { if (email.conversationId) { if (!results.conversations.has(email.conversationId)) { results.conversations.set(email.conversationId, []); } results.conversations.get(email.conversationId).push(email); } }); // Sort conversations by most recent activity const sortedConversations = Array.from(results.conversations.entries()) .map(([id, emails]) => ({ conversationId: id, emailCount: emails.length, mostRecent: emails[0].receivedDateTime || emails[0].sentDateTime, subject: emails[0].subject, emails: emails.slice(0, 3) // First 3 emails })) .sort((a, b) => new Date(b.mostRecent).getTime() - new Date(a.mostRecent).getTime()); // Format results const formattedResults = { summary: { person: params.with_person, topic: params.about, totalFound: allEmails.length, sentCount: results.sent.length, receivedCount: results.received.length, conversationCount: results.conversations.size, dateRange: params.date_range }, conversations: sortedConversations.slice(0, 10).map(conv => ({ id: conv.conversationId, subject: conv.subject, emailCount: conv.emailCount, mostRecent: conv.mostRecent, preview: conv.emails.map(e => ({ id: e.id, direction: results.sent.includes(e) ? 'sent' : 'received', date: e.receivedDateTime || e.sentDateTime, preview: params.include_preview ? e.bodyPreview?.substring(0, 100) : undefined })) })), recentEmails: { sent: results.sent.slice(0, 5).map(e => ({ id: e.id, subject: e.subject, to: e.toRecipients?.map((r) => r.emailAddress?.address || r.address || r).join(', ') || e.to?.join(', '), date: e.sentDateTime || e.receivedDateTime, hasAttachments: e.hasAttachments, preview: params.include_preview ? (e.bodyPreview || e.preview)?.substring(0, 100) : undefined })), received: results.received.slice(0, 5).map(e => ({ id: e.id, subject: e.subject, from: e.from?.emailAddress?.address || e.from || e.fromName, date: e.receivedDateTime || e.sentDateTime, hasAttachments: e.hasAttachments, preview: params.include_preview ? (e.bodyPreview || e.preview)?.substring(0, 100) : undefined })) }, searchTips: [ 'Use get_email_by_id to read full content of specific emails', 'Add --about parameter to filter by topic', 'Use --aliases to handle different names (e.g., nicknames)' ] }; return { content: [ { type: 'text', text: JSON.stringify(formattedResults, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error', tips: [ 'Try using full email address if you know it', 'Try searching for sent and received separately', 'Use simpler search terms' ] }, null, 2), }, ], }; } }, }; //# sourceMappingURL=findConversation.js.map