From 09dc476d8edd3315e8bf9043b65cf0c59e63e7e7 Mon Sep 17 00:00:00 2001 From: Amolith Date: Fri, 19 Dec 2025 08:59:10 -0700 Subject: [PATCH] refactor(style): auto-fix golangci-lint issues --- cmd/lunatask-mcp-server.go | 18 ++++++++---- tools/habits.go | 1 + tools/tasks.go | 57 ++++++++++++++++++++++++++++++++++++-- tools/tools.go | 14 ++++++++-- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/cmd/lunatask-mcp-server.go b/cmd/lunatask-mcp-server.go index 983fe76cdcfb2805754dc8c451a27d14cc55ba41..052526aa0b54f8a5a3c90005ac0f6e9de32f1172 100644 --- a/cmd/lunatask-mcp-server.go +++ b/cmd/lunatask-mcp-server.go @@ -17,7 +17,7 @@ import ( "git.sr.ht/~amolith/lunatask-mcp-server/tools" ) -// Goal represents a Lunatask goal with its name and ID +// Goal represents a Lunatask goal with its name and ID. type Goal struct { Name string `toml:"name"` ID string `toml:"id"` @@ -29,7 +29,7 @@ func (g Goal) GetName() string { return g.Name } // GetID returns the goal's ID. func (g Goal) GetID() string { return g.ID } -// Area represents a Lunatask area with its name, ID, and its goals +// Area represents a Lunatask area with its name, ID, and its goals. type Area struct { Name string `toml:"name"` ID string `toml:"id"` @@ -48,10 +48,11 @@ func (a Area) GetGoals() []tools.GoalProvider { for i, g := range a.Goals { providers[i] = g // Goal implements GoalProvider } + return providers } -// Habit represents a Lunatask habit with its name and ID +// Habit represents a Lunatask habit with its name and ID. type Habit struct { Name string `toml:"name"` ID string `toml:"id"` @@ -63,7 +64,7 @@ func (h Habit) GetName() string { return h.Name } // GetID returns the habit's ID. func (h Habit) GetID() string { return h.ID } -// ServerConfig holds the application's configuration loaded from TOML +// ServerConfig holds the application's configuration loaded from TOML. type ServerConfig struct { Host string `toml:"host"` Port int `toml:"port"` @@ -81,12 +82,14 @@ var version = "" func main() { configPath := "./config.toml" + for i, arg := range os.Args { switch arg { case "-v", "--version": if version == "" { version = "unknown, build with `just build` or copy/paste the build command from ./justfile" } + fmt.Println("lunatask-mcp-server:", version) os.Exit(0) case "-c", "--config": @@ -113,6 +116,7 @@ func main() { if area.Name == "" || area.ID == "" { log.Fatalf("All areas (areas[%d]) must have both a name and id", i) } + for j, goal := range area.Goals { if goal.Name == "" || goal.ID == "" { log.Fatalf("All goals (areas[%d].goals[%d]) must have both a name and id", i, j) @@ -131,12 +135,13 @@ func main() { sseServer := server.NewSSEServer(mcpServer, server.WithBaseURL(baseURL)) listenAddr := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port) log.Printf("SSE server listening on %s (baseURL: %s)", listenAddr, baseURL) + if err := sseServer.Start(listenAddr); err != nil { log.Fatalf("Server error: %v", err) } } -// closeFile properly closes a file, handling any errors +// closeFile properly closes a file, handling any errors. func closeFile(f *os.File) { err := f.Close() if err != nil { @@ -351,14 +356,17 @@ func createDefaultConfigFile(configPath string) { ID: "habit-id-placeholder", }}, } + file, err := os.OpenFile(configPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { log.Fatalf("Failed to create default config at %s: %v", configPath, err) } defer closeFile(file) + if err := toml.NewEncoder(file).Encode(defaultConfig); err != nil { log.Fatalf("Failed to encode default config to %s: %v", configPath, err) } + fmt.Printf("A default config has been created at %s.\nPlease edit it to provide your Lunatask access token, correct area/goal IDs, and your timezone (IANA/Olson format, e.g. 'America/New_York'), then restart the server.\n", configPath) os.Exit(1) } diff --git a/tools/habits.go b/tools/habits.go index 9505119d36c0956e630d23057392567b42fd4999..163327cea3139576132eb52b40f8d2701c3d152f 100644 --- a/tools/habits.go +++ b/tools/habits.go @@ -19,6 +19,7 @@ func (h *Handlers) HandleListHabitsAndActivities(ctx context.Context, request mc for _, habit := range h.config.Habits { fmt.Fprintf(&b, "- %s: %s\n", habit.GetName(), habit.GetID()) } + return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ diff --git a/tools/tasks.go b/tools/tasks.go index 1d88261d97a1ab9092f21970d8dc3127f2a5ab6c..32a3aa29c761b207ba01e36a22b245231870c421 100644 --- a/tools/tasks.go +++ b/tools/tasks.go @@ -27,12 +27,15 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq } var areaFoundProvider AreaProvider + for _, ap := range h.config.Areas { if ap.GetID() == areaID { areaFoundProvider = ap + break } } + if areaFoundProvider == nil { return reportMCPError("Area not found for given area_id") } @@ -40,15 +43,19 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq var goalID *string if goalIDStr, exists := arguments["goal_id"].(string); exists && goalIDStr != "" { found := false + for _, goal := range areaFoundProvider.GetGoals() { if goal.GetID() == goalIDStr { found = true + break } } + if !found { return reportMCPError("Goal not found in specified area for given goal_id") } + goalID = &goalIDStr } @@ -56,6 +63,7 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq if !ok || name == "" { return reportMCPError("Missing or invalid required argument: name") } + if len(name) > 100 { return reportMCPError("'name' must be 100 characters or fewer") } @@ -75,6 +83,7 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("Invalid type for 'priority' argument: expected string.") } + priorityMap := map[string]int{ "lowest": -2, "low": -1, @@ -82,10 +91,12 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq "high": 1, "highest": 2, } + translatedPriority, isValid := priorityMap[strings.ToLower(priorityStr)] if !isValid { return reportMCPError(fmt.Sprintf("Invalid 'priority' value: '%s'. Must be one of 'lowest', 'low', 'neutral', 'high', 'highest'.", priorityStr)) } + task.Priority = &translatedPriority } @@ -94,6 +105,7 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("Invalid type for 'eisenhower' argument: expected string.") } + eisenhowerMap := map[string]int{ "uncategorised": 0, "both urgent and important": 1, @@ -101,10 +113,12 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq "important, but not urgent": 3, "neither urgent nor important": 4, } + translatedEisenhower, isValid := eisenhowerMap[strings.ToLower(eisenhowerStr)] if !isValid { return reportMCPError(fmt.Sprintf("Invalid 'eisenhower' value: '%s'. Must be one of 'uncategorised', 'both urgent and important', 'urgent, but not important', 'important, but not urgent', 'neither urgent nor important'.", eisenhowerStr)) } + task.Eisenhower = &translatedEisenhower } @@ -113,11 +127,13 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("'motivation' must be a string") } + if motivation != "" { validMotivations := map[string]bool{"must": true, "should": true, "want": true} if !validMotivations[motivation] { return reportMCPError("'motivation' must be one of 'must', 'should', or 'want'") } + task.Motivation = &motivation } } @@ -127,11 +143,13 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("'status' must be a string") } + if status != "" { validStatus := map[string]bool{"later": true, "next": true, "started": true, "waiting": true, "completed": true} if !validStatus[status] { return reportMCPError("'status' must be one of 'later', 'next', 'started', 'waiting', or 'completed'") } + task.Status = &status } } @@ -141,10 +159,12 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("Invalid type for 'estimate' argument: expected number.") } + estimate := int(estimateVal) if estimate < 0 || estimate > 720 { return reportMCPError("'estimate' must be between 0 and 720 minutes") } + task.Estimate = &estimate } @@ -153,11 +173,13 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("Invalid type for scheduled_on argument: expected string.") } + if scheduledOnStr != "" { date, err := lunatask.ParseDate(scheduledOnStr) if err != nil { return reportMCPError(fmt.Sprintf("Invalid format for scheduled_on: '%s'. Must be YYYY-MM-DD.", scheduledOnStr)) } + task.ScheduledOn = &date } } @@ -171,6 +193,7 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq } client := lunatask.NewClient(h.config.AccessToken) + response, err := client.CreateTask(ctx, &task) if err != nil { return reportMCPError(fmt.Sprintf("%v", err)) @@ -191,7 +214,7 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq Content: []mcp.Content{ mcp.TextContent{ Type: "text", - Text: fmt.Sprintf("Task created successfully with ID: %s", response.ID), + Text: "Task created successfully with ID: " + response.ID, }, }, }, nil @@ -213,6 +236,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq updatePayload := lunatask.UpdateTaskRequest{} var specifiedAreaProvider AreaProvider + areaIDProvided := false if areaIDArg, exists := arguments["area_id"]; exists { @@ -220,19 +244,23 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok && areaIDArg != nil { return reportMCPError("Invalid type for area_id argument: expected string.") } + if ok && areaIDStr != "" { updatePayload.AreaID = &areaIDStr areaIDProvided = true found := false + for _, ap := range h.config.Areas { if ap.GetID() == areaIDStr { specifiedAreaProvider = ap found = true + break } } + if !found { - return reportMCPError(fmt.Sprintf("Area not found for given area_id: %s", areaIDStr)) + return reportMCPError("Area not found for given area_id: " + areaIDStr) } } } @@ -242,16 +270,21 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok && goalIDArg != nil { return reportMCPError("Invalid type for goal_id argument: expected string.") } + if ok && goalIDStr != "" { updatePayload.GoalID = &goalIDStr + if specifiedAreaProvider != nil { foundGoal := false + for _, goal := range specifiedAreaProvider.GetGoals() { if goal.GetID() == goalIDStr { foundGoal = true + break } } + if !foundGoal { return reportMCPError(fmt.Sprintf("Goal not found in specified area '%s' for given goal_id: %s", specifiedAreaProvider.GetName(), goalIDStr)) } @@ -266,6 +299,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if len(nameStr) > 100 { return reportMCPError("'name' must be 100 characters or fewer") } + updatePayload.Name = &nameStr } else { return reportMCPError("Invalid type for name argument: expected string.") @@ -276,6 +310,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok && noteArg != nil { return reportMCPError("Invalid type for note argument: expected string.") } + if ok { updatePayload.Note = ¬eStr } @@ -286,10 +321,12 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("Invalid type for estimate argument: expected number.") } + estimate := int(estimateVal) if estimate < 0 || estimate > 720 { return reportMCPError("'estimate' must be between 0 and 720 minutes") } + updatePayload.Estimate = &estimate } @@ -298,6 +335,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("Invalid type for 'priority' argument: expected string.") } + priorityMap := map[string]int{ "lowest": -2, "low": -1, @@ -305,10 +343,12 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq "high": 1, "highest": 2, } + translatedPriority, isValid := priorityMap[strings.ToLower(priorityStr)] if !isValid { return reportMCPError(fmt.Sprintf("Invalid 'priority' value: '%s'. Must be one of 'lowest', 'low', 'neutral', 'high', 'highest'.", priorityStr)) } + updatePayload.Priority = &translatedPriority } @@ -317,6 +357,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok { return reportMCPError("Invalid type for 'eisenhower' argument: expected string.") } + eisenhowerMap := map[string]int{ "uncategorised": 0, "both urgent and important": 1, @@ -324,10 +365,12 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq "important, but not urgent": 3, "neither urgent nor important": 4, } + translatedEisenhower, isValid := eisenhowerMap[strings.ToLower(eisenhowerStr)] if !isValid { return reportMCPError(fmt.Sprintf("Invalid 'eisenhower' value: '%s'. Must be one of 'uncategorised', 'both urgent and important', 'urgent, but not important', 'important, but not urgent', 'neither urgent nor important'.", eisenhowerStr)) } + updatePayload.Eisenhower = &translatedEisenhower } @@ -336,6 +379,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok && motivationArg != nil { return reportMCPError("Invalid type for motivation argument: expected string.") } + if ok { if motivationStr != "" { validMotivations := map[string]bool{"must": true, "should": true, "want": true} @@ -343,6 +387,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq return reportMCPError("'motivation' must be one of 'must', 'should', or 'want', or empty to clear.") } } + updatePayload.Motivation = &motivationStr } } @@ -352,6 +397,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok && statusArg != nil { return reportMCPError("Invalid type for status argument: expected string.") } + if ok { if statusStr != "" { validStatus := map[string]bool{"later": true, "next": true, "started": true, "waiting": true, "completed": true} @@ -359,6 +405,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq return reportMCPError("'status' must be one of 'later', 'next', 'started', 'waiting', 'completed', or empty.") } } + updatePayload.Status = &statusStr } } @@ -368,16 +415,19 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq if !ok && scheduledOnArg != nil { return reportMCPError("Invalid type for scheduled_on argument: expected string.") } + if ok && scheduledOnStr != "" { date, err := lunatask.ParseDate(scheduledOnStr) if err != nil { return reportMCPError(fmt.Sprintf("Invalid format for scheduled_on: '%s'. Must be YYYY-MM-DD.", scheduledOnStr)) } + updatePayload.ScheduledOn = &date } } client := lunatask.NewClient(h.config.AccessToken) + response, err := client.UpdateTask(ctx, taskID, &updatePayload) if err != nil { return reportMCPError(fmt.Sprintf("Failed to update task: %v", err)) @@ -387,7 +437,7 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq Content: []mcp.Content{ mcp.TextContent{ Type: "text", - Text: fmt.Sprintf("Task updated successfully. ID: %s", response.ID), + Text: "Task updated successfully. ID: " + response.ID, }, }, }, nil @@ -401,6 +451,7 @@ func (h *Handlers) HandleDeleteTask(ctx context.Context, request mcp.CallToolReq } client := lunatask.NewClient(h.config.AccessToken) + _, err := client.DeleteTask(ctx, taskID) if err != nil { return reportMCPError(fmt.Sprintf("Failed to delete task: %v", err)) diff --git a/tools/tools.go b/tools/tools.go index 00dc914af6f8ce2e7df8a0c08dcebada985ac68b..db41fc1e13b1aa1849d519085490fae51355671a 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -6,6 +6,7 @@ package tools import ( "context" + "errors" "fmt" "strings" "time" @@ -59,15 +60,17 @@ func reportMCPError(msg string) (*mcp.CallToolResult, error) { }, nil } -// LoadLocation loads a timezone location string, returning a *time.Location or error +// LoadLocation loads a timezone location string, returning a *time.Location or error. func LoadLocation(timezone string) (*time.Location, error) { if timezone == "" { - return nil, fmt.Errorf("timezone is not configured; please set the 'timezone' value in your config file (e.g. 'UTC' or 'America/New_York')") + return nil, errors.New("timezone is not configured; please set the 'timezone' value in your config file (e.g. 'UTC' or 'America/New_York')") } + loc, err := time.LoadLocation(timezone) if err != nil { - return nil, fmt.Errorf("could not load timezone '%s': %v", timezone, err) + return nil, fmt.Errorf("could not load timezone '%s': %w", timezone, err) } + return loc, nil } @@ -77,14 +80,17 @@ func (h *Handlers) HandleGetTimestamp(ctx context.Context, request mcp.CallToolR if !ok || natLangDate == "" { return reportMCPError("Missing or invalid required argument: natural_language_date") } + loc, err := LoadLocation(h.config.Timezone) if err != nil { return reportMCPError(err.Error()) } + parsedTime, err := anytime.Parse(natLangDate, time.Now().In(loc)) if err != nil { return reportMCPError(fmt.Sprintf("Could not parse natural language date: %v", err)) } + return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{ @@ -100,10 +106,12 @@ func (h *Handlers) HandleListAreasAndGoals(ctx context.Context, request mcp.Call var b strings.Builder for _, area := range h.config.Areas { fmt.Fprintf(&b, "- %s: %s\n", area.GetName(), area.GetID()) + for _, goal := range area.GetGoals() { fmt.Fprintf(&b, " - %s: %s\n", goal.GetName(), goal.GetID()) } } + return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{