pocketsmith-mcp
Version:
MCP server for managing budgets via PocketSmith API
307 lines (306 loc) • 13.1 kB
JavaScript
/**
* @fileoverview PocketSmith API service integration
* This module provides a service wrapper around the PocketSmith TypeScript client
* for use within the MCP server environment.
*/
import { createPocketSmithClient } from 'pocketsmith-ts';
import { logger } from '../utils/index.js';
import { BaseErrorCode, McpError } from '../types-global/errors.js';
export class PocketSmithService {
formatError(error) {
if (typeof error === 'string')
return error;
if (error && typeof error === 'object') {
return JSON.stringify(error, null, 2);
}
return String(error);
}
constructor(apiKey, accessToken) {
if (!apiKey && !accessToken) {
throw new McpError(BaseErrorCode.INITIALIZATION_FAILED, 'Either apiKey or accessToken must be provided');
}
this.client = createPocketSmithClient({
apiKey,
accessToken,
});
}
async getCurrentUser(context) {
logger.debug('Fetching current user', context);
const { data, error } = await this.client.GET('/me');
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch user: ${error}`);
}
return data;
}
async getAccounts(userId, context) {
logger.debug('Fetching user accounts', { ...context, userId });
const { data, error } = await this.client.GET('/users/{id}/accounts', {
params: { path: { id: userId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch accounts: ${error}`);
}
return data;
}
async getTransactions(userId, context, options) {
logger.debug('Fetching transactions', { ...context, userId, options });
const { data, error } = await this.client.GET('/users/{id}/transactions', {
params: {
path: { id: userId },
query: {
start_date: options?.startDate,
end_date: options?.endDate,
search: options?.search,
type: options?.type,
limit: options?.limit,
}
}
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch transactions: ${error}`);
}
return data;
}
async createTransaction(accountId, transaction, context) {
logger.debug('Creating transaction', { ...context, accountId, transaction });
const { data, error } = await this.client.POST('/transaction_accounts/{id}/transactions', {
params: { path: { id: accountId } },
body: transaction
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to create transaction: ${error}`);
}
return data;
}
async getTransaction(transactionId, context) {
logger.debug('Fetching transaction', { ...context, transactionId });
const { data, error } = await this.client.GET('/transactions/{id}', {
params: { path: { id: transactionId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch transaction: ${error}`);
}
return data;
}
async updateTransaction(transactionId, updates, context) {
logger.debug('Updating transaction', { ...context, transactionId, updates });
const { data, error } = await this.client.PUT('/transactions/{id}', {
params: { path: { id: transactionId } },
body: updates
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to update transaction: ${error}`);
}
return data;
}
async deleteTransaction(transactionId, context) {
logger.debug('Deleting transaction', { ...context, transactionId });
const { data, error } = await this.client.DELETE('/transactions/{id}', {
params: { path: { id: transactionId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to delete transaction: ${error}`);
}
return data;
}
async createCategory(userId, category, context) {
logger.debug('Creating category', { ...context, userId, category });
const { data, error } = await this.client.POST('/users/{id}/categories', {
params: { path: { id: userId } },
body: category
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to create category: ${error}`);
}
return data;
}
async updateCategory(categoryId, updates, context) {
logger.debug('Updating category', { ...context, categoryId, updates });
const { data, error } = await this.client.PUT('/categories/{id}', {
params: { path: { id: categoryId } },
body: updates
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to update category: ${error}`);
}
return data;
}
async getCategories(userId, context) {
logger.debug('Fetching categories', { ...context, userId });
const { data, error } = await this.client.GET('/users/{id}/categories', {
params: { path: { id: userId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch categories: ${error}`);
}
return data;
}
async getBudgets(userId, context) {
logger.debug('Fetching budgets', { ...context, userId });
const { data, error } = await this.client.GET('/users/{id}/budget', {
params: { path: { id: userId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch budgets: ${error}`);
}
return data;
}
async getBudgetSummary(userId, options, context) {
logger.debug('Fetching budget summary', { ...context, userId, options });
const { data, error } = await this.client.GET('/users/{id}/budget_summary', {
params: {
path: { id: userId },
query: {
period: options.period,
interval: options.interval,
start_date: options.startDate,
end_date: options.endDate,
}
}
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch budget summary: ${error}`);
}
return data;
}
async getAccountTransactions(accountId, context, options) {
logger.debug('Fetching account transactions', { ...context, accountId, options });
const { data, error } = await this.client.GET('/transaction_accounts/{id}/transactions', {
params: {
path: { id: accountId },
query: options?.startDate || options?.endDate || options?.search || options?.limit ? {
start_date: options?.startDate,
end_date: options?.endDate,
search: options?.search,
limit: options?.limit,
} : undefined
}
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch account transactions: ${this.formatError(error)}`);
}
return data;
}
async getCategoryTransactions(categoryIds, context, options) {
const categoryIdString = Array.isArray(categoryIds) ? categoryIds.join(',') : String(categoryIds);
logger.debug('Fetching category transactions', { ...context, categoryIds, options });
const { data, error } = await this.client.GET('/categories/{id}/transactions', {
params: {
path: { id: categoryIdString },
query: options?.startDate || options?.endDate || options?.page ? {
start_date: options?.startDate,
end_date: options?.endDate,
page: options?.page,
} : undefined
}
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch category transactions: ${error}`);
}
return data;
}
async getTransactionAttachments(transactionId, context) {
logger.debug('Fetching transaction attachments', { ...context, transactionId });
const { data, error } = await this.client.GET('/transactions/{id}/attachments', {
params: { path: { id: transactionId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch transaction attachments: ${error}`);
}
return data;
}
async getUserAttachments(userId, context) {
logger.debug('Fetching user attachments', { ...context, userId });
const { data, error } = await this.client.GET('/users/{id}/attachments', {
params: { path: { id: userId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch user attachments: ${error}`);
}
return data;
}
async createUserAttachment(userId, attachment, context) {
logger.debug('Creating user attachment', { ...context, userId, attachment: { ...attachment, file_data: attachment.file_data ? '[REDACTED]' : undefined } });
const { data, error } = await this.client.POST('/users/{id}/attachments', {
params: { path: { id: userId } },
body: attachment
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to create user attachment: ${error}`);
}
return data;
}
async assignAttachmentToTransaction(transactionId, attachmentId, context) {
logger.debug('Assigning attachment to transaction', { ...context, transactionId, attachmentId });
const { data, error } = await this.client.POST('/transactions/{id}/attachments', {
params: { path: { id: transactionId } },
body: { attachment_id: attachmentId }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to assign attachment to transaction: ${error}`);
}
return data;
}
async getRecurringEvents(userId, startDate, endDate, context) {
logger.debug('Fetching recurring events', { ...context, userId, startDate, endDate });
const { data, error } = await this.client.GET('/users/{id}/events', {
params: {
path: { id: userId },
query: { start_date: startDate, end_date: endDate }
}
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch recurring events: ${error}`);
}
return data;
}
async getCategoryRules(userId, context) {
logger.debug('Fetching category rules', { ...context, userId });
const { data, error } = await this.client.GET('/users/{id}/category_rules', {
params: { path: { id: userId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch category rules: ${error}`);
}
return data;
}
async createCategoryRule(categoryId, rule, context) {
logger.debug('Creating category rule', { ...context, categoryId, rule });
const { data, error } = await this.client.POST('/categories/{id}/category_rules', {
params: { path: { id: categoryId } },
body: rule
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to create category rule: ${error}`);
}
return data;
}
async getInstitutions(userId, context) {
logger.debug('Fetching institutions', { ...context, userId });
const { data, error } = await this.client.GET('/users/{id}/institutions', {
params: { path: { id: userId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch institutions: ${error}`);
}
return data;
}
async getTransactionAccounts(userId, context) {
logger.debug('Fetching transaction accounts', { ...context, userId });
const { data, error } = await this.client.GET('/users/{id}/transaction_accounts', {
params: { path: { id: userId } }
});
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch transaction accounts: ${error}`);
}
return data;
}
async getCurrencies(context) {
logger.debug('Fetching currencies', context);
const { data, error } = await this.client.GET('/currencies');
if (error) {
throw new McpError(BaseErrorCode.REQUEST_FAILED, `Failed to fetch currencies: ${error}`);
}
return data;
}
}