Sahidha Agent Guide
This document is for AI agents (Claude, ChatGPT, Gemini, others) — or humans wiring up an agent — that want to operate Sahidha programmatically.
If you're a human looking for the web UI, ignore this and use https://app.sahidha.com. If you're an agent and you stop reading after the first three sections, that's still enough to do useful work.
The REST API at https://api.sahidha.com/api/v1 is live now and is the
source of truth — see the API reference. The @sahidha/mcp and
@sahidha/cli npm packages are in private beta (not yet on the public
npm registry); the install commands below are how they'll work once published.
Architecture: one API, two optional skins
┌──────────────────────┐
│ Fastify REST API │ ← source of truth
│ /api/v1/… (62 eps) │ (RLS, audit, scopes)
└───────────┬──────────┘
│ Bearer ffp_…
┌─────────────────┴──────────────────┐
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ ff CLI │ │ MCP server │
│ apps/cli/ │ │ apps/mcp/ │
│ (humans) │ │ (AI agents) │
└───────────────┘ └────────────────────┘
shell, table, stdio JSON-RPC,
piped JSON typed tool schemas
Pick the right surface:
- MCP server (
apps/mcp/) → if you're an agent inside Claude Desktop, Cursor, Continue, Cline, or any MCP-aware host. Tools are typed, validated, discoverable. This is the recommended path for LLMs. - ff CLI (
apps/cli/) → if you're a human at a terminal, or an agent with only shell access. Same capability set, friendlier for piping. - Direct REST (
/api/v1) → if you're building your own integration (mobile app, custom dashboard). Authenticate with the sameffp_…token.
The REST API is the truth. Both skins are independent and optional — neither ships logic the other doesn't have. Choose based on your client, not your goals.
1. The shape of the system
Sahidha is a multi-tenant household-finance app. Every piece of data belongs to a family (the tenant). Inside a family there are members (human users), entity-members (a HUF, business, trust — non-person tenants), and optional cross-family guests (tax advisors who have read access into other families).
Everything else — accounts, transactions, insurance, investments, contacts, todos, reports — lives under a family. Tenant isolation is enforced at the database level via PostgreSQL Row-Level Security; an agent simply cannot read another family's data even if a route forgot to filter (defence in depth).
Roles
Every login has exactly one role per family. Agents inherit the role of the user whose token they use — there is no separate "agent" role.
| Role | Sees | Can change |
|---|---|---|
family_admin | Everything in the family | Everything, plus member & family administration |
member | Only their own accounts / data | Their own data; can pay on-behalf for others |
guest | All members' data (read-leaning) | Add new transactions / imports / todos only — cannot edit or delete anything that already exists |
Guest role — applicable rights
A guest is a read-leaning collaborator — typically a tax advisor or
accountant. A guest exists only as a cross-family guest
(Settings → Members → Cross-family guests): an admin invites an existing
Sahidha user from another family by email; that person keeps their own login and
switches into your family from the header chip. (The former in-family
"guest member" role has been retired — role=guest on member create/update is
now rejected; use the cross-family invite instead.)
A guest CAN:
- Read everything in the family — every member's accounts, transactions,
insurance, investments, transfers, contacts, todos, and all reports (unlike a
member, who sees only their own data). - Add new transactions (including on-behalf entries) and import bank statements (parse + confirm).
- Add todos.
- Mint API tokens scoped to their own (guest) permissions.
A guest CANNOT:
- Edit or delete any existing transaction or todo (no edit / delete / restore / bulk action / transfer-link).
- Create, edit, or delete accounts, insurance policies, or investments.
- Create or delete transfers, or link transfers / masters.
- Modify contacts, or touch cross-payments / IOUs.
- Excel export or import.
- Open Settings — member management, family settings, categories, or any other admin-only route.
In short: a guest can contribute new entries and read the whole family's finances, but cannot alter or remove existing records or change any structure.
2. Quick start (agents — MCP)
The MCP server is published to npm as @sahidha/mcp. Hosts that
speak MCP (Claude Desktop, Cursor, Continue, Cline) auto-discover and call
its 62 tools.
Wire it into your host's config. For Claude Desktop, edit
%APPDATA%\Claude\claude_desktop_config.json (Windows) or
~/Library/Application Support/Claude/claude_desktop_config.json (macOS):
{
"mcpServers": {
"sahidha": {
"command": "npx",
"args": ["-y", "@sahidha/mcp"],
"env": {
"SAHIDHA_API_URL": "https://api.sahidha.com/api/v1",
"SAHIDHA_TOKEN": "sa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}
npx -y fetches the package on first launch and caches it; nothing to
clone, nothing to build. For Cursor (~/.cursor/mcp.json) and
Continue the shape is the same. Restart the host; you should see
tools named sa_whoami, sa_accounts_list, sa_txns_create, etc.
Get a token from the web UI (Settings → API Tokens → New token, scope=write) or programmatically:
# Mint a fresh token for an agent (CLI, but the resulting token works for MCP).
ff auth login # one-time human login
ff tokens create --name "claude-desktop" --scope write --json
# → { "token": "ffp_...", … } ← capture once; never shown again
Verify tool surface from a host's tool picker — you should see 62 tools.
Pre-publish (workspace) alternative: if you're running from a git clone before the npm publish has landed, use the local path:
{"command": "node","args": ["C:/path/to/Sahidha/apps/mcp/bin/sahidha-mcp.mjs"]}
Hosted connector (Claude custom connector, read-only)
If you use Claude (web or desktop) and just want to read your family's finances, you don't need to install anything. Add the hosted connector:
Claude → Settings → Connectors → Add custom connector → URL
https://mcp.sahidha.com/mcp
Claude walks you through an OAuth sign-in (your normal Sahidha email +
password, or Google). It then has read-only access — the 18 list and
report tools, and nothing that can create, edit, or delete. Two independent
layers enforce that: only GET-equivalent tools are exposed, and the token the
connector mints for itself is read-scoped. You can revoke it any time from
Settings → API Tokens (it's named claude-connector).
Use the hosted connector for read-only Claude access; use the stdio
@sahidha/mcp server below when you need full read/write from Claude Desktop,
Cursor, Continue, or Cline with your own token.
3. Quick start (humans — CLI)
# Install globally — one command, no clone.
npm install -g @sahidha/cli
# Interactive login → mints + stores a token in ~/.sahidha/config.json (0600).
# Point at the hosted API (the default is localhost for local dev).
ff auth login --api https://api.sahidha.com/api/v1
# Smoke-test.
ff auth whoami --json
ff accounts list --json
ff txns list --from 2026-04-01 --to 2026-05-31 --json
For agents using the CLI fallback — always pass --json. The table view
is lossy.
The CLI honours these env vars (override the config file):
| Env var | Meaning |
|---|---|
SAHIDHA_API_URL | Base URL (default http://localhost:3000/api/v1) |
SAHIDHA_TOKEN | Bearer token (replaces config-file value) |
SAHIDHA_CONFIG_DIR | Override ~/.sahidha location |
FF_OUTPUT=json | Force JSON output, equivalent to --json |
4. Authentication
3.1 How to get a token
Three ways, pick the one that fits your situation:
- Interactive —
ff auth loginprompts for email + password, mints a token, stores it locally. Best for one-shot human-driven setup. - Web UI — sign in as a
family_adminormember, openSettings → API Tokens, click "New token", choose scope, copy the plaintext (shown once). Best for granting an agent its own dedicated key. - Programmatic — for tests or onboarding flows, you can POST to
/auth/loginyourself to get a session cookie, then POST/me/tokensto mint. Seesrc/login.tsin the CLI for the exact pattern.
3.2 Token format and storage
- Format:
ffp_<43-char-base64url>(ffp= Sahidha Personal). Total length 47 chars. Grep-able prefix is intentional so leaked tokens are easy to find. - Stored server-side as SHA-256 hash. The plaintext is shown once at creation; if you lose it, revoke and mint a new one.
- Scopes:
read— GET requests only. Read-only audits, reports, exports.write— GET + mutations the issuing user can perform.admin— Same aswritebut the issuer must befamily_adminat mint time. Allows admin-only routes (member CRUD, audit truncate, etc.).
- Expiry: optional. Pass
--expires-days Nat creation. No expiry by default — be deliberate about this for shared/agent keys.
3.3 Using the token
Pass it as a Bearer header to the API directly:
GET /api/v1/me HTTP/1.1
Authorization: Bearer ffp_abc123...
Or via the CLI, which adds the header for you (after ff auth login).
3.4 Revoking
ff tokens list --json # find the id
ff tokens revoke <id> # sets revokedAt; future calls 401
Or revoke from the web UI. Revocation is immediate — no propagation delay.
3.5 Multiple families
A user with cross-family guest memberships has one active family per session. Each API token is pinned to a single family at mint time and cannot switch. To act in a different family:
- Switch the web UI to that family (header chip).
- Mint a new token there.
- Use both tokens, one per family, in your agent.
5. CLI reference (every endpoint)
See ff --help and ff <cmd> --help for the up-to-date list. Below is the
full surface, grouped by resource.
Naming conventions:
- All flags are long-form (
--foo valueor--foo=value); boolean flags are--foo(true) or--no-foo(false).<positional>is required,[--opt]is optional.- IDs are UUIDs unless noted. Dates are
YYYY-MM-DD.- Amounts are positive numbers in INR. Sign is determined by
--type.
4.1 Auth & tokens
ff auth login [--email E] [--password P] [--token ffp_…] [--api URL] [--name LABEL]
ff auth logout
ff auth whoami
ff auth config
ff tokens list
ff tokens create --name LABEL [--scope read|write|admin] [--expires-days N]
ff tokens revoke <id>
4.2 Me (current user)
ff me get
ff me password --current PW --new PW
ff me families
ff me set-active-family --family-id <uuid> # cookie sessions only; tokens are pinned
4.3 Family (admin)
ff family update [--name S] [--fy-start 1..12]
ff family members list
ff family members create --name N --email E --password P
[--role family_admin|member]
[--entity-type huf|business|trust|partnership|proprietorship|other]
[--managed-by <userId>]
ff family members update <id> [--role …] [--active true|false] [--password …]
ff family members delete <id> # blocked if owns data
ff family guests list
ff family guests invite --email E # email must match an existing user in another family
ff family guests revoke <userId>
4.4 Accounts
ff accounts list [--owner <userId>]
ff accounts create --name N --type savings|current|overdraft|loan|credit_card|fd|ppf|investment
--bank B [--number LAST4] [--opening N] [--credit-limit N] [--contact <id>]
ff accounts update <id> [--name] [--bank] [--number] [--credit-limit] [--owner] [--active] [--contact]
ff accounts delete <id> # blocked if any transactions exist
ff accounts statements <accountId> list
ff accounts statements <accountId> download <archiveId> [--out FILE]
ff accounts statements <accountId> delete <archiveId>
4.5 Transactions (alias: txns)
ff txns list [--account <id>] [--owner <userId>]
[--type expense|income|investment|insurance|transfer]
[--category <id|"null"|"any">]
[--from YYYY-MM-DD] [--to YYYY-MM-DD] [--q "search text"]
[--include-deleted] [--deleted-only]
[--link-id <id> --link-type insurance|investment]
ff txns create --account <id> --date YYYY-MM-DD --description S
--type expense|income|investment|insurance|transfer
--amount N
[--category <id>] [--ref S]
[--on-behalf --beneficiary <userId>]
[--link-type insurance|investment --link-id <id>]
ff txns update <id> [--description] [--ref] [--category] [--date]
[--debit] [--credit] [--type expense|income]
ff txns delete <id> # soft-delete (transfer pair: hard-delete both legs)
ff txns restore <id> # undo soft-delete
ff txns transfer-candidates <id> [--window-days N]
ff txns link-transfer <id> --counterparty <txnId>
ff txns link-master <id> --type investment|insurance --id <masterId>
ff txns bulk --action recategorize|delete --ids id1,id2,id3 [--category <id|"null">]
4.6 Categories (admin for writes)
ff categories list # active + inactive, parent + children
ff categories create --name N --kind income|expense [--itr HEAD] [--parent <id>]
ff categories update <id> [--name] [--itr] [--parent] [--active true|false]
ff categories delete <id> # soft-deletes if any txn uses it
4.7 Insurance & attachments
ff insurance list [--owner <userId>]
ff insurance create --name N --insurer S --type Life|Term|Health|Accident|Home|Motor|Travel
--number S
[--sum-assured N] [--premium N]
[--frequency monthly|quarterly|yearly]
[--next-due YYYY-MM-DD] [--nominee S] [--start YYYY-MM-DD]
[--linked-account <id>] [--contact <id>]
ff insurance update <id> [--name] [--insurer] [--premium] [--next-due] [--owner] …
ff insurance delete <id> # blocked if any txn linked
ff insurance attachments <policyId> list
ff insurance attachments <policyId> upload --file PATH # PDF / image, ≤10 MB
ff insurance attachments <policyId> download <attId> [--out FILE]
ff insurance attachments <policyId> delete <attId>
4.8 Investments
ff investments list [--owner <userId>]
ff investments create --name N --type Mutual_Fund|Fixed_Deposit|PPF|Stocks|Bonds|NPS|Gold|Real_Estate
--folio S
[--invested-amount N] [--current-value N] [--monthly-sip N]
[--start YYYY-MM-DD] [--maturity YYYY-MM-DD] [--rate N]
[--linked-account <id>] [--contact <id>]
ff investments update <id> [--name] [--current-value] [--monthly-sip] [--owner] …
ff investments delete <id> # blocked if any txn linked
4.9 Transfers
ff transfers list
ff transfers create --from <accountId> --to <accountId> --date YYYY-MM-DD --amount N [--note S]
ff transfers delete <groupId> # reverses both legs
4.10 Cross-payments / IOUs (alias: cp)
ff cross-payments list
ff cross-payments create --beneficiary <userId> --amount N --description S
[--category S] [--date YYYY-MM-DD]
ff cross-payments update <id> [--description] [--category] [--date] [--amount] [--beneficiary] [--payer]
ff cross-payments delete <id>
ff cross-payments acknowledge <id> # called by the beneficiary
ff cross-payments settle <id> --account <settlementAccountId>
4.11 Contacts
ff contacts list [--kind rm|insurance|investment|tax|legal|medical|contractor|other] [--include-inactive]
ff contacts create --name N [--kind …] [--org] [--phone] [--email] [--address] [--notes]
ff contacts update <id> [--name] [--kind] [--org] [--phone] [--email] [--address] [--notes] [--active]
ff contacts delete <id>
4.12 Todos
ff todos list [--status open|done] [--assigned-to <userId>]
ff todos create --title T [--notes] [--due YYYY-MM-DD] [--assigned-to <userId>] [--visibility family|private]
ff todos update <id> [--title] [--notes] [--due] [--assigned-to] [--visibility] [--completed true|false]
ff todos delete <id>
4.13 Imports (bank statements)
ff imports parse --file PATH --type csv|xlsx|pdf
[--password PW] # only for encrypted PDFs
[--account <id>] # required for header-rememberment
[--header-row N]
# The output contains `archiveId`, `headers`, `rows`, and an optional `hint`
# (per-bank column mapping remembered from a prior successful import).
# Persist the rows to disk, edit if needed, then confirm:
ff imports confirm --rows-file PATH.json --account <id>
[--archive-id <id>] # echo from parse output
[--mapping '{"date":"Date","debit":"Withdrawal",…}']
[--swap-direction true|false]
[--header-row N]
Passwords are sent over HTTPS in the body, used once to decrypt, never logged or returned in errors, and never stored.
4.14 Excel
ff excel export [--out FILE] # downloads all data as one .xlsx
ff excel import --file PATH # uploads + routes rows by sheet name
4.15 Reports
ff reports income-expenditure [--fy YYYY-YY] [--owner <userId>]
ff reports account-statement --account <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
ff reports credit-cards [--owner <userId>]
ff reports net-worth [--fy YYYY] [--fy-start-month 1..12] # admin-only
4.16 Invitations (admin)
ff invitations create --email E [--role family_admin|member] # guests: use the cross-family guest invite, not this
# Returns { token, url, expiresAt } — the invitee uses the URL to set their password.
4.17 Audit (admin)
ff audit export [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--out FILE]
ff audit truncate --before YYYY-MM-DD
4.18 Raw HTTP escape hatch
For anything not yet wrapped, or for novel orchestration:
ff request GET /reports?type=net-worth
ff request POST /transactions --body '{"accountId":"…","amount":100,…}'
ff request POST /transactions --body-file txn.json
Path prefix /api/v1 is normalised — pass it or omit it, both work.
6. Common workflows
5.1 Reconcile a bank statement end-to-end
# 1. Parse — preview rows; do NOT auto-confirm.
ff imports parse --file ./statement-HDFC-may.pdf --type pdf --account "$ACC" --json > parsed.json
# 2. Inspect. Each row has { date, description, debit, credit, suggestedType }.
jq '.rows[:5]' parsed.json
# 3. Edit if needed (typical: drop a header-page artifact row).
# Then write back the cleaned rows:
jq '.rows' parsed.json > rows.json
# 4. Confirm.
ff imports confirm --rows-file rows.json --account "$ACC" \
--archive-id "$(jq -r .archiveId parsed.json)" --json
5.2 Add a SIP payment that links to an investment
# Find the investment id.
INV=$(ff investments list --json | jq -r '.[] | select(.folioNumber=="HDFC123") | .id')
# Add the transaction.
ff txns create --account "$ACC" --date 2026-05-01 \
--description "HDFC Top 100 SIP" --type investment --amount 5000 \
--link-type investment --link-id "$INV" --json
5.3 Pull a quarterly Income & Expenditure summary
ff reports income-expenditure --fy 2025-26 --json |
jq '{income: .totalIncome, expense: .totalExpense, net: .net}'
5.4 Bulk-recategorise all "Amazon" transactions
IDS=$(ff txns list --q "amazon" --json | jq -r '[.[].id] | join(",")')
ff txns bulk --action recategorize --ids "$IDS" --category "$SHOPPING_CAT_ID" --json
5.5 Snapshot a family's net worth nightly
ff reports net-worth --fy 2025 --json >> ~/snapshots/networth-$(date +%F).json
7. JSON I/O shapes
This section sketches the wire shapes the most-used endpoints. For the rest
either use --json and inspect, or read the corresponding route handler in
apps/api/src/routes/.
6.1 Common envelope
Successful responses are always JSON. Failures use the same content-type
with HTTP status >=400 and shape:
{ "error": "human-readable message" }
Binary endpoints (/excel/export, /audit/export, attachment downloads,
statement-archive downloads) return raw bytes with an application/...
content-type plus a Content-Disposition: attachment; filename="…" header.
The CLI saves to disk; the API SDK returns a Blob.
6.2 Transaction (read shape)
{
"id": "uuid",
"accountId": "uuid",
"txnDate": "2026-05-12",
"description": "Salary credit",
"refNumber": null,
"categoryId": "uuid|null",
"entryType": "income",
"debit": 0,
"credit": 250000,
"linkType": null,
"linkId": null,
"crossPaymentId": null,
"transferGroupId": null,
"source": "manual",
"financialYear": "2026-27",
"isReconciled": false,
"isDeleted": false,
"notes": null,
"createdAt": "ISO",
"updatedAt": "ISO"
}
6.3 Account
{
"id": "uuid",
"name": "HDFC Savings",
"accountType": "savings",
"bankName": "HDFC Bank",
"accountNumberMasked": "****4567",
"ownerUserId": "uuid",
"openingBalance": 100000,
"currentBalance": 152340.50,
"creditLimit": null,
"contactId": "uuid|null",
"isActive": true
}
6.4 Income & Expenditure report
{
"type": "income-expenditure",
"fy": "2025-26",
"income": { "Salary": [0,0,250000,...], "Other": [...] }, // 12 months, FY order (Apr-start)
"expense": { "Rent": [...], "Groceries": [...] },
"incomeIds": { "Salary": "uuid", "Other": null },
"expenseIds": { "Rent": "uuid", "Groceries": "uuid" },
"totalIncome": 1234567,
"totalExpense": 765432,
"net": 469135
}
6.5 Net worth report
{
"type": "net-worth",
"assets": 12_345_678,
"liabilities": 234_567,
"netWorth": 12_111_111,
"breakdown": { "bankAssets": 9_000_000, "investmentAssets": 3_000_000, "lifeAssets": 345_678, "liabilities": 234_567 },
"compare": {
"lastFyLabel": "FY 2024-25",
"lastFyEnd": "2025-03-31",
"thisFyLabel": "FY 2025-26",
"thisFyEnd": "2026-03-31",
"bankAssets": { "last": …, "now": …, "diff": … },
"investmentAssets": { "last": …, "now": …, "diff": … },
"lifeAssets": { "last": …, "now": …, "diff": … },
"liabilities": { "last": …, "now": …, "diff": … },
"assets": { "last": …, "now": …, "diff": … },
"netWorth": { "last": …, "now": …, "diff": … },
"snapshotNote": "Investment current value and Life-policy value aren't snapshotted historically; the same value is shown for both periods."
}
}
8. Error model & exit codes
The CLI maps HTTP status to shell exit code so scripts can branch:
| Exit | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic error / network failure / bad input |
| 2 | Unknown command / argv parse failure |
| 3 | 401 unauthorised — token missing or invalid |
| 4 | 403 forbidden — wrong role or wrong scope |
| 5 | 404 not found |
| 6 | 409 conflict — duplicate or constraint violation |
| 7 | 429 too many requests (reserved) |
| 8 | 5xx server error |
With --json, errors are emitted to stderr as { error, status, body }.
9. Safety guardrails for agents
- Always read before you write. Verify the target's state with a
listorgetbefore issuing PATCH / DELETE. The API is idempotent for some operations (txns deleteon an already-deleted txn is a no-op) but not for others (e.g.transfers createis not — it would create a duplicate). - Never log the token plaintext. Treat
SAHIDHA_TOKENlike a password. Logs uploaded to ChatGPT / Claude transcripts are not safe storage. - Don't bulk-delete from a regex. If you're operating on a list, print
the candidates first and ask the user to confirm —
ff txns list --q …thenff txns bulk --action delete --ids …. Same for accounts. - Watch the FY boundary. Reports and many filters are FY-aware (Apr-start
by default; configurable per family via
financialYearStartMonth).ff txns listwithout--from/--toreturns only the current FY. - Statement uploads dedup by content hash. Re-uploading the same file
does not duplicate transactions — the archive row updates in place and
the parse-confirm flow re-checks
duplicateHashper transaction. - Imports require human review. Parsed rows aren't transactions yet.
Always inspect, edit if necessary, and only then call
imports confirm. - Cross-family guest mode is read-leaning. Even with a
writetoken, a guest cannot edit accounts / insurance / investments / settings. Plan agent capabilities around that. - Audit logs are permanent. Every action via API token is logged with
the token's
name. Mutations cannot be hidden — agents should assume their behaviour is reviewed. - No financial-transaction execution. This app only records money
movements. Nothing here initiates a bank transfer or trade. If a user
asks an agent to "send the rent," instruct them to do it in their bank
app and then record it with
ff txns create.
10. MCP tool reference
The MCP server exposes 62 tools. All names are prefixed fh_ (Sahidha) so
they're easy to spot in a host's tool list. Grouped by resource:
| Group | Tools |
|---|---|
| identity | sa_whoami, sa_families |
| tokens | sa_tokens_list, sa_tokens_create, sa_tokens_revoke |
| accounts | sa_accounts_list/create/update/delete, sa_accounts_statements_list/delete |
| transactions | sa_txns_list/create/update/delete/restore, sa_txns_transfer_candidates, |
sa_txns_link_transfer, sa_txns_link_master, sa_txns_bulk | |
| categories | sa_categories_list/create/update/delete |
| insurance | sa_insurance_list/create/update/delete, sa_insurance_attachments_list/delete |
| investments | sa_investments_list/create/update/delete |
| transfers | sa_transfers_list/create/delete |
| cross-pay | sa_cross_payments_list/create/acknowledge/settle/delete |
| contacts | sa_contacts_list/create/update/delete |
| todos | sa_todos_list/create/update/delete |
| imports | sa_imports_parse, sa_imports_confirm |
| reports | sa_reports_income_expenditure, sa_reports_account_statement, |
sa_reports_credit_cards, sa_reports_net_worth | |
| family/admin | sa_family_members_list, sa_family_guests_list/invite/revoke, sa_invitations_create |
| raw | sa_request (escape hatch for any endpoint above) |
Each tool has a JSON Schema for inputs that the host validates BEFORE calling.
Look up the exact shape via tools/list, or read apps/mcp/src/tools.ts —
each entry is { name, description, inputSchema, run }.
Binary endpoints (excel export, statement download, attachment download)
return a binary-descriptor object instead of streaming bytes through the
MCP channel — agents that need the actual file should fall back to the CLI
or call the REST API directly with the same ffp_… token.
11. Where to look next
| Question | File |
|---|---|
| What endpoints exist? | apps/api/src/routes/*.ts |
| What's the wire shape of X? | The route's response-builder code |
| What does Y validate against? | packages/api-contract/src/*.ts (Zod schemas) |
| How does auth resolve a token? | apps/api/src/plugins/auth.ts |
| How is family scope enforced? | packages/db/prisma/rls/*.sql (Row-Level Security) |
| How do I add a new command to the CLI? | Append to the array in apps/cli/src/commands.ts |
| How do I add a new MCP tool? | Append to the array in apps/mcp/src/tools.ts |
| Why did my mutation fail with 403? | Check role + token scope. Run ff auth whoami. |
12. Versioning
This guide is versioned alongside the API. Breaking changes will:
- Bump the API to
/api/v2and keep/api/v1running for two releases. - Update this file in the same commit.
- Note the change in
CHANGELOG.md(forthcoming).
Until v1.0.0, additive changes only on /api/v1. Token format is stable.