Skip to main content
Glama
dgalarza

YNAB MCP Server

by dgalarza

compare_spending_by_year

Analyze category spending trends across multiple years to identify patterns and track budget performance over time with visual comparisons.

Instructions

Compare spending for a category across multiple years.

Args:
    budget_id: The ID of the budget (use 'last-used' for default budget)
    category_id: The category ID to analyze
    start_year: Starting year (e.g., 2020)
    num_years: Number of years to compare (default: 5)
    include_graph: Include terminal graph visualization (default: True)

Returns:
    JSON string with year-over-year comparison including totals, changes, percentage changes, and optional graph

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
budget_idYes
category_idYes
include_graphNo
num_yearsNo
start_yearYes

Implementation Reference

  • MCP tool registration for compare_spending_by_year. Thin wrapper that gets YNABClient singleton and calls its method, returning JSON.
    @mcp.tool()
    async def compare_spending_by_year(
        budget_id: str,
        category_id: str,
        start_year: int,
        num_years: int = 5,
        include_graph: bool = True,
    ) -> str:
        """Compare spending for a category across multiple years.
    
        Args:
            budget_id: The ID of the budget (use 'last-used' for default budget)
            category_id: The category ID to analyze
            start_year: Starting year (e.g., 2020)
            num_years: Number of years to compare (default: 5)
            include_graph: Include terminal graph visualization (default: True)
    
        Returns:
            JSON string with year-over-year comparison including totals, changes, percentage changes, and optional graph
        """
        client = get_ynab_client()
        result = await client.compare_spending_by_year(
            budget_id, category_id, start_year, num_years, include_graph
        )
        return json.dumps(result, indent=2)
  • Core handler implementation in YNABClient. Fetches transactions via YNAB API since start_year, filters by category_id and date range, aggregates yearly spending totals, computes year-over-year absolute and percentage changes, calculates average, and optionally generates a terminal graph using _generate_graph.
    async def compare_spending_by_year(
        self,
        budget_id: str,
        category_id: str,
        start_year: int,
        num_years: int = 5,
        include_graph: bool = True,
    ) -> dict[str, Any]:
        """Compare spending for a category across multiple years.
    
        Args:
            budget_id: The budget ID or 'last-used'
            category_id: The category ID to analyze
            start_year: Starting year (e.g., 2020)
            num_years: Number of years to compare (default: 5)
            include_graph: Include terminal graph visualization (default: True)
    
        Returns:
            Year-over-year comparison with totals and percentage changes
        """
        try:
            # Get all transactions since the start year
            since_date = f"{start_year}-01-01"
            end_year = start_year + num_years - 1
            until_date = f"{end_year}-12-31"
    
            url = f"{self.api_base_url}/budgets/{budget_id}/transactions"
            params = {"since_date": since_date}
    
            result = await self._make_request_with_retry("get", url, params=params)
    
            txn_data = result["data"]["transactions"]
    
            # Aggregate by year
            yearly_totals = {}
            for year in range(start_year, end_year + 1):
                yearly_totals[str(year)] = 0
    
            for txn in txn_data:
                # Filter by category and date range
                if txn.get("category_id") != category_id:
                    continue
                if txn["date"] > until_date:
                    continue
    
                year = txn["date"][:4]
                if year in yearly_totals:
                    amount = txn["amount"] / 1000 if txn.get("amount") else 0
                    yearly_totals[year] += amount
    
            # Calculate year-over-year changes
            comparisons = []
            years_sorted = sorted(yearly_totals.keys())
    
            for i, year in enumerate(years_sorted):
                year_data = {
                    "year": year,
                    "total_spent": yearly_totals[year],
                }
    
                if i > 0:
                    prev_year = years_sorted[i - 1]
                    prev_total = yearly_totals[prev_year]
                    change = yearly_totals[year] - prev_total
    
                    if prev_total != 0:
                        percent_change = (change / abs(prev_total)) * 100
                    else:
                        percent_change = 0 if change == 0 else float("inf")
    
                    year_data["change_from_previous"] = change
                    year_data["percent_change"] = percent_change
    
                comparisons.append(year_data)
    
            # Calculate overall statistics
            totals = [yearly_totals[year] for year in years_sorted]
            average_per_year = sum(totals) / len(totals) if totals else 0
    
            result_data = {
                "category_id": category_id,
                "years": f"{start_year}-{end_year}",
                "average_per_year": average_per_year,
                "yearly_comparison": comparisons,
            }
    
            # Add graph if requested
            if include_graph and yearly_totals:
                graph_data = [(year, yearly_totals[year]) for year in years_sorted]
                result_data["graph"] = self._generate_graph(
                    graph_data, f"Year-over-Year Comparison: {start_year}-{end_year}"
                )
    
            return result_data
        except Exception as e:
            raise Exception(f"Failed to compare spending by year: {e}") from e
  • Helper method to generate ASCII terminal bar graph using termgraph library. Captures stdout to return graph as string. Used in compare_spending_by_year for visual year-over-year comparison.
    def _generate_graph(self, data: list[tuple], title: str = "") -> str:
        """Generate a terminal graph using termgraph.
    
        Args:
            data: List of (label, value) tuples
            title: Graph title
    
        Returns:
            String containing the terminal graph
        """
        if not data:
            return ""
    
        # Capture termgraph output
        old_stdout = sys.stdout
        sys.stdout = StringIO()
    
        try:
            # Prepare data for termgraph
            labels = [label for label, _ in data]
            values = [[abs(value)] for _, value in data]
    
            # Configure termgraph
            args = {
                "stacked": False,
                "width": 50,
                "format": "{:.2f}",
                "suffix": "",
                "no_labels": False,
                "color": None,
                "vertical": False,
                "different_scale": False,
                "calendar": False,
                "start_dt": None,
                "custom_tick": "",
                "delim": "",
                "verbose": False,
                "label_before": False,
                "histogram": False,
                "no_values": False,
            }
    
            # Print title
            if title:
                print(f"\n{title}")
                print("=" * len(title))
    
            # Generate graph
            tg.chart(colors=[], data=values, args=args, labels=labels)
    
            # Get the output
            output = sys.stdout.getvalue()
            return output
    
        finally:
            sys.stdout = old_stdout

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dgalarza/ynab-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server