Skip to content

Default to read-only; disable credential/auth mutations#61

Draft
sakhalsa wants to merge 3 commits into
robcerda:mainfrom
sakhalsa:readonly-hardening
Draft

Default to read-only; disable credential/auth mutations#61
sakhalsa wants to merge 3 commits into
robcerda:mainfrom
sakhalsa:readonly-hardening

Conversation

@sakhalsa

Copy link
Copy Markdown

Summary

Hardens the MCP server so that, by default, it cannot mutate remote Monarch state or change locally stored auth.

  • Read-only default. New MONARCH_MCP_READ_ONLY env 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, and upload_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.
  • Auth mutation tools hard-disabled. monarch_login, monarch_login_with_token, and monarch_logout no 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 via python login_setup.py.
  • No env credential auto-login. Removed the MONARCH_EMAIL / MONARCH_PASSWORD fallback from get_monarch_client, removed load_dotenv() from app.py and login_setup.py, and dropped python-dotenv from the dependency list. Passwords are never silently read from the environment.
  • Read-only tools unaffected. Account, transaction, summary, financial, category-listing, and tag-listing read tools continue to function as before.

Testing

  • python -m pytest181 passed.
  • New 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.
  • Updated tests/test_auth.py (17 tests) proves the MCP auth-mutation tools are hard-disabled regardless of the read-only flag, that MONARCH_EMAIL does not trigger auto-login, and that neither client.py nor app.py reintroduce os.getenv of credential vars or load_dotenv.
  • tests/conftest.py adds 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

  • The standalone login_setup.py terminal 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=True paths for bulk_categorize_transactions and upload_account_balance_history are 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.
  • README still documents the previous elicitation login flow; this PR does not rewrite the README. A follow-up doc pass is recommended.

🤖 Generated by Computer

Claude Agent added 3 commits May 20, 2026 03:22
- 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).
@robcerda

Copy link
Copy Markdown
Owner

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:

  1. Block list is already stale. PRs Add merchant management and recurring stream review tools #56 and Add category update, details, and monthly cashflow tools #62 just landed and added update_merchant, review_recurring_stream, and update_category. None are in the read only list. Structural fix: a @mutating decorator that auto registers, so a new mutating tool cannot be added without being guarded.

  2. cookie_auth.py is duplicated with Add cookie-based auth for Monarch's May 2026 API change #59. Whichever lands second needs rebase. Suggest landing Add cookie-based auth for Monarch's May 2026 API change #59 first in isolation, then layering hardening on top here.

  3. Cookie file TOCTOU. Same flaw as Add cookie-based auth for Monarch's May 2026 API change #59 (write_text then chmod 600). Fix with os.open(path, O_CREAT|O_EXCL|O_WRONLY, 0o600).

  4. MONARCH_PASSWORD not surfaced in check_auth_status. Only MONARCH_EMAIL is reported as unused. A user could leave PASSWORD in their MCP config plaintext indefinitely with no warning.

  5. PR scope. Three concerns bundled (read only default, auth tools hard disabled, env credential and dotenv removal). Each is independently shippable and bisectable, worth splitting.

  6. refresh_accounts argument bug landed separately in Fix refresh_accounts crash and gql<4.0 dep conflict #63. No action needed here, just flagging the order dependency.

The shape of the guard is right.

thewoolleyman added a commit to thewoolleyman/monarch-mcp-server that referenced this pull request Jun 24, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants