add transaction mcp handler
This commit is contained in:
@@ -22,9 +22,6 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
const maximumTagsCountOfTransaction = 10
|
||||
const maximumPicturesCountOfTransaction = 10
|
||||
|
||||
// TransactionsApi represents transaction api
|
||||
type TransactionsApi struct {
|
||||
ApiUsingConfig
|
||||
@@ -682,7 +679,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
|
||||
return nil, errs.ErrTransactionTagIdInvalid
|
||||
}
|
||||
|
||||
if len(tagIds) > maximumTagsCountOfTransaction {
|
||||
if len(tagIds) > models.MaximumTagsCountOfTransaction {
|
||||
return nil, errs.ErrTransactionHasTooManyTags
|
||||
}
|
||||
|
||||
@@ -693,7 +690,7 @@ func (a *TransactionsApi) TransactionCreateHandler(c *core.WebContext) (any, *er
|
||||
return nil, errs.ErrTransactionPictureIdInvalid
|
||||
}
|
||||
|
||||
if len(pictureIds) > maximumPicturesCountOfTransaction {
|
||||
if len(pictureIds) > models.MaximumPicturesCountOfTransaction {
|
||||
return nil, errs.ErrTransactionHasTooManyPictures
|
||||
}
|
||||
|
||||
@@ -812,7 +809,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
|
||||
return nil, errs.ErrTransactionTagIdInvalid
|
||||
}
|
||||
|
||||
if len(tagIds) > maximumTagsCountOfTransaction {
|
||||
if len(tagIds) > models.MaximumTagsCountOfTransaction {
|
||||
return nil, errs.ErrTransactionHasTooManyTags
|
||||
}
|
||||
|
||||
@@ -823,7 +820,7 @@ func (a *TransactionsApi) TransactionModifyHandler(c *core.WebContext) (any, *er
|
||||
return nil, errs.ErrTransactionPictureIdInvalid
|
||||
}
|
||||
|
||||
if len(pictureIds) > maximumPicturesCountOfTransaction {
|
||||
if len(pictureIds) > models.MaximumPicturesCountOfTransaction {
|
||||
return nil, errs.ErrTransactionHasTooManyPictures
|
||||
}
|
||||
|
||||
@@ -1382,7 +1379,7 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er
|
||||
return nil, errs.ErrTransactionTagIdInvalid
|
||||
}
|
||||
|
||||
if len(tagIds) > maximumTagsCountOfTransaction {
|
||||
if len(tagIds) > models.MaximumTagsCountOfTransaction {
|
||||
return nil, errs.ErrTransactionHasTooManyTags
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
const transactionTypeIncome = "income"
|
||||
const transactionTypeExpense = "expense"
|
||||
const transactionTypeTransfer = "transfer"
|
||||
|
||||
// MCPAddTransactionRequest represents all parameters of the add transaction request
|
||||
type MCPAddTransactionRequest struct {
|
||||
Type string `json:"type" jsonschema:"enum=income,enum=expense,enum=transfer" jsonschema_description:"Transaction type (income, expense, transfer)"`
|
||||
Time string `json:"time" jsonschema:"format=date-time" jsonschema_description:"Transaction time in RFC 3339 format (e.g. 2023-01-01T12:00:00Z)"`
|
||||
SecondaryCategoryName string `json:"category_name" jsonschema_description:"Secondary category name for the transaction"`
|
||||
AccountName string `json:"account_name" jsonschema_description:"Account name for the transaction"`
|
||||
Amount string `json:"amount" jsonschema_description:"Transaction amount"`
|
||||
DestinationAccountName string `json:"destination_account_name,omitempty" jsonschema_description:"Destination account name for transfer transactions (optional)"`
|
||||
DestinationAmount string `json:"destination_amount,omitempty" jsonschema_description:"Destination amount for transfer transactions (optional)"`
|
||||
Tags []string `json:"tags,omitempty" jsonschema_description:"List of tags associated with the transaction (optional, maximum 10 tags allowed)"`
|
||||
Comment string `json:"comment,omitempty" jsonschema_description:"Transaction description"`
|
||||
DryRun bool `json:"dry_run,omitempty" jsonschema_description:"If true, the transaction will not be saved, only validated (optional)"`
|
||||
}
|
||||
|
||||
// MCPAddTransactionResponse represents the response structure for add transaction
|
||||
type MCPAddTransactionResponse struct {
|
||||
Success bool `json:"success" jsonschema_description:"Indicates whether the transaction was added successfully"`
|
||||
DryRun bool `json:"dry_run,omitempty" jsonschema_description:"Indicates whether this is a dry run (transaction not saved actually)"`
|
||||
AccountBalance string `json:"account_balance,omitempty" jsonschema_description:"Account balance (or outstanding balance for debt accounts) after the transaction"`
|
||||
DestinationAccountBalance string `json:"destination_account_balance,omitempty" jsonschema_description:"Destination account balance (or outstanding balance for debt accounts) after the transaction (only for transfer transactions)"`
|
||||
}
|
||||
|
||||
type mcpAddTransactionToolHandler struct{}
|
||||
|
||||
var MCPAddTransactionToolHandler = &mcpAddTransactionToolHandler{}
|
||||
|
||||
// Name returns the name of the MCP tool
|
||||
func (h *mcpAddTransactionToolHandler) Name() string {
|
||||
return "add_transaction"
|
||||
}
|
||||
|
||||
// Description returns the description of the MCP tool
|
||||
func (h *mcpAddTransactionToolHandler) Description() string {
|
||||
return "Add a new transaction in ezBookkeeping."
|
||||
}
|
||||
|
||||
// InputType returns the input type for the MCP tool request
|
||||
func (h *mcpAddTransactionToolHandler) InputType() reflect.Type {
|
||||
return reflect.TypeOf(&MCPAddTransactionRequest{})
|
||||
}
|
||||
|
||||
// OutputType returns the output type for the MCP tool response
|
||||
func (h *mcpAddTransactionToolHandler) OutputType() reflect.Type {
|
||||
return reflect.TypeOf(&MCPAddTransactionResponse{})
|
||||
}
|
||||
|
||||
// Handle processes the MCP call tool request and returns the response
|
||||
func (h *mcpAddTransactionToolHandler) Handle(c *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) (any, []*MCPTextContent, *errs.Error) {
|
||||
var addTransactionRequest MCPAddTransactionRequest
|
||||
|
||||
if callToolReq.Arguments != nil {
|
||||
if err := json.Unmarshal(callToolReq.Arguments, &addTransactionRequest); err != nil {
|
||||
return nil, nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
} else {
|
||||
return nil, nil, errs.ErrIncompleteOrIncorrectSubmission
|
||||
}
|
||||
|
||||
if addTransactionRequest.Type == transactionTypeTransfer {
|
||||
if addTransactionRequest.DestinationAccountName == "" || addTransactionRequest.DestinationAmount == "" {
|
||||
return nil, nil, errs.ErrIncompleteOrIncorrectSubmission
|
||||
}
|
||||
}
|
||||
|
||||
if len(addTransactionRequest.Tags) > models.MaximumTagsCountOfTransaction {
|
||||
return nil, nil, errs.ErrTransactionHasTooManyTags
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := services.GetUserService().GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.Errorf(c, "[add_transaction.Handle] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
allAccounts, err := services.GetAccountService().GetAllAccountsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[add_transaction.Handle] get account error, because %s", err.Error())
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
accountsMap := services.GetAccountService().GetVisibleAccountNameMapByList(allAccounts)
|
||||
sourceAccount, exists := accountsMap[addTransactionRequest.AccountName]
|
||||
|
||||
if !exists {
|
||||
log.Warnf(c, "[add_transaction.Handle] source account \"%s\" not found for user \"uid:%d\"", addTransactionRequest.AccountName, uid)
|
||||
return nil, nil, errs.ErrSourceAccountNotFound
|
||||
}
|
||||
|
||||
var destinationAccount *models.Account
|
||||
destinationAccountId := int64(0)
|
||||
|
||||
if addTransactionRequest.Type == transactionTypeTransfer {
|
||||
destinationAccount, exists = accountsMap[addTransactionRequest.DestinationAccountName]
|
||||
|
||||
if !exists {
|
||||
log.Warnf(c, "[add_transaction.Handle] destination account \"%s\" not found for user \"uid:%d\"", addTransactionRequest.DestinationAccountName, uid)
|
||||
return nil, nil, errs.ErrDestinationAccountNotFound
|
||||
}
|
||||
|
||||
destinationAccountId = destinationAccount.AccountId
|
||||
}
|
||||
|
||||
allCategories, err := services.GetTransactionCategoryService().GetAllCategoriesByUid(c, uid, 0, -1)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[add_transaction.Handle] get transaction category error, because %s", err.Error())
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
categoriesMap := services.GetTransactionCategoryService().GetVisibleCategoryNameMapByList(allCategories)
|
||||
category, exists := categoriesMap[addTransactionRequest.SecondaryCategoryName]
|
||||
|
||||
if !exists {
|
||||
log.Warnf(c, "[add_transaction.Handle] secondary category \"%s\" not found for user \"uid:%d\"", addTransactionRequest.SecondaryCategoryName, uid)
|
||||
return nil, nil, errs.ErrTransactionCategoryNotFound
|
||||
}
|
||||
|
||||
var tagIds []int64
|
||||
|
||||
if len(addTransactionRequest.Tags) > 0 {
|
||||
allTags, err := services.GetTransactionTagService().GetAllTagsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[add_transaction.Handle] get transaction tag ids error, because %s", err.Error())
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
tagMaps := services.GetTransactionTagService().GetTagNameMapByList(allTags)
|
||||
tagIds = make([]int64, 0, len(addTransactionRequest.Tags))
|
||||
|
||||
for _, tagName := range addTransactionRequest.Tags {
|
||||
if tag, exists := tagMaps[tagName]; exists {
|
||||
tagIds = append(tagIds, tag.TagId)
|
||||
} else {
|
||||
log.Warnf(c, "[add_transaction.Handle] transaction tag \"%s\" not found for user \"uid:%d\"", tagName, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transaction := h.createNewTransactionModel(uid, &addTransactionRequest, category.CategoryId, sourceAccount.AccountId, destinationAccountId, c.ClientIP())
|
||||
transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transaction.TimezoneUtcOffset)
|
||||
|
||||
if !transactionEditable {
|
||||
return nil, nil, errs.ErrCannotCreateTransactionWithThisTransactionTime
|
||||
}
|
||||
|
||||
if !addTransactionRequest.DryRun {
|
||||
err = services.GetTransactionService().CreateTransaction(c, transaction, tagIds, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[add_transaction.Handle] failed to create transaction \"id:%d\" for user \"uid:%d\", because %s", transaction.TransactionId, uid, err.Error())
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
log.Infof(c, "[add_transaction.Handle] user \"uid:%d\" has created a new transaction \"id:%d\" successfully", uid, transaction.TransactionId)
|
||||
|
||||
accountIds := []int64{sourceAccount.AccountId}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
accountIds = append(accountIds, destinationAccountId)
|
||||
}
|
||||
|
||||
newAccounts, err := services.GetAccountService().GetAccountsByAccountIds(c, uid, accountIds)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[add_transaction.Handle] failed to get latest accounts info after transaction created, because %s", err.Error())
|
||||
}
|
||||
|
||||
structuredResponse, response, err := h.createNewMCPAddTransactionResponse(c, transaction, newAccounts, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return structuredResponse, response, nil
|
||||
} else {
|
||||
newAccounts := make(map[int64]*models.Account)
|
||||
newAccounts[sourceAccount.AccountId] = sourceAccount
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE || transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
sourceAccount.Balance -= transaction.Amount
|
||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
sourceAccount.Balance += transaction.Amount
|
||||
}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT && destinationAccount != nil {
|
||||
newAccounts[destinationAccount.AccountId] = destinationAccount
|
||||
destinationAccount.Balance += transaction.RelatedAccountAmount
|
||||
}
|
||||
|
||||
structuredResponse, response, err := h.createNewMCPAddTransactionResponse(c, transaction, newAccounts, true)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return structuredResponse, response, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *mcpAddTransactionToolHandler) createNewTransactionModel(uid int64, addTransactionRequest *MCPAddTransactionRequest, categoryId int64, sourceAccountId int64, destinationAccountId int64, clientIp string) *models.Transaction {
|
||||
var transactionDbType models.TransactionDbType
|
||||
|
||||
if addTransactionRequest.Type == transactionTypeExpense {
|
||||
transactionDbType = models.TRANSACTION_DB_TYPE_EXPENSE
|
||||
} else if addTransactionRequest.Type == transactionTypeIncome {
|
||||
transactionDbType = models.TRANSACTION_DB_TYPE_INCOME
|
||||
} else if addTransactionRequest.Type == transactionTypeTransfer {
|
||||
transactionDbType = models.TRANSACTION_DB_TYPE_TRANSFER_OUT
|
||||
}
|
||||
|
||||
transactionTime, err := utils.ParseFromLongDateTimeWithTimezoneRFC3339Format(addTransactionRequest.Time)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
amount, err := utils.ParseAmount(addTransactionRequest.Amount)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
transaction := &models.Transaction{
|
||||
Uid: uid,
|
||||
Type: transactionDbType,
|
||||
CategoryId: categoryId,
|
||||
TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()),
|
||||
TimezoneUtcOffset: utils.GetTimezoneOffsetMinutes(transactionTime.Location()),
|
||||
AccountId: sourceAccountId,
|
||||
Amount: amount,
|
||||
HideAmount: false,
|
||||
Comment: addTransactionRequest.Comment,
|
||||
CreatedIp: clientIp,
|
||||
}
|
||||
|
||||
if addTransactionRequest.Type == transactionTypeTransfer {
|
||||
transaction.RelatedAccountId = destinationAccountId
|
||||
|
||||
destinationAmount, err := utils.ParseAmount(addTransactionRequest.DestinationAmount)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
transaction.RelatedAccountAmount = destinationAmount
|
||||
}
|
||||
|
||||
return transaction
|
||||
}
|
||||
|
||||
func (h *mcpAddTransactionToolHandler) createNewMCPAddTransactionResponse(c *core.WebContext, transaction *models.Transaction, accountsMap map[int64]*models.Account, dryRun bool) (any, []*MCPTextContent, error) {
|
||||
var sourceAccountInfo *models.AccountInfoResponse
|
||||
var destinationAccountInfo *models.AccountInfoResponse
|
||||
|
||||
if sourceAccount, exists := accountsMap[transaction.AccountId]; exists {
|
||||
sourceAccountInfo = sourceAccount.ToAccountInfoResponse()
|
||||
}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
if destinationAccount, exists := accountsMap[transaction.RelatedAccountId]; exists {
|
||||
destinationAccountInfo = destinationAccount.ToAccountInfoResponse()
|
||||
}
|
||||
}
|
||||
|
||||
response := MCPAddTransactionResponse{
|
||||
Success: true,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
|
||||
if sourceAccountInfo != nil {
|
||||
if sourceAccountInfo.IsAsset {
|
||||
response.AccountBalance = utils.FormatAmount(sourceAccountInfo.Balance)
|
||||
} else if sourceAccountInfo.IsLiability {
|
||||
response.AccountBalance = utils.FormatAmount(-sourceAccountInfo.Balance)
|
||||
}
|
||||
}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT && destinationAccountInfo != nil {
|
||||
if destinationAccountInfo.IsAsset {
|
||||
response.DestinationAccountBalance = utils.FormatAmount(destinationAccountInfo.Balance)
|
||||
} else if destinationAccountInfo.IsLiability {
|
||||
response.DestinationAccountBalance = utils.FormatAmount(-destinationAccountInfo.Balance)
|
||||
}
|
||||
}
|
||||
|
||||
content, err := json.Marshal(response)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return response, []*MCPTextContent{
|
||||
NewMCPTextContent(string(content)),
|
||||
}, nil
|
||||
}
|
||||
@@ -69,6 +69,8 @@ func InitializeMCPHandlers(config *settings.Config) error {
|
||||
mcpTools: make([]*MCPTool, 0),
|
||||
}
|
||||
|
||||
registerMCPTextContentToolHandler(container, MCPAddTransactionToolHandler)
|
||||
registerMCPTextContentToolHandler(container, MCPQueryTransactionsToolHandler)
|
||||
registerMCPTextContentToolHandler(container, MCPQueryAllAccountsToolHandler)
|
||||
registerMCPTextContentToolHandler(container, MCPQueryAllTransactionCategoriesToolHandler)
|
||||
registerMCPTextContentToolHandler(container, MCPQueryAllTransactionTagsToolHandler)
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// MCPQueryTransactionsRequest represents all parameters of the query transactions request
|
||||
type MCPQueryTransactionsRequest struct {
|
||||
StartTime string `json:"start_time" jsonschema:"format=date-time" jsonschema_description:"Start time for the query in RFC 3339 format (e.g. 2023-01-01T12:00:00Z)"`
|
||||
EndTime string `json:"end_time" jsonschema:"format=date-time" jsonschema_description:"End time for the query in RFC 3339 format or (e.g. 2023-01-01T12:00:00Z)"`
|
||||
Type string `json:"type,omitempty" jsonschema:"enum=income,enum=expense,enum=transfer" jsonschema_description:"Transaction type to filter by (income, expense, transfer) (optional)"`
|
||||
SecondaryCategoryName string `json:"category_name,omitempty" jsonschema_description:"Secondary category name to filter transactions by (optional)"`
|
||||
AccountName string `json:"account_name,omitempty" jsonschema_description:"Account name to filter transactions by (optional)"`
|
||||
Keyword string `json:"keyword,omitempty" jsonschema_description:"Keyword to search in transaction description (optional)"`
|
||||
Count int32 `json:"count,omitempty" jsonschema:"default=100" jsonschema_description:"Maximum number of results to return (default: 100)"`
|
||||
Page int32 `json:"page,omitempty" jsonschema:"default=1" jsonschema_description:"Page number for pagination (default: 1)"`
|
||||
ResponseFields string `json:"response_fields,omitempty" jsonschema_description:"Comma-separated list of fields to include in the response (optional, leave empty for all fields, available fields: time, currency, category_name, account_name, comment)"`
|
||||
}
|
||||
|
||||
// MCPQueryTransactionsResponse represents the response structure for querying transactions
|
||||
type MCPQueryTransactionsResponse struct {
|
||||
TotalCount int64 `json:"total_count" jsonschema_description:"Total number of transactions matching the query"`
|
||||
CurrentPage int32 `json:"current_page" jsonschema_description:"Current page number of the results"`
|
||||
TotalPage int32 `json:"total_page" jsonschema_description:"Total number of pages available for the query, calculated based on total_count and count"`
|
||||
Transactions []*MCPTransactionInfo `json:"transactions" jsonschema_description:"List of transactions matching the query"`
|
||||
}
|
||||
|
||||
// MCPTransactionInfo defines the structure of transaction information
|
||||
type MCPTransactionInfo struct {
|
||||
Time string `json:"time,omitempty" jsonschema_description:"Time of the transaction in RFC 3339 format (e.g. 2023-01-01T12:00:00Z)"`
|
||||
Type string `json:"type" jsonschema:"enum=income,enum=expense,enum=transfer" jsonschema_description:"Transaction type (income, expense, transfer)"`
|
||||
Amount string `json:"amount" jsonschema_description:"Amount of the transaction in the specified currency"`
|
||||
Currency string `json:"currency,omitempty" jsonschema_description:"Currency code of the transaction (e.g. USD, EUR)"`
|
||||
SecondaryCategoryName string `json:"category_name,omitempty" jsonschema_description:"Secondary category name for the transaction"`
|
||||
AccountName string `json:"account_name,omitempty" jsonschema_description:"Account name for the transaction"`
|
||||
DestinationAmount string `json:"destination_amount,omitempty" jsonschema_description:"Destination amount for transfer transactions (optional)"`
|
||||
DestinationCurrency string `json:"destination_currency,omitempty" jsonschema_description:"Currency code of the destination amount for transfer transactions (optional)"`
|
||||
DestinationAccountName string `json:"destination_account_name,omitempty" jsonschema_description:"Destination account name for transfer transactions (optional)"`
|
||||
Comment string `json:"comment,omitempty" jsonschema_description:"Description of the transaction"`
|
||||
}
|
||||
|
||||
type mcpQueryTransactionsToolHandler struct{}
|
||||
|
||||
var MCPQueryTransactionsToolHandler = &mcpQueryTransactionsToolHandler{}
|
||||
|
||||
// Name returns the name of the MCP tool
|
||||
func (h *mcpQueryTransactionsToolHandler) Name() string {
|
||||
return "query_transactions"
|
||||
}
|
||||
|
||||
// Description returns the description of the MCP tool
|
||||
func (h *mcpQueryTransactionsToolHandler) Description() string {
|
||||
return "Query transactions based on various filters."
|
||||
}
|
||||
|
||||
// InputType returns the input type for the MCP tool request
|
||||
func (h *mcpQueryTransactionsToolHandler) InputType() reflect.Type {
|
||||
return reflect.TypeOf(&MCPQueryTransactionsRequest{})
|
||||
}
|
||||
|
||||
// OutputType returns the output type for the MCP tool response
|
||||
func (h *mcpQueryTransactionsToolHandler) OutputType() reflect.Type {
|
||||
return reflect.TypeOf(&MCPQueryTransactionsResponse{})
|
||||
}
|
||||
|
||||
// Handle processes the MCP call tool request and returns the response
|
||||
func (h *mcpQueryTransactionsToolHandler) Handle(c *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) (any, []*MCPTextContent, *errs.Error) {
|
||||
var queryTransactionsRequest MCPQueryTransactionsRequest
|
||||
|
||||
if callToolReq.Arguments != nil {
|
||||
if err := json.Unmarshal(callToolReq.Arguments, &queryTransactionsRequest); err != nil {
|
||||
return nil, nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||
}
|
||||
} else {
|
||||
return nil, nil, errs.ErrIncompleteOrIncorrectSubmission
|
||||
}
|
||||
|
||||
uid := c.GetCurrentUid()
|
||||
maxTime, err := utils.ParseFromLongDateTimeWithTimezoneRFC3339Format(queryTransactionsRequest.EndTime)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, errs.ErrIncompleteOrIncorrectSubmission
|
||||
}
|
||||
|
||||
minTime, err := utils.ParseFromLongDateTimeWithTimezoneRFC3339Format(queryTransactionsRequest.StartTime)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, errs.ErrIncompleteOrIncorrectSubmission
|
||||
}
|
||||
|
||||
maxTransactionTime := utils.GetMaxTransactionTimeFromUnixTime(maxTime.Unix())
|
||||
minTransactionTime := utils.GetMinTransactionTimeFromUnixTime(minTime.Unix())
|
||||
|
||||
if queryTransactionsRequest.Count <= 0 {
|
||||
queryTransactionsRequest.Count = 100
|
||||
}
|
||||
|
||||
if queryTransactionsRequest.Page <= 0 {
|
||||
queryTransactionsRequest.Page = 1
|
||||
}
|
||||
|
||||
transactionType := models.TransactionType(byte(0))
|
||||
|
||||
if queryTransactionsRequest.Type == transactionTypeExpense {
|
||||
transactionType = models.TRANSACTION_TYPE_EXPENSE
|
||||
} else if queryTransactionsRequest.Type == transactionTypeIncome {
|
||||
transactionType = models.TRANSACTION_TYPE_INCOME
|
||||
} else if queryTransactionsRequest.Type == transactionTypeTransfer {
|
||||
transactionType = models.TRANSACTION_TYPE_TRANSFER
|
||||
}
|
||||
|
||||
allAccounts, err := services.GetAccountService().GetAllAccountsByUid(c, uid)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[add_transaction.Handle] get account error, because %s", err.Error())
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
accountsMap := services.GetAccountService().GetVisibleAccountNameMapByList(allAccounts)
|
||||
filterAccountIds := make([]int64, 0)
|
||||
|
||||
if queryTransactionsRequest.AccountName != "" {
|
||||
if account, exists := accountsMap[queryTransactionsRequest.AccountName]; exists {
|
||||
filterAccountIds = append(filterAccountIds, account.AccountId)
|
||||
} else {
|
||||
return nil, nil, errs.ErrAccountNotFound
|
||||
}
|
||||
}
|
||||
|
||||
allCategories, err := services.GetTransactionCategoryService().GetAllCategoriesByUid(c, uid, 0, -1)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf(c, "[add_transaction.Handle] get transaction category error, because %s", err.Error())
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
categoriesMap := services.GetTransactionCategoryService().GetVisibleCategoryNameMapByList(allCategories)
|
||||
filterCategoryIds := make([]int64, 0)
|
||||
|
||||
if queryTransactionsRequest.SecondaryCategoryName != "" {
|
||||
if category, exists := categoriesMap[queryTransactionsRequest.SecondaryCategoryName]; exists {
|
||||
filterCategoryIds = append(filterCategoryIds, category.CategoryId)
|
||||
} else {
|
||||
return nil, nil, errs.ErrTransactionCategoryNotFound
|
||||
}
|
||||
}
|
||||
|
||||
totalCount, err := services.GetTransactionService().GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", queryTransactionsRequest.Keyword)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
transactions, err := services.GetTransactionService().GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, models.TRANSACTION_TAG_FILTER_HAS_ANY, "", queryTransactionsRequest.Keyword, queryTransactionsRequest.Page, queryTransactionsRequest.Count, false, true)
|
||||
structuredResponse, response, err := h.createNewMCPQueryTransactionsResponse(c, &queryTransactionsRequest, transactions, totalCount, services.GetAccountService().GetAccountMapByList(allAccounts), services.GetTransactionCategoryService().GetCategoryMapByList(allCategories))
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
return structuredResponse, response, nil
|
||||
}
|
||||
|
||||
func (h *mcpQueryTransactionsToolHandler) createNewMCPQueryTransactionsResponse(c *core.WebContext, queryTransactionsRequest *MCPQueryTransactionsRequest, transactions []*models.Transaction, totalCount int64, accountsMap map[int64]*models.Account, categoriesMap map[int64]*models.TransactionCategory) (any, []*MCPTextContent, error) {
|
||||
response := MCPQueryTransactionsResponse{
|
||||
TotalCount: totalCount,
|
||||
CurrentPage: queryTransactionsRequest.Page,
|
||||
TotalPage: int32((totalCount + int64(queryTransactionsRequest.Count) - 1) / int64(queryTransactionsRequest.Count)),
|
||||
Transactions: make([]*MCPTransactionInfo, 0, len(transactions)),
|
||||
}
|
||||
|
||||
filteredFields := make(map[string]bool)
|
||||
|
||||
if queryTransactionsRequest.ResponseFields != "" {
|
||||
for _, field := range strings.Split(queryTransactionsRequest.ResponseFields, ",") {
|
||||
filteredFields[field] = true
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(transactions); i++ {
|
||||
transaction := transactions[i]
|
||||
transactionInfo := MCPTransactionInfo{
|
||||
Amount: utils.FormatAmount(transaction.Amount),
|
||||
}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
transactionInfo.Type = transactionTypeExpense
|
||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
transactionInfo.Type = transactionTypeIncome
|
||||
} else if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
transactionInfo.Type = transactionTypeTransfer
|
||||
}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
transactionInfo.DestinationAmount = utils.FormatAmount(transaction.RelatedAccountAmount)
|
||||
}
|
||||
|
||||
if _, exists := filteredFields["time"]; exists || len(filteredFields) == 0 {
|
||||
transactionUnixTime := utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime)
|
||||
transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60)
|
||||
transactionInfo.Time = utils.FormatUnixTimeToLongDateTimeWithTimezoneRFC3339Format(transactionUnixTime, transactionTimeZone)
|
||||
}
|
||||
|
||||
if _, exists := filteredFields["currency"]; exists || len(filteredFields) == 0 {
|
||||
if account, exists := accountsMap[transaction.AccountId]; exists && account != nil {
|
||||
transactionInfo.Currency = account.Currency
|
||||
}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT && transaction.RelatedAccountId > 0 {
|
||||
if destinationAccount, exists := accountsMap[transaction.RelatedAccountId]; exists && destinationAccount != nil {
|
||||
transactionInfo.DestinationCurrency = destinationAccount.Currency
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := filteredFields["category_name"]; exists || len(filteredFields) == 0 {
|
||||
if category, exists := categoriesMap[transaction.CategoryId]; exists && category != nil {
|
||||
transactionInfo.SecondaryCategoryName = category.Name
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := filteredFields["account_name"]; exists || len(filteredFields) == 0 {
|
||||
if account, exists := accountsMap[transaction.AccountId]; exists && account != nil {
|
||||
transactionInfo.AccountName = account.Name
|
||||
}
|
||||
|
||||
if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT && transaction.RelatedAccountId > 0 {
|
||||
if destinationAccount, exists := accountsMap[transaction.RelatedAccountId]; exists && destinationAccount != nil {
|
||||
transactionInfo.DestinationAccountName = destinationAccount.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := filteredFields["comment"]; exists || len(filteredFields) == 0 {
|
||||
transactionInfo.Comment = transaction.Comment
|
||||
}
|
||||
|
||||
response.Transactions = append(response.Transactions, &transactionInfo)
|
||||
}
|
||||
|
||||
content, err := json.Marshal(response)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return response, []*MCPTextContent{
|
||||
NewMCPTextContent(string(content)),
|
||||
}, nil
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
const MaximumTagsCountOfTransaction = 10
|
||||
const MaximumPicturesCountOfTransaction = 10
|
||||
|
||||
// TransactionType represents transaction type
|
||||
type TransactionType byte
|
||||
|
||||
|
||||
@@ -514,6 +514,23 @@ func (s *TransactionCategoryService) GetVisibleSubCategoryNameMapByList(categori
|
||||
return expenseCategoryMap, incomeCategoryMap, transferCategoryMap
|
||||
}
|
||||
|
||||
// GetVisibleCategoryNameMapByList returns visible transaction category map by a list
|
||||
func (s *TransactionCategoryService) GetVisibleCategoryNameMapByList(categories []*models.TransactionCategory) map[string]*models.TransactionCategory {
|
||||
categoryMap := make(map[string]*models.TransactionCategory)
|
||||
|
||||
for i := 0; i < len(categories); i++ {
|
||||
category := categories[i]
|
||||
|
||||
if category.Hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
categoryMap[category.Name] = category
|
||||
}
|
||||
|
||||
return categoryMap
|
||||
}
|
||||
|
||||
// GetCategoryNames returns a list with transaction category names from transaction category models list
|
||||
func (s *TransactionCategoryService) GetCategoryNames(categories []*models.TransactionCategory) []string {
|
||||
categoryNames := make([]string, len(categories))
|
||||
|
||||
Reference in New Issue
Block a user