Compare commits
3 Commits
01013b90c6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 09181d0991 | |||
| dafc1d6866 | |||
| 0094b5ea56 |
36
CLAUDE.md
Normal file
36
CLAUDE.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the project
|
||||||
|
dotnet build EmailSearch/EmailSearch.csproj
|
||||||
|
|
||||||
|
# Build release version
|
||||||
|
dotnet build EmailSearch/EmailSearch.csproj -c Release
|
||||||
|
|
||||||
|
# Run the MCP server (connects via stdio)
|
||||||
|
dotnet run --project EmailSearch/EmailSearch.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is an MCP (Model Context Protocol) server that provides Outlook email search capabilities to LLM clients. It runs as a stdio-based server using the Microsoft.Extensions.Hosting pattern.
|
||||||
|
|
||||||
|
**Key Components:**
|
||||||
|
|
||||||
|
- `Program.cs` - Entry point that configures the MCP server with stdio transport and registers `EmailSearchTools`
|
||||||
|
- `EmailSearchTools.cs` - MCP tool implementations decorated with `[McpServerTool]`:
|
||||||
|
- `SearchEmails` - Search emails with filters (keywords, sender, subject, date range, folder, attachments, importance, category, flag status)
|
||||||
|
- `ReadEmail` - Retrieve full email body by subject and date
|
||||||
|
- `SearchFilters.cs` - Filter parameter container for email searches
|
||||||
|
- `EmailResult.cs` - DTO for search results with factory method `FromMailItem()`
|
||||||
|
|
||||||
|
**Dependencies:**
|
||||||
|
|
||||||
|
- `ModelContextProtocol` - MCP SDK for .NET
|
||||||
|
- `NetOfficeFw.Outlook` - COM interop wrapper for Outlook automation
|
||||||
|
|
||||||
|
**Platform:** Windows-only (.NET 9.0-windows) due to Outlook COM dependency
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.14" />
|
<PackageReference Include="ModelContextProtocol" Version="0.1.0-preview.14" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||||
<PackageReference Include="NetOfficeFw.Outlook" Version="1.9.7" />
|
<PackageReference Include="NetOfficeFw.Outlook" Version="1.9.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class EmailSearchTools
|
|||||||
[Description("Number of days back to search (default 365)")] int daysBack = 365,
|
[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("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("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("Outlook folder to search: Inbox, SentMail, Drafts, DeletedItems, Junk, All, or any custom folder name (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 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 importance: High, Normal, or Low")] string? importance = null,
|
||||||
[Description("Filter by category name")] string? category = null,
|
[Description("Filter by category name")] string? category = null,
|
||||||
@@ -79,7 +79,7 @@ public class EmailSearchTools
|
|||||||
public static string ReadEmail(
|
public static string ReadEmail(
|
||||||
[Description("Exact or partial subject line to match")] string subject,
|
[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("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")
|
[Description("Outlook folder: Inbox, SentMail, Drafts, DeletedItems, Junk, All, or any custom folder name (default All)")] string folder = "All")
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -125,6 +125,9 @@ public class EmailSearchTools
|
|||||||
{
|
{
|
||||||
folders.Add(ns.GetDefaultFolder(OlDefaultFolders.olFolderInbox));
|
folders.Add(ns.GetDefaultFolder(OlDefaultFolders.olFolderInbox));
|
||||||
folders.Add(ns.GetDefaultFolder(OlDefaultFolders.olFolderSentMail));
|
folders.Add(ns.GetDefaultFolder(OlDefaultFolders.olFolderSentMail));
|
||||||
|
// Also search subfolders of Inbox for "All"
|
||||||
|
var inbox = ns.GetDefaultFolder(OlDefaultFolders.olFolderInbox);
|
||||||
|
AddSubfolders(inbox, folders);
|
||||||
}
|
}
|
||||||
else if (folderMap.TryGetValue(folder, out var olFolder))
|
else if (folderMap.TryGetValue(folder, out var olFolder))
|
||||||
{
|
{
|
||||||
@@ -137,10 +140,74 @@ public class EmailSearchTools
|
|||||||
// Folder may not exist (e.g., Archive on some configurations)
|
// Folder may not exist (e.g., Archive on some configurations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Search for custom folder by name
|
||||||
|
var customFolder = FindFolderByName(ns, folder);
|
||||||
|
if (customFolder != null)
|
||||||
|
{
|
||||||
|
folders.Add(customFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return folders;
|
return folders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MAPIFolder? FindFolderByName(_NameSpace ns, string folderName)
|
||||||
|
{
|
||||||
|
// Search through all accounts/stores
|
||||||
|
foreach (var store in ns.Stores)
|
||||||
|
{
|
||||||
|
if (store is Store s)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rootFolder = s.GetRootFolder() as MAPIFolder;
|
||||||
|
if (rootFolder != null)
|
||||||
|
{
|
||||||
|
var found = SearchFolderRecursive(rootFolder, folderName);
|
||||||
|
if (found != null)
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Skip stores that can't be accessed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MAPIFolder? SearchFolderRecursive(MAPIFolder parent, string folderName)
|
||||||
|
{
|
||||||
|
foreach (var subfolder in parent.Folders)
|
||||||
|
{
|
||||||
|
if (subfolder is MAPIFolder folder)
|
||||||
|
{
|
||||||
|
if (folder.Name.Equals(folderName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return folder;
|
||||||
|
|
||||||
|
var found = SearchFolderRecursive(folder, folderName);
|
||||||
|
if (found != null)
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddSubfolders(MAPIFolder parent, List<MAPIFolder> folders)
|
||||||
|
{
|
||||||
|
foreach (var subfolder in parent.Folders)
|
||||||
|
{
|
||||||
|
if (subfolder is MAPIFolder folder)
|
||||||
|
{
|
||||||
|
folders.Add(folder);
|
||||||
|
AddSubfolders(folder, folders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static MailItem? FindEmail(MAPIFolder folder, string subject, DateTime targetDate)
|
private static MailItem? FindEmail(MAPIFolder folder, string subject, DateTime targetDate)
|
||||||
{
|
{
|
||||||
var items = folder.Items;
|
var items = folder.Items;
|
||||||
|
|||||||
Reference in New Issue
Block a user