From 912b88bdbbc4260c17f4f3055efe05e2bf9bb67a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 07:30:10 +0000 Subject: [PATCH 1/2] chore(deps): bump the all-dependencies group across 1 directory with 3 updates Bumps the all-dependencies group with 3 updates in the / directory: [ynab](https://github.com/ynab/ynab-sdk-js), [zod](https://github.com/colinhacks/zod) and [typescript](https://github.com/microsoft/TypeScript). Updates `ynab` from 2.10.0 to 4.0.0 - [Release notes](https://github.com/ynab/ynab-sdk-js/releases) - [Commits](https://github.com/ynab/ynab-sdk-js/compare/v2.10.0...v4.0.0) Updates `zod` from 3.25.76 to 4.3.6 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v3.25.76...v4.3.6) Updates `typescript` from 5.9.3 to 6.0.2 - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2) --- updated-dependencies: - dependency-name: ynab dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-dependencies - dependency-name: zod dependency-version: 4.3.6 dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-dependencies - dependency-name: typescript dependency-version: 6.0.2 dependency-type: direct:development update-type: version-update:semver-major dependency-group: all-dependencies ... Signed-off-by: dependabot[bot] --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 631498d..40fea4f 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,8 @@ "commander": "^14.0.2", "conf": "^15.0.2", "dayjs": "^1.11.19", - "ynab": "^2.10.0", - "zod": "^3.25.0" + "ynab": "^4.0.0", + "zod": "^4.3.6" }, "devDependencies": { "@biomejs/biome": "^2.3.11", @@ -59,7 +59,7 @@ "oxlint": "^1.38.0", "tsup": "^8.0.0", "tsx": "^4.7.0", - "typescript": "^5.3.0", + "typescript": "^6.0.2", "vitest": "^4.0.4" }, "engines": { From 068e3288a215a2aaa3612651c90b5f260f9fb0a2 Mon Sep 17 00:00:00 2001 From: "(Boris the Agent)" Date: Wed, 17 Jun 2026 10:30:20 +0000 Subject: [PATCH 2/2] chore: adapt ynab-cli to dependency majors --- README.md | 4 +- bun.lock | 14 +++--- src/commands/api.ts | 4 +- src/commands/categories.ts | 10 ++++- src/commands/transactions.ts | 46 ++++++++++--------- src/lib/api-client.ts | 22 ++++----- src/lib/errors.ts | 1 + src/mcp/server.ts | 87 ++++++++++++++++++++---------------- tsconfig.json | 1 + 9 files changed, 108 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index c703552..9db1a1d 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,8 @@ ynab scheduled delete ### Raw API Access ```bash -ynab api GET /budgets -ynab api POST /budgets/{budget_id}/transactions --data '{"transaction": {...}}' +ynab api GET /plans +ynab api POST /plans/{plan_id}/transactions --data '{"transaction": {...}}' ``` ### MCP Server diff --git a/bun.lock b/bun.lock index 04117b9..f384049 100644 --- a/bun.lock +++ b/bun.lock @@ -10,8 +10,8 @@ "commander": "^14.0.2", "conf": "^15.0.2", "dayjs": "^1.11.19", - "ynab": "^2.10.0", - "zod": "^3.25.0", + "ynab": "^4.0.0", + "zod": "^4.3.6", }, "devDependencies": { "@biomejs/biome": "^2.3.11", @@ -19,7 +19,7 @@ "oxlint": "^1.38.0", "tsup": "^8.0.0", "tsx": "^4.7.0", - "typescript": "^5.3.0", + "typescript": "^6.0.2", "vitest": "^4.0.4", }, }, @@ -525,7 +525,7 @@ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], @@ -553,12 +553,14 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ynab": ["ynab@2.10.0", "", { "dependencies": { "fetch-ponyfill": "^7.1.0" } }, "sha512-zDH++4mbFpVDbDW1qIYS3pG3sPVtdth7c45aEfU7pVNqIcK6aVzK8eSksyOarrzaPKSq4OA9AXq1ixC7WGdpow=="], + "ynab": ["ynab@4.4.0", "", { "dependencies": { "fetch-ponyfill": "^7.1.0" } }, "sha512-LCl8+8MafsyVumSFBMoUYyNsyHecBosUFJVQSo/MwNmAf/odrdkOFoP7VeE0vHVCkKMAtmxZyg7Bj+vF6a+hvA=="], - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], diff --git a/src/commands/api.ts b/src/commands/api.ts index cc6370f..dc19f7c 100644 --- a/src/commands/api.ts +++ b/src/commands/api.ts @@ -13,8 +13,8 @@ export function createApiCommand(): Command { cmd .argument('', 'HTTP method (GET, POST, PUT, PATCH, DELETE)') - .argument('', 'API path (e.g., /budgets or /budgets/{budget_id}/transactions)') - .option('-b, --budget ', 'Budget ID (used to replace {budget_id} in path)') + .argument('', 'API path (e.g., /plans or /plans/{plan_id}/transactions)') + .option('-b, --budget ', 'Budget ID (used to replace {plan_id} or {budget_id} in path)') .option('--data ', 'JSON data for POST/PUT/PATCH requests') .description('Make raw API calls to YNAB') .action( diff --git a/src/commands/categories.ts b/src/commands/categories.ts index 5f8cee6..2dd95ec 100644 --- a/src/commands/categories.ts +++ b/src/commands/categories.ts @@ -88,7 +88,15 @@ export function createCategoriesCommand(): Command { updateData.goal_target = amountToMilliunits(options.goalTarget); } - const category = await client.updateCategory(id, { category: updateData }, options.budget); + const category = await client.updateCategory( + id, + { + category: updateData as NonNullable< + Parameters[1]['category'] + >, + }, + options.budget + ); outputJson(category); } ) diff --git a/src/commands/transactions.ts b/src/commands/transactions.ts index 7901b68..1c8ffa2 100644 --- a/src/commands/transactions.ts +++ b/src/commands/transactions.ts @@ -311,34 +311,38 @@ export function createTransactionsCommand(): Command { if (isAlreadySplit) { await client.deleteTransaction(id, options.budget); + const transaction = { + account_id: existingTransaction.account_id, + date: existingTransaction.date, + amount: existingTransaction.amount, + payee_id: existingTransaction.payee_id, + payee_name: existingTransaction.payee_name, + category_id: null, + memo: existingTransaction.memo, + cleared: existingTransaction.cleared, + approved: existingTransaction.approved, + flag_color: existingTransaction.flag_color, + subtransactions: splitsInMilliunits, + } as unknown as NonNullable< + Parameters[0]['transaction'] + >; + const recreatedTransaction = await client.createTransaction( - { - transaction: { - account_id: existingTransaction.account_id, - date: existingTransaction.date, - amount: existingTransaction.amount, - payee_id: existingTransaction.payee_id, - payee_name: existingTransaction.payee_name, - category_id: null, - memo: existingTransaction.memo, - cleared: existingTransaction.cleared, - approved: existingTransaction.approved, - flag_color: existingTransaction.flag_color, - subtransactions: splitsInMilliunits, - }, - }, + { transaction }, options.budget ); outputJson(recreatedTransaction); } else { + const transactionData = { + category_id: null, + subtransactions: splitsInMilliunits, + } as unknown as NonNullable< + Parameters[1]['transaction'] + >; + const transaction = await client.updateTransaction( id, - { - transaction: { - category_id: null, - subtransactions: splitsInMilliunits, - }, - }, + { transaction: transactionData }, options.budget ); outputJson(transaction); diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 84031d2..03f2ac6 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -63,9 +63,9 @@ export class YnabClient { async getBudgets(includeAccounts = false) { const api = await this.getApi(); - const response = await api.budgets.getBudgets(includeAccounts); + const response = await api.plans.getPlans(includeAccounts); return { - budgets: response.data.budgets, + budgets: response.data.plans, server_knowledge: 0, }; } @@ -73,9 +73,9 @@ export class YnabClient { async getBudget(budgetId?: string, lastKnowledgeOfServer?: number) { const api = await this.getApi(); const id = await this.getBudgetId(budgetId); - const response = await api.budgets.getBudgetById(id, lastKnowledgeOfServer); + const response = await api.plans.getPlanById(id, lastKnowledgeOfServer); return { - budget: response.data.budget, + budget: response.data.plan, server_knowledge: response.data.server_knowledge, }; } @@ -83,7 +83,7 @@ export class YnabClient { async getBudgetSettings(budgetId?: string) { const api = await this.getApi(); const id = await this.getBudgetId(budgetId); - const response = await api.budgets.getBudgetSettingsById(id); + const response = await api.plans.getPlanSettingsById(id); return response.data.settings; } @@ -174,7 +174,7 @@ export class YnabClient { async getBudgetMonths(budgetId?: string, lastKnowledgeOfServer?: number) { const api = await this.getApi(); const id = await this.getBudgetId(budgetId); - const response = await api.months.getBudgetMonths(id, lastKnowledgeOfServer); + const response = await api.months.getPlanMonths(id, lastKnowledgeOfServer); return { months: response.data.months, server_knowledge: response.data.server_knowledge, @@ -184,7 +184,7 @@ export class YnabClient { async getBudgetMonth(month: string, budgetId?: string) { const api = await this.getApi(); const id = await this.getBudgetId(budgetId); - const response = await api.months.getBudgetMonth(id, month); + const response = await api.months.getPlanMonth(id, month); return response.data.month; } @@ -369,9 +369,11 @@ export class YnabClient { async rawApiCall(method: string, path: string, data?: unknown, budgetId?: string) { await this.getApi(); - const fullPath = path.includes('{budget_id}') - ? path.replace('{budget_id}', await this.getBudgetId(budgetId)) - : path; + let fullPath = path; + if (path.includes('{budget_id}') || path.includes('{plan_id}')) { + const id = await this.getBudgetId(budgetId); + fullPath = path.replaceAll('{budget_id}', id).replaceAll('{plan_id}', id); + } const url = `https://api.ynab.com/v1${fullPath}`; const accessToken = (await auth.getAccessToken()) || process.env.YNAB_API_KEY; diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 78717e6..530983d 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -83,6 +83,7 @@ function formatErrorResponse(name: string, detail: string, statusCode: number): outputJson({ error: { name, detail: enhancedDetail, statusCode } }); process.exit(1); + throw new Error('process.exit returned unexpectedly'); } export function handleYnabError(error: unknown): never { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 62807a1..2bc7449 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1,6 +1,6 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { z } from 'zod'; +import { z } from 'zod/v3'; import { client } from '../lib/api-client.js'; import { auth } from '../lib/auth.js'; import { YnabCliError, sanitizeApiError, sanitizeErrorMessage } from '../lib/errors.js'; @@ -83,6 +83,15 @@ const _serverTool = server.tool.bind(server); return (_serverTool as Function).apply(null, args); }; +type ToolRegistrar = ( + name: string, + description: string, + paramsSchema: Record, + handler: (args: any) => unknown | Promise +) => unknown; + +const tool = server.tool.bind(server) as ToolRegistrar; + function jsonResponse(data: unknown) { return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] }; } @@ -91,35 +100,35 @@ function currencyResponse(data: unknown) { return jsonResponse(convertMilliunitsToAmounts(data)); } -server.tool( +tool( 'list_budgets', 'List all budgets in the YNAB account', { includeAccounts: z.boolean().optional().describe('Include account details') }, async ({ includeAccounts }) => currencyResponse(await client.getBudgets(includeAccounts)) ); -server.tool( +tool( 'get_budget', 'Get detailed information about a specific budget', { budgetId: z.string().optional().describe('Budget ID (uses default if not specified)') }, async ({ budgetId }) => currencyResponse(await client.getBudget(budgetId)) ); -server.tool( +tool( 'get_budget_settings', 'Get budget settings (date format, currency format, etc.)', { budgetId: z.string().optional().describe('Budget ID (uses default if not specified)') }, async ({ budgetId }) => jsonResponse(await client.getBudgetSettings(budgetId)) ); -server.tool( +tool( 'list_accounts', 'List all accounts in a budget', { budgetId: z.string().optional().describe('Budget ID (uses default if not specified)') }, async ({ budgetId }) => currencyResponse(await client.getAccounts(budgetId)) ); -server.tool( +tool( 'get_account', 'Get detailed information about a specific account', { @@ -129,14 +138,14 @@ server.tool( async ({ accountId, budgetId }) => currencyResponse(await client.getAccount(accountId, budgetId)) ); -server.tool( +tool( 'list_categories', 'List all category groups and categories in a budget', { budgetId: z.string().optional().describe('Budget ID (uses default if not specified)') }, async ({ budgetId }) => currencyResponse(await client.getCategories(budgetId)) ); -server.tool( +tool( 'get_category', 'Get detailed information about a specific category', { @@ -146,7 +155,7 @@ server.tool( async ({ categoryId, budgetId }) => currencyResponse(await client.getCategory(categoryId, budgetId)) ); -server.tool( +tool( 'update_category', 'Update category name, note, group, or goal target', { @@ -167,7 +176,7 @@ server.tool( } ); -server.tool( +tool( 'update_month_category', 'Set the budgeted amount for a category in a specific month', { @@ -182,7 +191,7 @@ server.tool( ) ); -server.tool( +tool( 'list_transactions', 'List transactions with optional filtering', { @@ -202,7 +211,7 @@ server.tool( } ); -server.tool( +tool( 'get_transaction', 'Get detailed information about a specific transaction', { @@ -212,7 +221,7 @@ server.tool( async ({ transactionId, budgetId }) => currencyResponse(await client.getTransaction(transactionId, budgetId)) ); -server.tool( +tool( 'create_transaction', 'Create a new transaction', { @@ -243,7 +252,7 @@ server.tool( } ); -server.tool( +tool( 'update_transaction', 'Update an existing transaction', { @@ -274,7 +283,7 @@ server.tool( } ); -server.tool( +tool( 'delete_transaction', 'Delete a transaction', { @@ -285,14 +294,14 @@ server.tool( currencyResponse(await client.deleteTransaction(transactionId, budgetId)) ); -server.tool( +tool( 'import_transactions', 'Trigger import of linked bank transactions', { budgetId: z.string().optional().describe('Budget ID (uses default if not specified)') }, async ({ budgetId }) => jsonResponse(await client.importTransactions(budgetId)) ); -server.tool( +tool( 'batch_update_transactions', 'Update multiple transactions in a single API call. Amounts in dollars.', { @@ -313,9 +322,9 @@ server.tool( budgetId: z.string().optional().describe('Budget ID (uses default if not specified)'), }, async ({ transactions, budgetId }) => { - const transactionsInMilliunits = transactions.map((update) => ({ + const transactionsInMilliunits = transactions.map((update: Record) => ({ ...update, - ...(update.amount !== undefined ? { amount: amountToMilliunits(update.amount) } : {}), + ...(typeof update.amount === 'number' ? { amount: amountToMilliunits(update.amount) } : {}), })); return currencyResponse( await client.updateTransactions( @@ -326,7 +335,7 @@ server.tool( } ); -server.tool( +tool( 'summarize_transactions', 'Get aggregate summary of transactions by payee, category, and status', { @@ -358,7 +367,7 @@ server.tool( } ); -server.tool( +tool( 'find_transfer_candidates', 'Find candidate transfer matches for a transaction across accounts', { @@ -386,7 +395,7 @@ server.tool( } ); -server.tool( +tool( 'list_transactions_by_account', 'List transactions for a specific account', { @@ -402,7 +411,7 @@ server.tool( } ); -server.tool( +tool( 'list_transactions_by_category', 'List transactions for a specific category', { @@ -418,7 +427,7 @@ server.tool( } ); -server.tool( +tool( 'list_transactions_by_payee', 'List transactions for a specific payee', { @@ -434,14 +443,14 @@ server.tool( } ); -server.tool( +tool( 'list_payees', 'List all payees in a budget', { budgetId: z.string().optional().describe('Budget ID (uses default if not specified)') }, async ({ budgetId }) => jsonResponse(await client.getPayees(budgetId)) ); -server.tool( +tool( 'update_payee', 'Rename a payee', { @@ -453,7 +462,7 @@ server.tool( jsonResponse(await client.updatePayee(payeeId, { payee: { name } }, budgetId)) ); -server.tool( +tool( 'list_payee_locations', 'List locations for a specific payee', { @@ -464,14 +473,14 @@ server.tool( jsonResponse(await client.getPayeeLocationsByPayee(payeeId, budgetId)) ); -server.tool( +tool( 'list_budget_months', 'List all budget months', { budgetId: z.string().optional().describe('Budget ID (uses default if not specified)') }, async ({ budgetId }) => currencyResponse(await client.getBudgetMonths(budgetId)) ); -server.tool( +tool( 'get_budget_month', 'Get budget details for a specific month', { @@ -481,14 +490,14 @@ server.tool( async ({ month, budgetId }) => currencyResponse(await client.getBudgetMonth(month, budgetId)) ); -server.tool( +tool( 'list_scheduled_transactions', 'List all scheduled transactions in a budget', { budgetId: z.string().optional().describe('Budget ID (uses default if not specified)') }, async ({ budgetId }) => currencyResponse(await client.getScheduledTransactions(budgetId)) ); -server.tool( +tool( 'get_scheduled_transaction', 'Get a single scheduled transaction', { @@ -499,7 +508,7 @@ server.tool( currencyResponse(await client.getScheduledTransaction(scheduledTransactionId, budgetId)) ); -server.tool( +tool( 'delete_scheduled_transaction', 'Delete a scheduled transaction', { @@ -510,34 +519,34 @@ server.tool( currencyResponse(await client.deleteScheduledTransaction(scheduledTransactionId, budgetId)) ); -server.tool( +tool( 'raw_api_call', 'Make a direct YNAB API call', { method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'), - path: z.string().describe('API path (e.g., /budgets/{budget_id}/accounts). {budget_id} is replaced with the default budget.'), - data: z.record(z.unknown()).optional().describe('Request body for POST/PUT/PATCH'), - budgetId: z.string().optional().describe('Budget ID for {budget_id} replacement (uses default if not specified)'), + path: z.string().describe('API path (e.g., /plans/{plan_id}/accounts). {plan_id} or {budget_id} is replaced with the default budget.'), + data: z.record(z.string(), z.unknown()).optional().describe('Request body for POST/PUT/PATCH'), + budgetId: z.string().optional().describe('Budget ID for {plan_id} or {budget_id} replacement (uses default if not specified)'), }, async ({ method, path, data, budgetId }) => jsonResponse(await client.rawApiCall(method, path, data, budgetId)) ); -server.tool( +tool( 'get_user', 'Get information about the authenticated user', {}, async () => jsonResponse(await client.getUser()) ); -server.tool( +tool( 'check_auth', 'Check if YNAB authentication is configured', {}, async () => jsonResponse({ authenticated: await auth.isAuthenticated() }) ); -server.tool( +tool( 'search_tools', 'Search for available tools by name or description using regex. Returns matching tool names.', { diff --git a/tsconfig.json b/tsconfig.json index d8d603d..1ae6a30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2022", "module": "ESNext", "lib": ["ES2022"], + "types": ["bun"], "moduleResolution": "bundler", "resolveJsonModule": true, "allowJs": true,