1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package task provides MCP tools for Lunatask task operations.
6package task
7
8import (
9 "context"
10
11 "git.secluded.site/go-lunatask"
12 "git.secluded.site/lune/internal/dateutil"
13 "git.secluded.site/lune/internal/mcp/shared"
14 "github.com/modelcontextprotocol/go-sdk/mcp"
15)
16
17// CreateToolName is the name of the create task tool.
18const CreateToolName = "create_task"
19
20// CreateToolDescription describes the create task tool for LLMs.
21const CreateToolDescription = `Creates a new task in Lunatask.
22
23Required:
24- name: Task title
25
26Optional:
27- area_id: Area UUID (get from lunatask://areas resource)
28- goal_id: Goal UUID (requires area_id; get from lunatask://areas resource)
29- status: later, next, started, waiting (default: later)
30- note: Markdown note/description for the task
31- priority: lowest, low, normal, high, highest
32- estimate: Time estimate in minutes (0-720)
33- motivation: must, should, want
34- important: true/false for Eisenhower matrix
35- urgent: true/false for Eisenhower matrix
36- scheduled_on: Date to schedule (YYYY-MM-DD or natural language)
37
38Returns the created task's ID and deep link.`
39
40// CreateInput is the input schema for creating a task.
41type CreateInput struct {
42 Name string `json:"name" jsonschema:"required"`
43 AreaID *string `json:"area_id,omitempty"`
44 GoalID *string `json:"goal_id,omitempty"`
45 Status *string `json:"status,omitempty"`
46 Note *string `json:"note,omitempty"`
47 Priority *string `json:"priority,omitempty"`
48 Estimate *int `json:"estimate,omitempty"`
49 Motivation *string `json:"motivation,omitempty"`
50 Important *bool `json:"important,omitempty"`
51 Urgent *bool `json:"urgent,omitempty"`
52 ScheduledOn *string `json:"scheduled_on,omitempty"`
53}
54
55// CreateOutput is the output schema for creating a task.
56type CreateOutput struct {
57 ID string `json:"id"`
58 DeepLink string `json:"deep_link"`
59}
60
61// Handler handles task-related MCP tool requests.
62type Handler struct {
63 client *lunatask.Client
64 areas []shared.AreaProvider
65}
66
67// NewHandler creates a new task handler.
68func NewHandler(accessToken string, areas []shared.AreaProvider) *Handler {
69 return &Handler{
70 client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
71 areas: areas,
72 }
73}
74
75// HandleCreate creates a new task.
76//
77//nolint:cyclop,funlen,gocognit,nilerr // MCP error pattern; repetitive field handling.
78func (h *Handler) HandleCreate(
79 ctx context.Context,
80 _ *mcp.CallToolRequest,
81 input CreateInput,
82) (*mcp.CallToolResult, CreateOutput, error) {
83 if input.AreaID != nil {
84 if err := lunatask.ValidateUUID(*input.AreaID); err != nil {
85 return shared.ErrorResult("invalid area_id: expected UUID"), CreateOutput{}, nil
86 }
87 }
88
89 if input.GoalID != nil {
90 if err := lunatask.ValidateUUID(*input.GoalID); err != nil {
91 return shared.ErrorResult("invalid goal_id: expected UUID"), CreateOutput{}, nil
92 }
93 }
94
95 if input.Estimate != nil {
96 if err := shared.ValidateEstimate(*input.Estimate); err != nil {
97 return shared.ErrorResult(err.Error()), CreateOutput{}, nil
98 }
99 }
100
101 builder := h.client.NewTask(input.Name)
102
103 if input.AreaID != nil {
104 builder.InArea(*input.AreaID)
105 }
106
107 if input.GoalID != nil {
108 builder.InGoal(*input.GoalID)
109 }
110
111 if input.Status != nil {
112 status, err := lunatask.ParseTaskStatus(*input.Status)
113 if err != nil {
114 return shared.ErrorResult(err.Error()), CreateOutput{}, nil
115 }
116
117 builder.WithStatus(status)
118 }
119
120 if input.Note != nil {
121 builder.WithNote(*input.Note)
122 }
123
124 if input.Priority != nil {
125 priority, err := lunatask.ParsePriority(*input.Priority)
126 if err != nil {
127 return shared.ErrorResult(err.Error()), CreateOutput{}, nil
128 }
129
130 builder.Priority(priority)
131 }
132
133 if input.Estimate != nil {
134 builder.WithEstimate(*input.Estimate)
135 }
136
137 if input.Motivation != nil {
138 motivation, err := lunatask.ParseMotivation(*input.Motivation)
139 if err != nil {
140 return shared.ErrorResult(err.Error()), CreateOutput{}, nil
141 }
142
143 builder.WithMotivation(motivation)
144 }
145
146 if input.Important != nil {
147 if *input.Important {
148 builder.Important()
149 } else {
150 builder.NotImportant()
151 }
152 }
153
154 if input.Urgent != nil {
155 if *input.Urgent {
156 builder.Urgent()
157 } else {
158 builder.NotUrgent()
159 }
160 }
161
162 if input.ScheduledOn != nil {
163 date, err := dateutil.Parse(*input.ScheduledOn)
164 if err != nil {
165 return shared.ErrorResult(err.Error()), CreateOutput{}, nil
166 }
167
168 builder.ScheduledOn(date)
169 }
170
171 task, err := builder.Create(ctx)
172 if err != nil {
173 return shared.ErrorResult(err.Error()), CreateOutput{}, nil
174 }
175
176 deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
177
178 return nil, CreateOutput{
179 ID: task.ID,
180 DeepLink: deepLink,
181 }, nil
182}