diff --git a/pkg/mcp/mcp_container.go b/pkg/mcp/mcp_container.go index f5f17105..7aed16c5 100644 --- a/pkg/mcp/mcp_container.go +++ b/pkg/mcp/mcp_container.go @@ -69,6 +69,9 @@ func InitializeMCPHandlers(config *settings.Config) error { mcpTools: make([]*MCPTool, 0), } + registerMCPTextContentToolHandler(container, MCPQueryAllAccountsToolHandler) + registerMCPTextContentToolHandler(container, MCPQueryAllTransactionCategoriesToolHandler) + registerMCPTextContentToolHandler(container, MCPQueryAllTransactionTagsToolHandler) registerMCPTextContentToolHandler(container, MCPQueryLatestExchangeRatesToolHandler) Container = container diff --git a/pkg/mcp/query_all_accounts.go b/pkg/mcp/query_all_accounts.go new file mode 100644 index 00000000..1a1639f7 --- /dev/null +++ b/pkg/mcp/query_all_accounts.go @@ -0,0 +1,146 @@ +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" +) + +// MCPQueryAllAccountsResponse represents the response structure for querying accounts +type MCPQueryAllAccountsResponse struct { + CashAccounts []string `json:"cashAccounts,omitempty" jsonschema_description:"List of cash account names"` + CheckingAccounts []string `json:"checkingAccounts,omitempty" jsonschema_description:"List of checking account names"` + SavingsAccounts []string `json:"savingsAccounts,omitempty" jsonschema_description:"List of savings account names"` + CreditCardAccounts []string `json:"creditCardAccounts,omitempty" jsonschema_description:"List of credit card account names"` + VirtualAccounts []string `json:"virtualAccounts,omitempty" jsonschema_description:"List of virtual account names"` + DebtAccounts []string `json:"debtAccounts,omitempty" jsonschema_description:"List of debt account names"` + ReceivableAccounts []string `json:"receivableAccounts,omitempty" jsonschema_description:"List of receivable account names"` + CertificateOfDepositAccounts []string `json:"certificateOfDepositAccounts,omitempty" jsonschema_description:"List of certificate of deposit account names"` + InvestmentAccounts []string `json:"investmentAccounts,omitempty" jsonschema_description:"List of investment account names"` +} + +type mcpQueryAllAccountsToolHandler struct{} + +var MCPQueryAllAccountsToolHandler = &mcpQueryAllAccountsToolHandler{} + +// Name returns the name of the MCP tool +func (h *mcpQueryAllAccountsToolHandler) Name() string { + return "query_all_accounts" +} + +// Description returns the description of the MCP tool +func (h *mcpQueryAllAccountsToolHandler) Description() string { + return "Query all accounts for the current user in ezBookkeeping." +} + +// InputType returns the input type for the MCP tool request +func (h *mcpQueryAllAccountsToolHandler) InputType() reflect.Type { + return nil +} + +// OutputType returns the output type for the MCP tool response +func (h *mcpQueryAllAccountsToolHandler) OutputType() reflect.Type { + return reflect.TypeOf(&MCPQueryAllAccountsResponse{}) +} + +// Handle processes the MCP call tool request and returns the response +func (h *mcpQueryAllAccountsToolHandler) Handle(c *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) (any, []*MCPTextContent, *errs.Error) { + uid := c.GetCurrentUid() + accounts, err := services.GetAccountService().GetAllAccountsByUid(c, uid) + + if err != nil { + log.Errorf(c, "[query_all_accounts.Handle] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, nil, errs.Or(err, errs.ErrOperationFailed) + } + + structuredResponse, response, err := h.createNewMCPQueryAllAccountsResponse(c, accounts) + + if err != nil { + return nil, nil, errs.Or(err, errs.ErrOperationFailed) + } + + return structuredResponse, response, nil +} + +func (h *mcpQueryAllAccountsToolHandler) createNewMCPQueryAllAccountsResponse(c *core.WebContext, accounts []*models.Account) (any, []*MCPTextContent, error) { + response := MCPQueryAllAccountsResponse{} + + for i := 0; i < len(accounts); i++ { + account := accounts[i] + + if account.Hidden || (account.Type == models.ACCOUNT_TYPE_MULTI_SUB_ACCOUNTS && account.ParentAccountId == models.LevelOneAccountParentId) { + continue + } + + if account.Category == models.ACCOUNT_CATEGORY_CASH { + if response.CashAccounts == nil { + response.CashAccounts = make([]string, 0) + } + + response.CashAccounts = append(response.CashAccounts, account.Name) + } else if account.Category == models.ACCOUNT_CATEGORY_CHECKING_ACCOUNT { + if response.CheckingAccounts == nil { + response.CheckingAccounts = make([]string, 0) + } + + response.CheckingAccounts = append(response.CheckingAccounts, account.Name) + } else if account.Category == models.ACCOUNT_CATEGORY_SAVINGS_ACCOUNT { + if response.SavingsAccounts == nil { + response.SavingsAccounts = make([]string, 0) + } + + response.SavingsAccounts = append(response.SavingsAccounts, account.Name) + } else if account.Category == models.ACCOUNT_CATEGORY_CREDIT_CARD { + if response.CreditCardAccounts == nil { + response.CreditCardAccounts = make([]string, 0) + } + + response.CreditCardAccounts = append(response.CreditCardAccounts, account.Name) + } else if account.Category == models.ACCOUNT_CATEGORY_VIRTUAL { + if response.VirtualAccounts == nil { + response.VirtualAccounts = make([]string, 0) + } + + response.VirtualAccounts = append(response.VirtualAccounts, account.Name) + } else if account.Category == models.ACCOUNT_CATEGORY_DEBT { + if response.DebtAccounts == nil { + response.DebtAccounts = make([]string, 0) + } + + response.DebtAccounts = append(response.DebtAccounts, account.Name) + } else if account.Category == models.ACCOUNT_CATEGORY_RECEIVABLES { + if response.ReceivableAccounts == nil { + response.ReceivableAccounts = make([]string, 0) + } + + response.ReceivableAccounts = append(response.ReceivableAccounts, account.Name) + } else if account.Category == models.ACCOUNT_CATEGORY_CERTIFICATE_OF_DEPOSIT { + if response.CertificateOfDepositAccounts == nil { + response.CertificateOfDepositAccounts = make([]string, 0) + } + + response.CertificateOfDepositAccounts = append(response.CertificateOfDepositAccounts, account.Name) + } else if account.Category == models.ACCOUNT_CATEGORY_INVESTMENT { + if response.InvestmentAccounts == nil { + response.InvestmentAccounts = make([]string, 0) + } + + response.InvestmentAccounts = append(response.InvestmentAccounts, account.Name) + } + } + + content, err := json.Marshal(response) + + if err != nil { + return nil, nil, err + } + + return response, []*MCPTextContent{ + NewMCPTextContent(string(content)), + }, nil +} diff --git a/pkg/mcp/query_all_transaction_categories.go b/pkg/mcp/query_all_transaction_categories.go new file mode 100644 index 00000000..b01f5a54 --- /dev/null +++ b/pkg/mcp/query_all_transaction_categories.go @@ -0,0 +1,135 @@ +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" +) + +// MCPQueryAllTransactionCategoriesResponse represents the response structure for querying transaction categories +type MCPQueryAllTransactionCategoriesResponse struct { + IncomeCategories map[string][]string `json:"incomeCategories" jsonschema_description:"List of income categories, field key is the primary category name, field value is the list of secondary category names"` + ExpenseCategories map[string][]string `json:"expenseCategories" jsonschema_description:"List of expense categories, field key is the primary category name, field value is the list of secondary category names"` + TransferCategories map[string][]string `json:"transferCategories" jsonschema_description:"List of transfer categories, field key is the primary category name, field value is the list of secondary category names"` +} + +type mcpQueryAllTransactionCategoriesToolHandler struct{} + +var MCPQueryAllTransactionCategoriesToolHandler = &mcpQueryAllTransactionCategoriesToolHandler{} + +// Name returns the name of the MCP tool +func (h *mcpQueryAllTransactionCategoriesToolHandler) Name() string { + return "query_all_transaction_categories" +} + +// Description returns the description of the MCP tool +func (h *mcpQueryAllTransactionCategoriesToolHandler) Description() string { + return "Query all transaction categories for the current user in ezBookkeeping." +} + +// InputType returns the input type for the MCP tool request +func (h *mcpQueryAllTransactionCategoriesToolHandler) InputType() reflect.Type { + return nil +} + +// OutputType returns the output type for the MCP tool response +func (h *mcpQueryAllTransactionCategoriesToolHandler) OutputType() reflect.Type { + return reflect.TypeOf(&MCPQueryAllTransactionCategoriesResponse{}) +} + +// Handle processes the MCP call tool request and returns the response +func (h *mcpQueryAllTransactionCategoriesToolHandler) Handle(c *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) (any, []*MCPTextContent, *errs.Error) { + uid := c.GetCurrentUid() + categories, err := services.GetTransactionCategoryService().GetAllCategoriesByUid(c, uid, 0, -1) + + if err != nil { + log.Errorf(c, "[query_all_transaction_categories.Handle] failed to get categories for user \"uid:%d\", because %s", uid, err.Error()) + return nil, nil, errs.Or(err, errs.ErrOperationFailed) + } + + structuredResponse, response, err := h.createNewMCPQueryAllTransactionCategoriesResponse(c, categories) + + if err != nil { + return nil, nil, errs.Or(err, errs.ErrOperationFailed) + } + + return structuredResponse, response, nil +} + +func (h *mcpQueryAllTransactionCategoriesToolHandler) createNewMCPQueryAllTransactionCategoriesResponse(c *core.WebContext, categories []*models.TransactionCategory) (any, []*MCPTextContent, error) { + response := MCPQueryAllTransactionCategoriesResponse{ + IncomeCategories: make(map[string][]string), + ExpenseCategories: make(map[string][]string), + TransferCategories: make(map[string][]string), + } + + categoriesMap := make(map[int64]*models.TransactionCategory, len(categories)) + + for i := 0; i < len(categories); i++ { + category := categories[i] + + if !category.Hidden { + categoriesMap[category.CategoryId] = category + } + } + + for i := 0; i < len(categories); i++ { + category := categories[i] + + if category.Hidden || category.ParentCategoryId == models.LevelOneTransactionCategoryParentId { + continue + } + + parentCategory, exists := categoriesMap[category.ParentCategoryId] + + if !exists || parentCategory == nil { + log.Warnf(c, "[query_all_transaction_categories.createNewMCPQueryAllTransactionCategoriesResponse] category \"id:%d\" has no parent category", category.CategoryId) + continue + } + + if parentCategory.Hidden { + continue + } + + if category.Type == models.CATEGORY_TYPE_INCOME { + _, exists := response.IncomeCategories[parentCategory.Name] + + if !exists { + response.IncomeCategories[parentCategory.Name] = make([]string, 0) + } + + response.IncomeCategories[parentCategory.Name] = append(response.IncomeCategories[parentCategory.Name], category.Name) + } else if category.Type == models.CATEGORY_TYPE_EXPENSE { + _, exists := response.ExpenseCategories[parentCategory.Name] + + if !exists { + response.ExpenseCategories[parentCategory.Name] = make([]string, 0) + } + + response.ExpenseCategories[parentCategory.Name] = append(response.ExpenseCategories[parentCategory.Name], category.Name) + } else if category.Type == models.CATEGORY_TYPE_TRANSFER { + _, exists := response.TransferCategories[parentCategory.Name] + + if !exists { + response.TransferCategories[parentCategory.Name] = make([]string, 0) + } + + response.TransferCategories[parentCategory.Name] = append(response.TransferCategories[parentCategory.Name], category.Name) + } + } + + content, err := json.Marshal(response) + + if err != nil { + return nil, nil, err + } + + return response, []*MCPTextContent{ + NewMCPTextContent(string(content)), + }, nil +} diff --git a/pkg/mcp/query_all_transaction_tags.go b/pkg/mcp/query_all_transaction_tags.go new file mode 100644 index 00000000..08cf1e07 --- /dev/null +++ b/pkg/mcp/query_all_transaction_tags.go @@ -0,0 +1,71 @@ +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/settings" +) + +// MCPAllQueryTransactionTagsResponse represents the response structure for querying transaction tags +type MCPAllQueryTransactionTagsResponse struct { + Tags []string `json:"tags" jsonschema_description:"List of transaction tags"` +} + +type mcpQueryAllTransactionTagsToolHandler struct{} + +var MCPQueryAllTransactionTagsToolHandler = &mcpQueryAllTransactionTagsToolHandler{} + +// Name returns the name of the MCP tool +func (h *mcpQueryAllTransactionTagsToolHandler) Name() string { + return "query_all_transaction_tags" +} + +// Description returns the description of the MCP tool +func (h *mcpQueryAllTransactionTagsToolHandler) Description() string { + return "Query transaction tags for the current user in ezBookkeeping." +} + +// InputType returns the input type for the MCP tool request +func (h *mcpQueryAllTransactionTagsToolHandler) InputType() reflect.Type { + return nil +} + +// OutputType returns the output type for the MCP tool response +func (h *mcpQueryAllTransactionTagsToolHandler) OutputType() reflect.Type { + return reflect.TypeOf(&MCPAllQueryTransactionTagsResponse{}) +} + +// Handle processes the MCP call tool request and returns the response +func (h *mcpQueryAllTransactionTagsToolHandler) Handle(c *core.WebContext, callToolReq *MCPCallToolRequest, currentConfig *settings.Config, services MCPAvailableServices) (any, []*MCPTextContent, *errs.Error) { + uid := c.GetCurrentUid() + tags, err := services.GetTransactionTagService().GetAllTagsByUid(c, uid) + + if err != nil { + log.Errorf(c, "[query_all_transaction_tags.Handle] failed to get tags for user \"uid:%d\", because %s", uid, err.Error()) + return nil, nil, errs.Or(err, errs.ErrOperationFailed) + } + + tagNames := make([]string, len(tags)) + + for i := 0; i < len(tags); i++ { + tagNames[i] = tags[i].Name + } + + response := MCPAllQueryTransactionTagsResponse{ + Tags: tagNames, + } + + content, err := json.Marshal(response) + + if err != nil { + return nil, nil, errs.Or(err, errs.ErrOperationFailed) + } + + return response, []*MCPTextContent{ + NewMCPTextContent(string(content)), + }, nil +}