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/config"
 13	"git.secluded.site/lune/internal/dateutil"
 14	"git.secluded.site/lune/internal/mcp/shared"
 15	"git.secluded.site/lune/internal/validate"
 16	"github.com/modelcontextprotocol/go-sdk/mcp"
 17)
 18
 19// CreateToolName is the name of the create task tool.
 20const CreateToolName = "create_task"
 21
 22// CreateToolDescription describes the create task tool for LLMs.
 23const CreateToolDescription = `Creates a new task in Lunatask.
 24
 25Required:
 26- name: Task title
 27- area_id: Area UUID, lunatask:// deep link, or config key
 28
 29Optional:
 30- goal_id: Goal UUID, lunatask:// deep link, or config key (requires area_id)
 31- status: later, next, started, waiting (default: later)
 32- note: Markdown note/description for the task
 33- priority: lowest, low, normal, high, highest
 34- estimate: Time estimate in minutes (0-720)
 35- motivation: must, should, want
 36- important: true/false for Eisenhower matrix
 37- urgent: true/false for Eisenhower matrix
 38- scheduled_on: Date to schedule (YYYY-MM-DD or natural language)
 39
 40Returns the created task's ID and deep link.`
 41
 42// CreateInput is the input schema for creating a task.
 43type CreateInput struct {
 44	Name        string  `json:"name"                   jsonschema:"required"`
 45	AreaID      string  `json:"area_id"                jsonschema:"required"`
 46	GoalID      *string `json:"goal_id,omitempty"`
 47	Status      *string `json:"status,omitempty"`
 48	Note        *string `json:"note,omitempty"`
 49	Priority    *string `json:"priority,omitempty"`
 50	Estimate    *int    `json:"estimate,omitempty"`
 51	Motivation  *string `json:"motivation,omitempty"`
 52	Important   *bool   `json:"important,omitempty"`
 53	Urgent      *bool   `json:"urgent,omitempty"`
 54	ScheduledOn *string `json:"scheduled_on,omitempty"`
 55}
 56
 57// CreateOutput is the output schema for creating a task.
 58type CreateOutput struct {
 59	DeepLink string `json:"deep_link"`
 60}
 61
 62// parsedCreateInput holds validated and parsed create input fields.
 63type parsedCreateInput struct {
 64	Name        string
 65	AreaID      string
 66	GoalID      *string
 67	Status      *lunatask.TaskStatus
 68	Note        *string
 69	Priority    *lunatask.Priority
 70	Estimate    *int
 71	Motivation  *lunatask.Motivation
 72	Important   *bool
 73	Urgent      *bool
 74	ScheduledOn *lunatask.Date
 75}
 76
 77// Handler handles task-related MCP tool requests.
 78type Handler struct {
 79	client *lunatask.Client
 80	cfg    *config.Config
 81	areas  []shared.AreaProvider
 82}
 83
 84// NewHandler creates a new task handler.
 85func NewHandler(accessToken string, cfg *config.Config, areas []shared.AreaProvider) *Handler {
 86	return &Handler{
 87		client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
 88		cfg:    cfg,
 89		areas:  areas,
 90	}
 91}
 92
 93// HandleCreate creates a new task.
 94func (h *Handler) HandleCreate(
 95	ctx context.Context,
 96	_ *mcp.CallToolRequest,
 97	input CreateInput,
 98) (*mcp.CallToolResult, CreateOutput, error) {
 99	parsed, errResult := parseCreateInput(h.cfg, input)
100	if errResult != nil {
101		return errResult, CreateOutput{}, nil
102	}
103
104	builder := h.client.NewTask(parsed.Name)
105	applyToTaskBuilder(builder, parsed)
106
107	task, err := builder.Create(ctx)
108	if err != nil {
109		return shared.ErrorResult(err.Error()), CreateOutput{}, nil
110	}
111
112	deepLink, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
113
114	return &mcp.CallToolResult{
115		Content: []mcp.Content{&mcp.TextContent{
116			Text: "Task created: " + deepLink,
117		}},
118	}, CreateOutput{DeepLink: deepLink}, nil
119}
120
121//nolint:cyclop,funlen
122func parseCreateInput(cfg *config.Config, input CreateInput) (*parsedCreateInput, *mcp.CallToolResult) {
123	parsed := &parsedCreateInput{
124		Name:      input.Name,
125		Note:      input.Note,
126		Estimate:  input.Estimate,
127		Important: input.Important,
128		Urgent:    input.Urgent,
129	}
130
131	areaID, err := validate.AreaRef(cfg, input.AreaID)
132	if err != nil {
133		return nil, shared.ErrorResult(err.Error())
134	}
135
136	parsed.AreaID = areaID
137
138	if input.GoalID != nil {
139		goalID, err := validate.GoalRef(cfg, parsed.AreaID, *input.GoalID)
140		if err != nil {
141			return nil, shared.ErrorResult(err.Error())
142		}
143
144		parsed.GoalID = &goalID
145	}
146
147	if input.Estimate != nil {
148		if err := shared.ValidateEstimate(*input.Estimate); err != nil {
149			return nil, shared.ErrorResult(err.Error())
150		}
151	}
152
153	if input.Status != nil {
154		status, err := lunatask.ParseTaskStatus(*input.Status)
155		if err != nil {
156			return nil, shared.ErrorResult(err.Error())
157		}
158
159		parsed.Status = &status
160	}
161
162	if input.Priority != nil {
163		priority, err := lunatask.ParsePriority(*input.Priority)
164		if err != nil {
165			return nil, shared.ErrorResult(err.Error())
166		}
167
168		parsed.Priority = &priority
169	}
170
171	if input.Motivation != nil {
172		motivation, err := lunatask.ParseMotivation(*input.Motivation)
173		if err != nil {
174			return nil, shared.ErrorResult(err.Error())
175		}
176
177		parsed.Motivation = &motivation
178	}
179
180	if input.ScheduledOn != nil {
181		date, err := dateutil.Parse(*input.ScheduledOn)
182		if err != nil {
183			return nil, shared.ErrorResult(err.Error())
184		}
185
186		parsed.ScheduledOn = &date
187	}
188
189	return parsed, nil
190}
191
192//nolint:cyclop
193func applyToTaskBuilder(builder *lunatask.TaskBuilder, parsed *parsedCreateInput) {
194	builder.InArea(parsed.AreaID)
195
196	if parsed.GoalID != nil {
197		builder.InGoal(*parsed.GoalID)
198	}
199
200	if parsed.Status != nil {
201		builder.WithStatus(*parsed.Status)
202	}
203
204	if parsed.Note != nil {
205		builder.WithNote(*parsed.Note)
206	}
207
208	if parsed.Priority != nil {
209		builder.Priority(*parsed.Priority)
210	}
211
212	if parsed.Estimate != nil {
213		builder.WithEstimate(*parsed.Estimate)
214	}
215
216	if parsed.Motivation != nil {
217		builder.WithMotivation(*parsed.Motivation)
218	}
219
220	if parsed.Important != nil {
221		if *parsed.Important {
222			builder.Important()
223		} else {
224			builder.NotImportant()
225		}
226	}
227
228	if parsed.Urgent != nil {
229		if *parsed.Urgent {
230			builder.Urgent()
231		} else {
232			builder.NotUrgent()
233		}
234	}
235
236	if parsed.ScheduledOn != nil {
237		builder.ScheduledOn(*parsed.ScheduledOn)
238	}
239}