msexchange-mcp
Version:
MCP server for Microsoft Exchange/Outlook email operations via Microsoft Graph API
201 lines (197 loc) • 9.87 kB
JavaScript
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