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// 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}