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 { ErrorHandler } from "../server/errorHandler.js";
import { responseFormatter } from "../server/responseFormatter.js";
import { withToolErrorHandling } from "../types/index.js";
import type { ToolFactory } from "../types/toolRegistration.js";
import { createAdapters, createBudgetResolver } from "./adapters.js";
import type { DeltaFetcher } from "./deltaFetcher.js";
import { resolveDeltaFetcherArgs } from "./deltaSupport.js";
import {
GetPayeeOutputSchema,
ListPayeesOutputSchema,
} from "./schemas/outputs/index.js";
import { ToolAnnotationPresets } from "./toolCategories.js";
/**
* Schema for ynab:list_payees tool parameters
*/
export const ListPayeesSchema = z
.object({
budget_id: z.string().min(1, "Budget ID is required"),
limit: z.number().int().positive().optional(),
})
.strict();
export type ListPayeesParams = z.infer<typeof ListPayeesSchema>;
/**
* Schema for ynab:get_payee tool parameters
*/
export const GetPayeeSchema = z
.object({
budget_id: z.string().min(1, "Budget ID is required"),
payee_id: z.string().min(1, "Payee ID is required"),
})
.strict();
export type GetPayeeParams = z.infer<typeof GetPayeeSchema>;
/**
* Handles the ynab:list_payees tool call
* Lists all payees for a specific budget
*/
export async function handleListPayees(
ynabAPI: ynab.API,
deltaFetcher: DeltaFetcher,
params: ListPayeesParams,
): Promise<CallToolResult>;
export async function handleListPayees(
ynabAPI: ynab.API,
params: ListPayeesParams,
): Promise<CallToolResult>;
export async function handleListPayees(
ynabAPI: ynab.API,
deltaFetcherOrParams: DeltaFetcher | ListPayeesParams,
maybeParams?: ListPayeesParams,
errorHandler?: ErrorHandler,
): Promise<CallToolResult> {
const { deltaFetcher, params } = resolveDeltaFetcherArgs(
ynabAPI,
deltaFetcherOrParams,
maybeParams,
);
return await withToolErrorHandling(
async () => {
const result = await deltaFetcher.fetchPayees(params.budget_id);
let payees = result.data;
const wasCached = result.wasCached;
// Apply limit if specified
const totalCount = payees.length;
if (params.limit !== undefined) {
payees = payees.slice(0, params.limit);
}
return {
content: [
{
type: "text",
text: responseFormatter.format({
payees: payees.map((payee) => ({
id: payee.id,
name: payee.name,
transfer_account_id: payee.transfer_account_id,
deleted: payee.deleted,
})),
total_count: totalCount,
returned_count: payees.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_payees",
"listing payees",
errorHandler,
);
}
/**
* Handles the ynab:get_payee tool call
* Gets detailed information for a specific payee
*/
export async function handleGetPayee(
ynabAPI: ynab.API,
params: GetPayeeParams,
errorHandler?: ErrorHandler,
): Promise<CallToolResult> {
return await withToolErrorHandling(
async () => {
// Use enhanced CacheManager wrap method
const cacheKey = CacheManager.generateKey(
CacheKeys.PAYEES,
"get",
params.budget_id,
params.payee_id,
);
const wasCached = cacheManager.has(cacheKey);
const payee = await cacheManager.wrap<ynab.Payee>(cacheKey, {
ttl: CACHE_TTLS.PAYEES,
loader: async () => {
const response = await ynabAPI.payees.getPayeeById(
params.budget_id,
params.payee_id,
);
return response.data.payee;
},
});
return {
content: [
{
type: "text",
text: responseFormatter.format({
payee: {
id: payee.id,
name: payee.name,
transfer_account_id: payee.transfer_account_id,
deleted: payee.deleted,
},
cached: wasCached,
cache_info: wasCached
? "Data retrieved from cache for improved performance"
: "Fresh data retrieved from YNAB API",
}),
},
],
};
},
"ynab:get_payee",
"getting payee details",
errorHandler,
);
}
/**
* Registers all payee-related tools with the registry.
*/
export const registerPayeeTools: ToolFactory = (registry, context) => {
const { adapt, adaptWithDelta } = createAdapters(context);
const budgetResolver = createBudgetResolver(context);
registry.register({
name: "list_payees",
description: "List all payees for a specific budget",
inputSchema: ListPayeesSchema,
outputSchema: ListPayeesOutputSchema,
handler: adaptWithDelta(handleListPayees),
defaultArgumentResolver: budgetResolver<z.infer<typeof ListPayeesSchema>>(),
metadata: {
annotations: {
...ToolAnnotationPresets.READ_ONLY_EXTERNAL,
title: "YNAB: List Payees",
},
},
});
registry.register({
name: "get_payee",
description: "Get detailed information for a specific payee",
inputSchema: GetPayeeSchema,
outputSchema: GetPayeeOutputSchema,
handler: adapt(handleGetPayee),
defaultArgumentResolver: budgetResolver<z.infer<typeof GetPayeeSchema>>(),
metadata: {
annotations: {
...ToolAnnotationPresets.READ_ONLY_EXTERNAL,
title: "YNAB: Get Payee Details",
},
},
});
};