diff --git a/MoneyMap.Mcp/Tools/AccountTools.cs b/MoneyMap.Mcp/Tools/AccountTools.cs index 0c387b5..523fa0c 100644 --- a/MoneyMap.Mcp/Tools/AccountTools.cs +++ b/MoneyMap.Mcp/Tools/AccountTools.cs @@ -1,67 +1,23 @@ using System.ComponentModel; -using System.Text.Json; -using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; -using MoneyMap.Data; namespace MoneyMap.Mcp.Tools; [McpServerToolType] public static class AccountTools { - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - [McpServerTool(Name = "list_accounts"), Description("List all accounts with transaction counts.")] public static async Task ListAccounts( - MoneyMapContext db = default!) + MoneyMapApiClient api = default!) { - var accounts = await db.Accounts - .Include(a => a.Cards) - .Include(a => a.Transactions) - .OrderBy(a => a.Institution).ThenBy(a => a.Last4) - .Select(a => new - { - a.Id, - a.Institution, - a.Last4, - a.Owner, - Label = a.DisplayLabel, - TransactionCount = a.Transactions.Count, - CardCount = a.Cards.Count - }) - .ToListAsync(); - - return JsonSerializer.Serialize(accounts, JsonOptions); + return await api.ListAccountsAsync(); } [McpServerTool(Name = "list_cards"), 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!) + MoneyMapApiClient api = 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 != null ? c.Account.Institution + " " + c.Account.Last4 : null, - AccountId = c.AccountId, - TransactionCount = c.Transactions.Count - }) - .ToListAsync(); - - return JsonSerializer.Serialize(cards, JsonOptions); + return await api.ListCardsAsync(accountId); } } diff --git a/MoneyMap.Mcp/Tools/BudgetTools.cs b/MoneyMap.Mcp/Tools/BudgetTools.cs index f80dd07..6ae13af 100644 --- a/MoneyMap.Mcp/Tools/BudgetTools.cs +++ b/MoneyMap.Mcp/Tools/BudgetTools.cs @@ -1,42 +1,17 @@ using System.ComponentModel; -using System.Text.Json; using ModelContextProtocol.Server; -using MoneyMap.Models; -using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; [McpServerToolType] public static class BudgetTools { - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - [McpServerTool(Name = "get_budget_status"), 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!) + MoneyMapApiClient api = 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); + return await api.GetBudgetStatusAsync(asOfDate); } [McpServerTool(Name = "create_budget"), Description("Create a new budget for a category or total spending.")] @@ -45,23 +20,9 @@ public static class BudgetTools [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!) + MoneyMapApiClient api = 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); + return await api.CreateBudgetAsync(category, amount, period, startDate); } [McpServerTool(Name = "update_budget"), Description("Update an existing budget's amount, period, or active status.")] @@ -70,27 +31,8 @@ public static class BudgetTools [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!) + MoneyMapApiClient api = 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); + return await api.UpdateBudgetAsync(budgetId, amount, period, isActive); } } diff --git a/MoneyMap.Mcp/Tools/CategoryTools.cs b/MoneyMap.Mcp/Tools/CategoryTools.cs index bc93b8a..90c6239 100644 --- a/MoneyMap.Mcp/Tools/CategoryTools.cs +++ b/MoneyMap.Mcp/Tools/CategoryTools.cs @@ -1,54 +1,24 @@ using System.ComponentModel; -using System.Text.Json; -using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; -using MoneyMap.Data; -using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; [McpServerToolType] public static class CategoryTools { - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - [McpServerTool(Name = "list_categories"), Description("List all categories with transaction counts.")] public static async Task ListCategories( - MoneyMapContext db = default!) + MoneyMapApiClient api = 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); + return await api.ListCategoriesAsync(); } [McpServerTool(Name = "get_category_mappings"), 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!) + MoneyMapApiClient api = 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); + return await api.GetCategoryMappingsAsync(category); } [McpServerTool(Name = "add_category_mapping"), Description("Add a new auto-categorization rule that maps transaction name patterns to categories.")] @@ -57,24 +27,8 @@ public static class CategoryTools [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!) + MoneyMapApiClient api = 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); + return await api.AddCategoryMappingAsync(pattern, category, merchantName, priority); } } diff --git a/MoneyMap.Mcp/Tools/DashboardTools.cs b/MoneyMap.Mcp/Tools/DashboardTools.cs index 98d80c2..8c2301d 100644 --- a/MoneyMap.Mcp/Tools/DashboardTools.cs +++ b/MoneyMap.Mcp/Tools/DashboardTools.cs @@ -1,68 +1,26 @@ using System.ComponentModel; -using System.Text.Json; -using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; -using MoneyMap.Data; -using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; [McpServerToolType] public static class DashboardTools { - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - [McpServerTool(Name = "get_dashboard"), 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!) + MoneyMapApiClient api = default!) { - var data = await dashboardService.GetDashboardDataAsync( - topCategoriesCount ?? 8, - recentTransactionsCount ?? 20); - - return JsonSerializer.Serialize(data, JsonOptions); + return await api.GetDashboardAsync(topCategoriesCount, recentTransactionsCount); } [McpServerTool(Name = "get_monthly_trend"), 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!) + MoneyMapApiClient api = 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); + return await api.GetMonthlyTrendAsync(months, category); } } diff --git a/MoneyMap.Mcp/Tools/MerchantTools.cs b/MoneyMap.Mcp/Tools/MerchantTools.cs index 2a7b3fb..13b02de 100644 --- a/MoneyMap.Mcp/Tools/MerchantTools.cs +++ b/MoneyMap.Mcp/Tools/MerchantTools.cs @@ -1,95 +1,25 @@ using System.ComponentModel; -using System.Text.Json; -using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; -using MoneyMap.Data; namespace MoneyMap.Mcp.Tools; [McpServerToolType] public static class MerchantTools { - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - [McpServerTool(Name = "list_merchants"), 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!) + MoneyMapApiClient api = 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); + return await api.ListMerchantsAsync(query); } [McpServerTool(Name = "merge_merchants"), 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!) + MoneyMapApiClient api = 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"; - - var transactions = await db.Transactions - .Where(t => t.MerchantId == sourceMerchantId) - .ToListAsync(); - - foreach (var t in transactions) - t.MerchantId = targetMerchantId; - - 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; - } - - 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); + return await api.MergeMerchantsAsync(sourceMerchantId, targetMerchantId); } } diff --git a/MoneyMap.Mcp/Tools/ReceiptTools.cs b/MoneyMap.Mcp/Tools/ReceiptTools.cs index 6b721d2..f3b6d6b 100644 --- a/MoneyMap.Mcp/Tools/ReceiptTools.cs +++ b/MoneyMap.Mcp/Tools/ReceiptTools.cs @@ -1,100 +1,25 @@ 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; [McpServerToolType] public static class ReceiptTools { - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - [McpServerTool(Name = "get_receipt_image"), Description("Get a receipt image for visual inspection. Returns the image as base64-encoded data. Useful for verifying transaction categories.")] public static async Task GetReceiptImage( [Description("Receipt ID")] long receiptId, - MoneyMapContext db = default!, - IReceiptStorageOptions storageOptions = default!) + MoneyMapApiClient api = default!) { - var receipt = await db.Receipts.FindAsync(receiptId); - if (receipt == null) - return "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 "Invalid receipt path"; - - if (!File.Exists(fullPath)) - return "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; - } - - var base64 = Convert.ToBase64String(imageBytes); - return JsonSerializer.Serialize(new { MimeType = mimeType, Data = base64, SizeBytes = imageBytes.Length }, JsonOptions); + return await api.GetReceiptImageAsync(receiptId); } [McpServerTool(Name = "get_receipt_text"), 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!) + MoneyMapApiClient api = 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", ParseStatus = receipt.ParseStatus.ToString() }, 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); + return await api.GetReceiptTextAsync(receiptId); } [McpServerTool(Name = "list_receipts"), Description("List receipts with their parse status and basic info.")] @@ -102,98 +27,16 @@ public static class ReceiptTools [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!) + MoneyMapApiClient api = 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, - ParseStatus = r.ParseStatus.ToString(), - 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); + return await api.ListReceiptsAsync(transactionId, parseStatus, limit); } [McpServerTool(Name = "get_receipt_details"), 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!) + MoneyMapApiClient api = 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, - ParseStatus = receipt.ParseStatus.ToString(), - 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); + return await api.GetReceiptDetailsAsync(receiptId); } } diff --git a/MoneyMap.Mcp/Tools/TransactionTools.cs b/MoneyMap.Mcp/Tools/TransactionTools.cs index c3412b5..25f8c98 100644 --- a/MoneyMap.Mcp/Tools/TransactionTools.cs +++ b/MoneyMap.Mcp/Tools/TransactionTools.cs @@ -1,17 +1,11 @@ using System.ComponentModel; -using System.Text.Json; -using Microsoft.EntityFrameworkCore; using ModelContextProtocol.Server; -using MoneyMap.Data; -using MoneyMap.Services; namespace MoneyMap.Mcp.Tools; [McpServerToolType] public static class TransactionTools { - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - [McpServerTool(Name = "search_transactions"), 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, @@ -26,108 +20,17 @@ public static class TransactionTools [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!) + MoneyMapApiClient api = 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!.Institution + " " + 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); + return await api.SearchTransactionsAsync(query, startDate, endDate, category, merchantName, minAmount, maxAmount, accountId, cardId, type, uncategorizedOnly, limit); } [McpServerTool(Name = "get_transaction"), Description("Get a single transaction with all details including receipts.")] public static async Task GetTransaction( [Description("Transaction ID")] long transactionId, - MoneyMapContext db = default!) + MoneyMapApiClient api = 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!.Institution + " " + 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); + return await api.GetTransactionAsync(transactionId); } [McpServerTool(Name = "get_spending_summary"), Description("Get spending totals grouped by category for a date range. Excludes transfers.")] @@ -135,29 +38,9 @@ public static class TransactionTools [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!) + MoneyMapApiClient api = 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); + return await api.GetSpendingSummaryAsync(startDate, endDate, accountId); } [McpServerTool(Name = "get_income_summary"), Description("Get income (credits) grouped by source/name for a date range.")] @@ -165,29 +48,9 @@ public static class TransactionTools [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!) + MoneyMapApiClient api = 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); + return await api.GetIncomeSummaryAsync(startDate, endDate, accountId); } [McpServerTool(Name = "update_transaction_category"), Description("Update the category (and optionally merchant) on one or more transactions.")] @@ -195,30 +58,9 @@ public static class TransactionTools [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!) + MoneyMapApiClient api = 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); + return await api.UpdateTransactionCategoryAsync(transactionIds, category, merchantName); } [McpServerTool(Name = "bulk_recategorize"), Description("Recategorize all transactions matching a name pattern. Use dryRun=true (default) to preview changes first.")] @@ -228,39 +70,8 @@ public static class TransactionTools [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!) + MoneyMapApiClient api = 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); + return await api.BulkRecategorizeAsync(namePattern, toCategory, fromCategory, merchantName, dryRun); } }