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!) { 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(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!) { 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); } }