UNPKG

pocketsmith-mcp

Version:

MCP server for managing budgets via PocketSmith API

178 lines (177 loc) 6.9 kB
/** * @fileoverview Create an attachment and assign it to a transaction in PocketSmith * This tool handles the two-step process: creating a user attachment then assigning it to a transaction */ 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 CreateTransactionAttachmentInputSchema = 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)"), transactionId: z .number() .int() .describe("ID of the transaction to add attachment to"), title: z .string() .optional() .describe("Title/name for the attachment (will be derived from filename if not provided)"), fileName: z .string() .optional() .describe("Original filename of the attachment"), fileData: z .string() .optional() .describe("Base64-encoded file contents (png, jpg, pdf, xls, xlsx, doc, docx)"), userId: z .number() .int() .optional() .describe("User ID (defaults to current user if not provided)"), }); export const CreateTransactionAttachmentResponseSchema = z.object({ attachment: z .object({ id: z.number(), title: z.string(), file_name: z.string().optional(), type: z.string().optional(), content_type: z.string().optional(), original_url: z.string().optional(), variants: z .object({ large_url: z.string().optional(), thumb_url: z.string().optional(), }) .optional(), created_at: z.string().optional(), updated_at: z.string().optional(), }) .describe("The created attachment"), transactionId: z .number() .describe("ID of the transaction the attachment was added to"), success: z .boolean() .describe("Whether the attachment was created successfully"), }); export async function createTransactionAttachmentLogic(params, context) { logger.debug("Processing create transaction attachment request", { ...context, toolInput: { ...params, apiKey: "[REDACTED]", accessToken: "[REDACTED]" }, }); 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 user ID if not provided let userId = params.userId; if (!userId) { const currentUser = await service.getCurrentUser(context); userId = currentUser.id ?? 0; } // Step 1: Create the attachment for the user const attachment = await service.createUserAttachment(userId, { title: params.title, file_name: params.fileName, file_data: params.fileData, }, context); // Step 2: Assign the attachment to the transaction await service.assignAttachmentToTransaction(params.transactionId, attachment.id ?? 0, context); const response = { attachment: { id: attachment.id ?? 0, title: attachment.title ?? "", file_name: attachment.file_name, type: attachment.type, content_type: attachment.content_type, original_url: attachment.original_url, variants: attachment.variants, created_at: attachment.created_at, updated_at: attachment.updated_at, }, transactionId: params.transactionId, success: true, }; logger.debug("Create transaction attachment processed successfully", { ...context, transactionId: params.transactionId, attachmentId: attachment.id, title: params.title, }); return response; } export const registerCreateTransactionAttachmentTool = async (server) => { const toolName = "create_transaction_attachment"; const toolDescription = "Add an attachment to a specific transaction for receipt or document management"; const registrationContext = requestContextService.createRequestContext({ operation: "RegisterTool", toolName: toolName, }); logger.info(`Registering tool: '${toolName}'`, registrationContext); await ErrorHandler.tryCatch(async () => { server.registerTool(toolName, { title: "Create PocketSmith Transaction Attachment", description: toolDescription, inputSchema: CreateTransactionAttachmentInputSchema.shape, outputSchema: CreateTransactionAttachmentResponseSchema.shape, annotations: { readOnlyHint: false, openWorldHint: false, }, }, async (params) => { const handlerContext = requestContextService.createRequestContext({ parentRequestId: registrationContext.requestId, operation: "HandleToolRequest", toolName: toolName, input: { ...params, apiKey: "[REDACTED]", accessToken: "[REDACTED]", }, }); try { const result = await createTransactionAttachmentLogic(params, handlerContext); return { structuredContent: result, content: [ { type: "text", text: JSON.stringify(result, null, 2) }, ], }; } catch (error) { const mcpError = ErrorHandler.handleError(error, { operation: "createTransactionAttachmentHandler", context: handlerContext, input: { ...params, apiKey: "[REDACTED]", accessToken: "[REDACTED]", }, }); 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, }); };