Skip to main content
Glama
lutfi-zain

finnplan-mcp

by lutfi-zain
README.md
# 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\`.
---
## ⚑ Quick Install: Claude Code Plugin & Desktop

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

### Claude Code Plugin (One-Command Install):
```bash
/plugin marketplace add lutfi-zain/reedrich-mcp
/plugin install reedrich-finance@reedrich-marketplace
```

### Claude Desktop (`claude_desktop_config.json`):
```json
{
  "mcpServers": {
    "reedrich": {
      "type": "http",
      "url": "https://reedrich-mcp.lutfidmz.workers.dev/mcp"
    }
  }
}
```

πŸ‘‰ **Full Claude Plugin Guide & Slash Commands**: See [docs/CLAUDE_PLUGIN.md](docs/CLAUDE_PLUGIN.md).

---


## πŸ”„ Authentication & Onboarding Workflow via MCP

1. **Register User via MCP Tool**:
   Call tool `register_user`:
   ```json
   {
     "firstName": "Budi",
     "lastName": "Setiawan",
     "email": "budi@example.com",
     "whatsappNumber": "+6281234567890"
   }
   ```
   **Response:**
   ```json
   {
     "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"`:
   ```json
   {
     "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`:
   ```json
   {
     "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`)

```bash
# 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)

```bash
# 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)

```bash
# 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)

```bash
# 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`)

```bash
# 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
```bash
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:
```bash
npm test
```

### 3. Type Checking & Build Dry-Run
```bash
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](docs/CODING_AGENTS.md)**.

### 1. Claude Code
```bash
claude mcp add --transport http reedrich https://reedrich-mcp.lutfidmz.workers.dev/mcp
```

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

### 3. Pi (`pi-mcp-adapter`)
```bash
# 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`:
```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
```bash
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)

```typescript
// 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)

```dart
// 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:

```bash
# 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**:
  ```json
  {
    "Authorization": "Bearer <YOUR_15_MIN_JWT_TOKEN_OR_rd_live_API_KEY>"
  }
  ```

---

## 🚒 Production Deployment

```bash
# 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
```