Skip to main content
Glama
dgalarza

YNAB MCP Server

by dgalarza

prepare_split_for_matching

Create an unapproved split transaction to match with an imported bank transaction in YNAB. This prepares the split for manual matching in the YNAB interface, allowing you to categorize different portions of a single transaction.

Instructions

Prepare a split transaction to match with an existing imported transaction.

This tool fetches an existing transaction's details and creates a new UNAPPROVED split
transaction with the same date, amount, account, and payee. You can then manually match
them together in the YNAB web or mobile UI.

Use this when you want to split an imported bank transaction - the new split will be
created as unapproved so you can match it with the original in YNAB's UI.

Args:
    budget_id: The ID of the budget (use 'last-used' for default budget)
    transaction_id: The ID of the existing transaction to split
    subtransactions: JSON string containing array of subtransactions. Each subtransaction should have:
        - amount (required): The subtransaction amount
        - category_id (optional): Category ID for this split
        - payee_id (optional): Payee ID for this split
        - memo (optional): Memo for this split
        Example: '[{"amount": -50.00, "category_id": "cat1", "memo": "Groceries"}, {"amount": -30.00, "category_id": "cat2", "memo": "Gas"}]'

Returns:
    JSON string with original transaction details, new split transaction details, and instructions

Workflow:
    1. This tool fetches the existing transaction details
    2. Creates a new unapproved split transaction with those details
    3. You manually match them in the YNAB UI
    4. YNAB merges them into one split transaction

Note:
    - The new split is created as UNAPPROVED for manual matching
    - The sum of subtransaction amounts should equal the original transaction amount
    - After matching in YNAB UI, the original transaction will become a split transaction

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
budget_idYes
subtransactionsYes
transaction_idYes

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • Core implementation of prepare_split_for_matching: fetches the original transaction, creates a duplicate unapproved split transaction with provided subtransactions for manual matching in YNAB UI, returns details of both with instructions.
    async def prepare_split_for_matching(
        self,
        budget_id: str,
        transaction_id: str,
        subtransactions: list[dict[str, Any]],
    ) -> dict[str, Any]:
        """Prepare a split transaction to match with an existing imported transaction.
    
        This fetches an existing transaction's details and creates a new unapproved split
        transaction with the same date, amount, account, and payee. You can then manually
        match them in the YNAB UI.
    
        Args:
            budget_id: The budget ID or 'last-used'
            transaction_id: The ID of the existing transaction to base the split on
            subtransactions: List of subtransaction dictionaries, each containing:
                - amount (float, required): Subtransaction amount
                - category_id (str, optional): Category ID for this subtransaction
                - payee_id (str, optional): Payee ID for this subtransaction
                - memo (str, optional): Memo for this subtransaction
    
        Returns:
            Dictionary with original transaction details and newly created split transaction
    
        Note:
            - The new split transaction is created as unapproved
            - You must manually match them in the YNAB UI
            - The sum of subtransaction amounts should equal the original transaction amount
        """
        try:
            # Fetch the original transaction details
            original = await self.get_transaction(budget_id, transaction_id)
    
            # Create a new split transaction with the same details but unapproved
            new_split = await self.create_split_transaction(
                budget_id=budget_id,
                account_id=original["account_id"],
                date=original["date"],
                amount=original["amount"],
                subtransactions=subtransactions,
                payee_name=original.get("payee_name"),
                memo=original.get("memo"),
                cleared=original.get("cleared", "uncleared"),
                approved=False,  # Always create as unapproved for manual matching
            )
    
            return {
                "original_transaction": {
                    "id": original["id"],
                    "date": original["date"],
                    "amount": original["amount"],
                    "payee_name": original.get("payee_name"),
                    "account_name": original.get("account_name"),
                },
                "new_split_transaction": new_split,
                "instructions": (
                    "A new unapproved split transaction has been created. "
                    "Go to YNAB and manually match these two transactions together. "
                    "Look for the match indicator in the YNAB UI."
                ),
            }
        except Exception as e:
            raise Exception(f"Failed to prepare split for matching: {e}") from e
  • MCP registration and wrapper handler: @mcp.tool() decorator registers the tool, function handles JSON input/output serialization and delegates to YNABClient method.
    @mcp.tool()
    async def prepare_split_for_matching(
        budget_id: str,
        transaction_id: str,
        subtransactions: str,
    ) -> str:
        """Prepare a split transaction to match with an existing imported transaction.
    
        This tool fetches an existing transaction's details and creates a new UNAPPROVED split
        transaction with the same date, amount, account, and payee. You can then manually match
        them together in the YNAB web or mobile UI.
    
        Use this when you want to split an imported bank transaction - the new split will be
        created as unapproved so you can match it with the original in YNAB's UI.
    
        Args:
            budget_id: The ID of the budget (use 'last-used' for default budget)
            transaction_id: The ID of the existing transaction to split
            subtransactions: JSON string containing array of subtransactions. Each subtransaction should have:
                - amount (required): The subtransaction amount
                - category_id (optional): Category ID for this split
                - payee_id (optional): Payee ID for this split
                - memo (optional): Memo for this split
                Example: '[{"amount": -50.00, "category_id": "cat1", "memo": "Groceries"}, {"amount": -30.00, "category_id": "cat2", "memo": "Gas"}]'
    
        Returns:
            JSON string with original transaction details, new split transaction details, and instructions
    
        Workflow:
            1. This tool fetches the existing transaction details
            2. Creates a new unapproved split transaction with those details
            3. You manually match them in the YNAB UI
            4. YNAB merges them into one split transaction
    
        Note:
            - The new split is created as UNAPPROVED for manual matching
            - The sum of subtransaction amounts should equal the original transaction amount
            - After matching in YNAB UI, the original transaction will become a split transaction
        """
        client = get_ynab_client()
    
        # Parse subtransactions JSON string
        try:
            subtransactions_list = json.loads(subtransactions)
        except json.JSONDecodeError as e:
            raise YNABValidationError(f"Invalid subtransactions JSON: {e}") from e
    
        result = await client.prepare_split_for_matching(
            budget_id, transaction_id, subtransactions_list
        )
        return json.dumps(result, indent=2)
  • Supporting helper: create_split_transaction - used by prepare_split_for_matching to create the new split txn.
    async def create_split_transaction(
        self,
        budget_id: str,
        account_id: str,
        date: str,
        amount: float,
        subtransactions: list[dict[str, Any]],
        payee_name: str | None = None,
        memo: str | None = None,
        cleared: str = "uncleared",
        approved: bool = False,
    ) -> dict[str, Any]:
        """Create a new split transaction with subtransactions.
    
        Args:
            budget_id: The budget ID or 'last-used'
            account_id: The account ID for this transaction
            date: Transaction date in YYYY-MM-DD format
            amount: Total transaction amount (positive for inflow, negative for outflow)
            subtransactions: List of subtransaction dictionaries, each containing:
                - amount (float, required): Subtransaction amount
                - category_id (str, optional): Category ID for this subtransaction
                - payee_id (str, optional): Payee ID for this subtransaction
                - memo (str, optional): Memo for this subtransaction
            payee_name: Name of the payee for the main transaction (optional)
            memo: Transaction memo (optional)
            cleared: Cleared status - 'cleared', 'uncleared', or 'reconciled' (default: 'uncleared')
            approved: Whether the transaction is approved (default: False)
    
        Returns:
            JSON string with the created split transaction
    
        Note:
            - The sum of subtransaction amounts should equal the total transaction amount
            - category_id on the main transaction will be set to null automatically for split transactions
        """
        try:
            url = f"{self.api_base_url}/budgets/{budget_id}/transactions"
    
            # Format subtransactions
            formatted_subtransactions = []
            for sub in subtransactions:
                sub_data = {
                    "amount": int(sub["amount"] * MILLIUNITS_FACTOR),
                }
                if sub.get("category_id"):
                    sub_data["category_id"] = sub["category_id"]
                if sub.get("payee_id"):
                    sub_data["payee_id"] = sub["payee_id"]
                if sub.get("memo"):
                    sub_data["memo"] = sub["memo"]
                formatted_subtransactions.append(sub_data)
    
            # Build transaction data with subtransactions
            transaction_data = {
                "account_id": account_id,
                "date": date,
                "amount": int(amount * MILLIUNITS_FACTOR),
                "category_id": None,  # Must be null for split transactions
                "subtransactions": formatted_subtransactions,
                "cleared": cleared,
                "approved": approved,
            }
    
            if payee_name is not None:
                transaction_data["payee_name"] = payee_name
            if memo is not None:
                transaction_data["memo"] = memo
    
            data = {"transaction": transaction_data}
    
            result = await self._make_request_with_retry("post", url, json=data)
    
            txn = result["data"]["transaction"]
    
            # Format subtransactions in response
            subtransactions_response = []
            if txn.get("subtransactions"):
                for sub in txn["subtransactions"]:
                    subtransactions_response.append(
                        {
                            "id": sub.get("id"),
                            "amount": sub["amount"] / MILLIUNITS_FACTOR if sub.get("amount") else 0,
                            "memo": sub.get("memo"),
                            "payee_id": sub.get("payee_id"),
                            "payee_name": sub.get("payee_name"),
                            "category_id": sub.get("category_id"),
                            "category_name": sub.get("category_name"),
                        }
                    )
    
            return {
                "id": txn["id"],
                "date": txn["date"],
                "amount": txn["amount"] / MILLIUNITS_FACTOR if txn.get("amount") else 0,
                "memo": txn.get("memo"),
                "cleared": txn.get("cleared"),
                "approved": txn.get("approved"),
                "account_id": txn.get("account_id"),
                "account_name": txn.get("account_name"),
                "payee_id": txn.get("payee_id"),
                "payee_name": txn.get("payee_name"),
                "category_id": txn.get("category_id"),
                "category_name": txn.get("category_name"),
                "subtransactions": subtransactions_response,
            }
        except Exception as e:
            raise Exception(f"Failed to create split transaction: {e}") from e
  • Supporting helper: get_transaction - used by prepare_split_for_matching to fetch original transaction details.
    async def get_transaction(
        self,
        budget_id: str,
        transaction_id: str,
    ) -> dict[str, Any]:
        """Get a single transaction with all details including subtransactions.
    
        Args:
            budget_id: The budget ID or 'last-used'
            transaction_id: The transaction ID to retrieve
    
        Returns:
            Transaction dictionary with full details
        """
        try:
            url = f"{self.api_base_url}/budgets/{budget_id}/transactions/{transaction_id}"
            result = await self._make_request_with_retry("get", url)
    
            txn = result["data"]["transaction"]
    
            # Format subtransactions if present
            subtransactions = []
            if txn.get("subtransactions"):
                for sub in txn["subtransactions"]:
                    subtransactions.append(
                        {
                            "id": sub.get("id"),
                            "amount": sub["amount"] / MILLIUNITS_FACTOR if sub.get("amount") else 0,
                            "memo": sub.get("memo"),
                            "payee_id": sub.get("payee_id"),
                            "payee_name": sub.get("payee_name"),
                            "category_id": sub.get("category_id"),
                            "category_name": sub.get("category_name"),
                        }
                    )
    
            return {
                "id": txn["id"],
                "date": txn["date"],
                "amount": txn["amount"] / MILLIUNITS_FACTOR if txn.get("amount") else 0,
                "memo": txn.get("memo"),
                "cleared": txn.get("cleared"),
                "approved": txn.get("approved"),
                "account_id": txn.get("account_id"),
                "account_name": txn.get("account_name"),
                "payee_id": txn.get("payee_id"),
                "payee_name": txn.get("payee_name"),
                "category_id": txn.get("category_id"),
                "category_name": txn.get("category_name"),
                "transfer_account_id": txn.get("transfer_account_id"),
                "subtransactions": subtransactions if subtransactions else None,
            }
        except Exception as e:
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden and does an excellent job disclosing behavioral traits. It explains the tool creates UNAPPROVED transactions for manual matching, describes the workflow steps, notes constraints (sum of subtransactions must equal original amount), and explains what happens after matching. The only minor gap is it doesn't mention error handling or rate limits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections (Args, Returns, Workflow, Note) and front-loads the core purpose. While comprehensive, it could be slightly more concise by integrating some of the workflow details into the initial explanation rather than as a separate section.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (preparing splits for matching), no annotations, and 0% schema coverage, the description provides complete context. It explains the purpose, parameters, workflow, constraints, and expected outcomes. The presence of an output schema means it doesn't need to detail return values, and it covers all essential aspects for this specialized operation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description fully compensates by providing detailed parameter semantics. It explains what budget_id, transaction_id, and subtransactions are for, provides a comprehensive example of the subtransactions JSON structure with all required and optional fields, and clarifies the 'last-used' special value for budget_id.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with specific verbs ('prepare', 'fetches', 'creates') and resources ('split transaction', 'existing imported transaction'). It distinguishes this from sibling tools by explaining it's specifically for preparing splits for matching rather than creating splits directly (like create_split_transaction) or other transaction operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly states when to use this tool: 'Use this when you want to split an imported bank transaction.' It also provides workflow context and distinguishes it from direct creation tools by explaining the manual matching step required in YNAB's UI.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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-dgalarza'

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