feat: modernize MCP server and align docs/tests - #3
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughAdds repository-local Beads issue tracking and Memory Bank docs; introduces per-instance config directory and UTF‑8 JSON persistence in the MCP YNAB server, explicit tool annotations, a new mutating categorize_transaction tool, dependency/test config updates, and refactored unit tests. ChangesBeads config & metadata
Repository wiring
Cursor / Memory Bank rules
Memory Bank documentation
Project docs & agents
Build / Test config
Server implementation (YNAB MCP server)
Tests
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as Client/API
participant Server as MCP YNAB Server
participant FS as Filesystem (config dir)
participant YNAB as YNAB SDK/API
Client->>Server: categorize_transaction(budget_id, tx_id, category_id, id_type)
Server->>FS: _resolve_config_dir(config_dir) -> read preferred/cache (UTF-8 JSON)
FS-->>Server: preferred_budget_id / cached_categories
Server->>YNAB: _get_client() -> fetch transactions
YNAB-->>Server: transactions list
Server->>Server: _find_transaction_by_id(...) -> modify transaction object
Server->>YNAB: update transaction via SDK/API
YNAB-->>Server: update confirmation
Server->>FS: persist preferred_budget_id / cache (UTF-8 JSON)
FS-->>Server: write ack
Server-->>Client: operation result (id/status)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/mcp_ynab/server.py (2)
602-605:⚠️ Potential issue | 🟠 MajorAvoid full transaction scans for default ID lookups.
Line 602 currently pulls a transaction list and then scans it in Python. For the common
idpath, this scales poorly and can noticeably slow tool calls on larger budgets.⚡ Suggested change
- response = transactions_api.get_transactions(budget_id, since_date=since_date) - target_transaction = _find_transaction_by_id( - response.data.transactions, transaction_id, id_type - ) + if id_type == "id": + try: + single = transactions_api.get_transaction_by_id(budget_id, transaction_id) + target_transaction = single.data.transaction + except Exception: + target_transaction = None + else: + response = transactions_api.get_transactions(budget_id, since_date=since_date) + target_transaction = _find_transaction_by_id( + response.data.transactions, transaction_id, id_type + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_ynab/server.py` around lines 602 - 605, The code currently calls transactions_api.get_transactions and then scans response.data.transactions with _find_transaction_by_id which is inefficient for the common id lookup; change the logic so when id_type == "id" you directly fetch the single transaction from the API (e.g., use a transactions_api method that returns a transaction by ID or a get_transaction/get_transaction_by_id-style call with budget_id and transaction_id) and set target_transaction from that response, and only fall back to transactions_api.get_transactions + _find_transaction_by_id for non-"id" id_type values; update references to transactions_api.get_transactions, _find_transaction_by_id, target_transaction, transaction_id, and id_type accordingly.
608-613:⚠️ Potential issue | 🟠 MajorPreserve transaction state when recategorizing.
The
ExistingTransactioncreated at line 609 omits fields likeapproved,memo,cleared, andflag_color. Since the YNAB API treats omitted fields as unset (using defaults like "unapproved" forapproved), this categorize-only operation unintentionally resets transaction state on each call.🛠️ Suggested fix
wrapper = PutTransactionWrapper( transaction=ExistingTransaction( account_id=target_transaction.account_id, + var_date=target_transaction.var_date, amount=target_transaction.amount, + payee_id=target_transaction.payee_id, + payee_name=target_transaction.payee_name, category_id=category_id, + memo=target_transaction.memo, + cleared=target_transaction.cleared, + approved=target_transaction.approved, + flag_color=target_transaction.flag_color, ) )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_ynab/server.py` around lines 608 - 613, When building the ExistingTransaction inside PutTransactionWrapper (created in PutTransactionWrapper and using ExistingTransaction with transaction=ExistingTransaction(...)), preserve the original transaction state by copying over fields from target_transaction instead of omitting them; include at minimum approved, memo, cleared, and flag_color (and any other stateful fields present on target_transaction such as payee_id/payee_name if applicable) so the categorize-only update does not reset those values—replace the current minimal ExistingTransaction construction with one that sets account_id, amount, category_id plus the preserved fields drawn from target_transaction.
🧹 Nitpick comments (8)
projectbrief.md (1)
1-17: Unfilled template — populate before merging.All sections remain placeholder text. If this is intended as living documentation for the project, filling it in at commit time prevents the repo accumulating stale scaffolding that nobody ever revisits.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@projectbrief.md` around lines 1 - 17, The Project Brief is a placeholder template and must be populated before merging: replace the header "# Project Brief: [Project Name]" with the real project name and fill the "## Overview", "## Core Requirements", "## Goals", and "## Project Scope" sections with concrete, actionable content (specific requirements, measurable goals, and clear in/out of scope boundaries) so this becomes living documentation rather than scaffolding; ensure each bullet under "## Core Requirements" and "## Goals" is specific and measurable and that the "## Project Scope" clearly lists included and excluded items.progress.md (1)
1-20: Unfilled template — same note asprojectbrief.md.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@progress.md` around lines 1 - 20, This file contains an unfilled template; replace placeholder headings and bracketed items under "Progress: [Project Name]", "## What Works", "## What's Left to Build", "## Known Issues and Limitations", and "## Evolution of Project Decisions" with real project-specific content: set the actual project name in the top heading, list implemented features under "What Works" (concrete feature names), enumerate remaining tasks under "What's Left to Build", detail reproducible known issues under "Known Issues and Limitations", and write a short narrative under "Evolution of Project Decisions" explaining major decisions and why they changed; remove the bracketed placeholders (e.g., "[Feature 1]") and ensure each section contains actionable, specific items..beads/.gitignore (1)
2-6: Minor redundancy:*.db?*already covers the explicit patterns below it.
*.db-wal,*.db-shm, and*.db-journalare all matched by*.db?*, so lines 5–6 and the journal entry are redundant. Leaving them is harmless and arguably self-documenting, but worth noting if you prefer minimal gitignore files.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.beads/.gitignore around lines 2 - 6, The .gitignore contains redundant patterns: the glob "*.db?*" already matches the explicit entries "*.db-journal", "*.db-wal" and "*.db-shm"; to clean this up either remove the explicit patterns ("*.db-journal", "*.db-wal", "*.db-shm") or delete the broader "*.db?*" and keep the explicit ones if you prefer self-documenting entries—update the file accordingly so only one of these matching strategies remains.techContext.md (1)
1-22: Unfilled template — same note asprojectbrief.md.All sections are placeholder text. Consider populating before merge.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@techContext.md` around lines 1 - 22, The techContext.md is a placeholder template — replace the stub entries under the headings "Technologies Used", "Development Setup", "Technical Constraints", "Dependencies", and "Tool Usage Patterns" with concrete project details: list the actual technologies and versions used, provide step‑by‑step development environment setup and any required environment variables, enumerate real technical constraints and rationale, list production and dev dependencies with versions and where they are declared, and describe how project tools (build, test, CI/CD, linters) are used in practice so the file is fully populated before merge.productContext.md (1)
1-13: Unfilled template — same note asprojectbrief.md.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@productContext.md` around lines 1 - 13, The Product Context file currently contains an unfilled template under the headings "## Problem Statement", "## User Experience Goals", and "## Success Metrics"; replace the placeholder lines with concrete content: write a concise Problem Statement describing the user problem this project solves, populate "## User Experience Goals" with 3–5 measurable UX goals (replace [UX Goal 1], etc.), and list 2–4 specific Success Metrics under "## Success Metrics" (replace [Metric 1], etc.); ensure the wording aligns with the corresponding entries in projectbrief.md and use the exact headings "## Problem Statement", "## User Experience Goals", and "## Success Metrics" so the document remains discoverable and consistent.systemPatterns.md (1)
1-20: Replace placeholder content or explicitly mark this as a template.Line 1 through Line 20 still contain unresolved placeholders. If this is meant to be a live memory-bank file, it currently provides no usable architecture context.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@systemPatterns.md` around lines 1 - 20, The document contains unresolved placeholders under headings like "System Architecture", "Key Technical Decisions", "Design Patterns in Use", "Component Relationships", and "Critical Implementation Paths"; either populate each section with concrete, project-specific information (a short architecture overview, 3–5 concrete technical decisions with rationale, listed design patterns in use, how major components interact, and the critical implementation paths) or explicitly mark the file as a template by replacing placeholder lines with a clear template header and TODO markers (e.g., "TEMPLATE: fill in project-specific architecture") so reviewers know it is not production content; ensure the "System Architecture" and "Critical Implementation Paths" sections at minimum contain actionable, non-placeholder descriptions.activeContext.md (1)
1-23: Populate this file with project-specific context (or rename as a template).Line 1 through Line 23 are placeholders, so this currently can’t serve as actionable active context for contributors/agents.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@activeContext.md` around lines 1 - 23, activeContext.md currently contains placeholder headings and cannot serve as actionable context; replace placeholders under the headings "Current Work Focus", "Recent Changes", "Next Steps", "Active Decisions and Considerations", "Important Patterns and Preferences", and "Learnings and Project Insights" with concrete, project-specific details (e.g., current ticket/feature IDs, short status, recent PRs/commits, imminent tasks, decision rationale and owners, coding/architecture patterns, and key learnings) or, if you intend it to remain a template, rename the file to indicate "template" and add a usage note at the top explaining how and when to populate it and who is responsible for updates so contributors can rely on it.pyproject.toml (1)
9-9: Add an upper bound forpydanticto future-proof against major-version changes.While Pydantic 3.0 has not yet been released, adding a constraint like
<3is a best practice that prevents unexpected breaking changes when the next major version becomes available. This is consistent with the principle of explicit version bounds for external dependencies.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pyproject.toml` at line 9, Update the pydantic dependency declaration in pyproject.toml to add an upper bound to prevent accidental upgrades to the next major version; locate the existing "pydantic>=2.10.0" entry and change it to include a <3 upper bound (e.g., "pydantic>=2.10.0,<3") so the project stays pinned to Pydantic 2.x until you've tested compatibility with 3.x.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.beads/README.md:
- Around line 63-65: Replace the unsafe single-line "curl | bash" install
instruction in .beads/README.md with a pinned, inspect-before-exec workflow:
reference the script URL at a specific commit (pin the raw file URL to a commit
hash), instruct users to download the script (e.g., save as install.sh), verify
its integrity with a provided checksum (SHA256) or GPG signature, inspect the
file before running, and only then run it with "bash install.sh"; update the
README snippet and include the checksum and the pinned URL so users can validate
and review the script prior to execution.
In @.cursor/rules/memory-bank.mdc:
- Around line 1-5: The rule file currently contains only YAML front matter with
alwaysApply: true and no rule body, which forces this empty rule into every AI
call; either add the intended rule content below the closing --- (providing the
rule text/conditions and actions) or change the front-matter key alwaysApply to
false until the body is implemented so the empty rule is not injected globally;
edit the .cursor/rules/memory-bank.mdc file and update the front matter or
append the rule body accordingly.
In @.gitattributes:
- Line 3: The .gitattributes entry declaring merge=beads for .beads/issues.jsonl
requires a corresponding git merge driver registration; add onboarding docs
(README or AGENTS.md) that instruct contributors to register the driver
(register merge.beads.driver and set its name) and show the exact git config
command to run locally, and mention the purpose (Beads JSONL merge driver) so
the .gitattributes merge=beads entry is effective.
In `@memory_bank_instructions.md`:
- Around line 10-20: The Mermaid diagrams (starting with the "flowchart TD"
blocks such as the one containing nodes PB, PC, SP, TC, AC, and P) must be
wrapped in fenced code blocks using ```mermaid before the diagram and ``` after
it so Markdown renderers will display diagrams correctly; locate each bare
"flowchart TD" block (and the similar blocks later in the file) and surround the
entire diagram text with the fenced mermaid code block markers.
---
Outside diff comments:
In `@src/mcp_ynab/server.py`:
- Around line 602-605: The code currently calls
transactions_api.get_transactions and then scans response.data.transactions with
_find_transaction_by_id which is inefficient for the common id lookup; change
the logic so when id_type == "id" you directly fetch the single transaction from
the API (e.g., use a transactions_api method that returns a transaction by ID or
a get_transaction/get_transaction_by_id-style call with budget_id and
transaction_id) and set target_transaction from that response, and only fall
back to transactions_api.get_transactions + _find_transaction_by_id for non-"id"
id_type values; update references to transactions_api.get_transactions,
_find_transaction_by_id, target_transaction, transaction_id, and id_type
accordingly.
- Around line 608-613: When building the ExistingTransaction inside
PutTransactionWrapper (created in PutTransactionWrapper and using
ExistingTransaction with transaction=ExistingTransaction(...)), preserve the
original transaction state by copying over fields from target_transaction
instead of omitting them; include at minimum approved, memo, cleared, and
flag_color (and any other stateful fields present on target_transaction such as
payee_id/payee_name if applicable) so the categorize-only update does not reset
those values—replace the current minimal ExistingTransaction construction with
one that sets account_id, amount, category_id plus the preserved fields drawn
from target_transaction.
---
Nitpick comments:
In @.beads/.gitignore:
- Around line 2-6: The .gitignore contains redundant patterns: the glob "*.db?*"
already matches the explicit entries "*.db-journal", "*.db-wal" and "*.db-shm";
to clean this up either remove the explicit patterns ("*.db-journal",
"*.db-wal", "*.db-shm") or delete the broader "*.db?*" and keep the explicit
ones if you prefer self-documenting entries—update the file accordingly so only
one of these matching strategies remains.
In `@activeContext.md`:
- Around line 1-23: activeContext.md currently contains placeholder headings and
cannot serve as actionable context; replace placeholders under the headings
"Current Work Focus", "Recent Changes", "Next Steps", "Active Decisions and
Considerations", "Important Patterns and Preferences", and "Learnings and
Project Insights" with concrete, project-specific details (e.g., current
ticket/feature IDs, short status, recent PRs/commits, imminent tasks, decision
rationale and owners, coding/architecture patterns, and key learnings) or, if
you intend it to remain a template, rename the file to indicate "template" and
add a usage note at the top explaining how and when to populate it and who is
responsible for updates so contributors can rely on it.
In `@productContext.md`:
- Around line 1-13: The Product Context file currently contains an unfilled
template under the headings "## Problem Statement", "## User Experience Goals",
and "## Success Metrics"; replace the placeholder lines with concrete content:
write a concise Problem Statement describing the user problem this project
solves, populate "## User Experience Goals" with 3–5 measurable UX goals
(replace [UX Goal 1], etc.), and list 2–4 specific Success Metrics under "##
Success Metrics" (replace [Metric 1], etc.); ensure the wording aligns with the
corresponding entries in projectbrief.md and use the exact headings "## Problem
Statement", "## User Experience Goals", and "## Success Metrics" so the document
remains discoverable and consistent.
In `@progress.md`:
- Around line 1-20: This file contains an unfilled template; replace placeholder
headings and bracketed items under "Progress: [Project Name]", "## What Works",
"## What's Left to Build", "## Known Issues and Limitations", and "## Evolution
of Project Decisions" with real project-specific content: set the actual project
name in the top heading, list implemented features under "What Works" (concrete
feature names), enumerate remaining tasks under "What's Left to Build", detail
reproducible known issues under "Known Issues and Limitations", and write a
short narrative under "Evolution of Project Decisions" explaining major
decisions and why they changed; remove the bracketed placeholders (e.g.,
"[Feature 1]") and ensure each section contains actionable, specific items.
In `@projectbrief.md`:
- Around line 1-17: The Project Brief is a placeholder template and must be
populated before merging: replace the header "# Project Brief: [Project Name]"
with the real project name and fill the "## Overview", "## Core Requirements",
"## Goals", and "## Project Scope" sections with concrete, actionable content
(specific requirements, measurable goals, and clear in/out of scope boundaries)
so this becomes living documentation rather than scaffolding; ensure each bullet
under "## Core Requirements" and "## Goals" is specific and measurable and that
the "## Project Scope" clearly lists included and excluded items.
In `@pyproject.toml`:
- Line 9: Update the pydantic dependency declaration in pyproject.toml to add an
upper bound to prevent accidental upgrades to the next major version; locate the
existing "pydantic>=2.10.0" entry and change it to include a <3 upper bound
(e.g., "pydantic>=2.10.0,<3") so the project stays pinned to Pydantic 2.x until
you've tested compatibility with 3.x.
In `@systemPatterns.md`:
- Around line 1-20: The document contains unresolved placeholders under headings
like "System Architecture", "Key Technical Decisions", "Design Patterns in Use",
"Component Relationships", and "Critical Implementation Paths"; either populate
each section with concrete, project-specific information (a short architecture
overview, 3–5 concrete technical decisions with rationale, listed design
patterns in use, how major components interact, and the critical implementation
paths) or explicitly mark the file as a template by replacing placeholder lines
with a clear template header and TODO markers (e.g., "TEMPLATE: fill in
project-specific architecture") so reviewers know it is not production content;
ensure the "System Architecture" and "Critical Implementation Paths" sections at
minimum contain actionable, non-placeholder descriptions.
In `@techContext.md`:
- Around line 1-22: The techContext.md is a placeholder template — replace the
stub entries under the headings "Technologies Used", "Development Setup",
"Technical Constraints", "Dependencies", and "Tool Usage Patterns" with concrete
project details: list the actual technologies and versions used, provide
step‑by‑step development environment setup and any required environment
variables, enumerate real technical constraints and rationale, list production
and dev dependencies with versions and where they are declared, and describe how
project tools (build, test, CI/CD, linters) are used in practice so the file is
fully populated before merge.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (23)
.beads/.gitignore.beads/.local_version.beads/README.md.beads/config.yaml.beads/issues.jsonl.beads/metadata.json.cursor/rules/memory-bank.mdc.cursor/rules/project-setup.mdc.gitattributesAGENTS.mdCLAUDE.mdCLAUDE.mdREADME.mdactiveContext.mdmemory_bank_instructions.mdproductContext.mdprogress.mdprojectbrief.mdpyproject.tomlsrc/mcp_ynab/server.pysystemPatterns.mdtechContext.mdtests/test_server.py
|
Addressed all review items in commit 43bfda2. Resolved items:
Verification run locally:
tests/test_environment.py .. [ 16%] ======================= 12 passed, 1 deselected in 0.19s ======================= -> 12 passed, 1 deselected |
|
Addressed all review items in commit Resolved items:
Verification run locally:
|
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
memory_bank_instructions.md (1)
81-87:⚠️ Potential issue | 🟡 MinorWrap the Act Mode Mermaid block in a fenced code block.
Line 82 starts a Mermaid flowchart without a
```mermaidfence, so it may render as plain text instead of a diagram.✏️ Proposed fix
### Act Mode +```mermaid flowchart TD Start[Start] --> Context[Check Memory Bank] Context --> Update[Update Documentation] Update --> Execute[Execute Task] Execute --> Document[Document Changes] +```🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@memory_bank_instructions.md` around lines 81 - 87, The "Act Mode" Mermaid flowchart block (the flowchart TD containing nodes Start, Context, Update, Execute, Document) is not wrapped in a fenced code block so it may render as plain text; wrap the entire Mermaid block with a starting ```mermaid fence before "flowchart TD" and a closing ``` fence after the last line to ensure it renders as a diagram.
🧹 Nitpick comments (1)
src/mcp_ynab/server.py (1)
703-711: Use explicit mutating annotations for idempotent write tools.At Line 703 and Line 710, these tools mutate local state but only set
idempotentHint=True. Consider explicitly settingreadOnlyHint=False(anddestructiveHint=False) for clearer MCP client behavior.♻️ Proposed refactor
READ_ONLY_TOOL = types.ToolAnnotations(readOnlyHint=True, idempotentHint=True) MUTATING_TOOL = types.ToolAnnotations(readOnlyHint=False, destructiveHint=True) +IDEMPOTENT_MUTATING_TOOL = types.ToolAnnotations( + readOnlyHint=False, idempotentHint=True, destructiveHint=False +) ... -@mcp.tool(annotations=types.ToolAnnotations(idempotentHint=True)) +@mcp.tool(annotations=IDEMPOTENT_MUTATING_TOOL) async def set_preferred_budget_id(budget_id: str) -> str: ... -@mcp.tool(annotations=types.ToolAnnotations(idempotentHint=True)) +@mcp.tool(annotations=IDEMPOTENT_MUTATING_TOOL) async def cache_categories(budget_id: str) -> str:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_ynab/server.py` around lines 703 - 711, The MCP tool decorators on set_preferred_budget_id and cache_categories currently only set idempotentHint=True but these functions mutate local state; update their annotations to explicitly mark readOnlyHint=False and destructiveHint=False in the types.ToolAnnotations passed to `@mcp.tool` (keep idempotentHint=True) so the MCP client understands these are idempotent writes rather than reads; modify the decorators on the functions named set_preferred_budget_id and cache_categories accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/mcp_ynab/server.py`:
- Around line 210-217: The _load_json_file function currently only catches
FileNotFoundError, so a malformed JSON will raise json.JSONDecodeError and break
startup; update _load_json_file to also catch json.JSONDecodeError, log a
warning via the module logger (or create one) including the filename and error
details, and return an empty dict {} as a safe fallback so corrupted cache files
do not crash resource loading.
- Around line 604-610: The broad except around
transactions_api.get_transaction_by_id is hiding real API failures; replace the
bare except by catching ynab.exceptions.ApiException (or NotFoundException) and
only treat 404 as "not found" by checking the exception's status (e.g., e.status
if using ApiException) before assigning target_transaction = None; re-raise the
exception for non-404 status codes so auth/network/server errors propagate.
Ensure the try/except references transactions_api.get_transaction_by_id and
target_transaction so the behavior is limited to the missing-transaction case.
---
Duplicate comments:
In `@memory_bank_instructions.md`:
- Around line 81-87: The "Act Mode" Mermaid flowchart block (the flowchart TD
containing nodes Start, Context, Update, Execute, Document) is not wrapped in a
fenced code block so it may render as plain text; wrap the entire Mermaid block
with a starting ```mermaid fence before "flowchart TD" and a closing ``` fence
after the last line to ensure it renders as a diagram.
---
Nitpick comments:
In `@src/mcp_ynab/server.py`:
- Around line 703-711: The MCP tool decorators on set_preferred_budget_id and
cache_categories currently only set idempotentHint=True but these functions
mutate local state; update their annotations to explicitly mark
readOnlyHint=False and destructiveHint=False in the types.ToolAnnotations passed
to `@mcp.tool` (keep idempotentHint=True) so the MCP client understands these are
idempotent writes rather than reads; modify the decorators on the functions
named set_preferred_budget_id and cache_categories accordingly.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (14)
.beads/.gitignore.beads/README.md.cursor/rules/memory-bank.mdcREADME.mdactiveContext.mdmemory_bank_instructions.mdproductContext.mdprogress.mdprojectbrief.mdpyproject.tomlsrc/mcp_ynab/server.pysystemPatterns.mdtechContext.mdtests/test_server.py
✅ Files skipped from review due to trivial changes (1)
- systemPatterns.md
🚧 Files skipped from review as they are similar to previous changes (6)
- .beads/.gitignore
- README.md
- pyproject.toml
- projectbrief.md
- .cursor/rules/memory-bank.mdc
- .beads/README.md
There was a problem hiding this comment.
♻️ Duplicate comments (2)
src/mcp_ynab/server.py (2)
613-622: 404-specific exception handling now correctly scoped.The previous broad
except Exceptionhas been replaced with a targetedexcept ApiException as excthat inspectsexc.statusand only swallows 404s, re-raising everything else. This aligns with the official YNAB SDK pattern offrom ynab.rest import ApiExceptionand correct status-code-based branching.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_ynab/server.py` around lines 613 - 622, Replace the broad exception catching with a targeted ApiException handler around the call to transactions_api.get_transaction_by_id so that you only swallow 404 responses: catch ApiException as exc, set target_transaction = None when exc.status == 404, and re-raise the exception for any other exc.status; ensure this change is applied in the try/except block that assigns target_transaction from single_response.data.transaction so other errors are not silently ignored.
213-228: UTF-8 encoding andJSONDecodeErrorfallback correctly applied.Both
_load_json_fileand_save_json_filenow explicitly specifyencoding="utf-8", and_load_json_filecatchesjson.JSONDecodeErrorwith a warning log and a safe{}fallback. Thetest_load_json_file_handles_invalid_jsontest in the test file validates this path end-to-end.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_ynab/server.py` around lines 213 - 228, _save_json_file currently writes directly and can fail silently on missing directories or write/serialization errors; update _save_json_file to ensure the target directory exists (Path(filename).parent.mkdir(parents=True, exist_ok=True)) and wrap the open/json.dump in a try/except that catches OSError and TypeError (or json.JSONEncodeError if available) and logs an error via logger.error with the filename and exception details, re-raising or returning as appropriate for your error semantics; reference the function name _save_json_file to locate the change and mirror the logging style used in _load_json_file.
🧹 Nitpick comments (5)
src/mcp_ynab/server.py (2)
233-237:MUTATING_TOOL(destructiveHint=True) may be too strong forcategorize_transaction.
destructiveHintandidempotentHintare only meaningful whenreadOnlyHintisfalse, anddestructiveHintflags operations that can irreversibly destroy or overwrite data. Re-categorizing a transaction is a reversible update: running it twice with the samecategory_idproduces no additional state change.IDEMPOTENT_MUTATING_TOOL(readOnlyHint=False, idempotentHint=True, destructiveHint=False) would be a more accurate annotation and would prevent MCP clients from showing unnecessary destructive-action confirmation prompts.♻️ Proposed fix
-@mcp.tool(annotations=MUTATING_TOOL) +@mcp.tool(annotations=IDEMPOTENT_MUTATING_TOOL) async def categorize_transaction(Also applies to: 581-581
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_ynab/server.py` around lines 233 - 237, The MUTATING_TOOL annotation is too strong for reversible actions like categorize_transaction; update usages so that categorize_transaction (and the other occurrence currently using MUTATING_TOOL) use IDEMPOTENT_MUTATING_TOOL instead (i.e., replace references to MUTATING_TOOL with IDEMPOTENT_MUTATING_TOOL for the categorize_transaction handler/function and the second location noted) so the tool is marked readOnlyHint=false, idempotentHint=true, destructiveHint=false.
44-54: Module-levelYNABResources()creates~/.config/mcp-ynabon every import.
_resolve_config_dircallsconfig_dir.mkdir(parents=True, exist_ok=True)unconditionally. Becauseynab_resources = YNABResources()is at module level (Line 294), importingmcp_ynab.serverin any context — including the test suite running in CI — creates the directory under$HOME/.config. Tests are properly isolated viatmp_path, but the side effect still pollutes the developer/CI home directory.Consider deferring directory creation to first write (e.g., inside
set_preferred_budget_id/cache_categories) rather than on init.Also applies to: 294-294
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/mcp_ynab/server.py` around lines 44 - 54, Module import currently creates ~/.config/mcp-ynab because _resolve_config_dir() always calls config_dir.mkdir(...) and YNABResources() is instantiated at module level as ynab_resources; change to defer directory creation: remove the unconditional mkdir from _resolve_config_dir (or stop calling _resolve_config_dir during module import) and instead ensure the directory is created lazily inside write-path methods such as YNABResources.set_preferred_budget_id and YNABResources.cache_categories (call mkdir(parents=True, exist_ok=True) just before writing files); also consider removing the module-level instantiation ynab_resources or replace it with a lazy getter to avoid side effects on import.tests/test_server.py (3)
253-260: Incomplete field preservation assertions —var_date,payee_id,payee_name, andsubtransactionsnot verified.The test name and PR description both emphasize that
categorize_transactionpreserves all existing transaction fields, but only six of the ten fields carried throughExistingTransactionare actually asserted. The payee and date fields (var_date,payee_id,payee_name) andsubtransactionsare set onDummyTransactionand passed throughExistingTransactionin server.py (lines 633–642) but never checked here.♻️ Proposed additional assertions
assert captured["flag_color"] == "blue" +assert captured["var_date"] == "2026-02-01" +assert captured["payee_id"] == "payee-1" +assert captured["payee_name"] == "Coffee Shop" +assert captured["subtransactions"] == [{"amount": -500, "category_id": "cat-old"}]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_server.py` around lines 253 - 260, The test currently asserts only six fields after calling server.categorize_transaction but omits var_date, payee_id, payee_name, and subtransactions which are set on DummyTransaction and passed through ExistingTransaction; update the test to also assert that captured["var_date"] matches the DummyTransaction var_date, captured["payee_id"] matches payee_id, captured["payee_name"] matches payee_name, and captured["subtransactions"] matches the DummyTransaction.subtransactions so all fields preserved by categorize_transaction/ExistingTransaction are verified.
216-216: Optional: same RUF012 mutable class attribute as above.♻️ Proposed fix
- subtransactions = [{"amount": -500, "category_id": "cat-old"}] + subtransactions: ClassVar[list] = [{"amount": -500, "category_id": "cat-old"}]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_server.py` at line 216, The test defines a mutable class/module-level list named subtransactions which can be mutated across tests (RUF012); change it to be created fresh where used (e.g., move the [{"amount": -500, "category_id": "cat-old"}] literal into the test function or into a helper function like make_subtransactions()) so each test gets a new list instance instead of sharing the module-level subtransactions variable.
126-126: Optional: annotate mutable class attribute to silence RUF012.
subtransactions = []on a class body is flagged by Ruff (RUF012). While there's no mutation risk here becauseDummyTransactionis only ever instantiated once per test, the fix is a one-liner:♻️ Proposed fix
+from typing import ClassVar ... class DummyTransaction: id = "tx-1" ... - subtransactions = [] + subtransactions: ClassVar[list] = []🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/test_server.py` at line 126, Annotate the mutable class attribute to silence RUF012: change the class-level declaration subtransactions = [] inside DummyTransaction to be explicitly typed as a ClassVar, e.g. subtransactions: ClassVar[list] = [], and add the necessary import for ClassVar from typing if missing; this keeps the attribute as a shared class container but satisfies the linter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/mcp_ynab/server.py`:
- Around line 613-622: Replace the broad exception catching with a targeted
ApiException handler around the call to transactions_api.get_transaction_by_id
so that you only swallow 404 responses: catch ApiException as exc, set
target_transaction = None when exc.status == 404, and re-raise the exception for
any other exc.status; ensure this change is applied in the try/except block that
assigns target_transaction from single_response.data.transaction so other errors
are not silently ignored.
- Around line 213-228: _save_json_file currently writes directly and can fail
silently on missing directories or write/serialization errors; update
_save_json_file to ensure the target directory exists
(Path(filename).parent.mkdir(parents=True, exist_ok=True)) and wrap the
open/json.dump in a try/except that catches OSError and TypeError (or
json.JSONEncodeError if available) and logs an error via logger.error with the
filename and exception details, re-raising or returning as appropriate for your
error semantics; reference the function name _save_json_file to locate the
change and mirror the logging style used in _load_json_file.
---
Nitpick comments:
In `@src/mcp_ynab/server.py`:
- Around line 233-237: The MUTATING_TOOL annotation is too strong for reversible
actions like categorize_transaction; update usages so that
categorize_transaction (and the other occurrence currently using MUTATING_TOOL)
use IDEMPOTENT_MUTATING_TOOL instead (i.e., replace references to MUTATING_TOOL
with IDEMPOTENT_MUTATING_TOOL for the categorize_transaction handler/function
and the second location noted) so the tool is marked readOnlyHint=false,
idempotentHint=true, destructiveHint=false.
- Around line 44-54: Module import currently creates ~/.config/mcp-ynab because
_resolve_config_dir() always calls config_dir.mkdir(...) and YNABResources() is
instantiated at module level as ynab_resources; change to defer directory
creation: remove the unconditional mkdir from _resolve_config_dir (or stop
calling _resolve_config_dir during module import) and instead ensure the
directory is created lazily inside write-path methods such as
YNABResources.set_preferred_budget_id and YNABResources.cache_categories (call
mkdir(parents=True, exist_ok=True) just before writing files); also consider
removing the module-level instantiation ynab_resources or replace it with a lazy
getter to avoid side effects on import.
In `@tests/test_server.py`:
- Around line 253-260: The test currently asserts only six fields after calling
server.categorize_transaction but omits var_date, payee_id, payee_name, and
subtransactions which are set on DummyTransaction and passed through
ExistingTransaction; update the test to also assert that captured["var_date"]
matches the DummyTransaction var_date, captured["payee_id"] matches payee_id,
captured["payee_name"] matches payee_name, and captured["subtransactions"]
matches the DummyTransaction.subtransactions so all fields preserved by
categorize_transaction/ExistingTransaction are verified.
- Line 216: The test defines a mutable class/module-level list named
subtransactions which can be mutated across tests (RUF012); change it to be
created fresh where used (e.g., move the [{"amount": -500, "category_id":
"cat-old"}] literal into the test function or into a helper function like
make_subtransactions()) so each test gets a new list instance instead of sharing
the module-level subtransactions variable.
- Line 126: Annotate the mutable class attribute to silence RUF012: change the
class-level declaration subtransactions = [] inside DummyTransaction to be
explicitly typed as a ClassVar, e.g. subtransactions: ClassVar[list] = [], and
add the necessary import for ClassVar from typing if missing; this keeps the
attribute as a shared class container but satisfies the linter.
Auto-committed by bd doctor --fix
- mcp-ynab-00h: raise docstring coverage to 80% in server.py - mcp-ynab-3w6: remove duplicate Session Completion section in AGENTS.md
- Add module docstrings to mcp_ynab/__init__.py, __main__.py, server.py - Add docstrings to AsyncYNABClient methods and YNABResources class+__init__ - Wire interrogate into pyproject.toml with fail-under=80 and add it to the dev dependency group - Add 'task docstrings' command Coverage rose from 81.8% to 100% (was 43.64% when CodeRabbit flagged it on PR #3). Closes mcp-ynab-00h.
Summary
Modernize the MCP YNAB server to align with newer MCP SDK/spec expectations, fix tool registration and runtime configuration bugs, refresh tests, and update repository conventions/docs.
Why
categorize_transactionwas not exposed as an MCP tool while a private helper was.Changes
MCP server and protocol alignment
categorize_transactionas an MCP tool._find_transaction_by_idhelper.YNAB_API_KEYlookup to runtime in_get_client.YNABResources(config_dir=...)for deterministic config/test behavior.Dependencies and modernization
mcp[cli]>=1.20.0,<2.0.0.pydanticfloor to>=2.10.0.httpx,xdg, andblackdependency entries.uv.lock(now resolvesmcp 1.26.0locally).Tests and docs
tests/test_server.pywith behavior-focused tests.Repository conventions
CLAUDE.md->AGENTS.md.Scope
This PR intentionally combines MCP upgrade work plus repository convention files already present in the working tree, per request to incorporate all current files in a new branch.
Breaking changes
httpx,xdg,black) from project config.Rollout plan
uv sync.$XDG_CONFIG_HOMEor~/.config.Testing
Run locally:
Observed on this branch:
ruff check: passpytest -q:10 passed, 1 deselectedIssues
Repository issues are disabled for
klauern/mcp-ynab, so noFixes #...links are available.Screenshots / Logs
Summary by CodeRabbit
New Features
Documentation
Chores
Tests