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:
2025-12-20 11:43:26 -05:00
parent b2fd117b83
commit 01c6c3f0ec
2 changed files with 409 additions and 0 deletions

View 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
View 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();