Shared class library (MoneyMap.Core) extraction with MCP console app for conversational financial analysis, category correction with receipt image verification, and budget feasibility modeling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
MoneyMap MCP Server — Design Spec
Overview
Create an MCP (Model Context Protocol) console application that exposes MoneyMap's personal finance data and operations to Claude Code. This enables conversational financial analysis, category correction with receipt image verification, and budget feasibility modeling.
Motivation
The primary use case is financial decision-making: analyzing household income vs. spending, identifying miscategorized transactions (with receipt images for verification), and modeling scenarios like income changes. Full read/write access allows Claude to both analyze and correct data in a single conversation.
Architecture
Solution Structure
MoneyMap.sln
├── MoneyMap.Core/ (NEW - shared class library)
│ ├── Models/ (moved from MoneyMap)
│ ├── Data/
│ │ └── MoneyMapContext.cs (moved from MoneyMap)
│ ├── Services/ (moved from MoneyMap)
│ │ ├── TransactionService.cs
│ │ ├── BudgetService.cs
│ │ ├── DashboardService.cs
│ │ ├── TransactionCategorizer.cs
│ │ ├── MerchantService.cs
│ │ ├── AccountService.cs
│ │ ├── CardService.cs
│ │ ├── ReceiptManager.cs
│ │ ├── ReceiptMatchingService.cs
│ │ ├── ReferenceDataService.cs
│ │ ├── TransactionStatisticsService.cs
│ │ └── AITools/
│ ├── ServiceCollectionExtensions.cs (shared DI registration)
│ └── MoneyMap.Core.csproj (EF Core, CsvHelper, Magick.NET, etc.)
│
├── MoneyMap/ (existing web app - slimmed)
│ ├── Pages/ (stays - thin PageModel delegation)
│ ├── Migrations/ (stays - deployment-specific)
│ ├── wwwroot/ (stays - includes receipt files)
│ ├── Program.cs (calls AddMoneyMapCore)
│ └── MoneyMap.csproj (references MoneyMap.Core)
│
├── MoneyMap.Mcp/ (NEW - MCP console app)
│ ├── Tools/
│ │ ├── TransactionTools.cs
│ │ ├── BudgetTools.cs
│ │ ├── CategoryTools.cs
│ │ ├── ReceiptTools.cs
│ │ ├── MerchantTools.cs
│ │ ├── AccountTools.cs
│ │ └── DashboardTools.cs
│ ├── ConfigReceiptStorageOptions.cs
│ ├── Program.cs
│ ├── appsettings.json
│ └── MoneyMap.Mcp.csproj (references MoneyMap.Core + MCP SDK)
│
└── MoneyMap.Tests/ (existing - updates reference to MoneyMap.Core)
Key Architectural Decisions
-
Shared class library (MoneyMap.Core): All models, DbContext, and services extracted into a shared library. Both the web app and MCP app reference it. This ensures logic never drifts between the two consumers.
-
Shared DI registration: A
ServiceCollectionExtensions.AddMoneyMapCore(IConfiguration)extension method registers DbContext and all services. Both apps call this method, preventing registration drift. -
Migrations stay in web project: EF migrations are deployment-specific and remain in the web app project.
-
Receipt files stay in wwwroot: The MCP server reads receipt images from the web app's
wwwroot/receipts/directory via a configured absolute path. -
TransactionImporter stays in web app: CSV import is a UI-driven workflow. It remains in
Upload.cshtml.csand is not exposed via MCP. -
ReceiptParseWorkerService stays in web app: The background hosted service for async receipt parsing stays tied to the web process.
-
Logging to stderr only: MCP uses stdio for the protocol. All logging in the MCP app must be directed to stderr (or file) to avoid corrupting the MCP stream. The
AddMoneyMapCoreextension must not force a stdout-based logging provider. -
No IWebHostEnvironment dependency in Core: Services that currently rely on
IWebHostEnvironmentto resolve file paths (e.g.,ReceiptManager) will be refactored to use anIReceiptStorageOptionsinterface backed by configuration. This allows both the web app and MCP app to provide the correct path without web-host coupling. -
MoneyMap.Core targets
Microsoft.NET.Sdk: The shared library must not depend onMicrosoft.AspNetCore.App. It uses onlyMicrosoft.Extensions.*packages to stay lightweight for the console app consumer. -
Path traversal protection: Receipt file access validates that resolved paths remain within the configured receipts directory to prevent directory traversal attacks.
MCP Tools
Transaction Tools
| Tool | Description | Parameters | Mutates |
|---|---|---|---|
search_transactions |
Filter and list transactions | query? (full-text across name/memo/category), startDate?, endDate?, category?, merchantName?, minAmount?, maxAmount?, accountId?, cardId?, type? (debit/credit), uncategorizedOnly?, limit? (default 50) |
No |
get_transaction |
Get single transaction with full details | transactionId |
No |
get_spending_summary |
Spending totals grouped by category for a date range (excludes transfers) | startDate, endDate, accountId? |
No |
get_income_summary |
Credit totals grouped by source/name for a date range | startDate, endDate, accountId? |
No |
update_transaction_category |
Change category and optionally merchant on transactions | transactionIds (array), category, merchantName? |
Yes |
bulk_recategorize |
Recategorize all transactions matching a name pattern | namePattern, fromCategory?, toCategory, merchantName?, dryRun? (default true — returns preview of affected transactions without applying changes) |
Yes |
Budget Tools
| Tool | Description | Parameters | Mutates |
|---|---|---|---|
get_budget_status |
All active budgets with current period spending vs. limit | asOfDate? |
No |
create_budget |
Create a new category or total budget | category? (null = total), amount, period (Weekly/Monthly/Yearly), startDate |
Yes |
update_budget |
Modify budget amount, period, or active status | budgetId, amount?, period?, isActive? |
Yes |
Category Tools
| Tool | Description | Parameters | Mutates |
|---|---|---|---|
list_categories |
All categories with transaction counts | (none) | No |
get_category_mappings |
Auto-categorization pattern rules | category? (filter) |
No |
add_category_mapping |
Add a new auto-categorization rule | pattern, category, merchantName?, priority? |
Yes |
Receipt Tools
| Tool | Description | Parameters | Mutates |
|---|---|---|---|
get_receipt_image |
Returns receipt as base64 image (PDF → PNG conversion). Path-traversal safe. | receiptId |
No |
get_receipt_text |
Returns already-parsed receipt data (merchant, date, amounts, line items) as structured text — avoids re-OCR when parsed data exists | receiptId |
No |
list_receipts |
List receipts with parse status | transactionId?, parseStatus?, limit? |
No |
get_receipt_details |
Full receipt metadata, parsed data, and line items | receiptId |
No |
Merchant Tools
| Tool | Description | Parameters | Mutates |
|---|---|---|---|
list_merchants |
All merchants with transaction counts and category mapping info | query? (filter by name) |
No |
merge_merchants |
Merge duplicate merchants — reassigns all transactions and mappings from source to target, then deletes source | sourceMerchantId, targetMerchantId |
Yes |
Account & Card Tools
| Tool | Description | Parameters | Mutates |
|---|---|---|---|
list_accounts |
All accounts with transaction counts | (none) | No |
list_cards |
All cards with account info and stats | accountId? |
No |
Dashboard Tools
| Tool | Description | Parameters | Mutates |
|---|---|---|---|
get_dashboard |
Top spending categories, recent transactions, aggregate stats | topCategoriesCount?, recentTransactionsCount? |
No |
get_monthly_trend |
Month-over-month spending totals | months? (default 6), category? |
No |
Internal Design
Program.cs
var builder = Host.CreateApplicationBuilder(args);
// MCP uses stdio — all logging must go to stderr
builder.Logging.ClearProviders();
builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace);
builder.Configuration.AddJsonFile("appsettings.json");
builder.Services.AddMoneyMapCore(builder.Configuration);
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly(typeof(Program).Assembly);
var app = builder.Build();
await app.RunAsync();
ServiceCollectionExtensions (MoneyMap.Core)
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMoneyMapCore(
this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<MoneyMapContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("MoneyMapDb")));
services.AddScoped<ITransactionService, TransactionService>();
services.AddScoped<ITransactionStatisticsService, TransactionStatisticsService>();
services.AddScoped<IBudgetService, BudgetService>();
services.AddScoped<ITransactionCategorizer, TransactionCategorizer>();
services.AddScoped<IMerchantService, MerchantService>();
services.AddScoped<IAccountService, AccountService>();
services.AddScoped<ICardService, CardService>();
services.AddScoped<IReceiptManager, ReceiptManager>();
services.AddScoped<IReceiptMatchingService, ReceiptMatchingService>();
services.AddScoped<IReferenceDataService, ReferenceDataService>();
services.AddScoped<IDashboardService, DashboardService>();
services.AddScoped<IAIToolExecutor, AIToolExecutor>();
services.AddScoped<IAIToolRegistry, AIToolRegistry>();
return services;
}
}
Tool Implementation Pattern
All tools return McpToolResult for consistency and to support multi-part responses (text + images).
[McpServerTool]
[Description("Get spending totals grouped by category for a date range. Excludes transfers.")]
public static async Task<McpToolResult> GetSpendingSummary(
[Description("Start date (inclusive), e.g. 2026-01-01")] string startDate,
[Description("End date (inclusive), e.g. 2026-01-31")] string endDate,
[Description("Optional: filter to specific account ID")] int? accountId,
MoneyMapContext db)
{
var start = DateTime.Parse(startDate);
var end = DateTime.Parse(endDate);
var query = db.Transactions
.Where(t => t.Date >= start && t.Date <= end)
.Where(t => t.Amount < 0) // debits only
.Where(t => t.TransferToAccountId == null); // exclude transfers
if (accountId.HasValue)
query = query.Where(t => t.AccountId == accountId.Value);
var summary = await query
.GroupBy(t => t.Category ?? "Uncategorized")
.Select(g => new { Category = g.Key, Total = g.Sum(t => Math.Abs(t.Amount)), Count = g.Count() })
.OrderByDescending(x => x.Total)
.ToListAsync();
return McpToolResult.Text(JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true }));
}
Receipt Image Tool
[McpServerTool]
[Description("Get a receipt image for visual inspection. Returns the image as base64. Useful for verifying transaction categories.")]
public static async Task<McpToolResult> GetReceiptImage(
[Description("Receipt ID")] long receiptId,
MoneyMapContext db,
IConfiguration config)
{
var receipt = await db.Receipts.FindAsync(receiptId);
if (receipt == null)
return McpToolResult.Text("Receipt not found");
var basePath = Path.GetFullPath(config["Receipts:StoragePath"]!);
var fullPath = Path.GetFullPath(Path.Combine(basePath, receipt.StoragePath));
// Path traversal protection
if (!fullPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase))
return McpToolResult.Text("Invalid receipt path");
if (!File.Exists(fullPath))
return McpToolResult.Text("Receipt file not found on disk");
byte[] imageBytes;
string mimeType;
if (receipt.ContentType == "application/pdf")
{
using var image = new MagickImage(fullPath + "[0]");
image.Density = new Density(220);
image.Format = MagickFormat.Png;
imageBytes = image.ToByteArray();
mimeType = "image/png";
}
else
{
imageBytes = await File.ReadAllBytesAsync(fullPath);
mimeType = receipt.ContentType;
}
return McpToolResult.Image(Convert.ToBase64String(imageBytes), mimeType);
}
Configuration
MoneyMap.Mcp/appsettings.json
{
"ConnectionStrings": {
"MoneyMapDb": "Server=(localdb)\\mssqllocaldb;Database=MoneyMap;Trusted_Connection=True;"
},
"Receipts": {
"StoragePath": "C:/Users/AJ/Desktop/Projects/MoneyMap/MoneyMap/wwwroot/receipts"
}
}
Publishing & Installation
Build
dotnet publish MoneyMap.Mcp -c Release -o "$USERPROFILE/.claude/mcp/MoneyMap.Mcp"
Register
claude mcp add --transport stdio --scope user moneymap -- "C:/Users/AJ/.claude/mcp/MoneyMap.Mcp/MoneyMap.Mcp.exe"
What Stays in the Web App
- EF Migrations (
MoneyMap/Migrations/) - Receipt physical files (
MoneyMap/wwwroot/receipts/) - PageModel code (
Pages/*.cshtml.cs) — thin delegation to Core services TransactionImporter&CardResolver— import-workflow-specificReceiptParseWorkerService— background worker tied to web processAIReceiptParser— tied to the parse worker and web upload flow
What Moves to MoneyMap.Core
- All models (
Models/) MoneyMapContext(Data/MoneyMapContext.cs)- All service interfaces and implementations (
Services/) - AITools (
Services/AITools/) - All DTOs and result types
NuGet Dependencies (MoneyMap.Core)
- Microsoft.EntityFrameworkCore.SqlServer
- CsvHelper
- Magick.NET-Q8-AnyCPU (for receipt PDF→PNG)
- UglyToad.PdfPig (for PDF processing)
NuGet Dependencies (MoneyMap.Mcp)
- ModelContextProtocol (prerelease)
- Microsoft.Extensions.Hosting
Refactoring Notes
IWebHostEnvironment Removal from Core
Services like ReceiptManager currently use IWebHostEnvironment.WebRootPath to resolve receipt file paths. This must be abstracted:
// MoneyMap.Core
public interface IReceiptStorageOptions
{
string ReceiptsBasePath { get; }
}
// MoneyMap (web app) implementation
public class WebReceiptStorageOptions : IReceiptStorageOptions
{
public WebReceiptStorageOptions(IWebHostEnvironment env)
=> ReceiptsBasePath = Path.Combine(env.WebRootPath, "receipts");
public string ReceiptsBasePath { get; }
}
// MoneyMap.Mcp implementation
public class ConfigReceiptStorageOptions : IReceiptStorageOptions
{
public ConfigReceiptStorageOptions(IConfiguration config)
=> ReceiptsBasePath = config["Receipts:StoragePath"]!;
public string ReceiptsBasePath { get; }
}
Each host registers its own implementation. AddMoneyMapCore does NOT register IReceiptStorageOptions — that's host-specific.
Risks & Mitigations
| Risk | Mitigation |
|---|---|
| Large refactoring breaks web app | Run existing tests after extraction; web app behavior unchanged |
| Namespace changes break references | Use find-and-replace for using statements; Roslyn Bridge can verify |
| Receipt path differs per machine | Configurable via IReceiptStorageOptions + appsettings.json |
| MCP binary needs Magick.NET native libs | Included via NuGet package (AnyCPU variant) |
| Concurrent DB access (web + MCP) | EF Core handles this fine with scoped DbContext per request |
| Logging corrupts MCP stdio stream | All logging redirected to stderr in MCP host |
| Path traversal on receipt reads | Resolved paths validated against base directory |
| MoneyMap.Core pulls in ASP.NET deps | Target Microsoft.NET.Sdk, avoid Microsoft.AspNetCore.App framework ref |