UNPKG

pocketsmith-mcp

Version:

MCP server for managing budgets via PocketSmith API

188 lines (187 loc) 8.26 kB
/** * @fileoverview Get budget summary with period analysis from PocketSmith */ import { z } from "zod"; import { BaseErrorCode } from "../../../types-global/errors.js"; import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js"; import { PocketSmithService } from "../../../services/pocketsmith.js"; export const GetBudgetSummaryInputSchema = z.object({ apiKey: z.string().optional().describe("PocketSmith API key (if not set via environment)"), accessToken: z.string().optional().describe("OAuth access token (if not using API key)"), period: z.enum(["weeks", "months", "years"]).describe("Period to analyze (weeks, months, or years)"), interval: z.number().int().min(1).describe("Period interval (e.g., 2 for fortnightly if period is weeks)"), startDate: z.string().describe("Start date for analysis (YYYY-MM-DD)"), endDate: z.string().describe("End date for analysis (YYYY-MM-DD)"), }); export const GetBudgetSummaryResponseSchema = z.object({ budgetSummary: z.array(z.object({ category: z.object({ id: z.number(), title: z.string(), }), periods: z.array(z.object({ period_label: z.string(), budget_amount: z.number(), actual_amount: z.number(), difference: z.number(), percentage_used: z.number(), })), overall: z.object({ total_budget: z.number(), total_actual: z.number(), total_difference: z.number(), average_percentage: z.number(), }), })).describe("Budget analysis by category and period"), summary: z.object({ analysis_period: z.string(), total_periods: z.number(), overall_budget: z.number(), overall_actual: z.number(), overall_difference: z.number(), overall_percentage: z.number(), categories_over_budget: z.number(), categories_under_budget: z.number(), }).describe("Overall budget summary"), }); export async function getBudgetSummaryLogic(params, context) { logger.debug("Processing get budget summary request", { ...context, toolInput: params, }); const apiKey = params.apiKey || process.env.POCKETSMITH_API_KEY; const accessToken = params.accessToken || process.env.POCKETSMITH_ACCESS_TOKEN; const service = new PocketSmithService(apiKey, accessToken); // Get current user first const user = await service.getCurrentUser(context); // Get budget summary const budgetData = await service.getBudgetSummary(user.id, { period: params.period, interval: params.interval, startDate: params.startDate, endDate: params.endDate, }, context); // Process budget data const processedBudgetSummary = budgetData.map((categoryData) => { const periodsArray = categoryData.periods; const periods = periodsArray?.map((period) => { const budgetAmount = period.budget_amount || 0; const actualAmount = period.actual_amount || 0; return { period_label: period.period_label || 'Unknown', budget_amount: budgetAmount, actual_amount: actualAmount, difference: budgetAmount - actualAmount, percentage_used: budgetAmount > 0 ? Math.round((actualAmount / budgetAmount) * 100) : 0, }; }) || []; const totalBudget = periods.reduce((sum, p) => sum + p.budget_amount, 0); const totalActual = periods.reduce((sum, p) => sum + p.actual_amount, 0); const totalDifference = totalBudget - totalActual; const averagePercentage = totalBudget > 0 ? Math.round((totalActual / totalBudget) * 100) : 0; const category = categoryData.category; return { category: { id: category?.id ?? 0, title: category?.title ?? 'Unknown', }, periods, overall: { total_budget: totalBudget, total_actual: totalActual, total_difference: totalDifference, average_percentage: averagePercentage, }, }; }); // Calculate overall summary const totalPeriods = processedBudgetSummary[0]?.periods.length || 0; const overallBudget = processedBudgetSummary.reduce((sum, cat) => sum + cat.overall.total_budget, 0); const overallActual = processedBudgetSummary.reduce((sum, cat) => sum + cat.overall.total_actual, 0); const overallDifference = overallBudget - overallActual; const overallPercentage = overallBudget > 0 ? Math.round((overallActual / overallBudget) * 100) : 0; const categoriesOverBudget = processedBudgetSummary.filter(cat => cat.overall.total_actual > cat.overall.total_budget).length; const categoriesUnderBudget = processedBudgetSummary.filter(cat => cat.overall.total_actual <= cat.overall.total_budget && cat.overall.total_budget > 0).length; const response = { budgetSummary: processedBudgetSummary, summary: { analysis_period: `${params.period} (interval: ${params.interval})`, total_periods: totalPeriods, overall_budget: overallBudget, overall_actual: overallActual, overall_difference: overallDifference, overall_percentage: overallPercentage, categories_over_budget: categoriesOverBudget, categories_under_budget: categoriesUnderBudget, }, }; logger.debug("Get budget summary processed successfully", { ...context, categoryCount: processedBudgetSummary.length, totalPeriods, overallBudget, overallActual, }); return response; } export const registerGetBudgetSummaryTool = async (server) => { const toolName = "get_budget_summary"; const toolDescription = "Get detailed budget summary with period analysis (weeks/months/years)"; const registrationContext = requestContextService.createRequestContext({ operation: "RegisterTool", toolName: toolName, }); logger.info(`Registering tool: '${toolName}'`, registrationContext); await ErrorHandler.tryCatch(async () => { server.registerTool(toolName, { title: "Get PocketSmith Budget Summary", description: toolDescription, inputSchema: GetBudgetSummaryInputSchema.shape, outputSchema: GetBudgetSummaryResponseSchema.shape, annotations: { readOnlyHint: true, openWorldHint: false, }, }, async (params) => { const handlerContext = requestContextService.createRequestContext({ parentRequestId: registrationContext.requestId, operation: "HandleToolRequest", toolName: toolName, input: params, }); try { const result = await getBudgetSummaryLogic(params, handlerContext); return { structuredContent: result, content: [ { type: "text", text: JSON.stringify(result, null, 2) }, ], }; } catch (error) { const mcpError = ErrorHandler.handleError(error, { operation: "getBudgetSummaryHandler", context: handlerContext, input: params, }); return { isError: true, content: [{ type: "text", text: `Error: ${mcpError.message}` }], structuredContent: { code: mcpError.code, message: mcpError.message, details: mcpError.details, }, }; } }); logger.info(`Tool '${toolName}' registered successfully.`, registrationContext); }, { operation: `RegisteringTool_${toolName}`, context: registrationContext, errorCode: BaseErrorCode.INITIALIZATION_FAILED, critical: true, }); };