feat(mcp): add MoneyMapApiClient typed HttpClient for API communication
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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}" : "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user