cbc46314db
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
7.3 KiB
C#
200 lines
7.3 KiB
C#
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!)
|
|
{
|
|
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);
|
|
}
|
|
|
|
[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!)
|
|
{
|
|
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);
|
|
}
|
|
|
|
[McpServerTool(Name = "list_receipts"), Description("List receipts with their parse status and basic info.")]
|
|
public static async Task<string> 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<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);
|
|
}
|
|
|
|
[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!)
|
|
{
|
|
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);
|
|
}
|
|
}
|