d831991ad0
14 tasks covering: Core library extraction, service migration, IWebHostEnvironment abstraction, shared DI registration, MCP project skeleton, and all 20 MCP tools across 7 tool files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1843 lines
62 KiB
Markdown
1843 lines
62 KiB
Markdown
# MoneyMap MCP Server Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Extract shared services into MoneyMap.Core class library and create MoneyMap.Mcp console app exposing financial data/operations via MCP protocol.
|
|
|
|
**Architecture:** Three-project solution — MoneyMap.Core (shared library with models, DbContext, services), MoneyMap (web app referencing Core), and MoneyMap.Mcp (MCP stdio console app referencing Core). ReceiptManager's IWebHostEnvironment dependency abstracted to IReceiptStorageOptions interface.
|
|
|
|
**Tech Stack:** .NET 8.0, EF Core 9.0, ModelContextProtocol SDK (prerelease), SQL Server, Magick.NET, stdio transport.
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-04-20-moneymap-mcp-design.md`
|
|
|
|
---
|
|
|
|
## Task 1: Create MoneyMap.Core Project and Move Models
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Core/MoneyMap.Core.csproj`
|
|
- Move: `MoneyMap/Models/*.cs` → `MoneyMap.Core/Models/`
|
|
- Move: `MoneyMap/Models/Api/*.cs` → `MoneyMap.Core/Models/Api/`
|
|
- Move: `MoneyMap/Models/Dashboard/*.cs` → `MoneyMap.Core/Models/Dashboard/`
|
|
- Move: `MoneyMap/Models/Import/*.cs` → `MoneyMap.Core/Models/Import/`
|
|
- Move: `MoneyMap/Data/MoneyMapContext.cs` → `MoneyMap.Core/Data/`
|
|
- Modify: `MoneyMap.sln` (add new project)
|
|
- Modify: `MoneyMap/MoneyMap.csproj` (add ProjectReference to Core, remove moved packages)
|
|
|
|
- [ ] **Step 1: Create MoneyMap.Core class library project**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet new classlib -n MoneyMap.Core --framework net8.0
|
|
rm MoneyMap.Core/Class1.cs
|
|
```
|
|
|
|
- [ ] **Step 2: Add NuGet packages to MoneyMap.Core**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap/MoneyMap.Core
|
|
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.9
|
|
dotnet add package CsvHelper --version 33.1.0
|
|
dotnet add package Magick.NET-Q16-AnyCPU --version 14.8.2
|
|
dotnet add package PdfPig --version 0.1.11
|
|
dotnet add package Microsoft.Extensions.Caching.Memory
|
|
dotnet add package Microsoft.Extensions.Http
|
|
```
|
|
|
|
- [ ] **Step 3: Enable nullable and implicit usings in MoneyMap.Core.csproj**
|
|
|
|
Edit `MoneyMap.Core/MoneyMap.Core.csproj` to contain:
|
|
|
|
```xml
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<TargetFramework>net8.0</TargetFramework>
|
|
<Nullable>enable</Nullable>
|
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
</PropertyGroup>
|
|
<ItemGroup>
|
|
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
|
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.8.2" />
|
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
|
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
|
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
|
<PackageReference Include="PdfPig" Version="0.1.11" />
|
|
</ItemGroup>
|
|
</Project>
|
|
```
|
|
|
|
- [ ] **Step 4: Move Models to MoneyMap.Core**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
mkdir -p MoneyMap.Core/Models/Api MoneyMap.Core/Models/Dashboard MoneyMap.Core/Models/Import
|
|
mv MoneyMap/Models/Account.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/Budget.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/Card.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/CategoryMapping.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/Merchant.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/Receipt.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/ReceiptLineItem.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/ReceiptParseLog.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/Transaction.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/Transfer.cs MoneyMap.Core/Models/
|
|
mv MoneyMap/Models/Api/FinancialAuditModels.cs MoneyMap.Core/Models/Api/
|
|
mv MoneyMap/Models/Dashboard/DashboardModels.cs MoneyMap.Core/Models/Dashboard/
|
|
mv MoneyMap/Models/Import/ImportContext.cs MoneyMap.Core/Models/Import/
|
|
mv MoneyMap/Models/Import/ImportResults.cs MoneyMap.Core/Models/Import/
|
|
mv MoneyMap/Models/Import/PaymentResolutionResult.cs MoneyMap.Core/Models/Import/
|
|
mv MoneyMap/Models/Import/TransactionCsvRow.cs MoneyMap.Core/Models/Import/
|
|
mv MoneyMap/Models/Import/TransactionCsvRowMap.cs MoneyMap.Core/Models/Import/
|
|
```
|
|
|
|
- [ ] **Step 5: Move Data folder to MoneyMap.Core**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
mkdir -p MoneyMap.Core/Data
|
|
mv MoneyMap/Data/MoneyMapContext.cs MoneyMap.Core/Data/
|
|
```
|
|
|
|
- [ ] **Step 6: Add MoneyMap.Core to the solution and add ProjectReference from MoneyMap**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet sln MoneyMap.sln add MoneyMap.Core/MoneyMap.Core.csproj
|
|
```
|
|
|
|
Add ProjectReference in `MoneyMap/MoneyMap.csproj`:
|
|
|
|
```xml
|
|
<ItemGroup>
|
|
<ProjectReference Include="..\MoneyMap.Core\MoneyMap.Core.csproj" />
|
|
</ItemGroup>
|
|
```
|
|
|
|
Remove from `MoneyMap/MoneyMap.csproj` the packages that moved to Core:
|
|
- `CsvHelper`
|
|
- `Magick.NET-Q16-AnyCPU`
|
|
- `Microsoft.EntityFrameworkCore` (keep `Microsoft.EntityFrameworkCore.Tools` for migrations)
|
|
- `Microsoft.EntityFrameworkCore.SqlServer`
|
|
- `PdfPig`
|
|
|
|
Keep `Microsoft.EntityFrameworkCore.Tools` (needed for migrations).
|
|
|
|
- [ ] **Step 7: Update MoneyMap.Tests to reference MoneyMap.Core**
|
|
|
|
In `MoneyMap.Tests/MoneyMap.Tests.csproj`, add:
|
|
|
|
```xml
|
|
<ProjectReference Include="..\MoneyMap.Core\MoneyMap.Core.csproj" />
|
|
```
|
|
|
|
Also add `Microsoft.EntityFrameworkCore.InMemory` remains (it's test-specific).
|
|
|
|
- [ ] **Step 8: Build and fix any namespace/reference issues**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.sln
|
|
```
|
|
|
|
Fix any issues — namespaces remain unchanged (`MoneyMap.Models`, `MoneyMap.Data`, etc.) so references should resolve through the ProjectReference. The web project may need the `Migrations` folder to reference the design-time DbContext from Core.
|
|
|
|
Add to `MoneyMap/MoneyMap.csproj` if migrations break:
|
|
|
|
```xml
|
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9">
|
|
<PrivateAssets>all</PrivateAssets>
|
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
</PackageReference>
|
|
```
|
|
|
|
- [ ] **Step 9: Run tests to verify nothing broke**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj
|
|
```
|
|
|
|
Expected: All existing tests pass.
|
|
|
|
- [ ] **Step 10: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "refactor: extract Models and Data into MoneyMap.Core shared library"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Move Services to MoneyMap.Core
|
|
|
|
**Files:**
|
|
- Move: `MoneyMap/Services/*.cs` → `MoneyMap.Core/Services/`
|
|
- Move: `MoneyMap/Services/AITools/*.cs` → `MoneyMap.Core/Services/AITools/`
|
|
- Keep in web: `ReceiptParseWorkerService.cs`, `ModelWarmupService.cs` (hosted services)
|
|
|
|
- [ ] **Step 1: Move core services to MoneyMap.Core**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
mkdir -p MoneyMap.Core/Services/AITools
|
|
|
|
# Core services (no web dependencies)
|
|
mv MoneyMap/Services/AccountService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/BudgetService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/CardResolver.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/CardService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/DashboardService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/FinancialAuditService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/MerchantService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/ReceiptAutoMapper.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/ReceiptMatchingService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/ReferenceDataService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/TransactionCategorizer.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/TransactionFilters.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/TransactionImporter.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/TransactionService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/TransactionStatisticsService.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/TransactionAICategorizer.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/PdfToImageConverter.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/AIReceiptParser.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/AIVisionClient.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/ReceiptManager.cs MoneyMap.Core/Services/
|
|
mv MoneyMap/Services/ReceiptParseQueue.cs MoneyMap.Core/Services/
|
|
|
|
# AI Tools
|
|
mv MoneyMap/Services/AITools/AIToolDefinitions.cs MoneyMap.Core/Services/AITools/
|
|
mv MoneyMap/Services/AITools/AIToolExecutor.cs MoneyMap.Core/Services/AITools/
|
|
```
|
|
|
|
- [ ] **Step 2: Keep hosted services in web project**
|
|
|
|
These stay in `MoneyMap/Services/`:
|
|
- `ReceiptParseWorkerService.cs` — BackgroundService tied to web lifecycle
|
|
- `ModelWarmupService.cs` — startup hosted service
|
|
|
|
Verify they're still there:
|
|
```bash
|
|
ls MoneyMap/Services/
|
|
```
|
|
|
|
Expected: Only `ReceiptParseWorkerService.cs` and `ModelWarmupService.cs` remain.
|
|
|
|
- [ ] **Step 3: Move the Prompts folder to MoneyMap.Core**
|
|
|
|
The `AIReceiptParser` reads `Prompts/ReceiptParserPrompt.txt` from `AppContext.BaseDirectory`. This needs to be embedded or copied with MoneyMap.Core.
|
|
|
|
```bash
|
|
mv MoneyMap/Prompts MoneyMap.Core/Prompts
|
|
```
|
|
|
|
Add to `MoneyMap.Core/MoneyMap.Core.csproj`:
|
|
|
|
```xml
|
|
<ItemGroup>
|
|
<None Update="Prompts\**\*">
|
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
</None>
|
|
</ItemGroup>
|
|
```
|
|
|
|
Remove from `MoneyMap/MoneyMap.csproj` the Prompts section:
|
|
|
|
```xml
|
|
<!-- Remove this -->
|
|
<ItemGroup>
|
|
<None Update="Prompts\**\*">
|
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
</None>
|
|
</ItemGroup>
|
|
```
|
|
|
|
- [ ] **Step 4: Build and fix issues**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.sln
|
|
```
|
|
|
|
Likely issues:
|
|
- `ReceiptManager` uses `IFormFile` (from `Microsoft.AspNetCore.Http`) — need to add this package to Core or abstract it. Since `IFormFile` is in `Microsoft.AspNetCore.Http.Features`, add to `MoneyMap.Core.csproj`:
|
|
|
|
```xml
|
|
<ItemGroup>
|
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
|
</ItemGroup>
|
|
```
|
|
|
|
**Wait** — this contradicts the spec's decision #9 (no ASP.NET deps in Core). The spec says to abstract `IWebHostEnvironment` via `IReceiptStorageOptions`, but `IFormFile` is also ASP.NET-specific. Since `ReceiptManager`'s upload methods (`UploadReceiptAsync`, `UploadUnmappedReceiptAsync`, `UploadManyUnmappedReceiptsAsync`) use `IFormFile` extensively and are only called from the web app, the cleanest approach is:
|
|
|
|
**Split ReceiptManager into two parts:**
|
|
1. `MoneyMap.Core/Services/ReceiptManager.cs` — read-only operations (GetReceiptPhysicalPath, GetReceiptAsync, DeleteReceiptAsync, MapReceiptToTransactionAsync, UnmapReceiptAsync, duplicate checks)
|
|
2. `MoneyMap/Services/ReceiptUploadService.cs` — upload operations using IFormFile (stays in web project)
|
|
|
|
But this is complex. Simpler: add the `FrameworkReference` to Core. It's a reference, not a deployment dependency — the MCP console app won't actually call upload methods. The alternative (splitting) adds complexity for no runtime benefit.
|
|
|
|
**Decision: Add `<FrameworkReference Include="Microsoft.AspNetCore.App" />` to MoneyMap.Core.** The MCP console app will need to declare it doesn't serve web traffic, but the types compile fine. For a local-only MCP tool this is acceptable.
|
|
|
|
Actually — on .NET 8 console apps, you CAN reference `Microsoft.AspNetCore.App` shared framework. It's available regardless. This is fine.
|
|
|
|
- [ ] **Step 5: Run tests**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj
|
|
```
|
|
|
|
Expected: All tests pass.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "refactor: move services and AITools to MoneyMap.Core"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: Abstract IWebHostEnvironment from ReceiptManager
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Core/Services/IReceiptStorageOptions.cs`
|
|
- Modify: `MoneyMap.Core/Services/ReceiptManager.cs` (replace IWebHostEnvironment with IReceiptStorageOptions)
|
|
- Create: `MoneyMap/Services/WebReceiptStorageOptions.cs`
|
|
- Modify: `MoneyMap/Program.cs` (register WebReceiptStorageOptions)
|
|
|
|
- [ ] **Step 1: Create IReceiptStorageOptions interface in Core**
|
|
|
|
Create `MoneyMap.Core/Services/IReceiptStorageOptions.cs`:
|
|
|
|
```csharp
|
|
namespace MoneyMap.Services;
|
|
|
|
public interface IReceiptStorageOptions
|
|
{
|
|
string ReceiptsBasePath { get; }
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Update ReceiptManager to use IReceiptStorageOptions**
|
|
|
|
In `MoneyMap.Core/Services/ReceiptManager.cs`:
|
|
|
|
Replace the constructor parameter `IWebHostEnvironment environment` with `IReceiptStorageOptions receiptStorage`.
|
|
|
|
Replace the field:
|
|
```csharp
|
|
private readonly IWebHostEnvironment _environment;
|
|
```
|
|
with:
|
|
```csharp
|
|
private readonly IReceiptStorageOptions _receiptStorage;
|
|
```
|
|
|
|
Replace the constructor assignment:
|
|
```csharp
|
|
_environment = environment;
|
|
```
|
|
with:
|
|
```csharp
|
|
_receiptStorage = receiptStorage;
|
|
```
|
|
|
|
Replace `GetReceiptsBasePath()`:
|
|
```csharp
|
|
private string GetReceiptsBasePath()
|
|
{
|
|
return _receiptStorage.ReceiptsBasePath;
|
|
}
|
|
```
|
|
|
|
Remove the `IConfiguration _configuration` field and constructor parameter (if only used for receipts path). Check if `_configuration` is used elsewhere in ReceiptManager first — if yes, keep it.
|
|
|
|
- [ ] **Step 3: Create WebReceiptStorageOptions in web project**
|
|
|
|
Create `MoneyMap/Services/WebReceiptStorageOptions.cs`:
|
|
|
|
```csharp
|
|
using MoneyMap.Services;
|
|
|
|
namespace MoneyMap.WebApp.Services;
|
|
|
|
public class WebReceiptStorageOptions : IReceiptStorageOptions
|
|
{
|
|
public string ReceiptsBasePath { get; }
|
|
|
|
public WebReceiptStorageOptions(IWebHostEnvironment env, IConfiguration config)
|
|
{
|
|
var relativePath = config["Receipts:StoragePath"] ?? "receipts";
|
|
// If the path is absolute (UNC or rooted), use directly; otherwise combine with wwwroot
|
|
if (Path.IsPathRooted(relativePath))
|
|
ReceiptsBasePath = relativePath;
|
|
else
|
|
ReceiptsBasePath = Path.Combine(env.WebRootPath, relativePath);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Register in web app's Program.cs**
|
|
|
|
Add to `MoneyMap/Program.cs`:
|
|
|
|
```csharp
|
|
using MoneyMap.WebApp.Services;
|
|
|
|
// After other service registrations:
|
|
builder.Services.AddSingleton<IReceiptStorageOptions, WebReceiptStorageOptions>();
|
|
```
|
|
|
|
- [ ] **Step 5: Build and run tests**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.sln
|
|
dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj
|
|
```
|
|
|
|
Expected: All tests pass.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "refactor: abstract IWebHostEnvironment to IReceiptStorageOptions"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Create ServiceCollectionExtensions in MoneyMap.Core
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Core/ServiceCollectionExtensions.cs`
|
|
- Modify: `MoneyMap/Program.cs` (replace individual registrations with AddMoneyMapCore call)
|
|
|
|
- [ ] **Step 1: Create the extension method**
|
|
|
|
Create `MoneyMap.Core/ServiceCollectionExtensions.cs`:
|
|
|
|
```csharp
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using MoneyMap.Data;
|
|
using MoneyMap.Services;
|
|
using MoneyMap.Services.AITools;
|
|
|
|
namespace 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.AddMemoryCache();
|
|
|
|
// Core transaction and import services
|
|
services.AddScoped<ITransactionImporter, TransactionImporter>();
|
|
services.AddScoped<ICardResolver, CardResolver>();
|
|
services.AddScoped<ITransactionCategorizer, TransactionCategorizer>();
|
|
services.AddScoped<ITransactionService, TransactionService>();
|
|
services.AddScoped<ITransactionStatisticsService, TransactionStatisticsService>();
|
|
|
|
// Entity management services
|
|
services.AddScoped<IAccountService, AccountService>();
|
|
services.AddScoped<ICardService, CardService>();
|
|
services.AddScoped<IMerchantService, MerchantService>();
|
|
services.AddScoped<IBudgetService, BudgetService>();
|
|
|
|
// Receipt services
|
|
services.AddScoped<IReceiptMatchingService, ReceiptMatchingService>();
|
|
services.AddScoped<IReceiptManager, ReceiptManager>();
|
|
services.AddScoped<IReceiptAutoMapper, ReceiptAutoMapper>();
|
|
services.AddScoped<IPdfToImageConverter, PdfToImageConverter>();
|
|
|
|
// Reference data and dashboard
|
|
services.AddScoped<IReferenceDataService, ReferenceDataService>();
|
|
services.AddScoped<IDashboardService, DashboardService>();
|
|
services.AddScoped<IDashboardStatsCalculator, DashboardStatsCalculator>();
|
|
services.AddScoped<ITopCategoriesProvider, TopCategoriesProvider>();
|
|
services.AddScoped<IRecentTransactionsProvider, RecentTransactionsProvider>();
|
|
services.AddScoped<ISpendTrendsProvider, SpendTrendsProvider>();
|
|
|
|
// AI services
|
|
services.AddScoped<IAIToolExecutor, AIToolExecutor>();
|
|
services.AddScoped<IFinancialAuditService, FinancialAuditService>();
|
|
|
|
return services;
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Update web app Program.cs to use AddMoneyMapCore**
|
|
|
|
Replace the individual service registrations in `MoneyMap/Program.cs` with:
|
|
|
|
```csharp
|
|
using MoneyMap.Core;
|
|
using MoneyMap.WebApp.Services;
|
|
|
|
// ... after builder creation ...
|
|
|
|
builder.Services.AddMoneyMapCore(builder.Configuration);
|
|
builder.Services.AddSingleton<IReceiptStorageOptions, WebReceiptStorageOptions>();
|
|
|
|
// Web-specific services that stay here:
|
|
builder.Services.AddSingleton<IReceiptParseQueue, ReceiptParseQueue>();
|
|
builder.Services.AddHostedService<ReceiptParseWorkerService>();
|
|
builder.Services.AddHttpClient<OpenAIVisionClient>();
|
|
builder.Services.AddHttpClient<ClaudeVisionClient>();
|
|
builder.Services.AddHttpClient<OllamaVisionClient>();
|
|
builder.Services.AddHttpClient<LlamaCppVisionClient>();
|
|
builder.Services.AddScoped<IAIVisionClientResolver, AIVisionClientResolver>();
|
|
builder.Services.AddScoped<IReceiptParser, AIReceiptParser>();
|
|
builder.Services.AddHttpClient<ITransactionAICategorizer, TransactionAICategorizer>();
|
|
builder.Services.AddHostedService<ModelWarmupService>();
|
|
```
|
|
|
|
Remove all the individual core service registrations that are now in `AddMoneyMapCore`.
|
|
|
|
- [ ] **Step 3: Build and run tests**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.sln
|
|
dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj
|
|
```
|
|
|
|
- [ ] **Step 4: Quick smoke test the web app**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap/MoneyMap
|
|
dotnet run
|
|
```
|
|
|
|
Verify it starts without errors, then stop it (Ctrl+C).
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "refactor: consolidate service registration into AddMoneyMapCore extension"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Create MoneyMap.Mcp Project Skeleton
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Mcp/MoneyMap.Mcp.csproj`
|
|
- Create: `MoneyMap.Mcp/Program.cs`
|
|
- Create: `MoneyMap.Mcp/ConfigReceiptStorageOptions.cs`
|
|
- Create: `MoneyMap.Mcp/appsettings.json`
|
|
- Modify: `MoneyMap.sln` (add project)
|
|
|
|
- [ ] **Step 1: Create the console project**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet new console -n MoneyMap.Mcp --framework net8.0
|
|
```
|
|
|
|
- [ ] **Step 2: Set up MoneyMap.Mcp.csproj**
|
|
|
|
Replace `MoneyMap.Mcp/MoneyMap.Mcp.csproj` with:
|
|
|
|
```xml
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<OutputType>Exe</OutputType>
|
|
<TargetFramework>net8.0</TargetFramework>
|
|
<Nullable>enable</Nullable>
|
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<PackageReference Include="ModelContextProtocol" Version="0.*-*" />
|
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<ProjectReference Include="..\MoneyMap.Core\MoneyMap.Core.csproj" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<None Update="appsettings.json">
|
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
</None>
|
|
</ItemGroup>
|
|
</Project>
|
|
```
|
|
|
|
- [ ] **Step 3: Add to solution**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet sln MoneyMap.sln add MoneyMap.Mcp/MoneyMap.Mcp.csproj
|
|
```
|
|
|
|
- [ ] **Step 4: Create appsettings.json**
|
|
|
|
Create `MoneyMap.Mcp/appsettings.json`:
|
|
|
|
```json
|
|
{
|
|
"ConnectionStrings": {
|
|
"MoneyMapDb": "Server=barge.lan;Database=MoneyMap;User Id=moneymap;Password=Cn87oXQPj7EEkx;TrustServerCertificate=True;"
|
|
},
|
|
"Receipts": {
|
|
"StoragePath": "\\\\TRUENAS\\receipts"
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Create ConfigReceiptStorageOptions**
|
|
|
|
Create `MoneyMap.Mcp/ConfigReceiptStorageOptions.cs`:
|
|
|
|
```csharp
|
|
using MoneyMap.Services;
|
|
|
|
namespace MoneyMap.Mcp;
|
|
|
|
public class ConfigReceiptStorageOptions : IReceiptStorageOptions
|
|
{
|
|
public string ReceiptsBasePath { get; }
|
|
|
|
public ConfigReceiptStorageOptions(IConfiguration config)
|
|
{
|
|
ReceiptsBasePath = config["Receipts:StoragePath"]
|
|
?? throw new InvalidOperationException("Receipts:StoragePath not configured");
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 6: Create Program.cs**
|
|
|
|
Replace `MoneyMap.Mcp/Program.cs` with:
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using ModelContextProtocol.Server;
|
|
using MoneyMap.Core;
|
|
using MoneyMap.Mcp;
|
|
using MoneyMap.Services;
|
|
|
|
var builder = Host.CreateApplicationBuilder(args);
|
|
|
|
builder.Logging.ClearProviders();
|
|
builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace);
|
|
|
|
builder.Services.AddMoneyMapCore(builder.Configuration);
|
|
builder.Services.AddSingleton<IReceiptStorageOptions, ConfigReceiptStorageOptions>();
|
|
|
|
builder.Services
|
|
.AddMcpServer()
|
|
.WithStdioServerTransport()
|
|
.WithToolsFromAssembly(typeof(Program).Assembly);
|
|
|
|
var app = builder.Build();
|
|
await app.RunAsync();
|
|
```
|
|
|
|
- [ ] **Step 7: Build to verify skeleton compiles**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj
|
|
```
|
|
|
|
- [ ] **Step 8: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat: add MoneyMap.Mcp project skeleton with stdio transport"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Implement Transaction Tools
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Mcp/Tools/TransactionTools.cs`
|
|
|
|
- [ ] **Step 1: Create TransactionTools.cs**
|
|
|
|
Create `MoneyMap.Mcp/Tools/TransactionTools.cs`:
|
|
|
|
```csharp
|
|
using System.ComponentModel;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using ModelContextProtocol.Server;
|
|
using MoneyMap.Data;
|
|
using MoneyMap.Services;
|
|
|
|
namespace MoneyMap.Mcp.Tools;
|
|
|
|
public static class TransactionTools
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
|
|
[McpServerTool, Description("Search and filter transactions. Returns matching transactions with details.")]
|
|
public static async Task<string> SearchTransactions(
|
|
[Description("Full-text search across name, memo, and category")] string? query = null,
|
|
[Description("Start date (inclusive), e.g. 2026-01-01")] string? startDate = null,
|
|
[Description("End date (inclusive), e.g. 2026-01-31")] string? endDate = null,
|
|
[Description("Filter by category name (exact match)")] string? category = null,
|
|
[Description("Filter by merchant name (contains)")] string? merchantName = null,
|
|
[Description("Minimum amount (absolute value)")] decimal? minAmount = null,
|
|
[Description("Maximum amount (absolute value)")] decimal? maxAmount = null,
|
|
[Description("Filter by account ID")] int? accountId = null,
|
|
[Description("Filter by card ID")] int? cardId = null,
|
|
[Description("Filter by type: 'debit' or 'credit'")] string? type = null,
|
|
[Description("Only show uncategorized transactions")] bool? uncategorizedOnly = null,
|
|
[Description("Max results to return (default 50)")] int? limit = null,
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var q = db.Transactions
|
|
.Include(t => t.Merchant)
|
|
.Include(t => t.Card)
|
|
.Include(t => t.Account)
|
|
.Include(t => t.Receipts)
|
|
.AsQueryable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(query))
|
|
q = q.Where(t => t.Name.Contains(query) || (t.Memo != null && t.Memo.Contains(query)) || (t.Category != null && t.Category.Contains(query)));
|
|
|
|
if (!string.IsNullOrWhiteSpace(startDate) && DateTime.TryParse(startDate, out var start))
|
|
q = q.Where(t => t.Date >= start);
|
|
|
|
if (!string.IsNullOrWhiteSpace(endDate) && DateTime.TryParse(endDate, out var end))
|
|
q = q.Where(t => t.Date <= end);
|
|
|
|
if (!string.IsNullOrWhiteSpace(category))
|
|
q = q.Where(t => t.Category == category);
|
|
|
|
if (!string.IsNullOrWhiteSpace(merchantName))
|
|
q = q.Where(t => t.Merchant != null && t.Merchant.Name.Contains(merchantName));
|
|
|
|
if (minAmount.HasValue)
|
|
q = q.Where(t => Math.Abs(t.Amount) >= minAmount.Value);
|
|
|
|
if (maxAmount.HasValue)
|
|
q = q.Where(t => Math.Abs(t.Amount) <= maxAmount.Value);
|
|
|
|
if (accountId.HasValue)
|
|
q = q.Where(t => t.AccountId == accountId.Value);
|
|
|
|
if (cardId.HasValue)
|
|
q = q.Where(t => t.CardId == cardId.Value);
|
|
|
|
if (type?.ToLower() == "debit")
|
|
q = q.Where(t => t.Amount < 0);
|
|
else if (type?.ToLower() == "credit")
|
|
q = q.Where(t => t.Amount > 0);
|
|
|
|
if (uncategorizedOnly == true)
|
|
q = q.Where(t => t.Category == null || t.Category == "");
|
|
|
|
var results = await q
|
|
.OrderByDescending(t => t.Date).ThenByDescending(t => t.Id)
|
|
.Take(limit ?? 50)
|
|
.Select(t => new
|
|
{
|
|
t.Id,
|
|
t.Date,
|
|
t.Name,
|
|
t.Memo,
|
|
t.Amount,
|
|
t.Category,
|
|
Merchant = t.Merchant != null ? t.Merchant.Name : null,
|
|
Account = t.Account.Issuer + " " + t.Account.Last4,
|
|
Card = t.Card != null ? t.Card.Issuer + " " + t.Card.Last4 : null,
|
|
ReceiptCount = t.Receipts.Count,
|
|
t.TransferToAccountId
|
|
})
|
|
.ToListAsync();
|
|
|
|
return JsonSerializer.Serialize(new { Count = results.Count, Transactions = results }, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Get a single transaction with all details including receipts.")]
|
|
public static async Task<string> GetTransaction(
|
|
[Description("Transaction ID")] long transactionId,
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var t = await db.Transactions
|
|
.Include(t => t.Merchant)
|
|
.Include(t => t.Card)
|
|
.Include(t => t.Account)
|
|
.Include(t => t.Receipts)
|
|
.Where(t => t.Id == transactionId)
|
|
.Select(t => new
|
|
{
|
|
t.Id,
|
|
t.Date,
|
|
t.Name,
|
|
t.Memo,
|
|
t.Amount,
|
|
t.TransactionType,
|
|
t.Category,
|
|
Merchant = t.Merchant != null ? t.Merchant.Name : null,
|
|
MerchantId = t.MerchantId,
|
|
Account = t.Account.Issuer + " " + t.Account.Last4,
|
|
AccountId = t.AccountId,
|
|
Card = t.Card != null ? t.Card.Issuer + " " + t.Card.Last4 : null,
|
|
CardId = t.CardId,
|
|
t.Notes,
|
|
t.TransferToAccountId,
|
|
Receipts = t.Receipts.Select(r => new { r.Id, r.FileName, r.ParseStatus, r.Merchant, r.Total }).ToList()
|
|
})
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (t == null)
|
|
return "Transaction not found";
|
|
|
|
return JsonSerializer.Serialize(t, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Get spending totals grouped by category for a date range. Excludes transfers.")]
|
|
public static async Task<string> GetSpendingSummary(
|
|
[Description("Start date (inclusive), e.g. 2026-01-01")] string startDate,
|
|
[Description("End date (inclusive), e.g. 2026-01-31")] string endDate,
|
|
[Description("Filter to specific account ID")] int? accountId = null,
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var start = DateTime.Parse(startDate);
|
|
var end = DateTime.Parse(endDate);
|
|
|
|
var q = db.Transactions
|
|
.Where(t => t.Date >= start && t.Date <= end)
|
|
.Where(t => t.Amount < 0)
|
|
.Where(t => t.TransferToAccountId == null)
|
|
.ExcludeTransfers();
|
|
|
|
if (accountId.HasValue)
|
|
q = q.Where(t => t.AccountId == accountId.Value);
|
|
|
|
var summary = await q
|
|
.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();
|
|
|
|
var grandTotal = summary.Sum(x => x.Total);
|
|
|
|
return JsonSerializer.Serialize(new { Period = $"{startDate} to {endDate}", GrandTotal = grandTotal, Categories = summary }, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Get income (credits) grouped by source/name for a date range.")]
|
|
public static async Task<string> GetIncomeSummary(
|
|
[Description("Start date (inclusive), e.g. 2026-01-01")] string startDate,
|
|
[Description("End date (inclusive), e.g. 2026-01-31")] string endDate,
|
|
[Description("Filter to specific account ID")] int? accountId = null,
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var start = DateTime.Parse(startDate);
|
|
var end = DateTime.Parse(endDate);
|
|
|
|
var q = db.Transactions
|
|
.Where(t => t.Date >= start && t.Date <= end)
|
|
.Where(t => t.Amount > 0)
|
|
.Where(t => t.TransferToAccountId == null)
|
|
.ExcludeTransfers();
|
|
|
|
if (accountId.HasValue)
|
|
q = q.Where(t => t.AccountId == accountId.Value);
|
|
|
|
var summary = await q
|
|
.GroupBy(t => t.Name)
|
|
.Select(g => new { Source = g.Key, Total = g.Sum(t => t.Amount), Count = g.Count() })
|
|
.OrderByDescending(x => x.Total)
|
|
.ToListAsync();
|
|
|
|
var grandTotal = summary.Sum(x => x.Total);
|
|
|
|
return JsonSerializer.Serialize(new { Period = $"{startDate} to {endDate}", GrandTotal = grandTotal, Sources = summary }, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Update the category (and optionally merchant) on one or more transactions.")]
|
|
public static async Task<string> UpdateTransactionCategory(
|
|
[Description("Array of transaction IDs to update")] long[] transactionIds,
|
|
[Description("New category to assign")] string category,
|
|
[Description("Merchant name to assign (creates if new)")] string? merchantName = null,
|
|
MoneyMapContext db = default!,
|
|
IMerchantService merchantService = default!)
|
|
{
|
|
var transactions = await db.Transactions
|
|
.Where(t => transactionIds.Contains(t.Id))
|
|
.ToListAsync();
|
|
|
|
if (!transactions.Any())
|
|
return "No transactions found with the provided IDs";
|
|
|
|
int? merchantId = null;
|
|
if (!string.IsNullOrWhiteSpace(merchantName))
|
|
merchantId = await merchantService.GetOrCreateIdAsync(merchantName);
|
|
|
|
foreach (var t in transactions)
|
|
{
|
|
t.Category = category;
|
|
if (merchantId.HasValue)
|
|
t.MerchantId = merchantId;
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return JsonSerializer.Serialize(new { Updated = transactions.Count, Category = category, Merchant = merchantName }, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Recategorize all transactions matching a name pattern. Use dryRun=true (default) to preview changes first.")]
|
|
public static async Task<string> BulkRecategorize(
|
|
[Description("Pattern to match in transaction name (case-insensitive contains)")] string namePattern,
|
|
[Description("New category to assign")] string toCategory,
|
|
[Description("Only recategorize transactions currently in this category")] string? fromCategory = null,
|
|
[Description("Merchant name to assign (creates if new)")] string? merchantName = null,
|
|
[Description("If true (default), only shows what would change without applying")] bool dryRun = true,
|
|
MoneyMapContext db = default!,
|
|
IMerchantService merchantService = default!)
|
|
{
|
|
var q = db.Transactions
|
|
.Where(t => t.Name.Contains(namePattern));
|
|
|
|
if (!string.IsNullOrWhiteSpace(fromCategory))
|
|
q = q.Where(t => t.Category == fromCategory);
|
|
|
|
var transactions = await q.ToListAsync();
|
|
|
|
if (!transactions.Any())
|
|
return JsonSerializer.Serialize(new { Message = "No transactions match the pattern", Pattern = namePattern, FromCategory = fromCategory }, JsonOptions);
|
|
|
|
if (dryRun)
|
|
{
|
|
var preview = transactions.Take(20).Select(t => new { t.Id, t.Date, t.Name, t.Amount, CurrentCategory = t.Category }).ToList();
|
|
return JsonSerializer.Serialize(new { DryRun = true, TotalMatches = transactions.Count, Preview = preview, ToCategory = toCategory }, JsonOptions);
|
|
}
|
|
|
|
int? merchantId = null;
|
|
if (!string.IsNullOrWhiteSpace(merchantName))
|
|
merchantId = await merchantService.GetOrCreateIdAsync(merchantName);
|
|
|
|
foreach (var t in transactions)
|
|
{
|
|
t.Category = toCategory;
|
|
if (merchantId.HasValue)
|
|
t.MerchantId = merchantId;
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return JsonSerializer.Serialize(new { Applied = true, Updated = transactions.Count, ToCategory = toCategory, Merchant = merchantName }, JsonOptions);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Build to verify**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(mcp): implement transaction tools (search, spending, income, categorize)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Implement Budget Tools
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Mcp/Tools/BudgetTools.cs`
|
|
|
|
- [ ] **Step 1: Create BudgetTools.cs**
|
|
|
|
Create `MoneyMap.Mcp/Tools/BudgetTools.cs`:
|
|
|
|
```csharp
|
|
using System.ComponentModel;
|
|
using System.Text.Json;
|
|
using ModelContextProtocol.Server;
|
|
using MoneyMap.Models;
|
|
using MoneyMap.Services;
|
|
|
|
namespace MoneyMap.Mcp.Tools;
|
|
|
|
public static class BudgetTools
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
|
|
[McpServerTool, Description("Get all active budgets with current period spending vs. limit.")]
|
|
public static async Task<string> GetBudgetStatus(
|
|
[Description("Date to calculate status for (defaults to today)")] string? asOfDate = null,
|
|
IBudgetService budgetService = default!)
|
|
{
|
|
DateTime? date = null;
|
|
if (!string.IsNullOrWhiteSpace(asOfDate) && DateTime.TryParse(asOfDate, out var parsed))
|
|
date = parsed;
|
|
|
|
var statuses = await budgetService.GetAllBudgetStatusesAsync(date);
|
|
|
|
var result = statuses.Select(s => new
|
|
{
|
|
s.Budget.Id,
|
|
Category = s.Budget.DisplayName,
|
|
s.Budget.Amount,
|
|
Period = s.Budget.Period.ToString(),
|
|
s.PeriodStart,
|
|
s.PeriodEnd,
|
|
s.Spent,
|
|
s.Remaining,
|
|
PercentUsed = Math.Round(s.PercentUsed, 1),
|
|
s.IsOverBudget
|
|
}).ToList();
|
|
|
|
return JsonSerializer.Serialize(result, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Create a new budget for a category or total spending.")]
|
|
public static async Task<string> CreateBudget(
|
|
[Description("Budget amount limit")] decimal amount,
|
|
[Description("Period: Weekly, Monthly, or Yearly")] string period,
|
|
[Description("Start date for period calculation, e.g. 2026-01-01")] string startDate,
|
|
[Description("Category name (omit for total spending budget)")] string? category = null,
|
|
IBudgetService budgetService = default!)
|
|
{
|
|
if (!Enum.TryParse<BudgetPeriod>(period, true, out var budgetPeriod))
|
|
return $"Invalid period '{period}'. Must be Weekly, Monthly, or Yearly.";
|
|
|
|
var budget = new Budget
|
|
{
|
|
Category = category,
|
|
Amount = amount,
|
|
Period = budgetPeriod,
|
|
StartDate = DateTime.Parse(startDate),
|
|
IsActive = true
|
|
};
|
|
|
|
var result = await budgetService.CreateBudgetAsync(budget);
|
|
|
|
return JsonSerializer.Serialize(new { result.Success, result.Message, BudgetId = budget.Id }, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Update an existing budget's amount, period, or active status.")]
|
|
public static async Task<string> UpdateBudget(
|
|
[Description("Budget ID to update")] int budgetId,
|
|
[Description("New budget amount")] decimal? amount = null,
|
|
[Description("New period: Weekly, Monthly, or Yearly")] string? period = null,
|
|
[Description("Set active/inactive")] bool? isActive = null,
|
|
IBudgetService budgetService = default!)
|
|
{
|
|
var budget = await budgetService.GetBudgetByIdAsync(budgetId);
|
|
if (budget == null)
|
|
return "Budget not found";
|
|
|
|
if (amount.HasValue)
|
|
budget.Amount = amount.Value;
|
|
|
|
if (!string.IsNullOrWhiteSpace(period))
|
|
{
|
|
if (!Enum.TryParse<BudgetPeriod>(period, true, out var budgetPeriod))
|
|
return $"Invalid period '{period}'. Must be Weekly, Monthly, or Yearly.";
|
|
budget.Period = budgetPeriod;
|
|
}
|
|
|
|
if (isActive.HasValue)
|
|
budget.IsActive = isActive.Value;
|
|
|
|
var result = await budgetService.UpdateBudgetAsync(budget);
|
|
|
|
return JsonSerializer.Serialize(new { result.Success, result.Message }, JsonOptions);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Build to verify**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(mcp): implement budget tools (status, create, update)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Implement Category Tools
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Mcp/Tools/CategoryTools.cs`
|
|
|
|
- [ ] **Step 1: Create CategoryTools.cs**
|
|
|
|
Create `MoneyMap.Mcp/Tools/CategoryTools.cs`:
|
|
|
|
```csharp
|
|
using System.ComponentModel;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using ModelContextProtocol.Server;
|
|
using MoneyMap.Data;
|
|
using MoneyMap.Services;
|
|
|
|
namespace MoneyMap.Mcp.Tools;
|
|
|
|
public static class CategoryTools
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
|
|
[McpServerTool, Description("List all categories with transaction counts.")]
|
|
public static async Task<string> ListCategories(
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var categories = await db.Transactions
|
|
.Where(t => t.Category != null && t.Category != "")
|
|
.GroupBy(t => t.Category!)
|
|
.Select(g => new { Category = g.Key, Count = g.Count(), TotalSpent = g.Where(t => t.Amount < 0).Sum(t => Math.Abs(t.Amount)) })
|
|
.OrderByDescending(x => x.Count)
|
|
.ToListAsync();
|
|
|
|
var uncategorized = await db.Transactions
|
|
.CountAsync(t => t.Category == null || t.Category == "");
|
|
|
|
return JsonSerializer.Serialize(new { Categories = categories, UncategorizedCount = uncategorized }, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Get auto-categorization pattern rules (CategoryMappings).")]
|
|
public static async Task<string> GetCategoryMappings(
|
|
[Description("Filter mappings to a specific category")] string? category = null,
|
|
ITransactionCategorizer categorizer = default!)
|
|
{
|
|
var mappings = await categorizer.GetAllMappingsAsync();
|
|
|
|
if (!string.IsNullOrWhiteSpace(category))
|
|
mappings = mappings.Where(m => m.Category.Equals(category, StringComparison.OrdinalIgnoreCase)).ToList();
|
|
|
|
var result = mappings.Select(m => new
|
|
{
|
|
m.Id,
|
|
m.Pattern,
|
|
m.Category,
|
|
m.MerchantId,
|
|
m.Priority
|
|
}).OrderBy(m => m.Category).ThenByDescending(m => m.Priority).ToList();
|
|
|
|
return JsonSerializer.Serialize(result, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Add a new auto-categorization rule that maps transaction name patterns to categories.")]
|
|
public static async Task<string> AddCategoryMapping(
|
|
[Description("Pattern to match in transaction name (case-insensitive)")] string pattern,
|
|
[Description("Category to assign when pattern matches")] string category,
|
|
[Description("Merchant name to assign (creates if new)")] string? merchantName = null,
|
|
[Description("Priority (higher = checked first, default 0)")] int priority = 0,
|
|
MoneyMapContext db = default!,
|
|
IMerchantService merchantService = default!)
|
|
{
|
|
int? merchantId = null;
|
|
if (!string.IsNullOrWhiteSpace(merchantName))
|
|
merchantId = await merchantService.GetOrCreateIdAsync(merchantName);
|
|
|
|
var mapping = new MoneyMap.Models.CategoryMapping
|
|
{
|
|
Pattern = pattern,
|
|
Category = category,
|
|
MerchantId = merchantId,
|
|
Priority = priority
|
|
};
|
|
|
|
db.CategoryMappings.Add(mapping);
|
|
await db.SaveChangesAsync();
|
|
|
|
return JsonSerializer.Serialize(new { Created = true, mapping.Id, mapping.Pattern, mapping.Category, Merchant = merchantName, mapping.Priority }, JsonOptions);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Build to verify**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(mcp): implement category tools (list, mappings, add mapping)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 9: Implement Receipt Tools
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Mcp/Tools/ReceiptTools.cs`
|
|
|
|
- [ ] **Step 1: Create ReceiptTools.cs**
|
|
|
|
Create `MoneyMap.Mcp/Tools/ReceiptTools.cs`:
|
|
|
|
```csharp
|
|
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;
|
|
|
|
public static class ReceiptTools
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
|
|
[McpServerTool, Description("Get a receipt image for visual inspection. Returns the image as base64. Useful for verifying transaction categories.")]
|
|
public static async Task<IEnumerable<Content>> GetReceiptImage(
|
|
[Description("Receipt ID")] long receiptId,
|
|
MoneyMapContext db = default!,
|
|
IReceiptStorageOptions storageOptions = default!)
|
|
{
|
|
var receipt = await db.Receipts.FindAsync(receiptId);
|
|
if (receipt == null)
|
|
return [new Content { Type = "text", Text = "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 [new Content { Type = "text", Text = "Invalid receipt path" }];
|
|
|
|
if (!File.Exists(fullPath))
|
|
return [new Content { Type = "text", Text = "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;
|
|
}
|
|
|
|
return [new Content
|
|
{
|
|
Type = "image",
|
|
MimeType = mimeType,
|
|
Data = Convert.ToBase64String(imageBytes)
|
|
}];
|
|
}
|
|
|
|
[McpServerTool, 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", receipt.ParseStatus }, 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, 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,
|
|
r.ParseStatus,
|
|
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, 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,
|
|
receipt.ParseStatus,
|
|
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);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Build to verify**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj
|
|
```
|
|
|
|
Note: The `Content` type and return type for `GetReceiptImage` may need adjustment based on the exact MCP SDK API. If it doesn't compile, check the SDK's approach for returning image content — it may use `[McpServerTool]` with a return type of `Task<McpToolResponse>` or similar. Adapt accordingly.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(mcp): implement receipt tools (image, text, list, details)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 10: Implement Merchant Tools
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Mcp/Tools/MerchantTools.cs`
|
|
|
|
- [ ] **Step 1: Create MerchantTools.cs**
|
|
|
|
Create `MoneyMap.Mcp/Tools/MerchantTools.cs`:
|
|
|
|
```csharp
|
|
using System.ComponentModel;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using ModelContextProtocol.Server;
|
|
using MoneyMap.Data;
|
|
using MoneyMap.Services;
|
|
|
|
namespace MoneyMap.Mcp.Tools;
|
|
|
|
public static class MerchantTools
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
|
|
[McpServerTool, Description("List all merchants with transaction counts and category mapping info.")]
|
|
public static async Task<string> ListMerchants(
|
|
[Description("Filter merchants by name (contains)")] string? query = null,
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var q = db.Merchants
|
|
.Include(m => m.Transactions)
|
|
.Include(m => m.CategoryMappings)
|
|
.AsQueryable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(query))
|
|
q = q.Where(m => m.Name.Contains(query));
|
|
|
|
var merchants = await q
|
|
.OrderBy(m => m.Name)
|
|
.Select(m => new
|
|
{
|
|
m.Id,
|
|
m.Name,
|
|
TransactionCount = m.Transactions.Count,
|
|
MappingCount = m.CategoryMappings.Count,
|
|
Categories = m.CategoryMappings.Select(cm => cm.Category).Distinct().ToList()
|
|
})
|
|
.ToListAsync();
|
|
|
|
return JsonSerializer.Serialize(new { Count = merchants.Count, Merchants = merchants }, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Merge duplicate merchants. Reassigns all transactions and category mappings from source to target, then deletes source.")]
|
|
public static async Task<string> MergeMerchants(
|
|
[Description("Merchant ID to merge FROM (will be deleted)")] int sourceMerchantId,
|
|
[Description("Merchant ID to merge INTO (will be kept)")] int targetMerchantId,
|
|
MoneyMapContext db = default!)
|
|
{
|
|
if (sourceMerchantId == targetMerchantId)
|
|
return "Source and target merchant cannot be the same";
|
|
|
|
var source = await db.Merchants.FindAsync(sourceMerchantId);
|
|
var target = await db.Merchants.FindAsync(targetMerchantId);
|
|
|
|
if (source == null)
|
|
return $"Source merchant {sourceMerchantId} not found";
|
|
if (target == null)
|
|
return $"Target merchant {targetMerchantId} not found";
|
|
|
|
// Reassign transactions
|
|
var transactions = await db.Transactions
|
|
.Where(t => t.MerchantId == sourceMerchantId)
|
|
.ToListAsync();
|
|
|
|
foreach (var t in transactions)
|
|
t.MerchantId = targetMerchantId;
|
|
|
|
// Reassign category mappings (delete duplicates)
|
|
var sourceMappings = await db.CategoryMappings
|
|
.Where(cm => cm.MerchantId == sourceMerchantId)
|
|
.ToListAsync();
|
|
|
|
var targetMappingPatterns = await db.CategoryMappings
|
|
.Where(cm => cm.MerchantId == targetMerchantId)
|
|
.Select(cm => cm.Pattern)
|
|
.ToListAsync();
|
|
|
|
foreach (var mapping in sourceMappings)
|
|
{
|
|
if (targetMappingPatterns.Contains(mapping.Pattern))
|
|
db.CategoryMappings.Remove(mapping);
|
|
else
|
|
mapping.MerchantId = targetMerchantId;
|
|
}
|
|
|
|
// Delete source merchant
|
|
db.Merchants.Remove(source);
|
|
await db.SaveChangesAsync();
|
|
|
|
return JsonSerializer.Serialize(new
|
|
{
|
|
Merged = true,
|
|
Source = new { source.Id, source.Name },
|
|
Target = new { target.Id, target.Name },
|
|
TransactionsReassigned = transactions.Count,
|
|
MappingsReassigned = sourceMappings.Count
|
|
}, JsonOptions);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Build to verify**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj
|
|
```
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(mcp): implement merchant tools (list, merge)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 11: Implement Account and Dashboard Tools
|
|
|
|
**Files:**
|
|
- Create: `MoneyMap.Mcp/Tools/AccountTools.cs`
|
|
- Create: `MoneyMap.Mcp/Tools/DashboardTools.cs`
|
|
|
|
- [ ] **Step 1: Create AccountTools.cs**
|
|
|
|
Create `MoneyMap.Mcp/Tools/AccountTools.cs`:
|
|
|
|
```csharp
|
|
using System.ComponentModel;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using ModelContextProtocol.Server;
|
|
using MoneyMap.Data;
|
|
|
|
namespace MoneyMap.Mcp.Tools;
|
|
|
|
public static class AccountTools
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
|
|
[McpServerTool, Description("List all accounts with transaction counts.")]
|
|
public static async Task<string> ListAccounts(
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var accounts = await db.Accounts
|
|
.Include(a => a.Cards)
|
|
.Include(a => a.Transactions)
|
|
.OrderBy(a => a.Issuer).ThenBy(a => a.Last4)
|
|
.Select(a => new
|
|
{
|
|
a.Id,
|
|
a.Issuer,
|
|
a.Last4,
|
|
a.Owner,
|
|
Label = a.DisplayLabel,
|
|
TransactionCount = a.Transactions.Count,
|
|
CardCount = a.Cards.Count
|
|
})
|
|
.ToListAsync();
|
|
|
|
return JsonSerializer.Serialize(accounts, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("List all cards with account info and transaction counts.")]
|
|
public static async Task<string> ListCards(
|
|
[Description("Filter cards by account ID")] int? accountId = null,
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var q = db.Cards
|
|
.Include(c => c.Account)
|
|
.Include(c => c.Transactions)
|
|
.AsQueryable();
|
|
|
|
if (accountId.HasValue)
|
|
q = q.Where(c => c.AccountId == accountId.Value);
|
|
|
|
var cards = await q
|
|
.OrderBy(c => c.Owner).ThenBy(c => c.Last4)
|
|
.Select(c => new
|
|
{
|
|
c.Id,
|
|
c.Issuer,
|
|
c.Last4,
|
|
c.Owner,
|
|
Label = c.DisplayLabel,
|
|
Account = c.Account.Issuer + " " + c.Account.Last4,
|
|
AccountId = c.AccountId,
|
|
TransactionCount = c.Transactions.Count
|
|
})
|
|
.ToListAsync();
|
|
|
|
return JsonSerializer.Serialize(cards, JsonOptions);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create DashboardTools.cs**
|
|
|
|
Create `MoneyMap.Mcp/Tools/DashboardTools.cs`:
|
|
|
|
```csharp
|
|
using System.ComponentModel;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using ModelContextProtocol.Server;
|
|
using MoneyMap.Data;
|
|
using MoneyMap.Services;
|
|
|
|
namespace MoneyMap.Mcp.Tools;
|
|
|
|
public static class DashboardTools
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
|
|
|
[McpServerTool, Description("Get dashboard overview: top spending categories, recent transactions, and aggregate stats.")]
|
|
public static async Task<string> GetDashboard(
|
|
[Description("Number of top categories to show (default 8)")] int? topCategoriesCount = null,
|
|
[Description("Number of recent transactions to show (default 20)")] int? recentTransactionsCount = null,
|
|
IDashboardService dashboardService = default!)
|
|
{
|
|
var data = await dashboardService.GetDashboardDataAsync(
|
|
topCategoriesCount ?? 8,
|
|
recentTransactionsCount ?? 20);
|
|
|
|
return JsonSerializer.Serialize(data, JsonOptions);
|
|
}
|
|
|
|
[McpServerTool, Description("Get month-over-month spending totals for trend analysis.")]
|
|
public static async Task<string> GetMonthlyTrend(
|
|
[Description("Number of months to include (default 6)")] int? months = null,
|
|
[Description("Filter to a specific category")] string? category = null,
|
|
MoneyMapContext db = default!)
|
|
{
|
|
var monthCount = months ?? 6;
|
|
var endDate = DateTime.Today;
|
|
var startDate = new DateTime(endDate.Year, endDate.Month, 1).AddMonths(-(monthCount - 1));
|
|
|
|
var q = db.Transactions
|
|
.Where(t => t.Date >= startDate && t.Date <= endDate)
|
|
.Where(t => t.Amount < 0)
|
|
.Where(t => t.TransferToAccountId == null)
|
|
.ExcludeTransfers();
|
|
|
|
if (!string.IsNullOrWhiteSpace(category))
|
|
q = q.Where(t => t.Category == category);
|
|
|
|
var monthly = await q
|
|
.GroupBy(t => new { t.Date.Year, t.Date.Month })
|
|
.Select(g => new
|
|
{
|
|
Year = g.Key.Year,
|
|
Month = g.Key.Month,
|
|
Total = g.Sum(t => Math.Abs(t.Amount)),
|
|
Count = g.Count()
|
|
})
|
|
.OrderBy(x => x.Year).ThenBy(x => x.Month)
|
|
.ToListAsync();
|
|
|
|
var result = monthly.Select(m => new
|
|
{
|
|
Period = $"{m.Year}-{m.Month:D2}",
|
|
m.Total,
|
|
m.Count
|
|
}).ToList();
|
|
|
|
return JsonSerializer.Serialize(new { Category = category ?? "All Spending", Months = result }, JsonOptions);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Build to verify**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.Mcp/MoneyMap.Mcp.csproj
|
|
```
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat(mcp): implement account, card, and dashboard tools"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 12: Build, Test, and Fix Compilation Issues
|
|
|
|
**Files:**
|
|
- Possibly modify multiple files to fix compilation errors
|
|
|
|
- [ ] **Step 1: Full solution build**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet build MoneyMap.sln
|
|
```
|
|
|
|
- [ ] **Step 2: Fix any compilation errors**
|
|
|
|
Common issues to watch for:
|
|
- Missing `using` statements for `MoneyMap.Core` namespace in Program.cs
|
|
- The `Content` type in ReceiptTools may need to match the MCP SDK's actual API (check `ModelContextProtocol.Protocol` namespace)
|
|
- `ExcludeTransfers()` extension method may need the `MoneyMap.Services` using
|
|
- `CategoryMappings` DbSet access — verify property name matches DbContext
|
|
|
|
Fix each error and rebuild.
|
|
|
|
- [ ] **Step 3: Run all tests**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet test MoneyMap.Tests/MoneyMap.Tests.csproj
|
|
```
|
|
|
|
- [ ] **Step 4: Quick smoke test the MCP server**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet run --project MoneyMap.Mcp -- --help 2>/dev/null
|
|
```
|
|
|
|
Or test stdio by piping an MCP initialize request:
|
|
|
|
```bash
|
|
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | dotnet run --project MoneyMap.Mcp
|
|
```
|
|
|
|
- [ ] **Step 5: Commit any fixes**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "fix: resolve compilation issues from MCP integration"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 13: Publish and Register MCP Server
|
|
|
|
**Files:**
|
|
- Modify: `~/.claude/CLAUDE.md` (update MCP server table)
|
|
|
|
- [ ] **Step 1: Publish the MCP server**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
dotnet publish MoneyMap.Mcp -c Release -o "$USERPROFILE/.claude/mcp/MoneyMap.Mcp"
|
|
```
|
|
|
|
- [ ] **Step 2: Register with Claude Code**
|
|
|
|
```bash
|
|
claude mcp add --transport stdio --scope user moneymap -- "C:/Users/AJ/.claude/mcp/MoneyMap.Mcp/MoneyMap.Mcp.exe"
|
|
```
|
|
|
|
- [ ] **Step 3: Update CLAUDE.md MCP table**
|
|
|
|
Add to the MCP Server Publishing table in `~/.claude/CLAUDE.md`:
|
|
|
|
```
|
|
| MoneyMapMcp | `MoneyMap.Mcp` | `~/.claude/mcp/MoneyMap.Mcp/` | Done |
|
|
```
|
|
|
|
- [ ] **Step 4: Verify MCP server loads in Claude Code**
|
|
|
|
Restart Claude Code and verify the moneymap tools appear. Run a simple tool like `list_accounts` to confirm end-to-end connectivity.
|
|
|
|
- [ ] **Step 5: Final commit**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/MoneyMap
|
|
git add -A
|
|
git commit -m "feat: publish MoneyMap MCP server and register with Claude Code"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 14: End-to-End Validation
|
|
|
|
- [ ] **Step 1: Test read-only tools**
|
|
|
|
In a new Claude Code session, verify:
|
|
- `list_accounts` returns accounts
|
|
- `search_transactions` with date range returns results
|
|
- `get_spending_summary` for last month works
|
|
- `get_budget_status` returns budget info
|
|
- `list_categories` shows categories with counts
|
|
- `list_merchants` shows merchants
|
|
|
|
- [ ] **Step 2: Test receipt image tool**
|
|
|
|
- `list_receipts` with `parseStatus=Completed` to find a parsed receipt
|
|
- `get_receipt_text` on that receipt to see parsed data
|
|
- `get_receipt_image` on a receipt to verify base64 image is returned
|
|
|
|
- [ ] **Step 3: Test mutation tools**
|
|
|
|
- `bulk_recategorize` with `dryRun=true` to preview
|
|
- `update_transaction_category` on a single transaction
|
|
- Verify the change via `get_transaction`
|
|
|
|
- [ ] **Step 4: Run the real use case**
|
|
|
|
Execute the income vs. spending analysis workflow:
|
|
1. `get_income_summary` for last 3 months
|
|
2. `get_spending_summary` for last 3 months
|
|
3. `search_transactions` with `uncategorizedOnly=true`
|
|
4. For ambiguous ones, use `get_receipt_image` to verify
|
|
5. `update_transaction_category` to fix miscategorized items
|
|
6. Re-run `get_spending_summary` to see corrected picture
|