# MoneyMap MCP Server Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Extract shared services into MoneyMap.Core class library and create MoneyMap.Mcp console app exposing financial data/operations via MCP protocol. **Architecture:** Three-project solution — MoneyMap.Core (shared library with models, DbContext, services), MoneyMap (web app referencing Core), and MoneyMap.Mcp (MCP stdio console app referencing Core). ReceiptManager's IWebHostEnvironment dependency abstracted to IReceiptStorageOptions interface. **Tech Stack:** .NET 8.0, EF Core 9.0, ModelContextProtocol SDK (prerelease), SQL Server, Magick.NET, stdio transport. **Spec:** `docs/superpowers/specs/2026-04-20-moneymap-mcp-design.md` --- ## Task 1: Create MoneyMap.Core Project and Move Models **Files:** - Create: `MoneyMap.Core/MoneyMap.Core.csproj` - Move: `MoneyMap/Models/*.cs` → `MoneyMap.Core/Models/` - Move: `MoneyMap/Models/Api/*.cs` → `MoneyMap.Core/Models/Api/` - Move: `MoneyMap/Models/Dashboard/*.cs` → `MoneyMap.Core/Models/Dashboard/` - Move: `MoneyMap/Models/Import/*.cs` → `MoneyMap.Core/Models/Import/` - Move: `MoneyMap/Data/MoneyMapContext.cs` → `MoneyMap.Core/Data/` - Modify: `MoneyMap.sln` (add new project) - Modify: `MoneyMap/MoneyMap.csproj` (add ProjectReference to Core, remove moved packages) - [ ] **Step 1: Create MoneyMap.Core class library project** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet new classlib -n MoneyMap.Core --framework net8.0 rm MoneyMap.Core/Class1.cs ``` - [ ] **Step 2: Add NuGet packages to MoneyMap.Core** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap/MoneyMap.Core dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.9 dotnet add package CsvHelper --version 33.1.0 dotnet add package Magick.NET-Q16-AnyCPU --version 14.8.2 dotnet add package PdfPig --version 0.1.11 dotnet add package Microsoft.Extensions.Caching.Memory dotnet add package Microsoft.Extensions.Http ``` - [ ] **Step 3: Enable nullable and implicit usings in MoneyMap.Core.csproj** Edit `MoneyMap.Core/MoneyMap.Core.csproj` to contain: ```xml net8.0 enable enable ``` - [ ] **Step 4: Move Models to MoneyMap.Core** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap mkdir -p MoneyMap.Core/Models/Api MoneyMap.Core/Models/Dashboard MoneyMap.Core/Models/Import mv MoneyMap/Models/Account.cs MoneyMap.Core/Models/ mv MoneyMap/Models/Budget.cs MoneyMap.Core/Models/ mv MoneyMap/Models/Card.cs MoneyMap.Core/Models/ mv MoneyMap/Models/CategoryMapping.cs MoneyMap.Core/Models/ mv MoneyMap/Models/Merchant.cs MoneyMap.Core/Models/ mv MoneyMap/Models/Receipt.cs MoneyMap.Core/Models/ mv MoneyMap/Models/ReceiptLineItem.cs MoneyMap.Core/Models/ mv MoneyMap/Models/ReceiptParseLog.cs MoneyMap.Core/Models/ mv MoneyMap/Models/Transaction.cs MoneyMap.Core/Models/ mv MoneyMap/Models/Transfer.cs MoneyMap.Core/Models/ mv MoneyMap/Models/Api/FinancialAuditModels.cs MoneyMap.Core/Models/Api/ mv MoneyMap/Models/Dashboard/DashboardModels.cs MoneyMap.Core/Models/Dashboard/ mv MoneyMap/Models/Import/ImportContext.cs MoneyMap.Core/Models/Import/ mv MoneyMap/Models/Import/ImportResults.cs MoneyMap.Core/Models/Import/ mv MoneyMap/Models/Import/PaymentResolutionResult.cs MoneyMap.Core/Models/Import/ mv MoneyMap/Models/Import/TransactionCsvRow.cs MoneyMap.Core/Models/Import/ mv MoneyMap/Models/Import/TransactionCsvRowMap.cs MoneyMap.Core/Models/Import/ ``` - [ ] **Step 5: Move Data folder to MoneyMap.Core** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap mkdir -p MoneyMap.Core/Data mv MoneyMap/Data/MoneyMapContext.cs MoneyMap.Core/Data/ ``` - [ ] **Step 6: Add MoneyMap.Core to the solution and add ProjectReference from MoneyMap** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet sln MoneyMap.sln add MoneyMap.Core/MoneyMap.Core.csproj ``` Add ProjectReference in `MoneyMap/MoneyMap.csproj`: ```xml ``` Remove from `MoneyMap/MoneyMap.csproj` the packages that moved to Core: - `CsvHelper` - `Magick.NET-Q16-AnyCPU` - `Microsoft.EntityFrameworkCore` (keep `Microsoft.EntityFrameworkCore.Tools` for migrations) - `Microsoft.EntityFrameworkCore.SqlServer` - `PdfPig` Keep `Microsoft.EntityFrameworkCore.Tools` (needed for migrations). - [ ] **Step 7: Update MoneyMap.Tests to reference MoneyMap.Core** In `MoneyMap.Tests/MoneyMap.Tests.csproj`, add: ```xml ``` Also add `Microsoft.EntityFrameworkCore.InMemory` remains (it's test-specific). - [ ] **Step 8: Build and fix any namespace/reference issues** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.sln ``` Fix any issues — namespaces remain unchanged (`MoneyMap.Models`, `MoneyMap.Data`, etc.) so references should resolve through the ProjectReference. The web project may need the `Migrations` folder to reference the design-time DbContext from Core. Add to `MoneyMap/MoneyMap.csproj` if migrations break: ```xml all runtime; build; native; contentfiles; analyzers; buildtransitive ``` - [ ] **Step 9: Run tests to verify nothing broke** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj ``` Expected: All existing tests pass. - [ ] **Step 10: Commit** ```bash git add -A git commit -m "refactor: extract Models and Data into MoneyMap.Core shared library" ``` --- ## Task 2: Move Services to MoneyMap.Core **Files:** - Move: `MoneyMap/Services/*.cs` → `MoneyMap.Core/Services/` - Move: `MoneyMap/Services/AITools/*.cs` → `MoneyMap.Core/Services/AITools/` - Keep in web: `ReceiptParseWorkerService.cs`, `ModelWarmupService.cs` (hosted services) - [ ] **Step 1: Move core services to MoneyMap.Core** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap mkdir -p MoneyMap.Core/Services/AITools # Core services (no web dependencies) mv MoneyMap/Services/AccountService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/BudgetService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/CardResolver.cs MoneyMap.Core/Services/ mv MoneyMap/Services/CardService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/DashboardService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/FinancialAuditService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/MerchantService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/ReceiptAutoMapper.cs MoneyMap.Core/Services/ mv MoneyMap/Services/ReceiptMatchingService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/ReferenceDataService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/TransactionCategorizer.cs MoneyMap.Core/Services/ mv MoneyMap/Services/TransactionFilters.cs MoneyMap.Core/Services/ mv MoneyMap/Services/TransactionImporter.cs MoneyMap.Core/Services/ mv MoneyMap/Services/TransactionService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/TransactionStatisticsService.cs MoneyMap.Core/Services/ mv MoneyMap/Services/TransactionAICategorizer.cs MoneyMap.Core/Services/ mv MoneyMap/Services/PdfToImageConverter.cs MoneyMap.Core/Services/ mv MoneyMap/Services/AIReceiptParser.cs MoneyMap.Core/Services/ mv MoneyMap/Services/AIVisionClient.cs MoneyMap.Core/Services/ mv MoneyMap/Services/ReceiptManager.cs MoneyMap.Core/Services/ mv MoneyMap/Services/ReceiptParseQueue.cs MoneyMap.Core/Services/ # AI Tools mv MoneyMap/Services/AITools/AIToolDefinitions.cs MoneyMap.Core/Services/AITools/ mv MoneyMap/Services/AITools/AIToolExecutor.cs MoneyMap.Core/Services/AITools/ ``` - [ ] **Step 2: Keep hosted services in web project** These stay in `MoneyMap/Services/`: - `ReceiptParseWorkerService.cs` — BackgroundService tied to web lifecycle - `ModelWarmupService.cs` — startup hosted service Verify they're still there: ```bash ls MoneyMap/Services/ ``` Expected: Only `ReceiptParseWorkerService.cs` and `ModelWarmupService.cs` remain. - [ ] **Step 3: Move the Prompts folder to MoneyMap.Core** The `AIReceiptParser` reads `Prompts/ReceiptParserPrompt.txt` from `AppContext.BaseDirectory`. This needs to be embedded or copied with MoneyMap.Core. ```bash mv MoneyMap/Prompts MoneyMap.Core/Prompts ``` Add to `MoneyMap.Core/MoneyMap.Core.csproj`: ```xml PreserveNewest ``` Remove from `MoneyMap/MoneyMap.csproj` the Prompts section: ```xml PreserveNewest ``` - [ ] **Step 4: Build and fix issues** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.sln ``` Likely issues: - `ReceiptManager` uses `IFormFile` (from `Microsoft.AspNetCore.Http`) — need to add this package to Core or abstract it. Since `IFormFile` is in `Microsoft.AspNetCore.Http.Features`, add to `MoneyMap.Core.csproj`: ```xml ``` **Wait** — this contradicts the spec's decision #9 (no ASP.NET deps in Core). The spec says to abstract `IWebHostEnvironment` via `IReceiptStorageOptions`, but `IFormFile` is also ASP.NET-specific. Since `ReceiptManager`'s upload methods (`UploadReceiptAsync`, `UploadUnmappedReceiptAsync`, `UploadManyUnmappedReceiptsAsync`) use `IFormFile` extensively and are only called from the web app, the cleanest approach is: **Split ReceiptManager into two parts:** 1. `MoneyMap.Core/Services/ReceiptManager.cs` — read-only operations (GetReceiptPhysicalPath, GetReceiptAsync, DeleteReceiptAsync, MapReceiptToTransactionAsync, UnmapReceiptAsync, duplicate checks) 2. `MoneyMap/Services/ReceiptUploadService.cs` — upload operations using IFormFile (stays in web project) But this is complex. Simpler: add the `FrameworkReference` to Core. It's a reference, not a deployment dependency — the MCP console app won't actually call upload methods. The alternative (splitting) adds complexity for no runtime benefit. **Decision: Add `` to MoneyMap.Core.** The MCP console app will need to declare it doesn't serve web traffic, but the types compile fine. For a local-only MCP tool this is acceptable. Actually — on .NET 8 console apps, you CAN reference `Microsoft.AspNetCore.App` shared framework. It's available regardless. This is fine. - [ ] **Step 5: Run tests** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj ``` Expected: All tests pass. - [ ] **Step 6: Commit** ```bash git add -A git commit -m "refactor: move services and AITools to MoneyMap.Core" ``` --- ## Task 3: Abstract IWebHostEnvironment from ReceiptManager **Files:** - Create: `MoneyMap.Core/Services/IReceiptStorageOptions.cs` - Modify: `MoneyMap.Core/Services/ReceiptManager.cs` (replace IWebHostEnvironment with IReceiptStorageOptions) - Create: `MoneyMap/Services/WebReceiptStorageOptions.cs` - Modify: `MoneyMap/Program.cs` (register WebReceiptStorageOptions) - [ ] **Step 1: Create IReceiptStorageOptions interface in Core** Create `MoneyMap.Core/Services/IReceiptStorageOptions.cs`: ```csharp namespace MoneyMap.Services; public interface IReceiptStorageOptions { string ReceiptsBasePath { get; } } ``` - [ ] **Step 2: Update ReceiptManager to use IReceiptStorageOptions** In `MoneyMap.Core/Services/ReceiptManager.cs`: Replace the constructor parameter `IWebHostEnvironment environment` with `IReceiptStorageOptions receiptStorage`. Replace the field: ```csharp private readonly IWebHostEnvironment _environment; ``` with: ```csharp private readonly IReceiptStorageOptions _receiptStorage; ``` Replace the constructor assignment: ```csharp _environment = environment; ``` with: ```csharp _receiptStorage = receiptStorage; ``` Replace `GetReceiptsBasePath()`: ```csharp private string GetReceiptsBasePath() { return _receiptStorage.ReceiptsBasePath; } ``` Remove the `IConfiguration _configuration` field and constructor parameter (if only used for receipts path). Check if `_configuration` is used elsewhere in ReceiptManager first — if yes, keep it. - [ ] **Step 3: Create WebReceiptStorageOptions in web project** Create `MoneyMap/Services/WebReceiptStorageOptions.cs`: ```csharp using MoneyMap.Services; namespace MoneyMap.WebApp.Services; public class WebReceiptStorageOptions : IReceiptStorageOptions { public string ReceiptsBasePath { get; } public WebReceiptStorageOptions(IWebHostEnvironment env, IConfiguration config) { var relativePath = config["Receipts:StoragePath"] ?? "receipts"; // If the path is absolute (UNC or rooted), use directly; otherwise combine with wwwroot if (Path.IsPathRooted(relativePath)) ReceiptsBasePath = relativePath; else ReceiptsBasePath = Path.Combine(env.WebRootPath, relativePath); } } ``` - [ ] **Step 4: Register in web app's Program.cs** Add to `MoneyMap/Program.cs`: ```csharp using MoneyMap.WebApp.Services; // After other service registrations: builder.Services.AddSingleton(); ``` - [ ] **Step 5: Build and run tests** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.sln dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj ``` Expected: All tests pass. - [ ] **Step 6: Commit** ```bash git add -A git commit -m "refactor: abstract IWebHostEnvironment to IReceiptStorageOptions" ``` --- ## Task 4: Create ServiceCollectionExtensions in MoneyMap.Core **Files:** - Create: `MoneyMap.Core/ServiceCollectionExtensions.cs` - Modify: `MoneyMap/Program.cs` (replace individual registrations with AddMoneyMapCore call) - [ ] **Step 1: Create the extension method** Create `MoneyMap.Core/ServiceCollectionExtensions.cs`: ```csharp using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MoneyMap.Data; using MoneyMap.Services; using MoneyMap.Services.AITools; namespace MoneyMap.Core; public static class ServiceCollectionExtensions { public static IServiceCollection AddMoneyMapCore( this IServiceCollection services, IConfiguration configuration) { services.AddDbContext(options => options.UseSqlServer(configuration.GetConnectionString("MoneyMapDb"))); services.AddMemoryCache(); // Core transaction and import services services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Entity management services services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Receipt services services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Reference data and dashboard services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // AI services services.AddScoped(); services.AddScoped(); return services; } } ``` - [ ] **Step 2: Update web app Program.cs to use AddMoneyMapCore** Replace the individual service registrations in `MoneyMap/Program.cs` with: ```csharp using MoneyMap.Core; using MoneyMap.WebApp.Services; // ... after builder creation ... builder.Services.AddMoneyMapCore(builder.Configuration); builder.Services.AddSingleton(); // Web-specific services that stay here: builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient(); builder.Services.AddHostedService(); ``` Remove all the individual core service registrations that are now in `AddMoneyMapCore`. - [ ] **Step 3: Build and run tests** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.sln dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj ``` - [ ] **Step 4: Quick smoke test the web app** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap/MoneyMap dotnet run ``` Verify it starts without errors, then stop it (Ctrl+C). - [ ] **Step 5: Commit** ```bash git add -A git commit -m "refactor: consolidate service registration into AddMoneyMapCore extension" ``` --- ## Task 5: Create MoneyMap.Mcp Project Skeleton **Files:** - Create: `MoneyMap.Mcp/MoneyMap.Mcp.csproj` - Create: `MoneyMap.Mcp/Program.cs` - Create: `MoneyMap.Mcp/ConfigReceiptStorageOptions.cs` - Create: `MoneyMap.Mcp/appsettings.json` - Modify: `MoneyMap.sln` (add project) - [ ] **Step 1: Create the console project** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet new console -n MoneyMap.Mcp --framework net8.0 ``` - [ ] **Step 2: Set up MoneyMap.Mcp.csproj** Replace `MoneyMap.Mcp/MoneyMap.Mcp.csproj` with: ```xml Exe net8.0 enable enable PreserveNewest ``` - [ ] **Step 3: Add to solution** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet sln MoneyMap.sln add MoneyMap.Mcp/MoneyMap.Mcp.csproj ``` - [ ] **Step 4: Create appsettings.json** Create `MoneyMap.Mcp/appsettings.json`: ```json { "ConnectionStrings": { "MoneyMapDb": "Server=barge.lan;Database=MoneyMap;User Id=moneymap;Password=Cn87oXQPj7EEkx;TrustServerCertificate=True;" }, "Receipts": { "StoragePath": "\\\\TRUENAS\\receipts" } } ``` - [ ] **Step 5: Create ConfigReceiptStorageOptions** Create `MoneyMap.Mcp/ConfigReceiptStorageOptions.cs`: ```csharp using MoneyMap.Services; namespace MoneyMap.Mcp; public class ConfigReceiptStorageOptions : IReceiptStorageOptions { public string ReceiptsBasePath { get; } public ConfigReceiptStorageOptions(IConfiguration config) { ReceiptsBasePath = config["Receipts:StoragePath"] ?? throw new InvalidOperationException("Receipts:StoragePath not configured"); } } ``` - [ ] **Step 6: Create Program.cs** Replace `MoneyMap.Mcp/Program.cs` with: ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; using MoneyMap.Core; using MoneyMap.Mcp; using MoneyMap.Services; var builder = Host.CreateApplicationBuilder(args); builder.Logging.ClearProviders(); builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace); builder.Services.AddMoneyMapCore(builder.Configuration); builder.Services.AddSingleton(); builder.Services .AddMcpServer() .WithStdioServerTransport() .WithToolsFromAssembly(typeof(Program).Assembly); var app = builder.Build(); await app.RunAsync(); ``` - [ ] **Step 7: Build to verify skeleton compiles** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj ``` - [ ] **Step 8: Commit** ```bash git add -A git commit -m "feat: add MoneyMap.Mcp project skeleton with stdio transport" ``` --- ## Task 6: Implement Transaction Tools **Files:** - Create: `MoneyMap.Mcp/Tools/TransactionTools.cs` - [ ] **Step 1: Create TransactionTools.cs** Create `MoneyMap.Mcp/Tools/TransactionTools.cs`: ```csharp using System.ComponentModel; using System.Text.Json; using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; using MoneyMap.Data; using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; public static class TransactionTools { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; [McpServerTool, Description("Search and filter transactions. Returns matching transactions with details.")] public static async Task SearchTransactions( [Description("Full-text search across name, memo, and category")] string? query = null, [Description("Start date (inclusive), e.g. 2026-01-01")] string? startDate = null, [Description("End date (inclusive), e.g. 2026-01-31")] string? endDate = null, [Description("Filter by category name (exact match)")] string? category = null, [Description("Filter by merchant name (contains)")] string? merchantName = null, [Description("Minimum amount (absolute value)")] decimal? minAmount = null, [Description("Maximum amount (absolute value)")] decimal? maxAmount = null, [Description("Filter by account ID")] int? accountId = null, [Description("Filter by card ID")] int? cardId = null, [Description("Filter by type: 'debit' or 'credit'")] string? type = null, [Description("Only show uncategorized transactions")] bool? uncategorizedOnly = null, [Description("Max results to return (default 50)")] int? limit = null, MoneyMapContext db = default!) { var q = db.Transactions .Include(t => t.Merchant) .Include(t => t.Card) .Include(t => t.Account) .Include(t => t.Receipts) .AsQueryable(); if (!string.IsNullOrWhiteSpace(query)) q = q.Where(t => t.Name.Contains(query) || (t.Memo != null && t.Memo.Contains(query)) || (t.Category != null && t.Category.Contains(query))); if (!string.IsNullOrWhiteSpace(startDate) && DateTime.TryParse(startDate, out var start)) q = q.Where(t => t.Date >= start); if (!string.IsNullOrWhiteSpace(endDate) && DateTime.TryParse(endDate, out var end)) q = q.Where(t => t.Date <= end); if (!string.IsNullOrWhiteSpace(category)) q = q.Where(t => t.Category == category); if (!string.IsNullOrWhiteSpace(merchantName)) q = q.Where(t => t.Merchant != null && t.Merchant.Name.Contains(merchantName)); if (minAmount.HasValue) q = q.Where(t => Math.Abs(t.Amount) >= minAmount.Value); if (maxAmount.HasValue) q = q.Where(t => Math.Abs(t.Amount) <= maxAmount.Value); if (accountId.HasValue) q = q.Where(t => t.AccountId == accountId.Value); if (cardId.HasValue) q = q.Where(t => t.CardId == cardId.Value); if (type?.ToLower() == "debit") q = q.Where(t => t.Amount < 0); else if (type?.ToLower() == "credit") q = q.Where(t => t.Amount > 0); if (uncategorizedOnly == true) q = q.Where(t => t.Category == null || t.Category == ""); var results = await q .OrderByDescending(t => t.Date).ThenByDescending(t => t.Id) .Take(limit ?? 50) .Select(t => new { t.Id, t.Date, t.Name, t.Memo, t.Amount, t.Category, Merchant = t.Merchant != null ? t.Merchant.Name : null, Account = t.Account.Issuer + " " + t.Account.Last4, Card = t.Card != null ? t.Card.Issuer + " " + t.Card.Last4 : null, ReceiptCount = t.Receipts.Count, t.TransferToAccountId }) .ToListAsync(); return JsonSerializer.Serialize(new { Count = results.Count, Transactions = results }, JsonOptions); } [McpServerTool, Description("Get a single transaction with all details including receipts.")] public static async Task GetTransaction( [Description("Transaction ID")] long transactionId, MoneyMapContext db = default!) { var t = await db.Transactions .Include(t => t.Merchant) .Include(t => t.Card) .Include(t => t.Account) .Include(t => t.Receipts) .Where(t => t.Id == transactionId) .Select(t => new { t.Id, t.Date, t.Name, t.Memo, t.Amount, t.TransactionType, t.Category, Merchant = t.Merchant != null ? t.Merchant.Name : null, MerchantId = t.MerchantId, Account = t.Account.Issuer + " " + t.Account.Last4, AccountId = t.AccountId, Card = t.Card != null ? t.Card.Issuer + " " + t.Card.Last4 : null, CardId = t.CardId, t.Notes, t.TransferToAccountId, Receipts = t.Receipts.Select(r => new { r.Id, r.FileName, r.ParseStatus, r.Merchant, r.Total }).ToList() }) .FirstOrDefaultAsync(); if (t == null) return "Transaction not found"; return JsonSerializer.Serialize(t, JsonOptions); } [McpServerTool, Description("Get spending totals grouped by category for a date range. Excludes transfers.")] public static async Task GetSpendingSummary( [Description("Start date (inclusive), e.g. 2026-01-01")] string startDate, [Description("End date (inclusive), e.g. 2026-01-31")] string endDate, [Description("Filter to specific account ID")] int? accountId = null, MoneyMapContext db = default!) { var start = DateTime.Parse(startDate); var end = DateTime.Parse(endDate); var q = db.Transactions .Where(t => t.Date >= start && t.Date <= end) .Where(t => t.Amount < 0) .Where(t => t.TransferToAccountId == null) .ExcludeTransfers(); if (accountId.HasValue) q = q.Where(t => t.AccountId == accountId.Value); var summary = await q .GroupBy(t => t.Category ?? "Uncategorized") .Select(g => new { Category = g.Key, Total = g.Sum(t => Math.Abs(t.Amount)), Count = g.Count() }) .OrderByDescending(x => x.Total) .ToListAsync(); var grandTotal = summary.Sum(x => x.Total); return JsonSerializer.Serialize(new { Period = $"{startDate} to {endDate}", GrandTotal = grandTotal, Categories = summary }, JsonOptions); } [McpServerTool, Description("Get income (credits) grouped by source/name for a date range.")] public static async Task GetIncomeSummary( [Description("Start date (inclusive), e.g. 2026-01-01")] string startDate, [Description("End date (inclusive), e.g. 2026-01-31")] string endDate, [Description("Filter to specific account ID")] int? accountId = null, MoneyMapContext db = default!) { var start = DateTime.Parse(startDate); var end = DateTime.Parse(endDate); var q = db.Transactions .Where(t => t.Date >= start && t.Date <= end) .Where(t => t.Amount > 0) .Where(t => t.TransferToAccountId == null) .ExcludeTransfers(); if (accountId.HasValue) q = q.Where(t => t.AccountId == accountId.Value); var summary = await q .GroupBy(t => t.Name) .Select(g => new { Source = g.Key, Total = g.Sum(t => t.Amount), Count = g.Count() }) .OrderByDescending(x => x.Total) .ToListAsync(); var grandTotal = summary.Sum(x => x.Total); return JsonSerializer.Serialize(new { Period = $"{startDate} to {endDate}", GrandTotal = grandTotal, Sources = summary }, JsonOptions); } [McpServerTool, Description("Update the category (and optionally merchant) on one or more transactions.")] public static async Task UpdateTransactionCategory( [Description("Array of transaction IDs to update")] long[] transactionIds, [Description("New category to assign")] string category, [Description("Merchant name to assign (creates if new)")] string? merchantName = null, MoneyMapContext db = default!, IMerchantService merchantService = default!) { var transactions = await db.Transactions .Where(t => transactionIds.Contains(t.Id)) .ToListAsync(); if (!transactions.Any()) return "No transactions found with the provided IDs"; int? merchantId = null; if (!string.IsNullOrWhiteSpace(merchantName)) merchantId = await merchantService.GetOrCreateIdAsync(merchantName); foreach (var t in transactions) { t.Category = category; if (merchantId.HasValue) t.MerchantId = merchantId; } await db.SaveChangesAsync(); return JsonSerializer.Serialize(new { Updated = transactions.Count, Category = category, Merchant = merchantName }, JsonOptions); } [McpServerTool, Description("Recategorize all transactions matching a name pattern. Use dryRun=true (default) to preview changes first.")] public static async Task BulkRecategorize( [Description("Pattern to match in transaction name (case-insensitive contains)")] string namePattern, [Description("New category to assign")] string toCategory, [Description("Only recategorize transactions currently in this category")] string? fromCategory = null, [Description("Merchant name to assign (creates if new)")] string? merchantName = null, [Description("If true (default), only shows what would change without applying")] bool dryRun = true, MoneyMapContext db = default!, IMerchantService merchantService = default!) { var q = db.Transactions .Where(t => t.Name.Contains(namePattern)); if (!string.IsNullOrWhiteSpace(fromCategory)) q = q.Where(t => t.Category == fromCategory); var transactions = await q.ToListAsync(); if (!transactions.Any()) return JsonSerializer.Serialize(new { Message = "No transactions match the pattern", Pattern = namePattern, FromCategory = fromCategory }, JsonOptions); if (dryRun) { var preview = transactions.Take(20).Select(t => new { t.Id, t.Date, t.Name, t.Amount, CurrentCategory = t.Category }).ToList(); return JsonSerializer.Serialize(new { DryRun = true, TotalMatches = transactions.Count, Preview = preview, ToCategory = toCategory }, JsonOptions); } int? merchantId = null; if (!string.IsNullOrWhiteSpace(merchantName)) merchantId = await merchantService.GetOrCreateIdAsync(merchantName); foreach (var t in transactions) { t.Category = toCategory; if (merchantId.HasValue) t.MerchantId = merchantId; } await db.SaveChangesAsync(); return JsonSerializer.Serialize(new { Applied = true, Updated = transactions.Count, ToCategory = toCategory, Merchant = merchantName }, JsonOptions); } } ``` - [ ] **Step 2: Build to verify** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj ``` - [ ] **Step 3: Commit** ```bash git add -A git commit -m "feat(mcp): implement transaction tools (search, spending, income, categorize)" ``` --- ## Task 7: Implement Budget Tools **Files:** - Create: `MoneyMap.Mcp/Tools/BudgetTools.cs` - [ ] **Step 1: Create BudgetTools.cs** Create `MoneyMap.Mcp/Tools/BudgetTools.cs`: ```csharp using System.ComponentModel; using System.Text.Json; using ModelContextProtocol.Server; using MoneyMap.Models; using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; public static class BudgetTools { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; [McpServerTool, Description("Get all active budgets with current period spending vs. limit.")] public static async Task GetBudgetStatus( [Description("Date to calculate status for (defaults to today)")] string? asOfDate = null, IBudgetService budgetService = default!) { DateTime? date = null; if (!string.IsNullOrWhiteSpace(asOfDate) && DateTime.TryParse(asOfDate, out var parsed)) date = parsed; var statuses = await budgetService.GetAllBudgetStatusesAsync(date); var result = statuses.Select(s => new { s.Budget.Id, Category = s.Budget.DisplayName, s.Budget.Amount, Period = s.Budget.Period.ToString(), s.PeriodStart, s.PeriodEnd, s.Spent, s.Remaining, PercentUsed = Math.Round(s.PercentUsed, 1), s.IsOverBudget }).ToList(); return JsonSerializer.Serialize(result, JsonOptions); } [McpServerTool, Description("Create a new budget for a category or total spending.")] public static async Task CreateBudget( [Description("Budget amount limit")] decimal amount, [Description("Period: Weekly, Monthly, or Yearly")] string period, [Description("Start date for period calculation, e.g. 2026-01-01")] string startDate, [Description("Category name (omit for total spending budget)")] string? category = null, IBudgetService budgetService = default!) { if (!Enum.TryParse(period, true, out var budgetPeriod)) return $"Invalid period '{period}'. Must be Weekly, Monthly, or Yearly."; var budget = new Budget { Category = category, Amount = amount, Period = budgetPeriod, StartDate = DateTime.Parse(startDate), IsActive = true }; var result = await budgetService.CreateBudgetAsync(budget); return JsonSerializer.Serialize(new { result.Success, result.Message, BudgetId = budget.Id }, JsonOptions); } [McpServerTool, Description("Update an existing budget's amount, period, or active status.")] public static async Task UpdateBudget( [Description("Budget ID to update")] int budgetId, [Description("New budget amount")] decimal? amount = null, [Description("New period: Weekly, Monthly, or Yearly")] string? period = null, [Description("Set active/inactive")] bool? isActive = null, IBudgetService budgetService = default!) { var budget = await budgetService.GetBudgetByIdAsync(budgetId); if (budget == null) return "Budget not found"; if (amount.HasValue) budget.Amount = amount.Value; if (!string.IsNullOrWhiteSpace(period)) { if (!Enum.TryParse(period, true, out var budgetPeriod)) return $"Invalid period '{period}'. Must be Weekly, Monthly, or Yearly."; budget.Period = budgetPeriod; } if (isActive.HasValue) budget.IsActive = isActive.Value; var result = await budgetService.UpdateBudgetAsync(budget); return JsonSerializer.Serialize(new { result.Success, result.Message }, JsonOptions); } } ``` - [ ] **Step 2: Build to verify** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj ``` - [ ] **Step 3: Commit** ```bash git add -A git commit -m "feat(mcp): implement budget tools (status, create, update)" ``` --- ## Task 8: Implement Category Tools **Files:** - Create: `MoneyMap.Mcp/Tools/CategoryTools.cs` - [ ] **Step 1: Create CategoryTools.cs** Create `MoneyMap.Mcp/Tools/CategoryTools.cs`: ```csharp using System.ComponentModel; using System.Text.Json; using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; using MoneyMap.Data; using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; public static class CategoryTools { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; [McpServerTool, Description("List all categories with transaction counts.")] public static async Task ListCategories( MoneyMapContext db = default!) { var categories = await db.Transactions .Where(t => t.Category != null && t.Category != "") .GroupBy(t => t.Category!) .Select(g => new { Category = g.Key, Count = g.Count(), TotalSpent = g.Where(t => t.Amount < 0).Sum(t => Math.Abs(t.Amount)) }) .OrderByDescending(x => x.Count) .ToListAsync(); var uncategorized = await db.Transactions .CountAsync(t => t.Category == null || t.Category == ""); return JsonSerializer.Serialize(new { Categories = categories, UncategorizedCount = uncategorized }, JsonOptions); } [McpServerTool, Description("Get auto-categorization pattern rules (CategoryMappings).")] public static async Task GetCategoryMappings( [Description("Filter mappings to a specific category")] string? category = null, ITransactionCategorizer categorizer = default!) { var mappings = await categorizer.GetAllMappingsAsync(); if (!string.IsNullOrWhiteSpace(category)) mappings = mappings.Where(m => m.Category.Equals(category, StringComparison.OrdinalIgnoreCase)).ToList(); var result = mappings.Select(m => new { m.Id, m.Pattern, m.Category, m.MerchantId, m.Priority }).OrderBy(m => m.Category).ThenByDescending(m => m.Priority).ToList(); return JsonSerializer.Serialize(result, JsonOptions); } [McpServerTool, Description("Add a new auto-categorization rule that maps transaction name patterns to categories.")] public static async Task AddCategoryMapping( [Description("Pattern to match in transaction name (case-insensitive)")] string pattern, [Description("Category to assign when pattern matches")] string category, [Description("Merchant name to assign (creates if new)")] string? merchantName = null, [Description("Priority (higher = checked first, default 0)")] int priority = 0, MoneyMapContext db = default!, IMerchantService merchantService = default!) { int? merchantId = null; if (!string.IsNullOrWhiteSpace(merchantName)) merchantId = await merchantService.GetOrCreateIdAsync(merchantName); var mapping = new MoneyMap.Models.CategoryMapping { Pattern = pattern, Category = category, MerchantId = merchantId, Priority = priority }; db.CategoryMappings.Add(mapping); await db.SaveChangesAsync(); return JsonSerializer.Serialize(new { Created = true, mapping.Id, mapping.Pattern, mapping.Category, Merchant = merchantName, mapping.Priority }, JsonOptions); } } ``` - [ ] **Step 2: Build to verify** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj ``` - [ ] **Step 3: Commit** ```bash git add -A git commit -m "feat(mcp): implement category tools (list, mappings, add mapping)" ``` --- ## Task 9: Implement Receipt Tools **Files:** - Create: `MoneyMap.Mcp/Tools/ReceiptTools.cs` - [ ] **Step 1: Create ReceiptTools.cs** Create `MoneyMap.Mcp/Tools/ReceiptTools.cs`: ```csharp using System.ComponentModel; using System.Text.Json; using ImageMagick; using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; using MoneyMap.Data; using MoneyMap.Models; using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; public static class ReceiptTools { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; [McpServerTool, Description("Get a receipt image for visual inspection. Returns the image as base64. Useful for verifying transaction categories.")] public static async Task> GetReceiptImage( [Description("Receipt ID")] long receiptId, MoneyMapContext db = default!, IReceiptStorageOptions storageOptions = default!) { var receipt = await db.Receipts.FindAsync(receiptId); if (receipt == null) return [new Content { Type = "text", Text = "Receipt not found" }]; var basePath = Path.GetFullPath(storageOptions.ReceiptsBasePath); var fullPath = Path.GetFullPath(Path.Combine(basePath, receipt.StoragePath)); if (!fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) return [new Content { Type = "text", Text = "Invalid receipt path" }]; if (!File.Exists(fullPath)) return [new Content { Type = "text", Text = "Receipt file not found on disk" }]; byte[] imageBytes; string mimeType; if (receipt.ContentType == "application/pdf") { var settings = new MagickReadSettings { Density = new Density(220) }; using var image = new MagickImage(fullPath + "[0]", settings); image.Format = MagickFormat.Png; image.BackgroundColor = MagickColors.White; image.Alpha(AlphaOption.Remove); imageBytes = image.ToByteArray(); mimeType = "image/png"; } else { imageBytes = await File.ReadAllBytesAsync(fullPath); mimeType = receipt.ContentType; } return [new Content { Type = "image", MimeType = mimeType, Data = Convert.ToBase64String(imageBytes) }]; } [McpServerTool, Description("Get already-parsed receipt data as structured text. Avoids re-analyzing the image when parse data exists.")] public static async Task GetReceiptText( [Description("Receipt ID")] long receiptId, MoneyMapContext db = default!) { var receipt = await db.Receipts .Include(r => r.LineItems) .Include(r => r.Transaction) .FirstOrDefaultAsync(r => r.Id == receiptId); if (receipt == null) return "Receipt not found"; if (receipt.ParseStatus != ReceiptParseStatus.Completed) return JsonSerializer.Serialize(new { Message = "Receipt has not been parsed yet", receipt.ParseStatus }, JsonOptions); var result = new { receipt.Id, receipt.Merchant, receipt.ReceiptDate, receipt.DueDate, receipt.Subtotal, receipt.Tax, receipt.Total, receipt.Currency, LinkedTransaction = receipt.Transaction != null ? new { receipt.Transaction.Id, receipt.Transaction.Name, receipt.Transaction.Category, receipt.Transaction.Amount } : null, LineItems = receipt.LineItems.OrderBy(li => li.LineNumber).Select(li => new { li.LineNumber, li.Description, li.Quantity, li.UnitPrice, li.LineTotal, li.Category }).ToList() }; return JsonSerializer.Serialize(result, JsonOptions); } [McpServerTool, Description("List receipts with their parse status and basic info.")] public static async Task ListReceipts( [Description("Filter by transaction ID")] long? transactionId = null, [Description("Filter by parse status: NotRequested, Queued, Parsing, Completed, Failed")] string? parseStatus = null, [Description("Max results (default 50)")] int? limit = null, MoneyMapContext db = default!) { var q = db.Receipts .Include(r => r.Transaction) .AsQueryable(); if (transactionId.HasValue) q = q.Where(r => r.TransactionId == transactionId.Value); if (!string.IsNullOrWhiteSpace(parseStatus) && Enum.TryParse(parseStatus, true, out var status)) q = q.Where(r => r.ParseStatus == status); var results = await q .OrderByDescending(r => r.UploadedAtUtc) .Take(limit ?? 50) .Select(r => new { r.Id, r.FileName, r.ParseStatus, r.Merchant, r.Total, r.ReceiptDate, r.UploadedAtUtc, TransactionId = r.TransactionId, TransactionName = r.Transaction != null ? r.Transaction.Name : null }) .ToListAsync(); return JsonSerializer.Serialize(new { Count = results.Count, Receipts = results }, JsonOptions); } [McpServerTool, Description("Get full receipt details including parsed data and all line items.")] public static async Task GetReceiptDetails( [Description("Receipt ID")] long receiptId, MoneyMapContext db = default!) { var receipt = await db.Receipts .Include(r => r.LineItems) .Include(r => r.Transaction) .Include(r => r.ParseLogs) .FirstOrDefaultAsync(r => r.Id == receiptId); if (receipt == null) return "Receipt not found"; var result = new { receipt.Id, receipt.FileName, receipt.ContentType, receipt.FileSizeBytes, receipt.UploadedAtUtc, receipt.ParseStatus, ParsedData = new { receipt.Merchant, receipt.ReceiptDate, receipt.DueDate, receipt.Subtotal, receipt.Tax, receipt.Total, receipt.Currency }, LinkedTransaction = receipt.Transaction != null ? new { receipt.Transaction.Id, receipt.Transaction.Name, receipt.Transaction.Date, receipt.Transaction.Amount, receipt.Transaction.Category } : null, LineItems = receipt.LineItems.OrderBy(li => li.LineNumber).Select(li => new { li.LineNumber, li.Description, li.Quantity, li.UnitPrice, li.LineTotal, li.Category }).ToList(), ParseHistory = receipt.ParseLogs.OrderByDescending(pl => pl.StartedAtUtc).Select(pl => new { pl.Provider, pl.Model, pl.Success, pl.Confidence, pl.Error, pl.StartedAtUtc }).ToList() }; return JsonSerializer.Serialize(result, JsonOptions); } } ``` - [ ] **Step 2: Build to verify** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj ``` Note: The `Content` type and return type for `GetReceiptImage` may need adjustment based on the exact MCP SDK API. If it doesn't compile, check the SDK's approach for returning image content — it may use `[McpServerTool]` with a return type of `Task` or similar. Adapt accordingly. - [ ] **Step 3: Commit** ```bash git add -A git commit -m "feat(mcp): implement receipt tools (image, text, list, details)" ``` --- ## Task 10: Implement Merchant Tools **Files:** - Create: `MoneyMap.Mcp/Tools/MerchantTools.cs` - [ ] **Step 1: Create MerchantTools.cs** Create `MoneyMap.Mcp/Tools/MerchantTools.cs`: ```csharp using System.ComponentModel; using System.Text.Json; using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; using MoneyMap.Data; using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; public static class MerchantTools { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; [McpServerTool, Description("List all merchants with transaction counts and category mapping info.")] public static async Task ListMerchants( [Description("Filter merchants by name (contains)")] string? query = null, MoneyMapContext db = default!) { var q = db.Merchants .Include(m => m.Transactions) .Include(m => m.CategoryMappings) .AsQueryable(); if (!string.IsNullOrWhiteSpace(query)) q = q.Where(m => m.Name.Contains(query)); var merchants = await q .OrderBy(m => m.Name) .Select(m => new { m.Id, m.Name, TransactionCount = m.Transactions.Count, MappingCount = m.CategoryMappings.Count, Categories = m.CategoryMappings.Select(cm => cm.Category).Distinct().ToList() }) .ToListAsync(); return JsonSerializer.Serialize(new { Count = merchants.Count, Merchants = merchants }, JsonOptions); } [McpServerTool, Description("Merge duplicate merchants. Reassigns all transactions and category mappings from source to target, then deletes source.")] public static async Task MergeMerchants( [Description("Merchant ID to merge FROM (will be deleted)")] int sourceMerchantId, [Description("Merchant ID to merge INTO (will be kept)")] int targetMerchantId, MoneyMapContext db = default!) { if (sourceMerchantId == targetMerchantId) return "Source and target merchant cannot be the same"; var source = await db.Merchants.FindAsync(sourceMerchantId); var target = await db.Merchants.FindAsync(targetMerchantId); if (source == null) return $"Source merchant {sourceMerchantId} not found"; if (target == null) return $"Target merchant {targetMerchantId} not found"; // Reassign transactions var transactions = await db.Transactions .Where(t => t.MerchantId == sourceMerchantId) .ToListAsync(); foreach (var t in transactions) t.MerchantId = targetMerchantId; // Reassign category mappings (delete duplicates) var sourceMappings = await db.CategoryMappings .Where(cm => cm.MerchantId == sourceMerchantId) .ToListAsync(); var targetMappingPatterns = await db.CategoryMappings .Where(cm => cm.MerchantId == targetMerchantId) .Select(cm => cm.Pattern) .ToListAsync(); foreach (var mapping in sourceMappings) { if (targetMappingPatterns.Contains(mapping.Pattern)) db.CategoryMappings.Remove(mapping); else mapping.MerchantId = targetMerchantId; } // Delete source merchant db.Merchants.Remove(source); await db.SaveChangesAsync(); return JsonSerializer.Serialize(new { Merged = true, Source = new { source.Id, source.Name }, Target = new { target.Id, target.Name }, TransactionsReassigned = transactions.Count, MappingsReassigned = sourceMappings.Count }, JsonOptions); } } ``` - [ ] **Step 2: Build to verify** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj ``` - [ ] **Step 3: Commit** ```bash git add -A git commit -m "feat(mcp): implement merchant tools (list, merge)" ``` --- ## Task 11: Implement Account and Dashboard Tools **Files:** - Create: `MoneyMap.Mcp/Tools/AccountTools.cs` - Create: `MoneyMap.Mcp/Tools/DashboardTools.cs` - [ ] **Step 1: Create AccountTools.cs** Create `MoneyMap.Mcp/Tools/AccountTools.cs`: ```csharp using System.ComponentModel; using System.Text.Json; using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; using MoneyMap.Data; namespace MoneyMap.Mcp.Tools; public static class AccountTools { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; [McpServerTool, Description("List all accounts with transaction counts.")] public static async Task ListAccounts( MoneyMapContext db = default!) { var accounts = await db.Accounts .Include(a => a.Cards) .Include(a => a.Transactions) .OrderBy(a => a.Issuer).ThenBy(a => a.Last4) .Select(a => new { a.Id, a.Issuer, a.Last4, a.Owner, Label = a.DisplayLabel, TransactionCount = a.Transactions.Count, CardCount = a.Cards.Count }) .ToListAsync(); return JsonSerializer.Serialize(accounts, JsonOptions); } [McpServerTool, Description("List all cards with account info and transaction counts.")] public static async Task ListCards( [Description("Filter cards by account ID")] int? accountId = null, MoneyMapContext db = default!) { var q = db.Cards .Include(c => c.Account) .Include(c => c.Transactions) .AsQueryable(); if (accountId.HasValue) q = q.Where(c => c.AccountId == accountId.Value); var cards = await q .OrderBy(c => c.Owner).ThenBy(c => c.Last4) .Select(c => new { c.Id, c.Issuer, c.Last4, c.Owner, Label = c.DisplayLabel, Account = c.Account.Issuer + " " + c.Account.Last4, AccountId = c.AccountId, TransactionCount = c.Transactions.Count }) .ToListAsync(); return JsonSerializer.Serialize(cards, JsonOptions); } } ``` - [ ] **Step 2: Create DashboardTools.cs** Create `MoneyMap.Mcp/Tools/DashboardTools.cs`: ```csharp using System.ComponentModel; using System.Text.Json; using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; using MoneyMap.Data; using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; public static class DashboardTools { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; [McpServerTool, Description("Get dashboard overview: top spending categories, recent transactions, and aggregate stats.")] public static async Task GetDashboard( [Description("Number of top categories to show (default 8)")] int? topCategoriesCount = null, [Description("Number of recent transactions to show (default 20)")] int? recentTransactionsCount = null, IDashboardService dashboardService = default!) { var data = await dashboardService.GetDashboardDataAsync( topCategoriesCount ?? 8, recentTransactionsCount ?? 20); return JsonSerializer.Serialize(data, JsonOptions); } [McpServerTool, Description("Get month-over-month spending totals for trend analysis.")] public static async Task GetMonthlyTrend( [Description("Number of months to include (default 6)")] int? months = null, [Description("Filter to a specific category")] string? category = null, MoneyMapContext db = default!) { var monthCount = months ?? 6; var endDate = DateTime.Today; var startDate = new DateTime(endDate.Year, endDate.Month, 1).AddMonths(-(monthCount - 1)); var q = db.Transactions .Where(t => t.Date >= startDate && t.Date <= endDate) .Where(t => t.Amount < 0) .Where(t => t.TransferToAccountId == null) .ExcludeTransfers(); if (!string.IsNullOrWhiteSpace(category)) q = q.Where(t => t.Category == category); var monthly = await q .GroupBy(t => new { t.Date.Year, t.Date.Month }) .Select(g => new { Year = g.Key.Year, Month = g.Key.Month, Total = g.Sum(t => Math.Abs(t.Amount)), Count = g.Count() }) .OrderBy(x => x.Year).ThenBy(x => x.Month) .ToListAsync(); var result = monthly.Select(m => new { Period = $"{m.Year}-{m.Month:D2}", m.Total, m.Count }).ToList(); return JsonSerializer.Serialize(new { Category = category ?? "All Spending", Months = result }, JsonOptions); } } ``` - [ ] **Step 3: Build to verify** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj ``` - [ ] **Step 4: Commit** ```bash git add -A git commit -m "feat(mcp): implement account, card, and dashboard tools" ``` --- ## Task 12: Build, Test, and Fix Compilation Issues **Files:** - Possibly modify multiple files to fix compilation errors - [ ] **Step 1: Full solution build** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet build MoneyMap.sln ``` - [ ] **Step 2: Fix any compilation errors** Common issues to watch for: - Missing `using` statements for `MoneyMap.Core` namespace in Program.cs - The `Content` type in ReceiptTools may need to match the MCP SDK's actual API (check `ModelContextProtocol.Protocol` namespace) - `ExcludeTransfers()` extension method may need the `MoneyMap.Services` using - `CategoryMappings` DbSet access — verify property name matches DbContext Fix each error and rebuild. - [ ] **Step 3: Run all tests** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj ``` - [ ] **Step 4: Quick smoke test the MCP server** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet run --project MoneyMap.Mcp -- --help 2>/dev/null ``` Or test stdio by piping an MCP initialize request: ```bash echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | dotnet run --project MoneyMap.Mcp ``` - [ ] **Step 5: Commit any fixes** ```bash git add -A git commit -m "fix: resolve compilation issues from MCP integration" ``` --- ## Task 13: Publish and Register MCP Server **Files:** - Modify: `~/.claude/CLAUDE.md` (update MCP server table) - [ ] **Step 1: Publish the MCP server** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap dotnet publish MoneyMap.Mcp -c Release -o "$USERPROFILE/.claude/mcp/MoneyMap.Mcp" ``` - [ ] **Step 2: Register with Claude Code** ```bash claude mcp add --transport stdio --scope user moneymap -- "C:/Users/AJ/.claude/mcp/MoneyMap.Mcp/MoneyMap.Mcp.exe" ``` - [ ] **Step 3: Update CLAUDE.md MCP table** Add to the MCP Server Publishing table in `~/.claude/CLAUDE.md`: ``` | MoneyMapMcp | `MoneyMap.Mcp` | `~/.claude/mcp/MoneyMap.Mcp/` | Done | ``` - [ ] **Step 4: Verify MCP server loads in Claude Code** Restart Claude Code and verify the moneymap tools appear. Run a simple tool like `list_accounts` to confirm end-to-end connectivity. - [ ] **Step 5: Final commit** ```bash cd C:/Users/AJ/Desktop/Projects/MoneyMap git add -A git commit -m "feat: publish MoneyMap MCP server and register with Claude Code" ``` --- ## Task 14: End-to-End Validation - [ ] **Step 1: Test read-only tools** In a new Claude Code session, verify: - `list_accounts` returns accounts - `search_transactions` with date range returns results - `get_spending_summary` for last month works - `get_budget_status` returns budget info - `list_categories` shows categories with counts - `list_merchants` shows merchants - [ ] **Step 2: Test receipt image tool** - `list_receipts` with `parseStatus=Completed` to find a parsed receipt - `get_receipt_text` on that receipt to see parsed data - `get_receipt_image` on a receipt to verify base64 image is returned - [ ] **Step 3: Test mutation tools** - `bulk_recategorize` with `dryRun=true` to preview - `update_transaction_category` on a single transaction - Verify the change via `get_transaction` - [ ] **Step 4: Run the real use case** Execute the income vs. spending analysis workflow: 1. `get_income_summary` for last 3 months 2. `get_spending_summary` for last 3 months 3. `search_transactions` with `uncategorizedOnly=true` 4. For ambiguous ones, use `get_receipt_image` to verify 5. `update_transaction_category` to fix miscategorized items 6. Re-run `get_spending_summary` to see corrected picture