Skip to content

Commit 220d4e1

Browse files
authored
feat: more mcp tools (#101)
* feat: add support for -header in mcp client * feat: add more mcp tools * feat: script rules
1 parent a034b0d commit 220d4e1

File tree

12 files changed

+1633
-34
lines changed

12 files changed

+1633
-34
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@ build/.aws-sam/
4141
server
4242

4343
bin/
44+
45+
mcp-client

cmd/mcp-client/main.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"log"
77
"os"
88
"os/signal"
9+
"strings"
910
"syscall"
1011

1112
"github.com/mark3labs/mcp-go/client"
@@ -14,9 +15,23 @@ import (
1415
"github.com/mark3labs/mcp-go/server"
1516
)
1617

18+
type headerFlags []string
19+
20+
func (h *headerFlags) String() string {
21+
return strings.Join(*h, ", ")
22+
}
23+
24+
func (h *headerFlags) Set(value string) error {
25+
*h = append(*h, value)
26+
return nil
27+
}
28+
1729
func main() {
30+
var headers headerFlags
31+
1832
serverURL := flag.String("server", "http://localhost:8080/mcp/", "Go Money MCP server URL")
1933
token := flag.String("token", "", "Service token for authentication (required)")
34+
flag.Var(&headers, "header", "Additional HTTP header in 'Key: Value' format (can be specified multiple times)")
2035
flag.Parse()
2136

2237
if *token == "" {
@@ -33,11 +48,21 @@ func main() {
3348
cancel()
3449
}()
3550

51+
httpHeaders := map[string]string{
52+
"Authorization": "Bearer " + *token,
53+
}
54+
55+
for _, h := range headers {
56+
parts := strings.SplitN(h, ":", 2)
57+
if len(parts) != 2 {
58+
log.Fatalf("invalid header format %q, expected 'Key: Value'", h)
59+
}
60+
httpHeaders[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
61+
}
62+
3663
httpTransport, err := transport.NewStreamableHTTP(
3764
*serverURL,
38-
transport.WithHTTPHeaders(map[string]string{
39-
"Authorization": "Bearer " + *token,
40-
}),
65+
transport.WithHTTPHeaders(httpHeaders),
4166
)
4267
if err != nil {
4368
log.Fatalf("failed to create transport: %v", err)

cmd/server/main.go

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -104,26 +104,6 @@ func main() {
104104
grpcServer.GetMux().Handle("/", handlers.SpaHandler(config.StaticFilesDirectory))
105105
}
106106

107-
if !config.MCP.Disable {
108-
logger.Info().Str("path", config.MCP.DocsDir).Msg("Reading mcp docs")
109-
mcpDocs, mcpErr := gomoneyMcp.ReadDocsFromPath(config.MCP.DocsDir)
110-
if mcpErr != nil {
111-
logger.Fatal().Err(mcpErr).Msg("failed to read mcp docs")
112-
}
113-
114-
if mcpDocs == "" {
115-
logger.Fatal().Msg("mcp docs are empty")
116-
}
117-
118-
mcpServer := gomoneyMcp.NewServer(&gomoneyMcp.ServerConfig{
119-
DB: database.GetDb(database.DbTypeReadonly),
120-
Docs: mcpDocs,
121-
})
122-
123-
grpcServer.GetMux().Handle("/mcp", middlewares.HTTPAuthMiddleware(jwtService, mcpServer.Handler()))
124-
logger.Info().Msg("MCP server enabled at /mcp")
125-
}
126-
127107
userService := users.NewService(&users.ServiceConfig{
128108
JwtSvc: jwtService,
129109
})
@@ -225,6 +205,29 @@ func main() {
225205
AccountSvc: accountSvc,
226206
})
227207

208+
if !config.MCP.Disable {
209+
logger.Info().Str("path", config.MCP.DocsDir).Msg("Reading mcp docs")
210+
mcpDocs, mcpErr := gomoneyMcp.ReadDocsFromPath(config.MCP.DocsDir)
211+
if mcpErr != nil {
212+
logger.Fatal().Err(mcpErr).Msg("failed to read mcp docs")
213+
}
214+
215+
if mcpDocs == "" {
216+
logger.Fatal().Msg("mcp docs are empty")
217+
}
218+
219+
mcpServer := gomoneyMcp.NewServer(&gomoneyMcp.ServerConfig{
220+
DB: database.GetDb(database.DbTypeMaster),
221+
Docs: mcpDocs,
222+
CategorySvc: categoriesSvc,
223+
RulesSvc: rulesSvc,
224+
DryRunSvc: dryRunSvc,
225+
})
226+
227+
grpcServer.GetMux().Handle("/mcp", middlewares.HTTPAuthMiddleware(jwtService, mcpServer.Handler()))
228+
logger.Info().Msg("MCP server enabled at /mcp")
229+
}
230+
228231
_ = handlers.NewTransactionApi(grpcServer, transactionSvc, applicableAccountSvc, mapper)
229232
_ = handlers.NewTagsApi(grpcServer, tagSvc)
230233
_ = handlers.NewRulesApi(grpcServer, &handlers.RulesApiConfig{

pkg/mcp/category_tool.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
categoriesv1 "buf.build/gen/go/xskydev/go-money-pb/protocolbuffers/go/gomoneypb/categories/v1"
8+
gomoneypbv1 "buf.build/gen/go/xskydev/go-money-pb/protocolbuffers/go/gomoneypb/v1"
9+
"github.com/ft-t/go-money/pkg/database"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
)
12+
13+
func (s *Server) handleCreateCategory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
14+
args := request.GetArguments()
15+
16+
name, ok := args["name"].(string)
17+
if !ok || name == "" {
18+
return mcp.NewToolResultError("name parameter is required"), nil
19+
}
20+
21+
queryCtx, cancel := context.WithTimeout(ctx, queryTimeout)
22+
defer cancel()
23+
24+
queryCtx = database.WithContext(queryCtx, s.db)
25+
26+
resp, err := s.cfg.CategorySvc.CreateCategory(queryCtx, &categoriesv1.CreateCategoryRequest{
27+
Category: &gomoneypbv1.Category{
28+
Name: name,
29+
},
30+
})
31+
if err != nil {
32+
return mcp.NewToolResultError(fmt.Sprintf("failed to create category: %v", err)), nil
33+
}
34+
35+
return mcp.NewToolResultText(fmt.Sprintf("Category created with id %d", resp.Category.Id)), nil
36+
}
37+
38+
func (s *Server) handleUpdateCategory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
39+
args := request.GetArguments()
40+
41+
categoryID, ok := args["id"].(float64)
42+
if !ok {
43+
return mcp.NewToolResultError("id parameter is required"), nil
44+
}
45+
46+
name, ok := args["name"].(string)
47+
if !ok || name == "" {
48+
return mcp.NewToolResultError("name parameter is required"), nil
49+
}
50+
51+
queryCtx, cancel := context.WithTimeout(ctx, queryTimeout)
52+
defer cancel()
53+
54+
queryCtx = database.WithContext(queryCtx, s.db)
55+
56+
resp, err := s.cfg.CategorySvc.UpdateCategory(queryCtx, &categoriesv1.UpdateCategoryRequest{
57+
Category: &gomoneypbv1.Category{
58+
Id: int32(categoryID),
59+
Name: name,
60+
},
61+
})
62+
if err != nil {
63+
return mcp.NewToolResultError(fmt.Sprintf("failed to update category: %v", err)), nil
64+
}
65+
66+
return mcp.NewToolResultText(fmt.Sprintf("Category %d updated to '%s'", resp.Category.Id, resp.Category.Name)), nil
67+
}

0 commit comments

Comments
 (0)