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 & Hyperdrive Domain Origin
Database connections from Cloudflare Workers edge run through **Cloudflare Hyperdrive** to PostgreSQL via PgBouncer with **Domain-Only Origin** routing:
```bash
# 1. Verify Hyperdrive origin uses domain hostname (zero raw IPs)
npx wrangler hyperdrive get 246e815964a44eed9464752afe346134
# 2. Set your production JWT secret (if not set)
npx wrangler secret put JWT_SECRET
# 3. Deploy worker
npm run deploy
```
> π‘οΈ **Zero-Raw-IP Invariant**: Direct IP origins are banned in Hyperdrive configurations. Origin routing MUST strictly resolve through FQDN domain hostnames. Host firewall (UFW) restricts port 6432 strictly to Cloudflare edge network IP ranges.
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues