refactor(mcp): rewrite all tools to use MoneyMapApiClient instead of direct DB access
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string> 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<string> 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<ReceiptParseStatus>(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<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user