create.go

  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// parsedCreateInput holds validated and parsed create input fields.
 62type parsedCreateInput struct {
 63	Name        string
 64	AreaID      *string
 65	GoalID      *string
 66	Status      *lunatask.TaskStatus
 67	Note        *string
 68	Priority    *lunatask.Priority
 69	Estimate    *int
 70	Motivation  *lunatask.Motivation
 71	Important   *bool
 72	Urgent      *bool
 73	ScheduledOn *lunatask.Date
 74}
 75
 76// Handler handles task-related MCP tool requests.
 77type Handler struct {
 78	client *lunatask.Client
 79	areas  []shared.AreaProvider
 80}
 81
 82// NewHandler creates a new task handler.
 83func NewHandler(accessToken string, areas []shared.AreaProvider) *Handler {
 84	return &Handler{
 85		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
 86		areas:  areas,
 87	}
 88}
 89
 90// HandleCreate creates a new task.
 91func (h *Handler) HandleCreate(
 92	ctx context.Context,
 93	_ *mcp.CallToolRequest,
 94	input CreateInput,
 95) (*mcp.CallToolResult, CreateOutput, error) {
 96	parsed, errResult := parseCreateInput(input)
 97	if errResult != nil {
 98		return errResult, CreateOutput{}, nil
 99	}
100
101	builder := h.client.NewTask(parsed.Name)
102	applyToTaskBuilder(builder, parsed)
103
104	task, err := builder.Create(ctx)
105	if err != nil {
106		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
107	}
108
109	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
110
111	return nil, CreateOutput{
112		ID:       task.ID,
113		DeepLink: deepLink,
114	}, nil
115}
116
117//nolint:cyclop,funlen
118func parseCreateInput(input CreateInput) (*parsedCreateInput, *mcp.CallToolResult) {
119	parsed := &parsedCreateInput{
120		Name:      input.Name,
121		AreaID:    input.AreaID,
122		GoalID:    input.GoalID,
123		Note:      input.Note,
124		Estimate:  input.Estimate,
125		Important: input.Important,
126		Urgent:    input.Urgent,
127	}
128
129	if input.AreaID != nil {
130		if err := lunatask.ValidateUUID(*input.AreaID); err != nil {
131			return nil, shared.ErrorResult("invalid area_id: expected UUID")
132		}
133	}
134
135	if input.GoalID != nil {
136		if err := lunatask.ValidateUUID(*input.GoalID); err != nil {
137			return nil, shared.ErrorResult("invalid goal_id: expected UUID")
138		}
139	}
140
141	if input.Estimate != nil {
142		if err := shared.ValidateEstimate(*input.Estimate); err != nil {
143			return nil, shared.ErrorResult(err.Error())
144		}
145	}
146
147	if input.Status != nil {
148		status, err := lunatask.ParseTaskStatus(*input.Status)
149		if err != nil {
150			return nil, shared.ErrorResult(err.Error())
151		}
152
153		parsed.Status = &status
154	}
155
156	if input.Priority != nil {
157		priority, err := lunatask.ParsePriority(*input.Priority)
158		if err != nil {
159			return nil, shared.ErrorResult(err.Error())
160		}
161
162		parsed.Priority = &priority
163	}
164
165	if input.Motivation != nil {
166		motivation, err := lunatask.ParseMotivation(*input.Motivation)
167		if err != nil {
168			return nil, shared.ErrorResult(err.Error())
169		}
170
171		parsed.Motivation = &motivation
172	}
173
174	if input.ScheduledOn != nil {
175		date, err := dateutil.Parse(*input.ScheduledOn)
176		if err != nil {
177			return nil, shared.ErrorResult(err.Error())
178		}
179
180		parsed.ScheduledOn = &date
181	}
182
183	return parsed, nil
184}
185
186//nolint:cyclop
187func applyToTaskBuilder(builder *lunatask.TaskBuilder, parsed *parsedCreateInput) {
188	if parsed.AreaID != nil {
189		builder.InArea(*parsed.AreaID)
190	}
191
192	if parsed.GoalID != nil {
193		builder.InGoal(*parsed.GoalID)
194	}
195
196	if parsed.Status != nil {
197		builder.WithStatus(*parsed.Status)
198	}
199
200	if parsed.Note != nil {
201		builder.WithNote(*parsed.Note)
202	}
203
204	if parsed.Priority != nil {
205		builder.Priority(*parsed.Priority)
206	}
207
208	if parsed.Estimate != nil {
209		builder.WithEstimate(*parsed.Estimate)
210	}
211
212	if parsed.Motivation != nil {
213		builder.WithMotivation(*parsed.Motivation)
214	}
215
216	if parsed.Important != nil {
217		if *parsed.Important {
218			builder.Important()
219		} else {
220			builder.NotImportant()
221		}
222	}
223
224	if parsed.Urgent != nil {
225		if *parsed.Urgent {
226			builder.Urgent()
227		} else {
228			builder.NotUrgent()
229		}
230	}
231
232	if parsed.ScheduledOn != nil {
233		builder.ScheduledOn(*parsed.ScheduledOn)
234	}
235}