pocketsmith-mcp
Version:
MCP server for managing budgets via PocketSmith API
182 lines (181 loc) • 7.38 kB
JavaScript
/**
* @fileoverview Get transactions for a specific account from PocketSmith
*/
import { z } from "zod";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
import { PocketSmithService } from "../../../services/pocketsmith.js";
export const GetAccountTransactionsInputSchema = 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)"),
accountId: z.number().int().describe("ID of the account to get transactions for"),
startDate: z.string().optional().describe("Start date for transaction search (YYYY-MM-DD)"),
endDate: z.string().optional().describe("End date for transaction search (YYYY-MM-DD)"),
search: z.string().optional().describe("Search term to filter transactions by payee or note"),
limit: z.number().int().min(1).max(1000).optional().default(50).describe("Maximum number of transactions to return (1-1000, default 50)"),
});
export const GetAccountTransactionsResponseSchema = z.object({
account: z.object({
id: z.number(),
name: z.string(),
type: z.string(),
currency_code: z.string(),
current_balance: z.number(),
}).describe("Account information"),
transactions: z.array(z.object({
id: z.number(),
payee: z.string(),
amount: z.number(),
date: z.string(),
note: z.string().nullable(),
category: z.object({
id: z.number(),
title: z.string(),
}).optional(),
currency_code: z.string(),
type: z.string(),
})).describe("List of transactions for the account"),
summary: z.object({
totalCount: z.number(),
totalCredit: z.number(),
totalDebit: z.number(),
netAmount: z.number(),
dateRange: z.string().optional(),
}).describe("Summary of account transactions"),
});
export async function getAccountTransactionsLogic(params, context) {
logger.debug("Processing get account transactions 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 to fetch transaction accounts
const user = await service.getCurrentUser(context);
const transactionAccounts = await service.getTransactionAccounts(user.id, context);
// Find the requested transaction account
const account = transactionAccounts.find(acc => acc.id === params.accountId);
if (!account) {
throw new McpError(BaseErrorCode.NOT_FOUND, `Transaction account with ID ${params.accountId} not found`);
}
// Get account transactions
const transactions = await service.getAccountTransactions(params.accountId, context, {
startDate: params.startDate,
endDate: params.endDate,
search: params.search,
limit: params.limit,
});
// Calculate summary
let totalCredit = 0;
let totalDebit = 0;
const processedTransactions = transactions.map(transaction => {
const amount = transaction.amount ?? 0;
if (amount > 0) {
totalCredit += amount;
}
else {
totalDebit += Math.abs(amount);
}
return {
id: transaction.id ?? 0,
payee: transaction.payee ?? '',
amount,
date: transaction.date ?? '',
note: transaction.note ?? null,
category: transaction.category ? {
id: transaction.category.id ?? 0,
title: transaction.category.title ?? '',
} : undefined,
currency_code: transaction.currency_code ?? '',
type: transaction.type ?? '',
};
});
const dateRange = params.startDate && params.endDate
? `${params.startDate} to ${params.endDate}`
: undefined;
const response = {
account: {
id: account.id ?? 0,
name: account.name ?? '',
type: account.type ?? '',
currency_code: account.currency_code ?? '',
current_balance: account.current_balance ?? 0,
},
transactions: processedTransactions,
summary: {
totalCount: transactions.length,
totalCredit,
totalDebit,
netAmount: totalCredit - totalDebit,
dateRange,
},
};
logger.debug("Get account transactions processed successfully", {
...context,
accountId: params.accountId,
transactionCount: transactions.length,
totalCredit,
totalDebit,
});
return response;
}
export const registerGetAccountTransactionsTool = async (server) => {
const toolName = "get_account_transactions";
const toolDescription = "Get transactions for a specific account with optional filtering";
const registrationContext = requestContextService.createRequestContext({
operation: "RegisterTool",
toolName: toolName,
});
logger.info(`Registering tool: '${toolName}'`, registrationContext);
await ErrorHandler.tryCatch(async () => {
server.registerTool(toolName, {
title: "Get Account Transactions",
description: toolDescription,
inputSchema: GetAccountTransactionsInputSchema.shape,
outputSchema: GetAccountTransactionsResponseSchema.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 getAccountTransactionsLogic(params, handlerContext);
return {
structuredContent: result,
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (error) {
const mcpError = ErrorHandler.handleError(error, {
operation: "getAccountTransactionsHandler",
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,
});
};