Files
MoneyMap/docs/superpowers/specs/2026-04-20-moneymap-mcp-design.md
T
aj dcb57c5cf6 Add design spec for MoneyMap MCP server
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>
2026-04-20 16:17:54 -04:00

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

  1. 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.

  2. Shared DI registration: A ServiceCollectionExtensions.AddMoneyMapCore(IConfiguration) extension method registers DbContext and all services. Both apps call this method, preventing registration drift.

  3. Migrations stay in web project: EF migrations are deployment-specific and remain in the web app project.

  4. Receipt files stay in wwwroot: The MCP server reads receipt images from the web app's wwwroot/receipts/ directory via a configured absolute path.

  5. TransactionImporter stays in web app: CSV import is a UI-driven workflow. It remains in Upload.cshtml.cs and is not exposed via MCP.

  6. ReceiptParseWorkerService stays in web app: The background hosted service for async receipt parsing stays tied to the web process.

  7. 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 AddMoneyMapCore extension must not force a stdout-based logging provider.

  8. No IWebHostEnvironment dependency in Core: Services that currently rely on IWebHostEnvironment to resolve file paths (e.g., ReceiptManager) will be refactored to use an IReceiptStorageOptions interface backed by configuration. This allows both the web app and MCP app to provide the correct path without web-host coupling.

  9. MoneyMap.Core targets Microsoft.NET.Sdk: The shared library must not depend on Microsoft.AspNetCore.App. It uses only Microsoft.Extensions.* packages to stay lightweight for the console app consumer.

  10. 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-specific
  • ReceiptParseWorkerService — background worker tied to web process
  • AIReceiptParser — 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