pocketsmith-mcp
Version:
MCP server for managing budgets via PocketSmith API
204 lines (203 loc) • 8.53 kB
JavaScript
/**
* @fileoverview Get comprehensive user financial summary 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 GetUserSummaryInputSchema = 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)"),
includeRecentTransactions: z.boolean().optional().default(true).describe("Include recent transactions in the summary"),
transactionLimit: z.number().int().min(1).max(50).optional().default(10).describe("Number of recent transactions to include (1-50)"),
});
export const GetUserSummaryResponseSchema = z.object({
user: z.object({
id: z.number(),
name: z.string(),
email: z.string(),
base_currency_code: z.string().optional(),
}).describe("User information"),
financialSummary: z.object({
totalBalance: z.number(),
totalAccounts: z.number(),
accountsByType: z.record(z.string(), z.number()),
netWorth: z.number(),
}).describe("Overall financial position"),
recentTransactions: z.array(z.object({
id: z.number(),
payee: z.string(),
amount: z.number(),
date: z.string(),
account: z.string(),
category: z.string().optional(),
})).optional().describe("Recent transactions"),
budgetHighlights: z.object({
categoriesTracked: z.number(),
overBudgetCategories: z.number(),
totalBudgetVariance: z.number(),
}).optional().describe("Budget performance highlights"),
categoryBreakdown: z.array(z.object({
category: z.string(),
count: z.number(),
isTransfer: z.boolean(),
isBill: z.boolean(),
})).describe("Category breakdown"),
});
export async function getUserSummaryLogic(params, context) {
logger.debug("Processing get user 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
const user = await service.getCurrentUser(context);
// Get accounts
const accounts = await service.getAccounts(user.id ?? 0, context);
// Calculate financial summary
const totalBalance = accounts.reduce((sum, account) => {
return sum + (account.current_balance_in_base_currency || 0);
}, 0);
const accountsByType = {};
accounts.forEach(account => {
const accountType = account.type ?? 'unknown';
accountsByType[accountType] = (accountsByType[accountType] || 0) + 1;
});
// Get categories
const categories = await service.getCategories(user.id ?? 0, context);
const categoryBreakdown = categories.map(category => ({
category: category.title ?? '',
count: category.children?.length || 0,
isTransfer: category.is_transfer ?? false,
isBill: category.is_bill ?? false,
}));
let recentTransactions;
if (params.includeRecentTransactions) {
const transactions = await service.getTransactions(user.id ?? 0, context, {
limit: params.transactionLimit,
});
recentTransactions = transactions.map(transaction => ({
id: transaction.id ?? 0,
payee: transaction.payee ?? '',
amount: transaction.amount ?? 0,
date: transaction.date ?? '',
account: transaction.transaction_account?.name ?? '',
category: transaction.category?.title,
}));
}
let budgetHighlights;
try {
const budgets = await service.getBudgets(user.id ?? 0, context);
const categoriesWithBudgets = budgets.filter((item) => {
const latestPeriod = item.expense?.periods?.[0];
const budgetAmount = latestPeriod?.forecast_amount ?? 0;
return budgetAmount > 0;
});
const overBudgetCategories = categoriesWithBudgets.filter((item) => {
const actualAmount = item.expense?.total_actual_amount ?? 0;
const latestPeriod = item.expense?.periods?.[0];
const budgetAmount = latestPeriod?.forecast_amount ?? 0;
return actualAmount > budgetAmount;
});
const totalVariance = categoriesWithBudgets.reduce((sum, item) => {
const actualAmount = item.expense?.total_actual_amount ?? 0;
const latestPeriod = item.expense?.periods?.[0];
const budgetAmount = latestPeriod?.forecast_amount ?? 0;
return sum + (budgetAmount - actualAmount);
}, 0);
budgetHighlights = {
categoriesTracked: categoriesWithBudgets.length,
overBudgetCategories: overBudgetCategories.length,
totalBudgetVariance: totalVariance,
};
}
catch (error) {
// Budget analysis might not be available for all users
logger.debug("Budget analysis not available", { ...context, error });
}
const response = {
user: {
id: user.id ?? 0,
name: user.name ?? '',
email: user.email ?? '',
base_currency_code: user.base_currency_code,
},
financialSummary: {
totalBalance,
totalAccounts: accounts.length,
accountsByType,
netWorth: totalBalance, // Simplified - could be more complex with assets/liabilities
},
recentTransactions,
budgetHighlights,
categoryBreakdown,
};
logger.debug("Get user summary processed successfully", {
...context,
accountCount: accounts.length,
categoryCount: categories.length,
totalBalance,
});
return response;
}
export const registerGetUserSummaryTool = async (server) => {
const toolName = "get_user_summary";
const toolDescription = "Get a comprehensive financial summary including accounts, recent transactions, budget highlights, and category breakdown";
const registrationContext = requestContextService.createRequestContext({
operation: "RegisterTool",
toolName: toolName,
});
logger.info(`Registering tool: '${toolName}'`, registrationContext);
await ErrorHandler.tryCatch(async () => {
server.registerTool(toolName, {
title: "Get PocketSmith User Summary",
description: toolDescription,
inputSchema: GetUserSummaryInputSchema.shape,
outputSchema: GetUserSummaryResponseSchema.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 getUserSummaryLogic(params, handlerContext);
return {
structuredContent: result,
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (error) {
const mcpError = ErrorHandler.handleError(error, {
operation: "getUserSummaryHandler",
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,
});
};