feat(mcp): add MoneyMapApiClient typed HttpClient for API communication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 20:37:36 -04:00
parent db1d96476b
commit 6c4f4bea7f
+241
View File
@@ -0,0 +1,241 @@
using System.Net;
using System.Text;
using System.Text.Json;
namespace MoneyMap.Mcp;
public class MoneyMapApiClient
{
private readonly HttpClient _http;
public MoneyMapApiClient(HttpClient http) => _http = http;
public async Task<string> HealthCheckAsync()
{
return await GetAsync("/api/health");
}
// --- Transactions ---
public async Task<string> SearchTransactionsAsync(
string? query, string? startDate, string? endDate, string? category,
string? merchantName, decimal? minAmount, decimal? maxAmount,
int? accountId, int? cardId, string? type, bool? uncategorizedOnly, int? limit)
{
var qs = BuildQueryString(
("query", query), ("startDate", startDate), ("endDate", endDate),
("category", category), ("merchantName", merchantName),
("minAmount", minAmount?.ToString()), ("maxAmount", maxAmount?.ToString()),
("accountId", accountId?.ToString()), ("cardId", cardId?.ToString()),
("type", type), ("uncategorizedOnly", uncategorizedOnly?.ToString()),
("limit", limit?.ToString()));
return await GetAsync($"/api/transactions{qs}");
}
public async Task<string> GetTransactionAsync(long transactionId)
{
return await GetAsync($"/api/transactions/{transactionId}");
}
public async Task<string> UpdateTransactionCategoryAsync(long[] transactionIds, string category, string? merchantName)
{
var body = new { TransactionIds = transactionIds, Category = category, MerchantName = merchantName };
return await PutAsync($"/api/transactions/{transactionIds[0]}/category", body);
}
public async Task<string> BulkRecategorizeAsync(string namePattern, string toCategory, string? fromCategory, string? merchantName, bool dryRun)
{
var body = new { NamePattern = namePattern, ToCategory = toCategory, FromCategory = fromCategory, MerchantName = merchantName, DryRun = dryRun };
return await PostAsync("/api/transactions/bulk-recategorize", body);
}
public async Task<string> GetSpendingSummaryAsync(string startDate, string endDate, int? accountId)
{
var qs = BuildQueryString(("startDate", startDate), ("endDate", endDate), ("accountId", accountId?.ToString()));
return await GetAsync($"/api/transactions/spending-summary{qs}");
}
public async Task<string> GetIncomeSummaryAsync(string startDate, string endDate, int? accountId)
{
var qs = BuildQueryString(("startDate", startDate), ("endDate", endDate), ("accountId", accountId?.ToString()));
return await GetAsync($"/api/transactions/income-summary{qs}");
}
// --- Budgets ---
public async Task<string> GetBudgetStatusAsync(string? asOfDate)
{
var qs = BuildQueryString(("asOfDate", asOfDate));
return await GetAsync($"/api/budgets/status{qs}");
}
public async Task<string> CreateBudgetAsync(string? category, decimal amount, string period, string startDate)
{
var body = new { Category = category, Amount = amount, Period = period, StartDate = startDate };
return await PostAsync("/api/budgets", body);
}
public async Task<string> UpdateBudgetAsync(int budgetId, decimal? amount, string? period, bool? isActive)
{
var body = new { Amount = amount, Period = period, IsActive = isActive };
return await PutAsync($"/api/budgets/{budgetId}", body);
}
// --- Categories ---
public async Task<string> ListCategoriesAsync()
{
return await GetAsync("/api/categories");
}
public async Task<string> GetCategoryMappingsAsync(string? category)
{
var qs = BuildQueryString(("category", category));
return await GetAsync($"/api/categories/mappings{qs}");
}
public async Task<string> AddCategoryMappingAsync(string pattern, string category, string? merchantName, int priority)
{
var body = new { Pattern = pattern, Category = category, MerchantName = merchantName, Priority = priority };
return await PostAsync("/api/categories/mappings", body);
}
// --- Receipts ---
public async Task<string> ListReceiptsAsync(long? transactionId, string? parseStatus, int? limit)
{
var qs = BuildQueryString(("transactionId", transactionId?.ToString()), ("parseStatus", parseStatus), ("limit", limit?.ToString()));
return await GetAsync($"/api/receipts{qs}");
}
public async Task<string> GetReceiptDetailsAsync(long receiptId)
{
return await GetAsync($"/api/receipts/{receiptId}");
}
public async Task<string> GetReceiptImageAsync(long receiptId)
{
return await GetAsync($"/api/receipts/{receiptId}/image");
}
public async Task<string> GetReceiptTextAsync(long receiptId)
{
return await GetAsync($"/api/receipts/{receiptId}/text");
}
// --- Merchants ---
public async Task<string> ListMerchantsAsync(string? query)
{
var qs = BuildQueryString(("query", query));
return await GetAsync($"/api/merchants{qs}");
}
public async Task<string> MergeMerchantsAsync(int sourceMerchantId, int targetMerchantId)
{
var body = new { SourceMerchantId = sourceMerchantId, TargetMerchantId = targetMerchantId };
return await PostAsync("/api/merchants/merge", body);
}
// --- Accounts ---
public async Task<string> ListAccountsAsync()
{
return await GetAsync("/api/accounts");
}
public async Task<string> ListCardsAsync(int? accountId)
{
var qs = BuildQueryString(("accountId", accountId?.ToString()));
return await GetAsync($"/api/accounts/cards{qs}");
}
// --- Dashboard ---
public async Task<string> GetDashboardAsync(int? topCategoriesCount, int? recentTransactionsCount)
{
var qs = BuildQueryString(("topCategoriesCount", topCategoriesCount?.ToString()), ("recentTransactionsCount", recentTransactionsCount?.ToString()));
return await GetAsync($"/api/dashboard{qs}");
}
public async Task<string> GetMonthlyTrendAsync(int? months, string? category)
{
var qs = BuildQueryString(("months", months?.ToString()), ("category", category));
return await GetAsync($"/api/dashboard/monthly-trend{qs}");
}
// --- HTTP Helpers ---
private async Task<string> GetAsync(string path)
{
try
{
var response = await _http.GetAsync(path);
if (response.IsSuccessStatusCode)
return await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.NotFound)
{
var body = await response.Content.ReadAsStringAsync();
return body.Length > 0 ? body : "Not found";
}
return $"API error: {(int)response.StatusCode} - {response.ReasonPhrase}";
}
catch (HttpRequestException ex)
{
return $"MoneyMap API is not reachable at {_http.BaseAddress}. Ensure the web app is running. Error: {ex.Message}";
}
}
private async Task<string> PostAsync(string path, object body)
{
try
{
var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await _http.PostAsync(path, content);
if (response.IsSuccessStatusCode)
return await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.NotFound)
{
var responseBody = await response.Content.ReadAsStringAsync();
return responseBody.Length > 0 ? responseBody : "Not found";
}
return $"API error: {(int)response.StatusCode} - {response.ReasonPhrase}";
}
catch (HttpRequestException ex)
{
return $"MoneyMap API is not reachable at {_http.BaseAddress}. Ensure the web app is running. Error: {ex.Message}";
}
}
private async Task<string> PutAsync(string path, object body)
{
try
{
var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await _http.PutAsync(path, content);
if (response.IsSuccessStatusCode)
return await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.NotFound)
{
var responseBody = await response.Content.ReadAsStringAsync();
return responseBody.Length > 0 ? responseBody : "Not found";
}
return $"API error: {(int)response.StatusCode} - {response.ReasonPhrase}";
}
catch (HttpRequestException ex)
{
return $"MoneyMap API is not reachable at {_http.BaseAddress}. Ensure the web app is running. Error: {ex.Message}";
}
}
private static string BuildQueryString(params (string key, string? value)[] parameters)
{
var pairs = parameters
.Where(p => !string.IsNullOrWhiteSpace(p.value))
.Select(p => $"{Uri.EscapeDataString(p.key)}={Uri.EscapeDataString(p.value!)}");
var qs = string.Join("&", pairs);
return qs.Length > 0 ? $"?{qs}" : "";
}
}