Skip to content

Add auto_assign_monthly_targets tool - #13

Open
TnTBass wants to merge 1 commit into
pragprogrammer:mainfrom
TnTBass:feature/auto-assign-monthly-targets
Open

Add auto_assign_monthly_targets tool#13
TnTBass wants to merge 1 commit into
pragprogrammer:mainfrom
TnTBass:feature/auto-assign-monthly-targets

Conversation

@TnTBass

@TnTBass TnTBass commented Apr 28, 2026

Copy link
Copy Markdown

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 a goal_target set, 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_id and update_category_for_month, lives in src/server/categories.py).

Behavior

  • Iterates cache.get_categories(plan_id) and calls cache.update_category_for_month for each eligible category.
  • Skips: hidden groups, deleted groups, hidden categories, deleted categories, categories without 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 as analytics.py).
  • Returns JSON: {month, categories_assigned, total_budgeted, assignments: [...]}.

Test plan

  • test_assigns_categories_with_goal_targets — happy path; categories with no goal_target are 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 to YYYY-MM-01 before any API call.
  • Full suite: uv run pytest → 101 passed.

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 pragprogrammer left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/server/categories.py
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:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/server/categories.py
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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hoisted.

Comment thread src/server/categories.py
assignments.append({
"name": updated.name,
"group": group.name,
"budgeted": updated.budgeted / 1000,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@TnTBass TnTBass May 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already done since #15 has landed. Synced upstream and switched to milliunits_to_dollars() in this commit.

Comment thread src/server/categories.py
"budgeted": updated.budgeted / 1000,
})

total = sum(a["budgeted"] for a in assignments)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to accumulating in milliunits and converting once at the end via milliunits_to_dollars() (which landed with #15, already synced).

Comment thread src/server/categories.py

groups = await _shared.cache.get_categories(plan_id)
assignments = []
for group in groups:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

TnTBass added a commit to TnTBass/mcp-ynab that referenced this pull request May 23, 2026
- 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>
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