pocketsmith-mcp
Version:
MCP server for managing budgets via PocketSmith API
159 lines (158 loc) • 6.08 kB
JavaScript
/**
* @fileoverview Get budget 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 GetBudgetsInputSchema = 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)"),
});
export const GetBudgetsResponseSchema = z.object({
budgetAnalysis: z.array(z.object({
category: z.object({
id: z.number(),
title: z.string(),
}),
budget: z.number().optional(),
actual: z.number(),
difference: z.number(),
percentage: z.number().optional(),
period: z.string().optional(),
})).describe("Budget analysis by category"),
summary: z.object({
totalBudgeted: z.number(),
totalActual: z.number(),
totalDifference: z.number(),
categoriesOverBudget: z.number(),
categoriesUnderBudget: z.number(),
}).describe("Budget summary"),
});
export async function getBudgetsLogic(params, context) {
logger.debug("Processing get budgets 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 analysis
const budgetData = await service.getBudgets(user.id ?? 0, context);
// Process budget analysis
const budgetAnalysis = budgetData.map((item) => {
// Use available properties from the budget response
const actual = item.expense?.total_actual_amount || 0;
// Use periods data for budget information if available
const latestPeriod = item.expense?.periods?.[0];
const budget = latestPeriod?.forecast_amount || 0;
const difference = budget - actual;
const percentage = budget > 0 ? (actual / budget) * 100 : undefined;
return {
category: {
id: item.category?.id ?? 0,
title: item.category?.title ?? '',
},
budget: budget > 0 ? budget : undefined,
actual,
difference,
percentage,
period: item.expense?.start_date && item.expense?.end_date ? `${item.expense.start_date} to ${item.expense.end_date}` : undefined,
};
});
// Calculate summary
let totalBudgeted = 0;
let totalActual = 0;
let categoriesOverBudget = 0;
let categoriesUnderBudget = 0;
budgetAnalysis.forEach(item => {
if (item.budget) {
totalBudgeted += item.budget;
if (item.actual > item.budget) {
categoriesOverBudget++;
}
else {
categoriesUnderBudget++;
}
}
totalActual += item.actual;
});
const response = {
budgetAnalysis,
summary: {
totalBudgeted,
totalActual,
totalDifference: totalBudgeted - totalActual,
categoriesOverBudget,
categoriesUnderBudget,
},
};
logger.debug("Get budgets processed successfully", {
...context,
budgetItems: budgetAnalysis.length,
totalBudgeted,
totalActual,
});
return response;
}
export const registerGetBudgetsTool = async (server) => {
const toolName = "get_budgets";
const toolDescription = "Get budget analysis from PocketSmith showing budget vs actual spending by category";
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 Analysis",
description: toolDescription,
inputSchema: GetBudgetsInputSchema.shape,
outputSchema: GetBudgetsResponseSchema.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 getBudgetsLogic(params, handlerContext);
return {
structuredContent: result,
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (error) {
const mcpError = ErrorHandler.handleError(error, {
operation: "getBudgetsHandler",
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,
});
};