From 2a75c9550e6a17f84c97ef5b6fac7fccb9d3c9f3 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 20 Apr 2026 19:45:13 -0400 Subject: [PATCH] chore: add docs/superpowers, .playwright-mcp, settings.local.json to gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 9 + .../element-2026-02-26T23-41-11-112Z.png | Bin 763 -> 0 bytes .../plans/2026-04-20-moneymap-mcp.md | 1842 ----------------- .../specs/2026-04-20-moneymap-mcp-design.md | 376 ---- 4 files changed, 9 insertions(+), 2218 deletions(-) delete mode 100644 .playwright-mcp/element-2026-02-26T23-41-11-112Z.png delete mode 100644 docs/superpowers/plans/2026-04-20-moneymap-mcp.md delete mode 100644 docs/superpowers/specs/2026-04-20-moneymap-mcp-design.md diff --git a/.gitignore b/.gitignore index 3eff65d..c7be5ce 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,12 @@ packages/ # Environment files with secrets .env + +# Local settings +settings.local.json + +# Superpowers plans/specs +docs/superpowers/ + +# Playwright MCP artifacts +.playwright-mcp/ diff --git a/.playwright-mcp/element-2026-02-26T23-41-11-112Z.png b/.playwright-mcp/element-2026-02-26T23-41-11-112Z.png deleted file mode 100644 index d8e521ac0443ca91f8dff5c7471c1a3edab735c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 763 zcmeAS@N?(olHy`uVBq!ia0vp^`9Q40!2~3qWEXyAU|`zd>EaktG3U*#zzm^6nFAl| zXP=I}b@#BUmZr*;4V;!mTY1V-Z9NifucSFTKAddpv1Yp5o2%OulK6Y&+yq>%+Iov`GO+@oK$A0GbSzx%&!jqN+XfbV6A6BQJs zpXi%5HN4Xc#9u~yHv4};aQ(~aPg{2{PX0OT zto$yM6B?IWe?Q(B@^t&<9c}K1-o3f;w7qq!Z%%y4k#xnV3zxQLJ~%h`i?Mvm_54dO z-@MJfUVM1rhT9izXr5O|x6Y0J`u>w;;e=$)jRopS%%}Q_9vxHJpIZNBNAS#&+$@=M z?`*3+F33#1a?w|~_eI9j1lg@K7y9yjw~POANOS+nFb^BSuw5~ z*~ry5V>U?rSH64mU`p<>0|Qg+>Ezi(d3wn+5twHPs>%!xVA zzXhCHa>9P~Ek2&P@+<89RQ_FaQ8E@Tsam0y9`Js)OLWw#$jSC$OZcA7{P)VSDC|d` zy>v*mX!lOlXBYIW?^c)@|F!jbbTKCETgB`rQ&vy9Qc=IC$0vNZ`G_ **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 diff --git a/docs/superpowers/specs/2026-04-20-moneymap-mcp-design.md b/docs/superpowers/specs/2026-04-20-moneymap-mcp-design.md deleted file mode 100644 index 6a41fa5..0000000 --- a/docs/superpowers/specs/2026-04-20-moneymap-mcp-design.md +++ /dev/null @@ -1,376 +0,0 @@ -# MoneyMap MCP Server — Design Spec - -## Overview - -Create an MCP (Model Context Protocol) console application that exposes MoneyMap's personal finance data and operations to Claude Code. This enables conversational financial analysis, category correction with receipt image verification, and budget feasibility modeling. - -## Motivation - -The primary use case is financial decision-making: analyzing household income vs. spending, identifying miscategorized transactions (with receipt images for verification), and modeling scenarios like income changes. Full read/write access allows Claude to both analyze and correct data in a single conversation. - -## Architecture - -### Solution Structure - -``` -MoneyMap.sln -├── MoneyMap.Core/ (NEW - shared class library) -│ ├── Models/ (moved from MoneyMap) -│ ├── Data/ -│ │ └── MoneyMapContext.cs (moved from MoneyMap) -│ ├── Services/ (moved from MoneyMap) -│ │ ├── TransactionService.cs -│ │ ├── BudgetService.cs -│ │ ├── DashboardService.cs -│ │ ├── TransactionCategorizer.cs -│ │ ├── MerchantService.cs -│ │ ├── AccountService.cs -│ │ ├── CardService.cs -│ │ ├── ReceiptManager.cs -│ │ ├── ReceiptMatchingService.cs -│ │ ├── ReferenceDataService.cs -│ │ ├── TransactionStatisticsService.cs -│ │ └── AITools/ -│ ├── ServiceCollectionExtensions.cs (shared DI registration) -│ └── MoneyMap.Core.csproj (EF Core, CsvHelper, Magick.NET, etc.) -│ -├── MoneyMap/ (existing web app - slimmed) -│ ├── Pages/ (stays - thin PageModel delegation) -│ ├── Migrations/ (stays - deployment-specific) -│ ├── wwwroot/ (stays - includes receipt files) -│ ├── Program.cs (calls AddMoneyMapCore) -│ └── MoneyMap.csproj (references MoneyMap.Core) -│ -├── MoneyMap.Mcp/ (NEW - MCP console app) -│ ├── Tools/ -│ │ ├── TransactionTools.cs -│ │ ├── BudgetTools.cs -│ │ ├── CategoryTools.cs -│ │ ├── ReceiptTools.cs -│ │ ├── MerchantTools.cs -│ │ ├── AccountTools.cs -│ │ └── DashboardTools.cs -│ ├── ConfigReceiptStorageOptions.cs -│ ├── Program.cs -│ ├── appsettings.json -│ └── MoneyMap.Mcp.csproj (references MoneyMap.Core + MCP SDK) -│ -└── MoneyMap.Tests/ (existing - updates reference to MoneyMap.Core) -``` - -### Key Architectural Decisions - -1. **Shared class library (MoneyMap.Core)**: All models, DbContext, and services extracted into a shared library. Both the web app and MCP app reference it. This ensures logic never drifts between the two consumers. - -2. **Shared DI registration**: A `ServiceCollectionExtensions.AddMoneyMapCore(IConfiguration)` extension method registers DbContext and all services. Both apps call this method, preventing registration drift. - -3. **Migrations stay in web project**: EF migrations are deployment-specific and remain in the web app project. - -4. **Receipt files stay in wwwroot**: The MCP server reads receipt images from the web app's `wwwroot/receipts/` directory via a configured absolute path. - -5. **TransactionImporter stays in web app**: CSV import is a UI-driven workflow. It remains in `Upload.cshtml.cs` and is not exposed via MCP. - -6. **ReceiptParseWorkerService stays in web app**: The background hosted service for async receipt parsing stays tied to the web process. - -7. **Logging to stderr only**: MCP uses stdio for the protocol. All logging in the MCP app must be directed to stderr (or file) to avoid corrupting the MCP stream. The `AddMoneyMapCore` extension must not force a stdout-based logging provider. - -8. **No IWebHostEnvironment dependency in Core**: Services that currently rely on `IWebHostEnvironment` to resolve file paths (e.g., `ReceiptManager`) will be refactored to use an `IReceiptStorageOptions` interface backed by configuration. This allows both the web app and MCP app to provide the correct path without web-host coupling. - -9. **MoneyMap.Core targets `Microsoft.NET.Sdk`**: The shared library must not depend on `Microsoft.AspNetCore.App`. It uses only `Microsoft.Extensions.*` packages to stay lightweight for the console app consumer. - -10. **Path traversal protection**: Receipt file access validates that resolved paths remain within the configured receipts directory to prevent directory traversal attacks. - -## MCP Tools - -### Transaction Tools - -| Tool | Description | Parameters | Mutates | -|------|-------------|------------|---------| -| `search_transactions` | Filter and list transactions | `query?` (full-text across name/memo/category), `startDate?`, `endDate?`, `category?`, `merchantName?`, `minAmount?`, `maxAmount?`, `accountId?`, `cardId?`, `type?` (debit/credit), `uncategorizedOnly?`, `limit?` (default 50) | No | -| `get_transaction` | Get single transaction with full details | `transactionId` | No | -| `get_spending_summary` | Spending totals grouped by category for a date range (excludes transfers) | `startDate`, `endDate`, `accountId?` | No | -| `get_income_summary` | Credit totals grouped by source/name for a date range | `startDate`, `endDate`, `accountId?` | No | -| `update_transaction_category` | Change category and optionally merchant on transactions | `transactionIds` (array), `category`, `merchantName?` | Yes | -| `bulk_recategorize` | Recategorize all transactions matching a name pattern | `namePattern`, `fromCategory?`, `toCategory`, `merchantName?`, `dryRun?` (default true — returns preview of affected transactions without applying changes) | Yes | - -### Budget Tools - -| Tool | Description | Parameters | Mutates | -|------|-------------|------------|---------| -| `get_budget_status` | All active budgets with current period spending vs. limit | `asOfDate?` | No | -| `create_budget` | Create a new category or total budget | `category?` (null = total), `amount`, `period` (Weekly/Monthly/Yearly), `startDate` | Yes | -| `update_budget` | Modify budget amount, period, or active status | `budgetId`, `amount?`, `period?`, `isActive?` | Yes | - -### Category Tools - -| Tool | Description | Parameters | Mutates | -|------|-------------|------------|---------| -| `list_categories` | All categories with transaction counts | (none) | No | -| `get_category_mappings` | Auto-categorization pattern rules | `category?` (filter) | No | -| `add_category_mapping` | Add a new auto-categorization rule | `pattern`, `category`, `merchantName?`, `priority?` | Yes | - -### Receipt Tools - -| Tool | Description | Parameters | Mutates | -|------|-------------|------------|---------| -| `get_receipt_image` | Returns receipt as base64 image (PDF → PNG conversion). Path-traversal safe. | `receiptId` | No | -| `get_receipt_text` | Returns already-parsed receipt data (merchant, date, amounts, line items) as structured text — avoids re-OCR when parsed data exists | `receiptId` | No | -| `list_receipts` | List receipts with parse status | `transactionId?`, `parseStatus?`, `limit?` | No | -| `get_receipt_details` | Full receipt metadata, parsed data, and line items | `receiptId` | No | - -### Merchant Tools - -| Tool | Description | Parameters | Mutates | -|------|-------------|------------|---------| -| `list_merchants` | All merchants with transaction counts and category mapping info | `query?` (filter by name) | No | -| `merge_merchants` | Merge duplicate merchants — reassigns all transactions and mappings from source to target, then deletes source | `sourceMerchantId`, `targetMerchantId` | Yes | - -### Account & Card Tools - -| Tool | Description | Parameters | Mutates | -|------|-------------|------------|---------| -| `list_accounts` | All accounts with transaction counts | (none) | No | -| `list_cards` | All cards with account info and stats | `accountId?` | No | - -### Dashboard Tools - -| Tool | Description | Parameters | Mutates | -|------|-------------|------------|---------| -| `get_dashboard` | Top spending categories, recent transactions, aggregate stats | `topCategoriesCount?`, `recentTransactionsCount?` | No | -| `get_monthly_trend` | Month-over-month spending totals | `months?` (default 6), `category?` | No | - -## Internal Design - -### Program.cs - -```csharp -var builder = Host.CreateApplicationBuilder(args); - -// MCP uses stdio — all logging must go to stderr -builder.Logging.ClearProviders(); -builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace); - -builder.Configuration.AddJsonFile("appsettings.json"); - -builder.Services.AddMoneyMapCore(builder.Configuration); - -builder.Services - .AddMcpServer() - .WithStdioServerTransport() - .WithToolsFromAssembly(typeof(Program).Assembly); - -var app = builder.Build(); -await app.RunAsync(); -``` - -### ServiceCollectionExtensions (MoneyMap.Core) - -```csharp -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddMoneyMapCore( - this IServiceCollection services, IConfiguration configuration) - { - services.AddDbContext(options => - options.UseSqlServer(configuration.GetConnectionString("MoneyMapDb"))); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - return services; - } -} -``` - -### Tool Implementation Pattern - -All tools return `McpToolResult` for consistency and to support multi-part responses (text + images). - -```csharp -[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("Optional: filter to specific account ID")] int? accountId, - MoneyMapContext db) -{ - var start = DateTime.Parse(startDate); - var end = DateTime.Parse(endDate); - - var query = db.Transactions - .Where(t => t.Date >= start && t.Date <= end) - .Where(t => t.Amount < 0) // debits only - .Where(t => t.TransferToAccountId == null); // exclude transfers - - if (accountId.HasValue) - query = query.Where(t => t.AccountId == accountId.Value); - - var summary = await query - .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(); - - return McpToolResult.Text(JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true })); -} -``` - -### Receipt Image Tool - -```csharp -[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, - IConfiguration config) -{ - var receipt = await db.Receipts.FindAsync(receiptId); - if (receipt == null) - return McpToolResult.Text("Receipt not found"); - - var basePath = Path.GetFullPath(config["Receipts:StoragePath"]!); - var fullPath = Path.GetFullPath(Path.Combine(basePath, receipt.StoragePath)); - - // Path traversal protection - if (!fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) - return McpToolResult.Text("Invalid receipt path"); - - if (!File.Exists(fullPath)) - return McpToolResult.Text("Receipt file not found on disk"); - - byte[] imageBytes; - string mimeType; - - if (receipt.ContentType == "application/pdf") - { - using var image = new MagickImage(fullPath + "[0]"); - image.Density = new Density(220); - image.Format = MagickFormat.Png; - imageBytes = image.ToByteArray(); - mimeType = "image/png"; - } - else - { - imageBytes = await File.ReadAllBytesAsync(fullPath); - mimeType = receipt.ContentType; - } - - return McpToolResult.Image(Convert.ToBase64String(imageBytes), mimeType); -} -``` - -## Configuration - -### MoneyMap.Mcp/appsettings.json - -```json -{ - "ConnectionStrings": { - "MoneyMapDb": "Server=(localdb)\\mssqllocaldb;Database=MoneyMap;Trusted_Connection=True;" - }, - "Receipts": { - "StoragePath": "C:/Users/AJ/Desktop/Projects/MoneyMap/MoneyMap/wwwroot/receipts" - } -} -``` - -## Publishing & Installation - -### Build - -```bash -dotnet publish MoneyMap.Mcp -c Release -o "$USERPROFILE/.claude/mcp/MoneyMap.Mcp" -``` - -### Register - -```bash -claude mcp add --transport stdio --scope user moneymap -- "C:/Users/AJ/.claude/mcp/MoneyMap.Mcp/MoneyMap.Mcp.exe" -``` - -## What Stays in the Web App - -- EF Migrations (`MoneyMap/Migrations/`) -- Receipt physical files (`MoneyMap/wwwroot/receipts/`) -- PageModel code (`Pages/*.cshtml.cs`) — thin delegation to Core services -- `TransactionImporter` & `CardResolver` — import-workflow-specific -- `ReceiptParseWorkerService` — background worker tied to web process -- `AIReceiptParser` — tied to the parse worker and web upload flow - -## What Moves to MoneyMap.Core - -- All models (`Models/`) -- `MoneyMapContext` (`Data/MoneyMapContext.cs`) -- All service interfaces and implementations (`Services/`) -- AITools (`Services/AITools/`) -- All DTOs and result types - -## NuGet Dependencies (MoneyMap.Core) - -- Microsoft.EntityFrameworkCore.SqlServer -- CsvHelper -- Magick.NET-Q8-AnyCPU (for receipt PDF→PNG) -- UglyToad.PdfPig (for PDF processing) - -## NuGet Dependencies (MoneyMap.Mcp) - -- ModelContextProtocol (prerelease) -- Microsoft.Extensions.Hosting - -## Refactoring Notes - -### IWebHostEnvironment Removal from Core - -Services like `ReceiptManager` currently use `IWebHostEnvironment.WebRootPath` to resolve receipt file paths. This must be abstracted: - -```csharp -// MoneyMap.Core -public interface IReceiptStorageOptions -{ - string ReceiptsBasePath { get; } -} - -// MoneyMap (web app) implementation -public class WebReceiptStorageOptions : IReceiptStorageOptions -{ - public WebReceiptStorageOptions(IWebHostEnvironment env) - => ReceiptsBasePath = Path.Combine(env.WebRootPath, "receipts"); - public string ReceiptsBasePath { get; } -} - -// MoneyMap.Mcp implementation -public class ConfigReceiptStorageOptions : IReceiptStorageOptions -{ - public ConfigReceiptStorageOptions(IConfiguration config) - => ReceiptsBasePath = config["Receipts:StoragePath"]!; - public string ReceiptsBasePath { get; } -} -``` - -Each host registers its own implementation. `AddMoneyMapCore` does NOT register `IReceiptStorageOptions` — that's host-specific. - -## Risks & Mitigations - -| Risk | Mitigation | -|------|-----------| -| Large refactoring breaks web app | Run existing tests after extraction; web app behavior unchanged | -| Namespace changes break references | Use find-and-replace for `using` statements; Roslyn Bridge can verify | -| Receipt path differs per machine | Configurable via `IReceiptStorageOptions` + appsettings.json | -| MCP binary needs Magick.NET native libs | Included via NuGet package (AnyCPU variant) | -| Concurrent DB access (web + MCP) | EF Core handles this fine with scoped DbContext per request | -| Logging corrupts MCP stdio stream | All logging redirected to stderr in MCP host | -| Path traversal on receipt reads | Resolved paths validated against base directory | -| MoneyMap.Core pulls in ASP.NET deps | Target `Microsoft.NET.Sdk`, avoid `Microsoft.AspNetCore.App` framework ref |