Default to read-only; disable credential/auth mutations#61
Conversation
- Add MONARCH_MCP_READ_ONLY env flag (default on); only an explicit falsey value disables it. Every Monarch SDK mutation tool refuses to run while read-only is active (create/update/delete transactions, splits, tags, categories, rules, budgets, refresh accounts, upload balance history). Dry-run paths remain available. - Hard-disable MCP auth/session mutation tools (monarch_login, monarch_login_with_token, monarch_logout) regardless of the flag so credentials can't flow through MCP transport and the stored session cannot be modified by a remote client. - Remove MONARCH_EMAIL/MONARCH_PASSWORD auto-login from the client factory and drop python-dotenv so passwords are never silently read from the environment. Authentication is performed out-of-band via login_setup.py. - Tests: per-tool refusal coverage, env-credential / dotenv guard tests, and an autouse opt-out so the existing mutation suites keep passing.
- Add a top-level Security model section: read-only default, disabled MCP auth tools, no env-credential auto-login, no dotenv loading. - Rewrite Installation for macOS to install the readonly-hardening branch into a venv via either pip+venv or uv. - Rewrite Authentication section as terminal-only: explicit warning not to paste credentials/MFA/tokens into Claude, both email/password and SSO-token-paste flows via login_setup.py, plus rotation steps. - Add a Running the tests section with the exact pytest invocation and a copy-pasteable manual refusal verification snippet. - Update Troubleshooting and Session Management to match the new model (read_only / disabled payloads, keyring locations, removed pickle file claim, removed obsolete elicit upgrade-hint block).
Monarch's API stopped accepting `Authorization: Token` on GraphQL in
May 2026 and now requires session-cookie auth: `session_id` (HttpOnly)
+ `csrftoken` cookies plus an `x-csrftoken` request header matching the
csrftoken cookie. The upstream monarchmoneycommunity library still
ships the old Token-header flow, so this integrates a parallel
cookie-auth path on top of the readonly-hardening branch.
Preserves all read-only hardening:
- MONARCH_MCP_READ_ONLY defaults to true; every mutation tool still
refuses (including dry-run preservation).
- MCP auth tools (monarch_login, monarch_login_with_token,
monarch_logout) remain hard-disabled — no MCP-side cookie login tool
was added. Cookies are entered out-of-band via login_setup.py only.
- No MONARCH_EMAIL/MONARCH_PASSWORD auto-login is reintroduced;
python-dotenv stays out of the client and out of login_setup.py.
Changes:
- New `MonarchMoneyCookieAuth` subclass overrides `_get_graphql_client`
to send cookies via AIOHTTPTransport with `x-csrftoken`, Origin, and
Referer set to app.monarch.com.
- `secure_session` gains `save_cookies` / `load_cookies` /
`_save_cookies_file` / `_load_cookies_file` helpers (mode 0600 file
fallback) and a `monarch-cookies` keyring entry alongside the legacy
`monarch-token` one. `get_authenticated_client` prefers cookies and
falls back to the legacy token with a clear log warning that it will
likely 401.
- `delete_token` clears both stores.
- `save_authenticated_session` dispatches on
`isinstance(MonarchMoneyCookieAuth)` to store cookies.
- `tools/auth.check_auth_status` and `debug_session_loading` now
surface cookie vs. legacy-token state and emit an explicit warning
when only a legacy token is stored ("Monarch's API rejects
Token-header auth (May 2026 change)").
- `setup_authentication` MCP tool text is rewritten to point at the
cookie flow as the only one that works.
- `login_setup.py` puts session cookies as the default menu option and
labels the legacy email/password and token-paste paths as currently
broken behind an extra confirmation. No dotenv import.
- README documents the cookie flow as Option A (required since May
2026), labels Option B as currently broken, updates Session
Management / Troubleshooting / Common Errors / Security model
references to cookies, and adds a manual cookie-store verification
snippet.
- Tests: 16 new assertions cover the cookie subclass headers/cookies,
cookie save/load/delete roundtrips + 0600 file mode, cookie
preference in `get_authenticated_client`, cookie-vs-token dispatch in
`save_authenticated_session`, the legacy-token-only warning in both
`check_auth_status` and `debug_session_loading`, login_setup.py menu
ordering, the broken-path labeling, and login_setup's absence of
dotenv. `conftest.py` now prefers the real `monarchmoney` package
when installed so the cookie subclass can initialize its real
`_headers` dict; the MagicMock stand-in remains as a fallback.
Tests: 197 passed (was 181).
|
The fail safe default and the string grep regression guards against load_dotenv and os.getenv MONARCH_EMAIL are excellent. A few items before this leaves draft:
The shape of the guard is right. |
Adds a compatibility layer around the monarchmoney dependency for Monarch's current web API, rebased onto the modular tools/ package (PR robcerda#46) instead of the old monolithic server.py. - monarch_auth.py: patch BASE_URL to api.monarch.com, send current web login fields (supports_email_otp/supports_recaptcha/email_otp), detect email-OTP responses separately from TOTP MFA, and build stored-token clients with the saved device-uuid. monarch-client-version is now a documented MONARCH_CLIENT_VERSION constant. - secure_session.py: store token + device-uuid as JSON (still accepting legacy raw-token entries) across the keyring and file-fallback backends. - client.py: use only an existing keyring session; MCP tool calls no longer initiate a noninteractive password login. - app.py: drop dotenv loading; suppress gql.transport INFO logging (with an inline comment) so account payloads are not written to logs. - tools/budgets.py: narrower budget query that avoids stale category-group fields and returns current-month category rows. - login_setup.py: prompt for email verification codes via the current auth flow; README documents email-OTP and keyring-only storage. No dotenv/.env loading and no MONARCH_PASSWORD setup path (addresses the read-only hardening concerns from robcerda#61). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Hardens the MCP server so that, by default, it cannot mutate remote Monarch state or change locally stored auth.
MONARCH_MCP_READ_ONLYenv flag (defaults on). Every Monarch SDK mutation tool now refuses to run while read-only is active:create_transaction,update_transaction,categorize_transaction,update_transaction_notes,mark_transaction_reviewed,bulk_categorize_transactions(real run; dry-run still allowed),delete_transaction,set_transaction_tags,create_transaction_tag,add_transaction_tag,create_transaction_category,split_transaction,create_transaction_rule,update_transaction_rule,delete_transaction_rule,set_budget_amount,refresh_accounts, andupload_account_balance_history(real run; dry-run still allowed). Operators can flip the flag to a falsey value (false/0/no/off) to permit mutations explicitly.monarch_login,monarch_login_with_token, andmonarch_logoutno longer accept credentials or modify stored auth state — regardless of the read-only flag. Credentials never flow through MCP transport; a remote MCP client cannot wipe the stored session. Authentication must be performed out-of-band viapython login_setup.py.MONARCH_EMAIL/MONARCH_PASSWORDfallback fromget_monarch_client, removedload_dotenv()fromapp.pyandlogin_setup.py, and droppedpython-dotenvfrom the dependency list. Passwords are never silently read from the environment.Testing
python -m pytest— 181 passed.tests/test_read_only.py(39 tests) proves every mutation tool refuses in read-only mode, that the upstream MonarchMoney client is never invoked when a refusal fires, and that dry-run paths remain usable.tests/test_auth.py(17 tests) proves the MCP auth-mutation tools are hard-disabled regardless of the read-only flag, thatMONARCH_EMAILdoes not trigger auto-login, and that neitherclient.pynorapp.pyreintroduceos.getenvof credential vars orload_dotenv.tests/conftest.pyadds an autouse fixture that opts existing mutation-test suites out of read-only mode so they continue to exercise the underlying client wiring.Remaining risks
login_setup.pyterminal script still performs login; it is now the only path that writes to the keyring. That's by design but means operator hygiene around running that script matters.dry_run=Truepaths forbulk_categorize_transactionsandupload_account_balance_historyare intentionally kept open in read-only mode. They do not call the mutation SDK methods, but they DO call read endpoints (e.g. fetching balance history). Reviewers may want to confirm that's acceptable.🤖 Generated by Computer