Add auto_assign_monthly_targets tool - #13
Conversation
Mirrors YNAB's Auto-Assign -> Monthly Targets button as a single MCP tool call. Iterates every category that has a goal_target set and assigns the goal amount as the budgeted value for the given month. Skips hidden, deleted, and internal groups (Internal Master Category, Credit Card Payments, Hidden Categories). Resolves the 'current' month alias to the first day of the current month before calling the YNAB API. Returns a JSON summary with the resolved month, count of categories assigned, total budgeted dollars, and per-category assignment details. Re-submission of pragprogrammer#10 against the new server-package layout (uses plan_id and update_category_for_month).
pragprogrammer
left a comment
There was a problem hiding this comment.
Thanks for the resubmission against the new server-package layout! Bunch of inline thoughts below, mostly around behavioral parity with YNAB's UI and a couple cleanups that'll get easier after #15 lands.
| if group.name in AUTO_ASSIGN_SKIP_GROUPS or group.hidden or group.deleted: | ||
| continue | ||
| for cat in group.categories: | ||
| if cat.hidden or cat.deleted or not cat.goal_target: |
There was a problem hiding this comment.
Big-picture question before we land this: YNAB's actual "Auto-Assign Monthly Targets" button only fires on monthly-cadence goals (MF, or NEED with goal_cadence == 1). This filter is broader, so a Target Balance goal like "Emergency Fund: $10,000" would get the full $10k dropped into a single month's budget. That's probably going to surprise users who expect the tool to match the UI behavior.
Could we add a goal_type / cadence check here to keep parity? Something like goal_type == "MF" or (goal_type == "NEED" and goal_cadence == 1). Open to your read on it though, was the broader scope intentional?
There was a problem hiding this comment.
Good catch. The broader scope was not intentional. Added the goal_type/cadence filter to match YNAB's UI: goal_type == "MF" or (goal_type == "NEED" and goal_cadence == 1). TB and other non-monthly goals are now skipped.
| plan_id: The plan ID (use list_plans to find available IDs) | ||
| month: Month in YYYY-MM-DD format (e.g. '2026-05-01') or 'current'. Defaults to current month. | ||
| """ | ||
| import json |
There was a problem hiding this comment.
Tiny nit: can we hoist this up to the module-level imports next to from datetime import date? Function-local imports always make me do a double-take.
| assignments.append({ | ||
| "name": updated.name, | ||
| "group": group.name, | ||
| "budgeted": updated.budgeted / 1000, |
There was a problem hiding this comment.
Heads up, PR #15 (which I'm hoping to land soon) introduces a milliunits_to_dollars() helper in src/models/common.py that's meant to be the one place we do this conversion. After whichever of us merges second, this hand-rolled / 1000 should swap over to the helper. Nothing to do right now, just flagging the rebase fixup.
There was a problem hiding this comment.
Already done since #15 has landed. Synced upstream and switched to milliunits_to_dollars() in this commit.
| "budgeted": updated.budgeted / 1000, | ||
| }) | ||
|
|
||
| total = sum(a["budgeted"] for a in assignments) |
There was a problem hiding this comment.
Two thoughts here: summing dollar floats then rounding can drift on bigger plans, and once #15 lands we'll have the milliunits_to_dollars() helper. Cleaner pattern is to keep a running int total in milliunits and convert once at the end, e.g.:
total_mu += updated.budgeted
...
"total_budgeted": milliunits_to_dollars(total_mu),Same fix as what I just did to analytics.py in #15 if you want a reference.
There was a problem hiding this comment.
Switched to accumulating in milliunits and converting once at the end via milliunits_to_dollars() (which landed with #15, already synced).
|
|
||
| groups = await _shared.cache.get_categories(plan_id) | ||
| assignments = [] | ||
| for group in groups: |
There was a problem hiding this comment.
Design thought, not a blocker: if update_category_for_month raises partway through, we end up with a partial budget assignment on YNAB's side and one error response back to the agent, with no easy way to know what got applied. No clean atomic option without a YNAB batch endpoint, but should we wrap each call in a try/except and return a per-category success/failure list in the response? Would make recovery a lot kinder on the agent driving this. Curious what you think.
There was a problem hiding this comment.
Good point. Wrapped each update_category_for_month call in try/except and added a status field per assignment ("ok" or "error"). categories_assigned now counts only successes, and the agent can see exactly which categories failed and why.
- Add goal_type/cadence filter (MF or NEED+monthly cadence) to match YNAB's Auto-Assign Monthly Targets UI behavior - Hoist import json to module level - Accumulate total in milliunits, convert once via milliunits_to_dollars() - Wrap per-category updates in try/except; return status per assignment - Update tests to set goal_type on auto-assign fixtures; add TB goal to verify non-monthly goals are skipped Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Adds a new MCP tool
auto_assign_monthly_targets(plan_id, month="current")that mirrors YNAB's Auto-Assign → Monthly Targets button as a single tool call. For every category with agoal_targetset, it assigns the goal amount as the budgeted value for the given month.This is a re-submission of #10 against the new server-package layout (uses
plan_idandupdate_category_for_month, lives insrc/server/categories.py).Behavior
cache.get_categories(plan_id)and callscache.update_category_for_monthfor each eligible category.goal_target, and the three internal groups:Internal Master Category,Credit Card Payments,Hidden Categories.month="current"resolves to the first day of the current month (same pattern asanalytics.py).{month, categories_assigned, total_budgeted, assignments: [...]}.Test plan
test_assigns_categories_with_goal_targets— happy path; categories with nogoal_targetare skipped.test_skips_internal_groups_and_hidden_categories— Internal Master / Credit Card Payments groups skipped, hidden + deleted categories skipped.test_resolves_current_to_first_of_month—"current"is normalized toYYYY-MM-01before any API call.uv run pytest→ 101 passed.