UNPKG

msexchange-mcp

Version:

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

202 lines 10.6 kB
import { z } from 'zod'; import { SmartQueryBuilder } from '../mail/queryBuilder.js'; import { SearchQuerySanitizer } from '../mail/searchQuerySanitizer.js'; import accountsConfig from '../config/accountsLoader.js'; export const smartEmailSearchTool = { name: 'smart_email_search', description: 'Intelligent email search that automatically handles numbers, dates, and complex queries. Combines natural language understanding with automatic query optimization.', parameters: z.object({ user_id: z.string().describe('The user ID or email address'), query: z.string().describe('Natural language search query (e.g., "emails from john about tax 2024", "invoices from last month")'), date_range: z.object({ start: z.string().optional().describe('Start date in ISO format (e.g., "2025-01-01")'), end: z.string().optional().describe('End date in ISO format (e.g., "2025-01-31")') }).optional().describe('Date range filter'), sender: z.string().optional().describe('Filter by specific sender email'), limit: z.number().optional().default(20).describe('Maximum results (automatic pagination for large queries)'), include_preview: z.boolean().optional().default(true).describe('Include email preview'), include_attachments: z.boolean().optional().default(true).describe('Include attachment information'), sort: z.enum(['newest', 'oldest', 'relevance']).optional().default('newest').describe('Sort order'), }), 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 queryBuilder = new SmartQueryBuilder(account.email); // Analyze the query for issues const searchAnalysis = SearchQuerySanitizer.suggestSearchStrategy(params.query); const tokens = SearchQuerySanitizer.extractSearchTokens(params.query); // Build comprehensive query options const queryOptions = { // Use sender if provided, otherwise check if query contains email from: params.sender || (tokens.emails.length > 0 ? tokens.emails[0] : undefined), // Add date range if provided dateRange: params.date_range, // Use sanitized search or filter based on analysis ...(searchAnalysis.strategy === 'filter' ? { filter: searchAnalysis.filterExpression } : { search: searchAnalysis.sanitizedQuery || params.query }), limit: params.limit || 20, orderBy: params.sort === 'oldest' ? 'receivedDateTime ASC' : 'receivedDateTime DESC', includeBody: false, // Never include body in search to avoid token limits }; // Log search strategy for transparency console.log(`🔍 Smart Search Analysis:`); console.log(` Query: "${params.query}"`); console.log(` Strategy: ${searchAnalysis.strategy}`); if (searchAnalysis.warnings.length > 0) { console.log(` ⚠️ Warnings: ${searchAnalysis.warnings.join('; ')}`); } if (searchAnalysis.suggestions.length > 0) { console.log(` 💡 Actions: ${searchAnalysis.suggestions.join('; ')}`); } if (tokens.years.length > 0) { console.log(` 📅 Years detected: ${tokens.years.join(', ')}`); } if (tokens.emails.length > 0) { console.log(` 📧 Emails detected: ${tokens.emails.join(', ')}`); } // Execute search with smart handling const result = await queryBuilder.executeQuery(queryOptions); // Calculate relevance scores if needed let emails = result.emails; if (params.sort === 'relevance' && tokens.keywords.length > 0) { emails = scoreAndSortByRelevance(emails, tokens.keywords); } // Format results with helpful information const formattedEmails = emails.map(email => ({ id: email.id, subject: email.subject, from: email.from?.emailAddress?.address, fromName: email.from?.emailAddress?.name, to: email.toRecipients?.map(r => r.emailAddress.address).join(', '), date: email.receivedDateTime, hasAttachments: email.hasAttachments, attachmentCount: email.hasAttachments ? (email.attachments?.length || '?') : 0, attachmentNames: params.include_attachments && email.attachments ? email.attachments.map((a) => a.name).join(', ') : undefined, preview: params.include_preview ? email.bodyPreview?.substring(0, 200) : undefined, isRead: email.isRead, importance: email.importance, categories: email.categories, })); // Provide search insights const insights = []; if (result.emails.length === 0) { insights.push('No results found. Try:'); if (tokens.hasNumbers) { insights.push('• Searching without numbers (e.g., remove "2024")'); } if (!params.sender && tokens.emails.length === 0) { insights.push('• Adding sender email with --sender parameter'); } if (!params.date_range) { insights.push('• Adding date range to narrow results'); } insights.push('• Using simpler keywords'); } else if (result.emails.length === params.limit) { insights.push(`Showing first ${params.limit} results. To see more:`); insights.push('• Increase --limit parameter'); insights.push('• Add more specific search terms'); insights.push('• Use --sender or --date_range to filter'); } return { content: [ { type: 'text', text: JSON.stringify({ summary: { query: params.query, resultCount: formattedEmails.length, searchStrategy: searchAnalysis.strategy, hasMore: result.emails.length === (params.limit || 20), dateRange: params.date_range, sender: params.sender || tokens.emails[0], }, insights: insights.length > 0 ? insights : undefined, searchAnalysis: { detectedYears: tokens.years, detectedEmails: tokens.emails, keywords: tokens.keywords.slice(0, 5), warnings: searchAnalysis.warnings, }, emails: formattedEmails, nextSteps: formattedEmails.length > 0 ? [ 'Use get_email_by_id to fetch full content of specific emails', 'Refine search with --sender or --date_range parameters', 'Create a draft reply with create_draft tool' ] : undefined, }, null, 2), }, ], }; } catch (error) { // Provide helpful error recovery suggestions const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const suggestions = []; if (errorMessage.includes('token')) { suggestions.push('Reduce --limit parameter'); suggestions.push('Search without email previews using --include_preview false'); suggestions.push('Use more specific search terms'); } else if (errorMessage.includes('character') && errorMessage.includes('not valid')) { suggestions.push('Remove numbers from your search'); suggestions.push('Use --sender parameter instead of searching'); suggestions.push('Try simpler keywords without special characters'); } return { content: [ { type: 'text', text: JSON.stringify({ error: errorMessage, suggestions, originalQuery: params.query, tip: 'Smart search automatically handles most query issues. If you still see errors, try the suggestions above.', }, null, 2), }, ], }; } }, }; /** * Score and sort emails by relevance to keywords */ function scoreAndSortByRelevance(emails, keywords) { const scoredEmails = emails.map(email => { let score = 0; const subject = (email.subject || '').toLowerCase(); const preview = (email.bodyPreview || '').toLowerCase(); const from = (email.from?.emailAddress?.address || '').toLowerCase(); keywords.forEach(keyword => { const kw = keyword.toLowerCase(); // Higher weight for subject matches if (subject.includes(kw)) score += 3; // Medium weight for preview matches if (preview.includes(kw)) score += 2; // Lower weight for sender matches if (from.includes(kw)) score += 1; }); return { email, score }; }); // Sort by score descending, then by date return scoredEmails .sort((a, b) => { if (a.score !== b.score) return b.score - a.score; // If scores are equal, sort by date return new Date(b.email.receivedDateTime).getTime() - new Date(a.email.receivedDateTime).getTime(); }) .map(item => item.email); } //# sourceMappingURL=smartEmailSearch.js.map