@@ -202,7 +202,7 @@ func NewMCPServer(appConfig *Config) *server.MCPServer {
mcp.Description("Natural language date expression. Examples: 'tomorrow', '1 week', 'sunday at 19:00', 'january 15 at 2pm', 'next friday', 'now'. The tool will parse this into a properly formatted timestamp for use with task scheduling."),
mcp.Required(),
),
- ), toolHandlers.HandleGetTimestamp)
+ ), mcp.NewTypedToolHandler(toolHandlers.HandleGetTimestamp))
mcpServer.AddTool(
mcp.NewTool(
@@ -252,7 +252,7 @@ func NewMCPServer(appConfig *Config) *server.MCPServer {
mcp.WithString("scheduled_on",
mcp.Description("Scheduled date/time for the task. Must use the formatted timestamp returned by get_timestamp tool. Only include if user specifies when the task should be done."),
),
- ), toolHandlers.HandleCreateTask)
+ ), mcp.NewTypedToolHandler(toolHandlers.HandleCreateTask))
mcpServer.AddTool(mcp.NewTool("update_task",
mcp.WithDescription("Updates an existing task. Only provided fields will be updated. WORKFLOW: Use list_areas_and_goals first if changing area/goal, then get_timestamp if changing schedule. Only include parameters that are being changed. Empty strings will clear existing values for text fields."),
@@ -297,7 +297,7 @@ func NewMCPServer(appConfig *Config) *server.MCPServer {
mcp.WithString("scheduled_on",
mcp.Description("New scheduled date/time for the task. Must use the formatted timestamp returned by get_timestamp tool. Sending an empty string might clear the scheduled date. Only include if changing the schedule."),
),
- ), toolHandlers.HandleUpdateTask)
+ ), mcp.NewTypedToolHandler(toolHandlers.HandleUpdateTask))
mcpServer.AddTool(mcp.NewTool("delete_task",
mcp.WithDescription("Permanently deletes an existing task from Lunatask. This action cannot be undone."),
@@ -305,7 +305,7 @@ func NewMCPServer(appConfig *Config) *server.MCPServer {
mcp.Description("ID of the task to delete. This must be a valid task ID from an existing task in Lunatask."),
mcp.Required(),
),
- ), toolHandlers.HandleDeleteTask)
+ ), mcp.NewTypedToolHandler(toolHandlers.HandleDeleteTask))
mcpServer.AddTool(
mcp.NewTool(
@@ -325,7 +325,7 @@ func NewMCPServer(appConfig *Config) *server.MCPServer {
mcp.Description("Timestamp when the habit was performed. Must use the formatted timestamp returned by get_timestamp tool. Examples: if user says 'I did this yesterday', use get_timestamp with 'yesterday'."),
mcp.Required(),
),
- ), toolHandlers.HandleTrackHabitActivity)
+ ), mcp.NewTypedToolHandler(toolHandlers.HandleTrackHabitActivity))
return mcpServer
}
@@ -15,22 +15,53 @@ import (
"github.com/mark3labs/mcp-go/mcp"
)
-// HandleCreateTask handles the create_task tool call.
-func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- arguments := request.Params.Arguments
+// CreateTaskArgs defines the arguments for create_task tool call.
+type CreateTaskArgs struct {
+ AreaID string `json:"area_id"`
+ GoalID string `json:"goal_id,omitempty"`
+ Name string `json:"name"`
+ Note string `json:"note,omitempty"`
+ Estimate float64 `json:"estimate,omitempty"`
+ Priority string `json:"priority,omitempty"`
+ Eisenhower string `json:"eisenhower,omitempty"`
+ Motivation string `json:"motivation,omitempty"`
+ Status string `json:"status,omitempty"`
+ ScheduledOn string `json:"scheduled_on,omitempty"`
+}
+
+// UpdateTaskArgs defines the arguments for update_task tool call.
+type UpdateTaskArgs struct {
+ TaskID string `json:"task_id"`
+ AreaID string `json:"area_id,omitempty"`
+ GoalID string `json:"goal_id,omitempty"`
+ Name string `json:"name"`
+ Note string `json:"note,omitempty"`
+ Estimate float64 `json:"estimate,omitempty"`
+ Priority string `json:"priority,omitempty"`
+ Eisenhower string `json:"eisenhower,omitempty"`
+ Motivation string `json:"motivation,omitempty"`
+ Status string `json:"status,omitempty"`
+ ScheduledOn string `json:"scheduled_on,omitempty"`
+}
+
+// DeleteTaskArgs defines the arguments for delete_task tool call.
+type DeleteTaskArgs struct {
+ TaskID string `json:"task_id"`
+}
+// HandleCreateTask handles the create_task tool call.
+func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolRequest, args CreateTaskArgs) (*mcp.CallToolResult, error) {
if _, err := LoadLocation(h.config.Timezone); err != nil {
return reportMCPError(err.Error())
}
- areaID, ok := arguments["area_id"].(string)
- if !ok || areaID == "" {
+ if args.AreaID == "" {
return reportMCPError("Missing or invalid required argument: area_id")
}
var areaFoundProvider AreaProvider
for _, ap := range h.config.Areas {
- if ap.GetID() == areaID {
+ if ap.GetID() == args.AreaID {
areaFoundProvider = ap
break
}
@@ -39,10 +70,10 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq
return reportMCPError("Area not found for given area_id")
}
- if goalID, exists := arguments["goal_id"].(string); exists && goalID != "" {
+ if args.GoalID != "" {
found := false
for _, goal := range areaFoundProvider.GetGoals() {
- if goal.GetID() == goalID {
+ if goal.GetID() == args.GoalID {
found = true
break
}
@@ -52,6 +83,20 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq
}
}
+ // Create a map to store the final arguments for JSON marshaling
+ finalArgs := make(map[string]any)
+ finalArgs["area_id"] = args.AreaID
+ if args.GoalID != "" {
+ finalArgs["goal_id"] = args.GoalID
+ }
+ finalArgs["name"] = args.Name
+ if args.Note != "" {
+ finalArgs["note"] = args.Note
+ }
+ if args.Estimate > 0 {
+ finalArgs["estimate"] = int(args.Estimate)
+ }
+
priorityMap := map[string]int{
"lowest": -2,
"low": -1,
@@ -60,16 +105,12 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq
"highest": 2,
}
- if priorityArg, exists := arguments["priority"]; exists && priorityArg != nil {
- priorityStr, ok := priorityArg.(string)
- if !ok {
- return reportMCPError("Invalid type for 'priority' argument: expected string.")
- }
- translatedPriority, isValid := priorityMap[strings.ToLower(priorityStr)]
+ if args.Priority != "" {
+ translatedPriority, isValid := priorityMap[strings.ToLower(args.Priority)]
if !isValid {
- return reportMCPError(fmt.Sprintf("Invalid 'priority' value: '%s'. Must be one of 'lowest', 'low', 'neutral', 'high', 'highest'.", priorityStr))
+ return reportMCPError(fmt.Sprintf("Invalid 'priority' value: '%s'. Must be one of 'lowest', 'low', 'neutral', 'high', 'highest'.", args.Priority))
}
- arguments["priority"] = translatedPriority
+ finalArgs["priority"] = translatedPriority
}
eisenhowerMap := map[string]int{
@@ -80,57 +121,40 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq
"neither urgent nor important": 4,
}
- if eisenhowerArg, exists := arguments["eisenhower"]; exists && eisenhowerArg != nil {
- eisenhowerStr, ok := eisenhowerArg.(string)
- if !ok {
- return reportMCPError("Invalid type for 'eisenhower' argument: expected string.")
- }
- translatedEisenhower, isValid := eisenhowerMap[strings.ToLower(eisenhowerStr)]
+ if args.Eisenhower != "" {
+ translatedEisenhower, isValid := eisenhowerMap[strings.ToLower(args.Eisenhower)]
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))
+ 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'.", args.Eisenhower))
}
- arguments["eisenhower"] = translatedEisenhower
+ finalArgs["eisenhower"] = translatedEisenhower
}
- if motivationVal, exists := arguments["motivation"]; exists && motivationVal != nil {
- if motivation, ok := motivationVal.(string); ok && 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'")
- }
- } else if ok {
- // empty string is allowed
- } else {
- return reportMCPError("'motivation' must be a string")
+ if args.Motivation != "" {
+ validMotivations := map[string]bool{"must": true, "should": true, "want": true}
+ if !validMotivations[args.Motivation] {
+ return reportMCPError("'motivation' must be one of 'must', 'should', or 'want'")
}
+ finalArgs["motivation"] = args.Motivation
}
- if statusVal, exists := arguments["status"]; exists && statusVal != nil {
- if status, ok := statusVal.(string); ok && 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'")
- }
- } else if ok {
- // empty string is allowed
- } else {
- return reportMCPError("'status' must be a string")
+ if args.Status != "" {
+ validStatus := map[string]bool{"later": true, "next": true, "started": true, "waiting": true, "completed": true}
+ if !validStatus[args.Status] {
+ return reportMCPError("'status' must be one of 'later', 'next', 'started', 'waiting', or 'completed'")
}
+ finalArgs["status"] = args.Status
}
- if scheduledOnArg, exists := arguments["scheduled_on"]; exists {
- if scheduledOnStr, ok := scheduledOnArg.(string); ok && scheduledOnStr != "" {
- if _, err := time.Parse(time.RFC3339, scheduledOnStr); err != nil {
- return reportMCPError(fmt.Sprintf("Invalid format for scheduled_on: '%s'. Must be RFC3339 timestamp (e.g., YYYY-MM-DDTHH:MM:SSZ). Use get_timestamp tool first.", scheduledOnStr))
- }
- } else if !ok {
- return reportMCPError("Invalid type for scheduled_on argument: expected string.")
+ if args.ScheduledOn != "" {
+ if _, err := time.Parse(time.RFC3339, args.ScheduledOn); err != nil {
+ return reportMCPError(fmt.Sprintf("Invalid format for scheduled_on: '%s'. Must be RFC3339 timestamp (e.g., YYYY-MM-DDTHH:MM:SSZ). Use get_timestamp tool first.", args.ScheduledOn))
}
+ finalArgs["scheduled_on"] = args.ScheduledOn
}
client := lunatask.NewClient(h.config.AccessToken)
var task lunatask.CreateTaskRequest
- argBytes, err := json.Marshal(arguments)
+ argBytes, err := json.Marshal(finalArgs)
if err != nil {
return reportMCPError(fmt.Sprintf("Failed to process arguments: %v", err))
}
@@ -165,11 +189,8 @@ func (h *Handlers) HandleCreateTask(ctx context.Context, request mcp.CallToolReq
}
// HandleUpdateTask handles the update_task tool call.
-func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- arguments := request.Params.Arguments
-
- taskID, ok := arguments["task_id"].(string)
- if !ok || taskID == "" {
+func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolRequest, args UpdateTaskArgs) (*mcp.CallToolResult, error) {
+ if args.TaskID == "" {
return reportMCPError("Missing or invalid required argument: task_id")
}
@@ -182,80 +203,50 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq
var specifiedAreaProvider AreaProvider
areaIDProvided := false
- if areaIDArg, exists := arguments["area_id"]; exists {
- if areaIDStr, ok := areaIDArg.(string); 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))
+ if args.AreaID != "" {
+ updatePayload.AreaID = args.AreaID
+ areaIDProvided = true
+ found := false
+ for _, ap := range h.config.Areas {
+ if ap.GetID() == args.AreaID {
+ specifiedAreaProvider = ap
+ found = true
+ break
}
- } else if !ok && areaIDArg != nil {
- return reportMCPError("Invalid type for area_id argument: expected string.")
}
- // If area_id is not provided or is empty, we don't set it in the updatePayload
- // This will leave the task in its current area
- }
-
- if goalIDArg, exists := arguments["goal_id"]; exists {
- if goalIDStr, ok := goalIDArg.(string); 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))
- }
- } else if areaIDProvided {
- return reportMCPError("Internal error: area_id provided but area details not loaded for goal validation.")
- }
- // If area_id is not provided, we're not moving the task to a different area
- // In this case, the goal validation should be skipped as we don't know the current area
- } else if !ok && goalIDArg != nil {
- return reportMCPError("Invalid type for goal_id argument: expected string.")
+ if !found {
+ return reportMCPError(fmt.Sprintf("Area not found for given area_id: %s", args.AreaID))
}
}
- nameArg := arguments["name"]
- if nameStr, ok := nameArg.(string); ok {
- updatePayload.Name = nameStr
- } else {
- return reportMCPError("Invalid type for name argument: expected string.")
- }
-
- if noteArg, exists := arguments["note"]; exists {
- if noteStr, ok := noteArg.(string); ok {
- updatePayload.Note = noteStr
- } else if !ok && noteArg != nil {
- return reportMCPError("Invalid type for note argument: expected string.")
+ if args.GoalID != "" {
+ updatePayload.GoalID = args.GoalID
+ if specifiedAreaProvider != nil {
+ foundGoal := false
+ for _, goal := range specifiedAreaProvider.GetGoals() {
+ if goal.GetID() == args.GoalID {
+ foundGoal = true
+ break
+ }
+ }
+ if !foundGoal {
+ return reportMCPError(fmt.Sprintf("Goal not found in specified area '%s' for given goal_id: %s", specifiedAreaProvider.GetName(), args.GoalID))
+ }
+ } else if areaIDProvided {
+ return reportMCPError("Internal error: area_id provided but area details not loaded for goal validation.")
}
+ // If area_id is not provided, we're not moving the task to a different area
+ // In this case, the goal validation should be skipped as we don't know the current area
}
- if estimateArg, exists := arguments["estimate"]; exists && estimateArg != nil {
- if estimateVal, ok := estimateArg.(float64); ok {
- updatePayload.Estimate = int(estimateVal)
- } else {
- return reportMCPError("Invalid type for estimate argument: expected number.")
- }
+ updatePayload.Name = args.Name
+ updatePayload.Note = args.Note
+
+ if args.Estimate > 0 {
+ updatePayload.Estimate = int(args.Estimate)
}
- if priorityArg, exists := arguments["priority"]; exists && priorityArg != nil {
- priorityStr, ok := priorityArg.(string)
- if !ok {
- return reportMCPError("Invalid type for 'priority' argument: expected string.")
- }
+ if args.Priority != "" {
priorityMap := map[string]int{
"lowest": -2,
"low": -1,
@@ -263,18 +254,14 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq
"high": 1,
"highest": 2,
}
- translatedPriority, isValid := priorityMap[strings.ToLower(priorityStr)]
+ translatedPriority, isValid := priorityMap[strings.ToLower(args.Priority)]
if !isValid {
- return reportMCPError(fmt.Sprintf("Invalid 'priority' value: '%s'. Must be one of 'lowest', 'low', 'neutral', 'high', 'highest'.", priorityStr))
+ return reportMCPError(fmt.Sprintf("Invalid 'priority' value: '%s'. Must be one of 'lowest', 'low', 'neutral', 'high', 'highest'.", args.Priority))
}
updatePayload.Priority = translatedPriority
}
- if eisenhowerArg, exists := arguments["eisenhower"]; exists && eisenhowerArg != nil {
- eisenhowerStr, ok := eisenhowerArg.(string)
- if !ok {
- return reportMCPError("Invalid type for 'eisenhower' argument: expected string.")
- }
+ if args.Eisenhower != "" {
eisenhowerMap := map[string]int{
"uncategorised": 0,
"both urgent and important": 1,
@@ -282,56 +269,38 @@ 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)]
+ translatedEisenhower, isValid := eisenhowerMap[strings.ToLower(args.Eisenhower)]
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))
+ 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'.", args.Eisenhower))
}
updatePayload.Eisenhower = translatedEisenhower
}
- if motivationArg, exists := arguments["motivation"]; exists {
- if motivationStr, ok := motivationArg.(string); ok {
- if motivationStr != "" {
- validMotivations := map[string]bool{"must": true, "should": true, "want": true}
- if !validMotivations[motivationStr] {
- return reportMCPError("'motivation' must be one of 'must', 'should', or 'want', or empty to clear.")
- }
- }
- updatePayload.Motivation = motivationStr
- } else if !ok && motivationArg != nil {
- return reportMCPError("Invalid type for motivation argument: expected string.")
+ if args.Motivation != "" {
+ validMotivations := map[string]bool{"must": true, "should": true, "want": true}
+ if !validMotivations[args.Motivation] {
+ return reportMCPError("'motivation' must be one of 'must', 'should', or 'want', or empty to clear.")
}
}
+ updatePayload.Motivation = args.Motivation
- if statusArg, exists := arguments["status"]; exists {
- if statusStr, ok := statusArg.(string); ok {
- if statusStr != "" {
- validStatus := map[string]bool{"later": true, "next": true, "started": true, "waiting": true, "completed": true}
- if !validStatus[statusStr] {
- return reportMCPError("'status' must be one of 'later', 'next', 'started', 'waiting', 'completed', or empty.")
- }
- }
- updatePayload.Status = statusStr
- } else if !ok && statusArg != nil {
- return reportMCPError("Invalid type for status argument: expected string.")
+ if args.Status != "" {
+ validStatus := map[string]bool{"later": true, "next": true, "started": true, "waiting": true, "completed": true}
+ if !validStatus[args.Status] {
+ return reportMCPError("'status' must be one of 'later', 'next', 'started', 'waiting', 'completed', or empty.")
}
}
+ updatePayload.Status = args.Status
- if scheduledOnArg, exists := arguments["scheduled_on"]; exists {
- if scheduledOnStr, ok := scheduledOnArg.(string); ok {
- if scheduledOnStr != "" {
- if _, err := time.Parse(time.RFC3339, scheduledOnStr); err != nil {
- return reportMCPError(fmt.Sprintf("Invalid format for scheduled_on: '%s'. Must be RFC3339. Use get_timestamp tool.", scheduledOnStr))
- }
- }
- updatePayload.ScheduledOn = scheduledOnStr
- } else if !ok && scheduledOnArg != nil {
- return reportMCPError("Invalid type for scheduled_on argument: expected string.")
+ if args.ScheduledOn != "" {
+ if _, err := time.Parse(time.RFC3339, args.ScheduledOn); err != nil {
+ return reportMCPError(fmt.Sprintf("Invalid format for scheduled_on: '%s'. Must be RFC3339. Use get_timestamp tool.", args.ScheduledOn))
}
}
+ updatePayload.ScheduledOn = args.ScheduledOn
client := lunatask.NewClient(h.config.AccessToken)
- response, err := client.UpdateTask(ctx, taskID, &updatePayload)
+ response, err := client.UpdateTask(ctx, args.TaskID, &updatePayload)
if err != nil {
return reportMCPError(fmt.Sprintf("Failed to update task: %v", err))
}
@@ -347,14 +316,13 @@ func (h *Handlers) HandleUpdateTask(ctx context.Context, request mcp.CallToolReq
}
// HandleDeleteTask handles the delete_task tool call.
-func (h *Handlers) HandleDeleteTask(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- taskID, ok := request.Params.Arguments["task_id"].(string)
- if !ok || taskID == "" {
+func (h *Handlers) HandleDeleteTask(ctx context.Context, request mcp.CallToolRequest, args DeleteTaskArgs) (*mcp.CallToolResult, error) {
+ if args.TaskID == "" {
return reportMCPError("Missing or invalid required argument: task_id")
}
client := lunatask.NewClient(h.config.AccessToken)
- _, err := client.DeleteTask(ctx, taskID)
+ _, err := client.DeleteTask(ctx, args.TaskID)
if err != nil {
return reportMCPError(fmt.Sprintf("Failed to delete task: %v", err))
}