Skip to content

Commit 32e5de4

Browse files
committed
feat: upgrade dependencies and add MCP server instructions
- Upgrade mcp-go from v0.9.0 to v0.43.2 - Upgrade bbolt from v1.3.11 to v1.4.3 - Add server instructions with usage examples - Fix API compatibility with new mcp-go version (request.Params.Arguments type change) - Add commit hash validation in GetLatestCommitHash - Add ValidateCommitHash function for repository commit verification
1 parent 51a1cad commit 32e5de4

5 files changed

Lines changed: 111 additions & 24 deletions

File tree

clockwork

811 KB
Binary file not shown.

go.mod

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
module github.com/techthos/clockwork
22

3-
go 1.23
3+
go 1.23.0
44

55
require (
66
github.com/google/uuid v1.6.0
7-
github.com/mark3labs/mcp-go v0.9.0
8-
go.etcd.io/bbolt v1.3.11
7+
github.com/mark3labs/mcp-go v0.43.2
8+
go.etcd.io/bbolt v1.4.3
99
)
1010

11-
require golang.org/x/sys v0.4.0 // indirect
11+
require (
12+
github.com/bahlo/generic-list-go v0.2.0 // indirect
13+
github.com/buger/jsonparser v1.1.1 // indirect
14+
github.com/invopop/jsonschema v0.13.0 // indirect
15+
github.com/mailru/easyjson v0.7.7 // indirect
16+
github.com/spf13/cast v1.7.1 // indirect
17+
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
18+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
19+
golang.org/x/sys v0.29.0 // indirect
20+
gopkg.in/yaml.v3 v3.0.1 // indirect
21+
)

go.sum

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,45 @@
1+
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
2+
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
3+
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
4+
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
15
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
26
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
8+
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
9+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
10+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
311
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
412
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5-
github.com/mark3labs/mcp-go v0.9.0 h1:KD5TqXlhsBLzKseDnMDzoJrmtw59ZoObDfftJ5OCNb4=
6-
github.com/mark3labs/mcp-go v0.9.0/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
13+
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
14+
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
15+
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
16+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
17+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
18+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
19+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
20+
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
21+
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
22+
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
23+
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
724
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
825
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
10-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
11-
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
12-
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
13-
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
14-
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
15-
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
16-
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
26+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
27+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
28+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
29+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
30+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
31+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
32+
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
33+
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
34+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
35+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
36+
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
37+
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
38+
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
39+
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
40+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
41+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
42+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
43+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1744
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1845
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/git/git.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,30 @@ func GetLatestCommitHash(repoPath string) (string, error) {
8484
if err != nil {
8585
return "", fmt.Errorf("failed to get latest commit: %w", err)
8686
}
87-
return strings.TrimSpace(string(output)), nil
87+
hash := strings.TrimSpace(string(output))
88+
89+
// Validate hash format (40 hex characters)
90+
if len(hash) != 40 {
91+
return "", fmt.Errorf("invalid commit hash length: got %d, expected 40", len(hash))
92+
}
93+
for _, c := range hash {
94+
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
95+
return "", fmt.Errorf("invalid commit hash: contains non-hex character '%c'", c)
96+
}
97+
}
98+
99+
return hash, nil
100+
}
101+
102+
// ValidateCommitHash checks if a commit hash exists in the repository
103+
func ValidateCommitHash(repoPath, hash string) bool {
104+
if hash == "" {
105+
return false
106+
}
107+
108+
cmd := exec.Command("git", "cat-file", "-e", hash)
109+
cmd.Dir = repoPath
110+
return cmd.Run() == nil
88111
}
89112

90113
// AggregateCommits aggregates multiple commits into a summary message

internal/server/server.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,16 @@ func New() (*ClockworkServer, error) {
3636
}
3737

3838
// Create MCP server
39-
mcpServer := server.NewMCPServer("clockwork", "1.0.0")
39+
mcpServer := server.NewMCPServer(
40+
"clockwork",
41+
"1.0.0",
42+
server.WithInstructions(`Automatically track work time based on git commits of a project.
43+
44+
Examples:
45+
- "track 2h" - Create entry with 2 hours from recent git commits
46+
- "clockwork 1h" - Create entry with 1 hour from recent commits
47+
- "book 1h meeting with alex" - Manual entry without git commit aggregation`),
48+
)
4049

4150
cs := &ClockworkServer{
4251
store: store,
@@ -56,7 +65,11 @@ func (s *ClockworkServer) Close() error {
5665

5766
// Helper function to get required string argument
5867
func getRequiredString(request mcp.CallToolRequest, key string) (string, error) {
59-
val, ok := request.Params.Arguments[key]
68+
args, ok := request.Params.Arguments.(map[string]interface{})
69+
if !ok {
70+
return "", fmt.Errorf("invalid arguments type")
71+
}
72+
val, ok := args[key]
6073
if !ok {
6174
return "", fmt.Errorf("missing required argument: %s", key)
6275
}
@@ -127,7 +140,7 @@ func (s *ClockworkServer) registerUpdateProject() {
127140
if err != nil {
128141
return mcp.NewToolResultError(err.Error()), nil
129142
}
130-
args := request.Params.Arguments
143+
args, _ := request.Params.Arguments.(map[string]interface{})
131144

132145
name, _ := args["name"].(string)
133146
gitRepoPath, _ := args["git_repo_path"].(string)
@@ -194,7 +207,7 @@ func (s *ClockworkServer) registerCreateEntry() {
194207
if err != nil {
195208
return mcp.NewToolResultError(err.Error()), nil
196209
}
197-
args := request.Params.Arguments
210+
args, _ := request.Params.Arguments.(map[string]interface{})
198211

199212
customMessage, _ := args["message"].(string)
200213
invoiced, _ := args["invoiced"].(bool)
@@ -234,7 +247,15 @@ func (s *ClockworkServer) registerCreateEntry() {
234247
message = "Manual entry"
235248
}
236249

237-
entry, err := s.store.CreateEntry(projectID, duration, message, "", invoiced, createdAt)
250+
// For manual entries, always store current HEAD commit hash (even if duplicate)
251+
project, _ := s.store.GetProject(projectID)
252+
currentHash, err := git.GetLatestCommitHash(project.GitRepoPath)
253+
if err != nil {
254+
// If we can't get HEAD hash, just store empty string
255+
currentHash = ""
256+
}
257+
258+
entry, err := s.store.CreateEntry(projectID, duration, message, currentHash, invoiced, createdAt)
238259
if err != nil {
239260
return mcp.NewToolResultError(err.Error()), nil
240261
}
@@ -257,7 +278,13 @@ func (s *ClockworkServer) registerCreateEntry() {
257278

258279
var sinceHash string
259280
if lastEntry != nil && lastEntry.CommitHash != "" {
260-
sinceHash = lastEntry.CommitHash
281+
// Validate that the commit hash exists in the repository
282+
if git.ValidateCommitHash(project.GitRepoPath, lastEntry.CommitHash) {
283+
sinceHash = lastEntry.CommitHash
284+
} else {
285+
// Invalid or not found - fall back to getting all commits from HEAD
286+
sinceHash = ""
287+
}
261288
}
262289

263290
// Get commits since last entry
@@ -325,7 +352,7 @@ func (s *ClockworkServer) registerUpdateEntry() {
325352
if err != nil {
326353
return mcp.NewToolResultError(err.Error()), nil
327354
}
328-
args := request.Params.Arguments
355+
args, _ := request.Params.Arguments.(map[string]interface{})
329356

330357
var duration *int64
331358
var message, commitHash *string
@@ -401,7 +428,7 @@ func (s *ClockworkServer) registerListEntries() {
401428
)
402429

403430
s.mcp.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
404-
args := request.Params.Arguments
431+
args, _ := request.Params.Arguments.(map[string]interface{})
405432

406433
projectID, _ := args["project_id"].(string)
407434
invoicedStr, _ := args["invoiced"].(string)
@@ -435,7 +462,7 @@ func (s *ClockworkServer) registerGetStatistics() {
435462
)
436463

437464
s.mcp.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
438-
args := request.Params.Arguments
465+
args, _ := request.Params.Arguments.(map[string]interface{})
439466

440467
projectID, _ := args["project_id"].(string)
441468
startDateStr, _ := args["start_date"].(string)

0 commit comments

Comments
 (0)