feat: implement MCP server with Outlook email search tools
Add EmailSearchTools with two MCP tools: - SearchEmails: Search by keywords, sender, subject, date range, attachments, importance, category, and flag status with pagination - ReadEmail: Retrieve full email body by subject and date Configure MCP server with stdio transport in Program.cs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
397
EmailSearch/EmailSearchTools.cs
Normal file
397
EmailSearch/EmailSearchTools.cs
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using OutlookApp = NetOffice.OutlookApi.Application;
|
||||||
|
using NetOffice.OutlookApi;
|
||||||
|
using NetOffice.OutlookApi.Enums;
|
||||||
|
|
||||||
|
[McpServerToolType]
|
||||||
|
public class EmailSearchTools
|
||||||
|
{
|
||||||
|
private static readonly string[] SupportedDateFormats =
|
||||||
|
[
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
"MM/dd/yyyy",
|
||||||
|
"dd/MM/yyyy",
|
||||||
|
"yyyy/MM/dd",
|
||||||
|
"MM-dd-yyyy",
|
||||||
|
"dd-MM-yyyy"
|
||||||
|
];
|
||||||
|
|
||||||
|
[McpServerTool, Description("Search emails in Outlook by keywords, sender, subject, or date range. Returns matching emails with subject, sender, date, and body preview.")]
|
||||||
|
public static string SearchEmails(
|
||||||
|
[Description("Keywords to search for in email subject and body")] string? keywords = null,
|
||||||
|
[Description("Filter by sender email or name")] string? sender = null,
|
||||||
|
[Description("Filter by subject contains")] string? subject = null,
|
||||||
|
[Description("Number of days back to search (default 365)")] int daysBack = 365,
|
||||||
|
[Description("Maximum number of results to return (default 25)")] int maxResults = 25,
|
||||||
|
[Description("Number of results to skip for pagination (default 0)")] int offset = 0,
|
||||||
|
[Description("Outlook folder to search: Inbox, SentMail, Drafts, DeletedItems, Junk, or All (default All)")] string folder = "All",
|
||||||
|
[Description("Filter by attachment: 'true' for emails with attachments, 'false' for without, or filename to search")] string? hasAttachment = null,
|
||||||
|
[Description("Filter by importance: High, Normal, or Low")] string? importance = null,
|
||||||
|
[Description("Filter by category name")] string? category = null,
|
||||||
|
[Description("Filter by flag status: Flagged, Completed, or NotFlagged")] string? flagStatus = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var outlookApp = new OutlookApp();
|
||||||
|
var ns = outlookApp.GetNamespace("MAPI");
|
||||||
|
var foldersToSearch = GetFoldersToSearch(ns, folder);
|
||||||
|
var cutoffDate = DateTime.Now.AddDays(-daysBack);
|
||||||
|
|
||||||
|
var filters = new SearchFilters
|
||||||
|
{
|
||||||
|
Keywords = keywords,
|
||||||
|
Sender = sender,
|
||||||
|
Subject = subject,
|
||||||
|
HasAttachment = hasAttachment,
|
||||||
|
Importance = ParseImportance(importance),
|
||||||
|
Category = category,
|
||||||
|
FlagStatus = ParseFlagStatus(flagStatus)
|
||||||
|
};
|
||||||
|
|
||||||
|
var allResults = new List<EmailResult>();
|
||||||
|
foreach (var mailFolder in foldersToSearch)
|
||||||
|
{
|
||||||
|
SearchFolder(mailFolder, filters, cutoffDate, maxResults + offset, allResults);
|
||||||
|
if (allResults.Count >= maxResults + offset)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
var pagedResults = allResults.Skip(offset).Take(maxResults).ToList();
|
||||||
|
|
||||||
|
if (pagedResults.Count == 0)
|
||||||
|
return offset > 0
|
||||||
|
? $"No more emails found. Showing results {offset + 1}+ of {allResults.Count} total."
|
||||||
|
: "No emails found matching the search criteria.";
|
||||||
|
|
||||||
|
return FormatSearchResults(pagedResults, offset, allResults.Count);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
return $"Error searching emails: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description("Read the full body of a specific email by subject and date. Use after SearchEmails to get complete email content.")]
|
||||||
|
public static string ReadEmail(
|
||||||
|
[Description("Exact or partial subject line to match")] string subject,
|
||||||
|
[Description("Date of the email (supports: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy)")] string date,
|
||||||
|
[Description("Outlook folder: Inbox, SentMail, Drafts, DeletedItems, Junk, or All (default All)")] string folder = "All")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!TryParseDate(date, out var targetDate))
|
||||||
|
return $"Invalid date format '{date}'. Supported formats: yyyy-MM-dd, MM/dd/yyyy, dd/MM/yyyy";
|
||||||
|
|
||||||
|
using var outlookApp = new OutlookApp();
|
||||||
|
var ns = outlookApp.GetNamespace("MAPI");
|
||||||
|
var foldersToSearch = GetFoldersToSearch(ns, folder);
|
||||||
|
|
||||||
|
foreach (var mailFolder in foldersToSearch)
|
||||||
|
{
|
||||||
|
var mail = FindEmail(mailFolder, subject, targetDate);
|
||||||
|
if (mail != null)
|
||||||
|
return FormatFullEmail(mail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Email not found with the specified subject and date.";
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
return $"Error reading email: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<MAPIFolder> GetFoldersToSearch(_NameSpace ns, string folder)
|
||||||
|
{
|
||||||
|
var folders = new List<MAPIFolder>();
|
||||||
|
var folderMap = new Dictionary<string, OlDefaultFolders>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["Inbox"] = OlDefaultFolders.olFolderInbox,
|
||||||
|
["SentMail"] = OlDefaultFolders.olFolderSentMail,
|
||||||
|
["Sent"] = OlDefaultFolders.olFolderSentMail,
|
||||||
|
["Drafts"] = OlDefaultFolders.olFolderDrafts,
|
||||||
|
["DeletedItems"] = OlDefaultFolders.olFolderDeletedItems,
|
||||||
|
["Deleted"] = OlDefaultFolders.olFolderDeletedItems,
|
||||||
|
["Trash"] = OlDefaultFolders.olFolderDeletedItems,
|
||||||
|
["Junk"] = OlDefaultFolders.olFolderJunk,
|
||||||
|
["Spam"] = OlDefaultFolders.olFolderJunk
|
||||||
|
};
|
||||||
|
|
||||||
|
if (folder.Equals("All", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
folders.Add(ns.GetDefaultFolder(OlDefaultFolders.olFolderInbox));
|
||||||
|
folders.Add(ns.GetDefaultFolder(OlDefaultFolders.olFolderSentMail));
|
||||||
|
}
|
||||||
|
else if (folderMap.TryGetValue(folder, out var olFolder))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
folders.Add(ns.GetDefaultFolder(olFolder));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Folder may not exist (e.g., Archive on some configurations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MailItem? FindEmail(MAPIFolder folder, string subject, DateTime targetDate)
|
||||||
|
{
|
||||||
|
var items = folder.Items;
|
||||||
|
items.Sort("[ReceivedTime]", true);
|
||||||
|
|
||||||
|
// Use date filter for performance
|
||||||
|
var filter = $"[ReceivedTime] >= '{targetDate:MM/dd/yyyy}' AND [ReceivedTime] < '{targetDate.AddDays(1):MM/dd/yyyy}'";
|
||||||
|
var filteredItems = items.Restrict(filter);
|
||||||
|
|
||||||
|
foreach (var item in filteredItems)
|
||||||
|
{
|
||||||
|
if (item is MailItem mail &&
|
||||||
|
mail.Subject != null &&
|
||||||
|
mail.Subject.Contains(subject, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return mail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SearchFolder(
|
||||||
|
MAPIFolder folder,
|
||||||
|
SearchFilters filters,
|
||||||
|
DateTime cutoffDate,
|
||||||
|
int maxResults,
|
||||||
|
List<EmailResult> results)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = folder.Items;
|
||||||
|
items.Sort("[ReceivedTime]", true);
|
||||||
|
|
||||||
|
var filter = $"[ReceivedTime] >= '{cutoffDate:MM/dd/yyyy}'";
|
||||||
|
var filteredItems = items.Restrict(filter);
|
||||||
|
|
||||||
|
foreach (var item in filteredItems)
|
||||||
|
{
|
||||||
|
if (results.Count >= maxResults)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (item is MailItem mail && MatchesFilters(mail, filters))
|
||||||
|
{
|
||||||
|
results.Add(EmailResult.FromMailItem(mail, folder.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Skip folders that can't be accessed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesFilters(MailItem mail, SearchFilters filters)
|
||||||
|
{
|
||||||
|
// Sender filter
|
||||||
|
if (!string.IsNullOrEmpty(filters.Sender))
|
||||||
|
{
|
||||||
|
var senderMatch =
|
||||||
|
(mail.SenderName?.Contains(filters.Sender, StringComparison.OrdinalIgnoreCase) ?? false) ||
|
||||||
|
(mail.SenderEmailAddress?.Contains(filters.Sender, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||||
|
if (!senderMatch) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject filter
|
||||||
|
if (!string.IsNullOrEmpty(filters.Subject))
|
||||||
|
{
|
||||||
|
if (!(mail.Subject?.Contains(filters.Subject, StringComparison.OrdinalIgnoreCase) ?? false))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keywords filter
|
||||||
|
if (!string.IsNullOrEmpty(filters.Keywords))
|
||||||
|
{
|
||||||
|
var keywordMatch =
|
||||||
|
(mail.Subject?.Contains(filters.Keywords, StringComparison.OrdinalIgnoreCase) ?? false) ||
|
||||||
|
(mail.Body?.Contains(filters.Keywords, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||||
|
if (!keywordMatch) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment filter
|
||||||
|
if (!string.IsNullOrEmpty(filters.HasAttachment))
|
||||||
|
{
|
||||||
|
if (filters.HasAttachment.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (mail.Attachments.Count == 0) return false;
|
||||||
|
}
|
||||||
|
else if (filters.HasAttachment.Equals("false", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (mail.Attachments.Count > 0) return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Search for attachment by filename
|
||||||
|
var hasMatchingAttachment = false;
|
||||||
|
foreach (var attachment in mail.Attachments)
|
||||||
|
{
|
||||||
|
if (attachment is Attachment att &&
|
||||||
|
att.FileName.Contains(filters.HasAttachment, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
hasMatchingAttachment = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasMatchingAttachment) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Importance filter
|
||||||
|
if (filters.Importance.HasValue)
|
||||||
|
{
|
||||||
|
if (mail.Importance != filters.Importance.Value) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category filter
|
||||||
|
if (!string.IsNullOrEmpty(filters.Category))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(mail.Categories)) return false;
|
||||||
|
if (!mail.Categories.Contains(filters.Category, StringComparison.OrdinalIgnoreCase)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag status filter
|
||||||
|
if (filters.FlagStatus.HasValue)
|
||||||
|
{
|
||||||
|
if (mail.FlagStatus != filters.FlagStatus.Value) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatSearchResults(List<EmailResult> results, int offset, int totalCount)
|
||||||
|
{
|
||||||
|
var output = new StringBuilder();
|
||||||
|
|
||||||
|
if (offset > 0 || totalCount > results.Count)
|
||||||
|
output.AppendLine($"Showing {offset + 1}-{offset + results.Count} of {totalCount} email(s):");
|
||||||
|
else
|
||||||
|
output.AppendLine($"Found {results.Count} email(s):");
|
||||||
|
|
||||||
|
output.AppendLine();
|
||||||
|
|
||||||
|
for (int i = 0; i < results.Count; i++)
|
||||||
|
{
|
||||||
|
var email = results[i];
|
||||||
|
output.AppendLine($"--- Email {offset + i + 1} ---");
|
||||||
|
output.AppendLine($"Subject: {email.Subject}");
|
||||||
|
output.AppendLine($"From: {email.Sender}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(email.CC))
|
||||||
|
output.AppendLine($"CC: {email.CC}");
|
||||||
|
if (!string.IsNullOrEmpty(email.BCC))
|
||||||
|
output.AppendLine($"BCC: {email.BCC}");
|
||||||
|
|
||||||
|
output.AppendLine($"Date: {email.ReceivedDate:yyyy-MM-dd HH:mm}");
|
||||||
|
output.AppendLine($"Folder: {email.Folder}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(email.Importance) && email.Importance != "Normal")
|
||||||
|
output.AppendLine($"Importance: {email.Importance}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(email.Categories))
|
||||||
|
output.AppendLine($"Categories: {email.Categories}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(email.FlagStatus) && email.FlagStatus != "NotFlagged")
|
||||||
|
output.AppendLine($"Flag: {email.FlagStatus}");
|
||||||
|
|
||||||
|
if (email.AttachmentCount > 0)
|
||||||
|
output.AppendLine($"Attachments ({email.AttachmentCount}): {email.AttachmentNames}");
|
||||||
|
|
||||||
|
output.AppendLine($"Preview: {email.BodyPreview}");
|
||||||
|
output.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatFullEmail(MailItem mail)
|
||||||
|
{
|
||||||
|
var output = new StringBuilder();
|
||||||
|
output.AppendLine($"Subject: {mail.Subject}");
|
||||||
|
output.AppendLine($"From: {mail.SenderName} <{mail.SenderEmailAddress}>");
|
||||||
|
output.AppendLine($"To: {mail.To}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(mail.CC))
|
||||||
|
output.AppendLine($"CC: {mail.CC}");
|
||||||
|
if (!string.IsNullOrEmpty(mail.BCC))
|
||||||
|
output.AppendLine($"BCC: {mail.BCC}");
|
||||||
|
|
||||||
|
output.AppendLine($"Date: {mail.ReceivedTime:yyyy-MM-dd HH:mm}");
|
||||||
|
|
||||||
|
if (mail.Importance != OlImportance.olImportanceNormal)
|
||||||
|
output.AppendLine($"Importance: {mail.Importance.ToString().Replace("olImportance", "")}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(mail.Categories))
|
||||||
|
output.AppendLine($"Categories: {mail.Categories}");
|
||||||
|
|
||||||
|
var attachmentNames = GetAttachmentNames(mail);
|
||||||
|
if (attachmentNames.Count > 0)
|
||||||
|
output.AppendLine($"Attachments ({attachmentNames.Count}): {string.Join(", ", attachmentNames)}");
|
||||||
|
|
||||||
|
output.AppendLine("---");
|
||||||
|
output.AppendLine();
|
||||||
|
output.Append(mail.Body);
|
||||||
|
|
||||||
|
return output.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> GetAttachmentNames(MailItem mail)
|
||||||
|
{
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var attachment in mail.Attachments)
|
||||||
|
{
|
||||||
|
if (attachment is Attachment att)
|
||||||
|
names.Add(att.FileName);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseDate(string dateString, out DateTime result)
|
||||||
|
{
|
||||||
|
return DateTime.TryParseExact(
|
||||||
|
dateString,
|
||||||
|
SupportedDateFormats,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None,
|
||||||
|
out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OlImportance? ParseImportance(string? importance)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(importance)) return null;
|
||||||
|
|
||||||
|
return importance.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"high" => OlImportance.olImportanceHigh,
|
||||||
|
"low" => OlImportance.olImportanceLow,
|
||||||
|
"normal" => OlImportance.olImportanceNormal,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OlFlagStatus? ParseFlagStatus(string? flagStatus)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(flagStatus)) return null;
|
||||||
|
|
||||||
|
return flagStatus.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"flagged" or "marked" => OlFlagStatus.olFlagMarked,
|
||||||
|
"completed" or "complete" => OlFlagStatus.olFlagComplete,
|
||||||
|
"notflagged" or "none" or "clear" => OlFlagStatus.olNoFlag,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
12
EmailSearch/Program.cs
Normal file
12
EmailSearch/Program.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
builder.Services
|
||||||
|
.AddMcpServer()
|
||||||
|
.WithStdioServerTransport()
|
||||||
|
.WithTools<EmailSearchTools>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
await app.RunAsync();
|
||||||
Reference in New Issue
Block a user