import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type * as ynab from "ynab";
import { z } from "zod/v4";
import { CacheKeys } from "../server/cacheKeys.js";
import {
CACHE_TTLS,
CacheManager,
cacheManager,
} from "../server/cacheManager.js";
import type { DeltaCache } from "../server/deltaCache.js";
import type { ErrorHandler } from "../server/errorHandler.js";
import { responseFormatter } from "../server/responseFormatter.js";
import type { ServerKnowledgeStore } from "../server/serverKnowledgeStore.js";
import { withToolErrorHandling } from "../types/index.js";
import type { ToolFactory } from "../types/toolRegistration.js";
import { milliunitsToAmount } from "../utils/amountUtils.js";
import { createAdapters, createBudgetResolver } from "./adapters.js";
import type { DeltaFetcher } from "./deltaFetcher.js";
import {
resolveDeltaFetcherArgs,
resolveDeltaWriteArgs,
} from "./deltaSupport.js";
import {
CreateAccountOutputSchema,
GetAccountOutputSchema,
ListAccountsOutputSchema,
} from "./schemas/outputs/index.js";
import { ToolAnnotationPresets } from "./toolCategories.js";
/**
* Schema for ynab:list_accounts tool parameters
*/
export const ListAccountsSchema = z
.object({
budget_id: z.string().min(1, "Budget ID is required"),
limit: z.number().int().positive().optional(),
})
.strict();
export type ListAccountsParams = z.infer<typeof ListAccountsSchema>;
/**
* Schema for ynab:get_account tool parameters
*/
export const GetAccountSchema = z
.object({
budget_id: z.string().min(1, "Budget ID is required"),
account_id: z.string().min(1, "Account ID is required"),
})
.strict();
export type GetAccountParams = z.infer<typeof GetAccountSchema>;
/**
* Schema for ynab:create_account tool parameters
*/
export const CreateAccountSchema = z
.object({
budget_id: z.string().min(1, "Budget ID is required"),
name: z.string().min(1, "Account name is required"),
type: z.enum([
"checking",
"savings",
"creditCard",
"cash",
"lineOfCredit",
"otherAsset",
"otherLiability",
]),
balance: z.number().optional(),
dry_run: z.boolean().optional(),
})
.strict();
export type CreateAccountParams = z.infer<typeof CreateAccountSchema>;
/**
* Handles the ynab:list_accounts tool call
* Lists all accounts for a specific budget
*/
export async function handleListAccounts(
ynabAPI: ynab.API,
deltaFetcher: DeltaFetcher,
params: ListAccountsParams,
): Promise<CallToolResult>;
export async function handleListAccounts(
ynabAPI: ynab.API,
params: ListAccountsParams,
): Promise<CallToolResult>;
export async function handleListAccounts(
ynabAPI: ynab.API,
deltaFetcherOrParams: DeltaFetcher | ListAccountsParams,
maybeParams?: ListAccountsParams,
errorHandler?: ErrorHandler,
): Promise<CallToolResult> {
const { deltaFetcher, params } = resolveDeltaFetcherArgs(
ynabAPI,
deltaFetcherOrParams,
maybeParams,
);
return await withToolErrorHandling(
async () => {
const result = await deltaFetcher.fetchAccounts(params.budget_id);
let accounts = result.data;
const wasCached = result.wasCached;
// Apply limit if specified
const totalCount = accounts.length;
if (params.limit !== undefined) {
accounts = accounts.slice(0, params.limit);
}
return {
content: [
{
type: "text",
text: responseFormatter.format({
accounts: accounts.map((account) => ({
id: account.id,
name: account.name,
type: account.type,
on_budget: account.on_budget,
closed: account.closed,
note: account.note,
balance: milliunitsToAmount(account.balance),
cleared_balance: milliunitsToAmount(account.cleared_balance),
uncleared_balance: milliunitsToAmount(
account.uncleared_balance,
),
transfer_payee_id: account.transfer_payee_id,
direct_import_linked: account.direct_import_linked,
direct_import_in_error: account.direct_import_in_error,
})),
total_count: totalCount,
returned_count: accounts.length,
cached: wasCached,
cache_info: wasCached
? `Data retrieved from cache for improved performance${result.usedDelta ? " (delta merge applied)" : ""}`
: "Fresh data retrieved from YNAB API",
}),
},
],
};
},
"ynab:list_accounts",
"listing accounts",
errorHandler,
);
}
/**
* Handles the ynab:get_account tool call
* Gets detailed information for a specific account
*/
export async function handleGetAccount(
ynabAPI: ynab.API,
params: GetAccountParams,
errorHandler?: ErrorHandler,
): Promise<CallToolResult> {
return await withToolErrorHandling(
async () => {
// Use enhanced CacheManager wrap method
const cacheKey = CacheManager.generateKey(
CacheKeys.ACCOUNTS,
"get",
params.budget_id,
params.account_id,
);
const wasCached = cacheManager.has(cacheKey);
const account = await cacheManager.wrap<ynab.Account>(cacheKey, {
ttl: CACHE_TTLS.ACCOUNTS,
loader: async () => {
const response = await ynabAPI.accounts.getAccountById(
params.budget_id,
params.account_id,
);
return response.data.account;
},
});
return {
content: [
{
type: "text",
text: responseFormatter.format({
account: {
id: account.id,
name: account.name,
type: account.type,
on_budget: account.on_budget,
closed: account.closed,
note: account.note,
balance: milliunitsToAmount(account.balance),
cleared_balance: milliunitsToAmount(account.cleared_balance),
uncleared_balance: milliunitsToAmount(
account.uncleared_balance,
),
transfer_payee_id: account.transfer_payee_id,
direct_import_linked: account.direct_import_linked,
direct_import_in_error: account.direct_import_in_error,
},
cached: wasCached,
cache_info: wasCached
? "Data retrieved from cache for improved performance"
: "Fresh data retrieved from YNAB API",
}),
},
],
};
},
"ynab:get_account",
"getting account details",
errorHandler,
);
}
/**
* Handles the ynab:create_account tool call
* Creates a new account in the specified budget
*/
export async function handleCreateAccount(
ynabAPI: ynab.API,
deltaCache: DeltaCache,
knowledgeStore: ServerKnowledgeStore,
params: CreateAccountParams,
): Promise<CallToolResult>;
export async function handleCreateAccount(
ynabAPI: ynab.API,
params: CreateAccountParams,
): Promise<CallToolResult>;
export async function handleCreateAccount(
ynabAPI: ynab.API,
deltaCacheOrParams: DeltaCache | CreateAccountParams,
knowledgeStoreOrParams?: ServerKnowledgeStore | CreateAccountParams,
maybeParams?: CreateAccountParams,
errorHandler?: ErrorHandler,
): Promise<CallToolResult> {
const { deltaCache, params } = resolveDeltaWriteArgs(
deltaCacheOrParams,
knowledgeStoreOrParams,
maybeParams,
);
return await withToolErrorHandling(
async () => {
if (params.dry_run) {
return {
content: [
{
type: "text",
text: responseFormatter.format({
dry_run: true,
action: "create_account",
request: {
budget_id: params.budget_id,
name: params.name,
type: params.type,
balance: params.balance ?? 0,
},
}),
},
],
};
}
const accountData: ynab.SaveAccount = {
name: params.name,
type: params.type as ynab.Account["type"],
balance: params.balance ? params.balance * 1000 : 0, // Convert to milliunits
};
const response = await ynabAPI.accounts.createAccount(params.budget_id, {
account: accountData,
});
const account = response.data.account;
// Invalidate accounts list cache after successful account creation
const accountsListCacheKey = CacheManager.generateKey(
CacheKeys.ACCOUNTS,
"list",
params.budget_id,
);
cacheManager.delete(accountsListCacheKey);
deltaCache.invalidate(params.budget_id, CacheKeys.ACCOUNTS);
return {
content: [
{
type: "text",
text: responseFormatter.format({
account: {
id: account.id,
name: account.name,
type: account.type,
on_budget: account.on_budget,
closed: account.closed,
note: account.note,
balance: milliunitsToAmount(account.balance),
cleared_balance: milliunitsToAmount(account.cleared_balance),
uncleared_balance: milliunitsToAmount(
account.uncleared_balance,
),
transfer_payee_id: account.transfer_payee_id,
direct_import_linked: account.direct_import_linked,
direct_import_in_error: account.direct_import_in_error,
},
}),
},
],
};
},
"ynab:create_account",
"creating account",
errorHandler,
);
}
/**
* Registers all account-related tools with the registry.
*/
export const registerAccountTools: ToolFactory = (registry, context) => {
const { adapt, adaptWithDelta, adaptWrite } = createAdapters(context);
const budgetResolver = createBudgetResolver(context);
registry.register({
name: "list_accounts",
description: "List all accounts for a specific budget",
inputSchema: ListAccountsSchema,
outputSchema: ListAccountsOutputSchema,
handler: adaptWithDelta(handleListAccounts),
defaultArgumentResolver:
budgetResolver<z.infer<typeof ListAccountsSchema>>(),
metadata: {
annotations: {
...ToolAnnotationPresets.READ_ONLY_EXTERNAL,
title: "YNAB: List Accounts",
},
},
});
registry.register({
name: "get_account",
description: "Get detailed information for a specific account",
inputSchema: GetAccountSchema,
outputSchema: GetAccountOutputSchema,
handler: adapt(handleGetAccount),
defaultArgumentResolver: budgetResolver<z.infer<typeof GetAccountSchema>>(),
metadata: {
annotations: {
...ToolAnnotationPresets.READ_ONLY_EXTERNAL,
title: "YNAB: Get Account Details",
},
},
});
registry.register({
name: "create_account",
description: "Create a new account in the specified budget",
inputSchema: CreateAccountSchema,
outputSchema: CreateAccountOutputSchema,
handler: adaptWrite(handleCreateAccount),
defaultArgumentResolver:
budgetResolver<z.infer<typeof CreateAccountSchema>>(),
metadata: {
annotations: {
...ToolAnnotationPresets.WRITE_EXTERNAL_CREATE,
title: "YNAB: Create Account",
},
},
});
};