Skip to main content
Glama
lutfi-zain

finnplan-mcp

by lutfi-zain

Reedrich MCP Server (Cloudflare Workers + PostgreSQL via Hyperdrive)

Stateless Model Context Protocol (MCP) server for personal finance & deterministic wealth planning, inspired by Reed Richards (Mister Fantastic)β€”giving AI Agents mathematical superpowers to project, optimize, and solve user finances on Cloudflare Workers with PostgreSQL 16 (via Cloudflare Hyperdrive & PgBouncer) and Drizzle ORM.


πŸš€ Features

  • Stateless HTTP Transport: Implements Web Standard Streamable HTTP & SSE (/mcp and /sse) via @modelcontextprotocol/sdk.

  • Pure MCP-Native Authentication: Register and login directly using MCP tools (register_user & login_user) without external REST endpoints.

  • 15-Minute Self-Contained JWT: Cryptographic token verification with zero database queries required for auth on finance tool calls.

  • Multi-Tenant Row-Level Security (RLS): Automatically isolates user data via userId extracted directly from JWT token payload.

  • Full CRUD & Entity Lifecycle Parity: Complete symmetric lifecycle management (List, Get by ID, Create, Update, Delete) across both Model Context Protocol (MCP) and REST API (`/api/v1/*`) transports.

  • Financial Invariants & Integrity Guards:

    • Wallet deletion guard: rejects deletion if balance != 0 or if actively linked to in-progress goals or active recurring templates.

    • System category protection: internal `"Adjustment"` category cannot be renamed or deleted.

    • Transaction deletion: automatically executes atomic balance reversal (applyBalanceDelta(..., -1)).

    • Transaction type switching: in-place mutation between expense, income, and transfer with two-phase atomic balance reconciliation and target wallet invariants.

  • 19 MCP Tools Registry:

    • `get_user_profile`: Discover authenticated user identity (name, email, WhatsApp, registration date) with zero required arguments.

    • `register_user` & `login_user`: Pure MCP user onboarding and persistent API key authentication.

    • `submit_feedback`: Submit user feedback, bug reports, questions, or feature requests directly to internal D1 database.

    • manage_wallet: Create, list, update, or delete wallets (with balance/link zero-guards, zero ghost transactions, and date-bounded balance snapshots).

    • `manage_category`: Create, list, update, delete, or bulk-seed standard categories (`action: "seed_defaults"`).

    • `manage_budget`: Create, list, check status, update, or delete spending budgets.

    • `manage_debt_loan`: Create, list, record repayments, update, or delete liabilities and receivables.

    • `manage_goal`: Create, list, update, delete, or link/unlink dedicated wallets with derived balance calculations.

    • `manage_recurring_template`: Create, list, update, or delete recurring income, expense, and transfer schedules.

    • `apply_recurring_template`: Realize a materialized planned occurrence and atomically advance recurrence schedule.

    • `record_transaction`, `transfer_funds`, `update_transaction`, `delete_transaction`: Complete financial transaction lifecycle with automatic atomic balance synchronization.

    • `list_transactions`: Dynamic filtering across date ranges, wallets, categories, budgets, and planning status with pagination.

    • `financial_summary`, `get_account_detail`, `get_horizon_projections`: Comprehensive situational reporting, multi-currency net worth (live FX), Safe-to-Spend runway, cashflow breakdowns, and deterministic multi-period horizon board projections.

  • 5 MCP Resources:

    • `reedrich://user/profile`: Authenticated user profile and contact metadata.

    • `reedrich://db/schema`: Database schema and relationship documentation.

    • `reedrich://wallets/list`: Live list of authenticated user wallets and balances.

    • `reedrich://budgets/active`: Current active budgets with spending utilization percentages.

    • `reedrich://debts/active`: Active liabilities and receivables with total remaining balances.

  • 4 MCP Prompts (AI Workflow Playbooks):

    • `onboarding_assistant`: Step-by-step guidance for setting up initial wallets and standard categories.

    • `daily_briefing`: Comprehensive financial health overview (balances, active budgets, upcoming debt/loan due dates).

    • `financial_planning`: Goal timeline projection with deterministic math based on net savings and debt commitments.

    • `debt_loan_advisor`: Prioritization and repayment strategy for active debts and loan collections.

  • REST API (`/api/v1/*`):

    • Full single-resource retrieval (`GET /:id`) for all 7 domain resources.

    • Enhanced transaction queries: universal pagination headers (`X-Total-Count`, `X-Limit`, `X-Offset`, `X-Has-Next-Page`), opt-in envelope (`?envelope=true`), keyword search (`?q=`), multi-value filters, and status aliases.

    • Multi-Period Horizon Board Projections: GET /api/v1/analytics/horizon (flexible 2D date intervals, payday cycles, and 1-24 month forward roadmap).

    • Unified Account Snapshot: GET /api/v1/account-detail (atomic multi-currency net worth, spendable vs locked wallets, cashflow, enriched budgets with pacing & dates, goals, and obligations).

    • Full mutation parity (`PATCH`) and deletion (`DELETE`) with HTTP standard status codes (`200`, `201`, `400`, `401`, `404`).

  • Interactive Scalar API Docs & OpenAPI 3.0: Full interactive Scalar API Reference UI served at `/docs` (and `/reference`) with machine-readable OpenAPI 3.0 specification at `/openapi.json`.


Related MCP server: Firefly III MCP Server - Cloudflare Worker

⚑ Quick Install: Claude Code Plugin & Desktop

Install Reedrich directly in Claude Code CLI or connect with Claude Desktop:

Claude Code Plugin (One-Command Install):

/plugin marketplace add lutfi-zain/reedrich-mcp
/plugin install reedrich-finance@reedrich-marketplace

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "reedrich": {
      "type": "http",
      "url": "https://reedrich-mcp.lutfidmz.workers.dev/mcp"
    }
  }
}

πŸ‘‰ Full Claude Plugin Guide & Slash Commands: See docs/CLAUDE_PLUGIN.md.


πŸ”„ Authentication & Onboarding Workflow via MCP

  1. Register User via MCP Tool: Call tool register_user:

    {
      "firstName": "Budi",
      "lastName": "Setiawan",
      "email": "budi@example.com",
      "whatsappNumber": "+6281234567890"
    }

    Response:

    {
      "userId": "usr_k8f9a2...",
      "name": "Budi Setiawan",
      "email": "budi@example.com",
      "whatsappNumber": "+6281234567890",
      "apiKey": "rd_live_8f3d9b2c...",
      "token": "eyJhbGciOi...",
      "tokenType": "Bearer",
      "expiresIn": 900,
      "onboarding": {
        "isComplete": false,
        "needs": ["wallet", "categories"],
        "suggestions": ["budget"],
        "message": "Please set up: wallet, categories. Use the onboarding_assistant prompt for guidance."
      }
    }
  2. Seed Default Categories (On User Confirmation): Call tool manage_category with action: "seed_defaults":

    {
      "action": "seed_defaults"
    }

    Populates 10 standard categories: Makanan & Minuman πŸ”, Transportasi πŸš—, Belanja πŸ›οΈ, Tagihan & Utilitas πŸ’‘, Hiburan 🎬, Kesehatan πŸ’Š, Gaji πŸ’Ό, Investasi & Bunga πŸ“ˆ, Usaha / Freelance πŸ’», Pemasukan Lainnya 🎁.

  3. Call Finance Tools: Set Authorization: Bearer <token> or Authorization: Bearer <apiKey> in your MCP client headers to execute manage_wallet, record_transaction, etc.

  4. Re-Login when Token Expires (after 15 minutes): When a token expires, call tool login_user:

    {
      "apiKey": "rd_live_8f3d9b2c..."
    }

    Response: Fresh 15-minute JWT token with live onboarding status.


πŸ” Stateless OAuth 2.1 for Perplexity & ChatGPT (RFC 8414/9728, PKCE S256)

Reedrich MCP now exposes 100% stateless OAuth discovery and token endpoints for Perplexity Pro Connectors and ChatGPT Custom Actions (Streamable HTTP /mcp with RFC 9728 protected-resource discovery). All state is encoded in HMAC-SHA256 JWTs via Web Crypto β€” zero D1 tables, zero D1 writes, zero KV/DO.

Backward Compatibility: Existing Authorization: Bearer rd_live_... / fp_live_... and 15-minute reedrich-mcp JWTs remain fully functional on /mcp and /sse alongside new OAuth access tokens. No migration required.

Discovery (Public, Cache-Control: public, max-age=3600)

# Authorization Server Metadata (RFC 8414)
curl https://reedrich-mcp.lutfidmz.workers.dev/.well-known/oauth-authorization-server
# -> { issuer, authorization_endpoint, token_endpoint, registration_endpoint,
#      scopes_supported: ["mcp"], response_types_supported: ["code"],
#      grant_types_supported: ["authorization_code","refresh_token"],
#      code_challenge_methods_supported: ["S256"],
#      token_endpoint_auth_methods_supported: ["none","client_secret_basic","client_secret_post"],
#      revocation_endpoint }

# Protected Resource Metadata (RFC 9728)
curl https://reedrich-mcp.lutfidmz.workers.dev/.well-known/oauth-protected-resource
# -> { resource: "https://<host>/mcp", authorization_servers: ["https://<host>"],
#      scopes_supported: ["mcp"], bearer_methods_supported: ["header"], resource_name: "Reedrich MCP" }

# OpenID Discovery Alias
curl https://reedrich-mcp.lutfidmz.workers.dev/.well-known/openid-configuration

All /.well-known/* endpoints are public (ignore Authorization header), support OPTIONS 204 with Access-Control-Allow-Origin: *, and reflect the request Host dynamically (new URL(c.req.url).origin β€” no hard-coded domain).

Dynamic Client Registration (RFC 7591, Stateless)

# Public client (Perplexity) β€” no secret
curl -X POST https://reedrich-mcp.lutfidmz.workers.dev/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"Perplexity","redirect_uris":["https://perplexity.ai/oauth/callback"],"grant_types":["authorization_code","refresh_token"],"response_types":["code"],"token_endpoint_auth_method":"none"}'
# -> 201 { client_id: "550e8400-...", client_id_issued_at: 1234567890,
#          redirect_uris, grant_types, response_types, scope: "mcp",
#          token_endpoint_auth_method: "none" }   # no client_secret

# Confidential client (ChatGPT) β€” HMAC-derived secret (43 chars, 256-bit)
curl -X POST https://reedrich-mcp.lutfidmz.workers.dev/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"ChatGPT","redirect_uris":["https://chat.openai.com/aip/callback"],"token_endpoint_auth_method":"client_secret_basic"}'
# -> 201 { client_id, client_secret: "Na71QEn2...", client_secret_expires_at: 0, ... }
# client_secret is deterministic: HMAC-SHA256(JWT_SECRET, "oauth:client-secret:"+clientId) + base64url
# Verification is stateless via re-derivation + constant-time compare β€” zero D1 writes.

Validation: redirect_uris must be https (or http://localhost/http://127.0.0.1 loopback), grant_types βŠ† ["authorization_code","refresh_token"], response_types βŠ† ["code"], token_endpoint_auth_method ∈ ["none","client_secret_basic","client_secret_post"]. Defaults: grant_types β†’ ["authorization_code","refresh_token"], response_types β†’ ["code"], scope β†’ "mcp".

Authorization Code + PKCE S256 (5-Minute JWT)

# 1. Compute challenge (Node) β€” RFC 7636 Appendix B vector:
# verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
# challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM  (BASE64URL(SHA256(verifier)))
node -e "import('node:crypto').then(async m=>{ const v='dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; const h=await m.subtle.digest('SHA-256', Buffer.from(v)); console.log(Buffer.from(h).toString('base64url')) })"

# 2. Authorize (user must be authenticated via rd_live_ or JWT)
curl -G https://reedrich-mcp.lutfidmz.workers.dev/oauth/authorize \
  --data-urlencode "response_type=code" \
  --data-urlencode "client_id=<client_id>" \
  --data-urlencode "redirect_uri=https://perplexity.ai/oauth/callback" \
  --data-urlencode "scope=mcp" \
  --data-urlencode "state=xyz123" \
  --data-urlencode "code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" \
  --data-urlencode "code_challenge_method=S256" \
  -H "Authorization: Bearer rd_live_..."
# -> 302 Location: https://perplexity.ai/oauth/callback?code=<JWT 5m>&state=xyz123
# code JWT payload: { sub, client_id, redirect_uri, scope, code_challenge, code_challenge_method:"S256", iss, aud, iat, exp=iat+300, jti }
# Rejects: plain, missing challenge, invalid scope, mismatched redirect_uri (400), unauthenticated (401 login_required)

Token Exchange (15m Access + 30d Refresh, Stateless)

# Authorization Code Grant (form or JSON lenient)
curl -X POST https://reedrich-mcp.lutfidmz.workers.dev/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=<jwt>&redirect_uri=https://perplexity.ai/oauth/callback&client_id=<id>&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
# Also accepts: Authorization: Basic base64(client_id:client_secret) for confidential clients
# -> 200 { access_token: <JWT 15m>, token_type:"Bearer", expires_in:900, refresh_token: <JWT 30d>, scope:"mcp" }
# access JWT: { sub, client_id, scope, iss, aud, iat, exp=iat+900, jti }
# refresh JWT: { sub, client_id, scope, token_type:"refresh", iss, aud, iat, exp=iat+2592000, jti }

# Refresh Token Grant (rotation, scope narrowing)
curl -X POST https://reedrich-mcp.lutfidmz.workers.dev/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token&refresh_token=<refresh_jwt>&client_id=<id>&scope=mcp"
# -> 200 { access_token: <new 15m>, refresh_token: <new 30d rotated>, scope }
# Rejects: broader scope (400 invalid_scope), expired/mismatched client (400 invalid_grant), access_token as refresh (400), wrong secret (401 invalid_client + WWW-Authenticate: Basic)

# Revocation (stateless best-effort, always 200)
curl -X POST https://reedrich-mcp.lutfidmz.workers.dev/oauth/revoke \
  -d "token=<refresh_jwt>&token_type_hint=refresh_token"
# -> 200 {}  (expiry-based revocation; revoked token remains valid until exp β€” documented)

Protected Resource Gate (/mcp, /sse)

# Unauthenticated -> 401 with RFC 9728 WWW-Authenticate
curl -i -X POST https://reedrich-mcp.lutfidmz.workers.dev/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# <- 401 WWW-Authenticate: Bearer resource_metadata="https://<host>/.well-known/oauth-protected-resource", error="invalid_token"
#    { error:"invalid_token", error_description:"Authentication required" }
#    Cache-Control: no-store, Pragma: no-cache, Access-Control-Allow-Origin: *

# Valid OAuth, rd_live_, or legacy JWT -> 200 MCP
curl -X POST https://reedrich-mcp.lutfidmz.workers.dev/mcp \
  -H "Authorization: Bearer <oauth_access_token>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"manage_wallet","arguments":{"action":"list"}}}'
# Also: Authorization: Bearer rd_live_...  or  Bearer <legacy reedrich-mcp JWT>

resource_metadata reflects the request Host (https://<host>/.well-known/oauth-protected-resource), enabling custom domains and workers.dev without config.

Cryptographic Invariants

  • Zero Storage: No oauth_* D1 tables, no KV/DO/R2, no new migrations. All codes/tokens are HMAC-SHA256 JWTs (hono/jwt + Web Crypto).

  • Lifetimes: T_code=300s (5m), T_access=900s (15m), T_refresh=2592000s (30d), CLOCK_SKEW=60s.

  • PKCE: code_challenge = BASE64URL(SHA256(ASCII(verifier))), 43 ≀ len ≀ 128, alphabet A-Za-z0-9-._~, constant-time compare.

  • Client Secret: HMAC-SHA256(JWT_SECRET, "oauth:client-secret:"+clientId) β†’ base64url (43 chars, 256-bit); verified via re-derivation.


πŸ› οΈ Project Setup & Local Development

1. Install Dependencies

npm install

2. Run Tests

Runs the complete test suite covering all 12 tools, 4 resources, 4 prompts, RLS tenant isolation, input validations, and pure MCP lifecycle:

npm test

3. Type Checking & Build Dry-Run

npm run typecheck
npm run build

πŸ€– Coding Agents Quick Start (Claude Code, OpenCode, Pi, OMP)

Connect Reedrich MCP to your AI coding agents in seconds. For comprehensive configuration and example prompts, see the Coding Agents Setup Guide.

1. Claude Code

claude mcp add --transport http reedrich https://reedrich-mcp.lutfidmz.workers.dev/mcp

2. OpenCode

In opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "reedrich": {
      "type": "remote",
      "url": "https://reedrich-mcp.lutfidmz.workers.dev/mcp"
    }
  }
}

3. Pi (pi-mcp-adapter)

# 1. Install adapter
pi install npm:pi-mcp-adapter

# 2. Add to .mcp.json
{
  "mcpServers": {
    "reedrich": {
      "url": "https://reedrich-mcp.lutfidmz.workers.dev/mcp"
    }
  }
}

4. OMP (Oh My Pi)

In .omp/mcp.json or .mcp.json:

{
  "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
  "mcpServers": {
    "reedrich": {
      "url": "https://reedrich-mcp.lutfidmz.workers.dev/mcp"
    }
  }
}

⚑ Print Agent Snippets via CLI

npm run agent:snippet [claude|opencode|pi|omp|all]

πŸ—οΈ Architecture & Project Structure

Reedrich uses a 2-layer architecture (Transport Adapters β†’ Shared Service Layer) deployed on Cloudflare Workers (workerd) with D1 SQLite:

src/
β”œβ”€β”€ index.ts              # Hono app: CORS, observability, OAuth & MCP mounting
β”œβ”€β”€ mcp.ts                # MCP server: tool, resource, and prompt declarations (thin adapters)
β”œβ”€β”€ middleware/           # Shared cross-cutting middleware
β”‚   β”œβ”€β”€ auth.ts           # Unified credential resolver (Bearer, API key, JWT, tool args)
β”‚   └── observability.ts  # Request ID propagation (X-Request-ID) & response timing (X-Response-Time)
β”œβ”€β”€ routes/               # Modular REST API endpoints (/api/v1/*)
β”‚   β”œβ”€β”€ index.ts          # Sub-router with scoped auth middleware & central error handling
β”‚   β”œβ”€β”€ wallets.ts        # GET /api/v1/wallets
β”‚   β”œβ”€β”€ categories.ts     # GET /api/v1/categories
β”‚   β”œβ”€β”€ budgets.ts        # GET /api/v1/budgets
β”‚   β”œβ”€β”€ transactions.ts   # GET /api/v1/transactions
β”‚   β”œβ”€β”€ debts-loans.ts    # GET /api/v1/debts-loans
β”‚   β”œβ”€β”€ goals.ts          # GET & POST /api/v1/goals
β”‚   β”œβ”€β”€ recurring-templates.ts # GET, POST, apply /api/v1/recurring-templates
β”‚   β”œβ”€β”€ summary.ts        # GET /api/v1/summary
β”‚   └── feedback.ts       # POST /api/v1/feedback
β”œβ”€β”€ services/             # Pure transport-neutral business logic & typed errors
β”‚   β”œβ”€β”€ errors.ts         # ServiceError class with discriminated error codes
β”‚   β”œβ”€β”€ auth.ts           # registerUser, loginUser, evaluateOnboarding
β”‚   β”œβ”€β”€ wallet.ts         # listWallets, createWallet, updateWallet
β”‚   β”œβ”€β”€ category.ts       # listCategories, createCategory, seedDefaults
β”‚   β”œβ”€β”€ budget.ts         # listBudgets, createBudget, budgetStatus
β”‚   β”œβ”€β”€ transaction.ts    # listTransactions, recordTransaction, updateTransaction, applyBalanceDelta
β”‚   β”œβ”€β”€ transfer.ts       # transferFunds
β”‚   β”œβ”€β”€ debt-loan.ts      # listDebtsLoans, createDebtLoan, repayDebtLoan, updateDebtLoan
β”‚   β”œβ”€β”€ goal.ts           # listGoals, createGoal, updateGoal, deleteGoal
β”‚   β”œβ”€β”€ recurring.ts      # listRecurringTemplates, create/update/delete/apply template
β”‚   β”œβ”€β”€ summary.ts        # financialSummary
β”‚   └── feedback.ts       # submitFeedback
β”œβ”€β”€ db/
β”‚   └── schema.ts         # Drizzle SQLite D1 schema & indexes
└── utils/                # Pure utilities (date, fx, goals, recurring, oauth, token, pkce)

πŸ“– Interactive Scalar API Reference

Explore, inspect schemas, and test REST endpoints interactively in your browser:

  • Interactive Documentation UI: https://<host>/docs (or /reference)

  • Machine-Readable OpenAPI Spec: https://<host>/openapi.json

  • Features dark mode, search, schema inspector, and interactive "Test Request" console with Bearer token authentication.


πŸ“± Frontend & Client Integration Guide (Web & Mobile)

If you or an AI coding agent is building a user-facing interface on top of Reedrich (such as a Vite React / Svelte / Vue SPA deployed on Cloudflare Pages or a Flutter / React Native mobile app), use the REST API (/api/v1/*) as your backend.

πŸ” Choosing the Right Authorization Method

Method

Best For

How to Use

Security & Token Lifetime

1. Persistent API Key (rd_live_...)

Personal dashboards, internal tools, scripts, local dev

Header Authorization: Bearer <rd_live_...> or X-API-Key: <rd_live_...>

Permanent, never expires. Store in .env / secure storage.

2. OAuth 2.0 PKCE with Google Login

Public multi-user Web SPAs & Mobile apps

Standard PKCE S256 (/oauth/authorize + /oauth/token)

15-min stateless JWT access token + 30-day rotatable refresh token. Supports Google OAuth federation.

3. Ephemeral JWT

Quick testing or headless agent session

Obtained via MCP register_user or login_user

15-minute expiration. Verified at edge with zero DB lookups.

πŸ› οΈ Client Integration Recipes

Option A: TypeScript / Fetch Client (Vite React, Svelte, Vue)

// src/lib/reedrich.ts
export class ReedrichClient {
  private baseUrl: string;
  private token?: string;

  constructor(config: { baseUrl?: string; apiKey?: string; accessToken?: string }) {
    this.baseUrl = config.baseUrl || "https://reedrich-mcp.lutfidmz.workers.dev";
    this.token = config.apiKey || config.accessToken;
  }

  setToken(token: string) {
    this.token = token;
  }

  private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
    const headers = new Headers(options.headers);
    headers.set("Content-Type", "application/json");
    if (this.token) {
      headers.set("Authorization", `Bearer ${this.token}`);
    }

    const res = await fetch(`${this.baseUrl}${path}`, { ...options, headers });
    if (!res.ok) {
      const err = await res.json().catch(() => ({ message: res.statusText }));
      throw new Error(err.message || `Request failed with status ${res.status}`);
    }
    return res.json() as Promise<T>;
  }

  getSummary(params?: { startDate?: string; endDate?: string; baseCurrency?: string }) {
    const query = new URLSearchParams(params as Record<string, string>).toString();
    return this.request<any>(`/api/v1/summary${query ? `?${query}` : ""}`);
  }

  getWallets() {
    return this.request<any[]>("/api/v1/wallets");
  }

  getTransactions(params?: Record<string, string | number | boolean>) {
    const query = new URLSearchParams(params as Record<string, string>).toString();
    return this.request<any[]>(`/api/v1/transactions${query ? `?${query}` : ""}`);
  }

  getBudgets() {
    return this.request<any[]>("/api/v1/budgets");
  }

  getGoals(status?: "in_progress" | "completed" | "cancelled") {
    const query = status ? `?status=${status}` : "";
    return this.request<any[]>(`/api/v1/goals${query}`);
  }

  createGoal(goal: { name: string; targetAmount: number; targetDate?: string; currency?: string }) {
    return this.request<any>("/api/v1/goals", { method: "POST", body: JSON.stringify(goal) });
  }
}

Option B: Dart / HTTP Client (Flutter Mobile)

// lib/services/reedrich_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class ReedrichService {
  final String baseUrl;
  final String apiKey;

  ReedrichService({
    this.baseUrl = "https://reedrich-mcp.lutfidmz.workers.dev",
    required this.apiKey,
  });

  Map<String, String> get _headers => {
    "Content-Type": "application/json",
    "Authorization": "Bearer $apiKey",
  };

  Future<Map<String, dynamic>> getSummary({String baseCurrency = "IDR"}) async {
    final uri = Uri.parse("$baseUrl/api/v1/summary?baseCurrency=$baseCurrency");
    final response = await http.get(uri, headers: _headers);
    if (response.statusCode == 200) {
      return jsonDecode(response.body) as Map<String, dynamic>;
    }
    throw Exception("Failed to load summary: ${response.body}");
  }

  Future<List<dynamic>> getWallets() async {
    final uri = Uri.parse("$baseUrl/api/v1/wallets");
    final response = await http.get(uri, headers: _headers);
    if (response.statusCode == 200) {
      return jsonDecode(response.body) as List<dynamic>;
    }
    throw Exception("Failed to load wallets: ${response.body}");
  }

  Future<List<dynamic>> getTransactions({String? type, int limit = 50}) async {
    final params = {"limit": limit.toString()};
    if (type != null) params["type"] = type;
    final uri = Uri.parse("$baseUrl/api/v1/transactions").replace(queryParameters: params);
    final response = await http.get(uri, headers: _headers);
    if (response.statusCode == 200) {
      return jsonDecode(response.body) as List<dynamic>;
    }
    throw Exception("Failed to load transactions: ${response.body}");
  }
}

⚠️ Error Handling & Observability Headers

All endpoints return uniform typed errors and observability headers:

  • X-Request-ID: Trace ID for tracking requests across client and server.

  • X-Response-Time: Edge execution duration in milliseconds (e.g. 2ms).

  • Error codes:

    • VALIDATION (400) β€” Input schema or validation constraint failure.

    • UNAUTHORIZED (401) β€” Missing or invalid token/API key.

    • FORBIDDEN (403) β€” Permission denied.

    • NOT_FOUND (404) β€” Entity not found or belongs to another user (RLS isolation).

    • CONFLICT (409) β€” Unique constraint conflict.

    • INTERNAL (500) β€” Unexpected edge runtime error.

πŸ’Ύ Local D1 Setup & Migrations

Apply migrations to your local D1 database:

# 1. Execute database migrations locally
npx wrangler d1 execute finance_db --local --file=./drizzle/0002_table_prefixed_schema_and_tz.sql
npx wrangler d1 execute finance_db --local --file=./drizzle/0003_add_debts_loans.sql

# 2. Start local development server
npm run dev

πŸ”Œ Connecting with MCP Clients

  • Endpoint: http://localhost:8787/mcp (or your deployed https://reedrich-mcp.lutfidmz.workers.dev/mcp)

  • Initial Connection: No headers required to call register_user or login_user.

  • Authenticated Calls:

    {
      "Authorization": "Bearer <YOUR_15_MIN_JWT_TOKEN_OR_rd_live_API_KEY>"
    }

🚒 Production Deployment

# 1. Set your production JWT secret (if not set)
npx wrangler secret put JWT_SECRET

# 2. Apply migrations to remote D1 database
npx wrangler d1 execute finance_db --remote --file=./drizzle/0003_add_debts_loans.sql

# 3. Deploy worker
npm run deploy

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI tools to interact with Firefly III personal finance manager through the MCP protocol, deployed globally on Cloudflare Workers for low latency.
    17 npm
    ISC
  • A
    license
    Not graded
    quality
    D
    maintenance
    An incremental financial-data MCP server built on Cloudflare, currently offering a read-only Hono API for accounts, transactions, and analytics with D1 and Drizzle. Future plans include OAuth and MCP integration.
    7 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI tools to interact with a Firefly III personal finance instance via MCP protocol, deployed on Cloudflare Workers for low-latency global access.
    17 npm
    ISC