ZenMoney MCP Server
MCP server for trustworthy personal-finance analytics and explicitly confirmed user-entity changes over the ZenMoney API. The project started as a fork of nnslvp/zenmoney-mcp and is maintained here as a substantially extended version. It keeps its working data local and adds financially conservative calculations, atomic sync, and a two-step write workflow.
Complete tool catalog
Both local and remote modes expose 59 tools. They share 57 tools and use two mode-specific tools for synchronization and category suggestions.
| Area | Tools |
|---|---|
| Financial analytics | get_net_worth, get_liquidity, analyze_spending, analyze_income, analyze_merchants, check_budget_health, get_upcoming_payments, analyze_trends, detect_recurring, get_account_flow, analyze_transfers, detect_anomalies, get_debts, convert_currency, get_exchange_rates, search_transactions |
| Planning analytics | get_financial_snapshot, get_financial_position, get_cash_flow, get_spending_baseline, compare_periods, get_emergency_fund_status, get_debt_service, forecast_cash_flow |
| Decision support | plan_emergency_fund, plan_debt_payoff, compare_debt_strategies, plan_financial_goal, plan_multiple_goals, run_financial_scenario, build_financial_plan |
| Entity reads | list_accounts, get_account, list_tags, get_tag, list_merchants, get_merchant, list_reminders, get_reminder, list_reminder_markers, get_reminder_marker, list_transactions, get_transaction, list_budgets, get_budget |
| Confirmed entity changes | prepare_account_changes, prepare_tag_changes, prepare_merchant_changes, prepare_reminder_changes, prepare_reminder_marker_changes, prepare_transaction_changes, prepare_budget_changes, prepare_changes, prepare_mixed_changes, prepare_recurring_payment, get_change_proposal, apply_changes |
| Mode | Mode-specific tools |
|---|---|
| Local stdio | sync_data, suggest_category |
| Remote through OpenAI Secure MCP Tunnel | force_sync, get_sync_status |
Analytics
| Question | Tool |
|---|---|
| How much money do I have? | get_net_worth |
| Can I afford a purchase? | get_liquidity |
| Where does my money go? | analyze_spending |
| Where does my income come from? | analyze_income |
| Which merchants receive my money? | analyze_merchants |
| Am I within budget? | check_budget_health |
| What subscriptions do I have? | detect_recurring |
| How are income and spending changing? | analyze_trends |
| What transfers did I make? | analyze_transfers |
| Are there unusual or duplicate expenses? | detect_anomalies |
| Who owes whom? | get_debts |
| What payments are coming up? | get_upcoming_payments |
| Find matching transactions | search_transactions |
| What happened on this account? | get_account_flow |
| Convert currencies | convert_currency, get_exchange_rates |
| Economic assets, liabilities, net worth, and free cash flow | get_financial_position |
| Legacy in-balance financial snapshot | get_financial_snapshot |
| Monthly cash flow | get_cash_flow |
| Normal spending level | get_spending_baseline |
| Compare periods | compare_periods |
| Emergency fund coverage | get_emergency_fund_status |
| Debt burden | get_debt_service |
| 30/60/90 day forecast | forecast_cash_flow |
The server also exposes paginated collection and exact resources for Account,
Tag, Merchant, Reminder, ReminderMarker, Transaction, and Budget, plus currencies,
synchronization status, and a cache-only financial snapshot at
zenmoney://financial-snapshot. Reading a resource never starts synchronization.
Synchronization and anomaly contracts
Public synchronization timestamps (requested_at, started_at, finished_at,
last_sync_time, and planning data_quality.last_sync) are RFC3339 UTC strings
with a Z suffix; an unavailable value is null. The cache and control state
continue to store Unix-epoch integers. last_server_timestamp remains ZenMoney's
numeric delta cursor, not a synchronization timestamp.
detect_anomalies returns bounded exact_duplicates (same day,
converted amount to the cent, normalized merchant/payee, category, and outcome
account), same_merchant_amount_close_timestamp (same merchant/payee, exact
amount, and up to one day apart), near_duplicates (same normalized
merchant/payee and category, up to two days and 5% amount difference),
periodic_recurrences, and unusually_large_one_off. The cache has dates rather
than transaction times, so duplicate signals declare timestamp_precision: "day". Recurrence detection reads at most 400 days of history before the
selected period end, groups equal category and cent-rounded user-currency amounts
by unique dates, and returns only recurrences touching the selected period:
monthly 25–35 days (at least 3 events), quarterly 80–100, semiannual 170–195,
and annual 350–380 (at least 2 each). Periodic transaction IDs are excluded from
one-off outliers. Every new collection, plus the retained outliers and
possible_duplicates aliases, returns at most 15 results; summary contains
full counts and results_truncated.
MCP discovery declares the generic output schema {"type":"object"} for every
tool. At the protocol boundary each response contains the native object in
structuredContent and the identical JSON object as TextContent for clients
that still require the text fallback.
Confirmed user-entity changes
All writes use two separate calls. Choose the entity-specific prepare tool for
ordinary work, or the preferred prepare_changes when one proposal creates or
changes several related entity types. prepare_mixed_changes is a compatible
alias with the same strict operations[] schema:
prepare_account_changes prepare_tag_changes
prepare_merchant_changes prepare_reminder_changes
prepare_reminder_marker_changes
prepare_transaction_changes prepare_budget_changes
prepare_changes prepare_mixed_changes (compatible alias)
prepare_recurring_payment
get_change_proposal apply_changes
Prepare validates 1–100 operations and returns an immutable field-by-field
preview without writing to ZenMoney. After reviewing it, pass only its
proposal_id to apply_changes; get_change_proposal reports state and results.
The limit is 100 resulting items, including every split part. For larger requests,
prepare separate reviewed proposals, keeping dependent operations together. An
unchanged update rejects the whole preparation; remove it before trying again.
invalid_changes includes safe details.reason, details.message, and the
zero-based details.operation_index when an individual operation caused the
rejection. Batch-size errors include details.max_items.
Preparation requires a successful full sync so that untouched ZenMoney fields
can be preserved. Apply synchronizes and rejects the whole proposal before
writing if any source entity changed since preparation. {"ref": "..."} links
between creates are resolved while preparing, so one proposal is one mixed
/v8/diff/ write request. This is the atomicity boundary: after a send failure,
the result is unknown, the proposal becomes needs_review with
write_result_unknown, and it is never retried automatically. Applying any
terminal proposal again does not send another write.
Running and completed proposals include diagnostics: the execution stage,
monotonic duration_ms, and a timeline committed to SQLite before each stage
and HTTP request. Receipt of an HTTP response is recorded before decoding it or
applying it to the local snapshot. Restart recovery preserves that timeline and
adds interrupted and recovered_at; it never replays an ambiguous write.
finished_at records completion or recovery time. Existing ledger rows remain
compatible; the diagnostic column is added automatically on startup.
Write errors distinguish transport, HTTP response, JSON decoding, and local
response application. Diagnostics include HTTP status, attempt number, timeout,
payload sizes, per-entity item counts, and bounded exception types and code
locations. Recognized upstream validation errors retain the entity type, field,
and zero-based proposal item position when the API object matches a submitted
item. Unrecognized API codes become unknown with a stable fingerprint; unknown
message formats are omitted. Verification mismatches list positions, entity
types, whether the entity exists, and differing field names ($entity means
missing), without values. HTTP errors still require review because the write
outcome may be uncertain.
Structured JSON logs correlate MCP calls (request_id), proposals (proposal_id),
sync runs (sync_run_id), and individual HTTP attempts (http_request_id). They
include UTC timestamps, package version, process ID, durations, and outcomes.
Response bodies, tool arguments, exception messages, source text, and financial
values are not logged. Compose retains separate rotating MCP and worker journals
on its existing persistent control volume; see the
incident runbook.
New ReminderMarkers inherit comment from their parent Reminder. Preparation
includes that comment in the preview, including parent changes in the same
proposal. Omit the marker comment or supply the same value; a different explicit
comment is rejected. A parent comment change before apply stops the write and
requires a new proposal. Comment verification remains strict. Transaction creates
accept server-generated originalPayee only when it equals the requested payee;
amounts and other requested fields are still checked.
Create and update are supported for all seven user entities. Safe delete archives
an Account, marks a Transaction or ReminderMarker deleted, or clears a Budget.
Tag, Merchant, and Reminder deletion and all physical purge operations are not
exposed. Prepared proposals expire after 24 hours. Terminal proposals are retained
for 30 days, and an uncertain write or verification result becomes needs_review.
prepare_transaction_changes also supports a one-sided income or outcome split.
The first part keeps the source transaction ID; the remaining parts receive new
IDs. All parts are submitted in one Diff batch, preserve the source raw metadata,
and must add up exactly. Use one optional remainder part to avoid decimal drift:
{
"operations": [{
"operation": "split",
"transaction_id": "transaction-id",
"parts": [
{"amount": 730, "category_id": "groceries-category-id"},
{"amount": "remainder", "category_id": "household-category-id"}
]
}]
}
prepare_recurring_payment prepares one ordinary monthly expense and its first
planned occurrence without writing. Its exact payload is:
{
"name": "T-Bank credit card",
"amount": 28060,
"account_id": "account-id",
"category_id": "category-id",
"frequency": "monthly",
"day_of_month": 18,
"start_date": "2026-09-18",
"end_date": null,
"notify": true
}
Only positive amounts and monthly are supported. start_date must be an ISO
date whose day equals day_of_month; an optional end_date cannot precede it.
The account must be active and the category must belong to its owner. The result
is the ordinary mixed proposal containing a Reminder (interval="month",
step=1, points=[0]) and its first planned ReminderMarker on start_date.
Both use the same account/instrument, category, notify, payee, and one-sided
expense (income=0, outcome=amount); only apply_changes can write them.
get_spending_baseline uses 3–24 completed budget months. With
include_current_partial_month=true, it appends the current period through the
calculation date to canonical monthly_series; monthly remains a compatible
alias. A partial row has complete=false, days_elapsed, and days_total, but
is excluded from all statistics and pattern detection. trimmed_mean sorts the
completed values and removes floor(n * 10%) from each tail before fmean.
expense_patterns groups completed one-sided operating expenses by normalized
merchant/payee and category, after conversion to the user currency. It reports
recurring_monthly (at least 3 events, 25–35-day intervals),
likely_quarterly (at least 2, 80–100), likely_semiannual (at least 2,
170–195), likely_annual (at least 2, 350–380), one_off (one event), or
unknown. Periodic classes also require amount spread no greater than 20% of
their mean. This is historical heuristic output, not a prediction: the response
includes class counts and totals, returns only the 100 largest groups, and marks
truncation explicitly.
Planning analytics are deliberately conservative:
- emergency-fund coverage requires explicit essential category IDs or a monthly essential-spending override;
get_cash_flowseparates operating expenses, financing inflows, and cash debt service;get_debt_serviceincludes every active negative-balance account regardless ofinBalance;get_financial_positionapplies that same economic boundary to every positive asset and negative liability, then reports three complete months of operating cash flow and debt service;- unknown APR and payment terms remain
nullunless supplied explicitly; - recurring-payment detection is a historical heuristic and is labeled as such;
- cash-flow forecasts are transparent scenarios, not prediction guarantees.
search_transactions supports stable cursor pagination, date or converted-amount
sorting, category/account arrays, and explicit category presence. To page through
all uncategorized expenses for an arbitrary period from largest to smallest, reuse
the returned next_cursor with the same filters:
{
"start_date": "2026-01-01",
"end_date": "2026-08-31",
"type": "outcome",
"category_state": "uncategorized",
"sort_by": "amount",
"sort_order": "desc",
"limit": 50
}
Financial Planning
The server adds deterministic decision support on top of the factual analytics. Every result exposes inputs, assumptions, constraints, reasons, alternatives, and measurable outcomes; it does not execute or write a financial decision.
| Question | Tool |
|---|---|
| How fast can I build a 6-month emergency fund? | plan_emergency_fund |
| Should I pay the high-interest loan first? | plan_debt_payoff, compare_debt_strategies |
| Can I afford a car in 18 months? | plan_financial_goal |
| Which of my goals conflict? | plan_multiple_goals |
| What happens if my income falls by 20%? | run_financial_scenario |
| How should I allocate my monthly free cash flow? | build_financial_plan |
Planning inputs that ZenMoney does not contain must be supplied explicitly:
{
"emergency_fund": {
"target_months": 6,
"essential_category_ids": ["category-id"]
},
"debt_accounts": {
"loan-account-id": {
"apr_pct": 19.9,
"minimum_payment": 15000
}
},
"goals": []
}
Missing APR, minimum payments, or essential-spending configuration returns
configuration_required; the server never invents those values. Calculations
use zero investment return, Decimal money arithmetic, and future calendar
month-end snapshots. Restricted deposits are excluded from emergency reserves
unless explicitly enabled, and credit capacity is always excluded.
Debt payoff supports four explicit models. Existing negative-balance accounts
may use fixed_loan, credit_card, or installment; a liability absent from
ZenMoney must use arbitrary with an explicit balance. For example:
{
"strategy": "avalanche",
"monthly_extra_payment": 5000,
"debt_accounts": {
"loan-account-id": {
"liability_type": "fixed_loan",
"apr_pct": 19.9,
"fixed_payment": 15000
},
"card-account-id": {
"liability_type": "credit_card",
"apr_pct": 29.9,
"minimum_payment": 5000,
"statement_balance": 42000,
"grace_period_payment": 42000,
"grace_period_due_date": "2026-09-15"
},
"installment-account-id": {
"liability_type": "installment",
"payment_schedule": [{"date": "2026-09-30", "amount": 10000}]
},
"family-loan": {
"liability_type": "arbitrary",
"title": "Family loan",
"balance": 50000,
"minimum_payment": 5000
}
}
}
See docs/planning-semantics.md for the priority
policy, formulas, rounding, data-quality labels, and limitations.
Runtime modes
The installed zenmoney-mcp command is the local stdio server for Codex,
ChatGPT Desktop, Claude Desktop, and Cursor. It uses the shared SDK v2
registry and hardened runtime directly; it does not preserve an upstream
server through a runtime overlay.
For a private remote deployment, zenmoney-mcp-http exposes Streamable HTTP
at /mcp only inside Docker, and the OpenAI Secure MCP Tunnel client connects
outbound to OpenAI. The remote registry excludes the local API-dependent
sync_data and suggest_category tools. Its analytical tools remain read-only.
Remote force_sync can request a cache refresh, while confirmed entity-change
proposals are queued for the separate credentialed worker. get_sync_status
and get_change_proposal report their respective progress. The MCP
container still receives no ZenMoney token and cannot write the financial
snapshot or call ZenMoney directly. See the
remote operations runbook and
threat model.
force_sync(force_full=false, wait_until_complete=false) is asynchronous by
default. With wait_until_complete=true, it waits only for the same request ID,
polling the validated control state every 0.25 seconds for a fixed 60 seconds.
A terminal result is completed or failed; a timeout returns status: "timeout", the current pending or running state, and
wait_timed_out: true without cancelling the worker. Invalid or replaced state
fails closed with invalid_sync_state; a pending or running request remains
single-flight.
Hardening in this fork
The installed zenmoney-mcp command runs zenmoney_mcp.entrypoint against a
shared SDK v2 registry with hardened database, synchronization, and analytics
implementations:
HardenedDatabaseadds idempotent migrations and strict FX handling;HardenedSyncEnginevalidates responses and atomically replaces the live cache;- corrected analytics cover net worth, liquidity, budgets, debts, account flow, spending, transaction search, upcoming payments, and FX;
- remaining analytics receive bounded runtime validation;
- MCP discovery advertises the same limits enforced at runtime.
Key semantics:
net_worthincludes only active accounts within_balance=true;- excluded accounts are returned separately in
net_worth_all_accounts; - credit is borrowing capacity, not an asset;
- accessible savings and term deposits are not treated as equivalent liquidity;
- budget periods respect the user's configured month-start day;
- zero-budget and unbudgeted spending are surfaced explicitly;
- active negative balances remain obligations regardless of
inBalance; - debt-account balances are authoritative and attribution gaps remain visible;
- account flow includes signed transfers in native and user currency;
- missing or zero exchange rates fail explicitly instead of becoming a 1:1 rate;
- full sync replaces the cache, preventing stale rows from surviving.
More detail is available in
README-HARDENING.md.
Local installation with uvx
Install uv, then run the server from PyPI:
Get a personal API token at zerro.app/token, as documented in the official ZenMoney API wiki.
export ZENMONEY_TOKEN="replace-with-your-token"
uvx --from zenmoney-mcp-server zenmoney-mcp
uvx downloads the package into an isolated cached environment; cloning the
repository or creating a virtual environment is not required.
To run the current main branch before its next PyPI release, use
uvx --from git+https://github.com/ekho/zenmoney-mcp.git zenmoney-mcp.
The first hardened start performs additive SQLite migrations. Back up
~/.cache/zenmoney-mcp/zenmoney.db before the first run when preserving an
existing cache matters. A full sync can recreate the cache from ZenMoney.
Private ChatGPT installation with OpenAI Secure MCP Tunnel
ChatGPT web cannot start the local stdio command. For private remote access, run the included Docker Compose deployment and connect it through OpenAI Secure MCP Tunnel. The MCP endpoint stays inside the Docker network; only the tunnel client makes an outbound connection to OpenAI.
This mode requires Docker Engine with Compose v2, a ZenMoney token, an OpenAI tunnel runtime API key, and a tunnel ID associated with the target ChatGPT workspace. Clone the repository and create the non-secret environment file:
git clone https://github.com/ekho/zenmoney-mcp.git ~/zenmoney-mcp
cd ~/zenmoney-mcp
cp deploy/remote-mcp/.env.example deploy/remote-mcp/.env
Set CONTROL_PLANE_TUNNEL_ID in deploy/remote-mcp/.env. Provision the
ZenMoney token and OpenAI key as separate file-backed Compose secrets using the
ownership and permission commands in the
remote operations runbook; do not put either
secret in .env. Then pull and start the deployment:
docker compose --env-file deploy/remote-mcp/.env \
-f deploy/remote-mcp/compose.yaml pull
docker compose --env-file deploy/remote-mcp/.env \
-f deploy/remote-mcp/compose.yaml up -d --no-build --pull never
docker compose --env-file deploy/remote-mcp/.env \
-f deploy/remote-mcp/compose.yaml ps
Complete the health checks and tunnel-client doctor from the runbook, then add
the MCP app in ChatGPT Developer Mode with Connection = Tunnel and scan its
tools.
ChatGPT Desktop and Codex
Add the server to ~/.codex/config.toml:
[mcp_servers.zenmoney]
command = "uvx"
args = ["--from", "git+https://github.com/ekho/zenmoney-mcp.git", "zenmoney-mcp"]
env_vars = ["ZENMONEY_TOKEN"]
tool_timeout_sec = 120
Restart the desktop client after changing MCP configuration. For ChatGPT web, use the private remote Streamable HTTP + Secure MCP Tunnel deployment in the operations runbook, not a local executable.
Claude Desktop or Cursor
{
"mcpServers": {
"zenmoney": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/ekho/zenmoney-mcp.git",
"zenmoney-mcp"
],
"env": {
"ZENMONEY_TOKEN": "replace-with-your-token"
}
}
}
}
Development
Clone the repository only when changing or testing the code locally:
git clone https://github.com/ekho/zenmoney-mcp.git ~/zenmoney-mcp
cd ~/zenmoney-mcp
uv sync --extra dev
Data flow
- Local
sync_datareads/v8/diff/directly through the local sync engine. - In the remote deployment, the periodic worker reads
/v8/diff/; remoteforce_synconly asks that credentialed worker to run immediately. - Both modes publish a SQLite cache at
~/.cache/zenmoney-mcp/zenmoney.dbor the configuredZENMONEY_DB_PATH; analytics read that cache locally. - A local confirmed proposal is written synchronously. A remote confirmed proposal is persisted on the control volume and written by the worker.
- Only the local process or credentialed worker can call ZenMoney.
Testing
uv sync --extra dev
uv run python -m compileall -q src tests
uv run python -m pytest tests/ -v --ignore=tests/test_integration.py
The live integration test requires ZENMONEY_TOKEN and is excluded from CI.
License
MIT
Metadata
Release files for zenmoney-mcp-server 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| zenmoney_mcp_server-0.10.0.tar.gz | 123.6 kB | Without provenance | Details for zenmoney_mcp_server-0.10.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| zenmoney_mcp_server-0.10.0-py3-none-any.whl | Python 3 | none | any | Details for zenmoney_mcp_server-0.10.0-py3-none-any.whl |
Total release size: 263.7 kB
Release files / zenmoney_mcp_server-0.10.0.tar.gz
| Download URL | zenmoney_mcp_server-0.10.0.tar.gz |
|---|---|
| Size | 123.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d9a773e0d72ba944609fd7e5d3ab1bbc66bb85071249b00fdac7b71cda86b97f
|
|
BLAKE2b-256 checksum How to use checksums |
67a99d858cf2ea306fb27e70804cfb19f2519a47565731998e6853042cd9d6b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / zenmoney_mcp_server-0.10.0-py3-none-any.whl
| Download URL | zenmoney_mcp_server-0.10.0-py3-none-any.whl |
|---|---|
| Size | 140.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5a77e208f16090084d864f146bc4379112a7e05f40b5609734e209acf84f83a4
|
|
BLAKE2b-256 checksum How to use checksums |
30656d8baed52a108d7dd89e146704944ab6a54d3f9128c8fa1f48ae0fab2b64
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for zenmoney-mcp-server 0.10.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.