msexchange-mcp
Version:
MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API
202 lines • 10.6 kB
JavaScript
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