1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package task
6
7import (
8 "context"
9 "time"
10
11 "git.secluded.site/go-lunatask"
12 "git.secluded.site/lune/internal/mcp/shared"
13 "github.com/modelcontextprotocol/go-sdk/mcp"
14)
15
16// ListToolName is the name of the list tasks tool.
17const ListToolName = "list_tasks"
18
19// ListToolDescription describes the list tasks tool for LLMs.
20const ListToolDescription = `Lists tasks from Lunatask.
21
22Optional filters:
23- area_id: Filter by area UUID
24- status: Filter by status (later, next, started, waiting, completed)
25- include_completed: Include completed tasks (default: false, only shows today's)
26
27Note: Due to end-to-end encryption, task names and notes are not available.
28Only metadata (ID, status, dates, priority, etc.) is returned.
29
30Returns a list of tasks with their metadata and deep links.`
31
32// ListInput is the input schema for listing tasks.
33type ListInput struct {
34 AreaID *string `json:"area_id,omitempty"`
35 Status *string `json:"status,omitempty"`
36 IncludeCompleted *bool `json:"include_completed,omitempty"`
37}
38
39// ListOutput is the output schema for listing tasks.
40type ListOutput struct {
41 Tasks []Summary `json:"tasks"`
42 Count int `json:"count"`
43}
44
45// Summary represents a task in list output.
46type Summary struct {
47 ID string `json:"id"`
48 DeepLink string `json:"deep_link"`
49 Status *string `json:"status,omitempty"`
50 Priority *int `json:"priority,omitempty"`
51 ScheduledOn *string `json:"scheduled_on,omitempty"`
52 CreatedAt string `json:"created_at"`
53 AreaID *string `json:"area_id,omitempty"`
54 GoalID *string `json:"goal_id,omitempty"`
55}
56
57// HandleList lists tasks.
58func (h *Handler) HandleList(
59 ctx context.Context,
60 _ *mcp.CallToolRequest,
61 input ListInput,
62) (*mcp.CallToolResult, ListOutput, error) {
63 if input.AreaID != nil {
64 if err := lunatask.ValidateUUID(*input.AreaID); err != nil {
65 return shared.ErrorResult("invalid area_id: expected UUID"), ListOutput{}, nil
66 }
67 }
68
69 if input.Status != nil {
70 if _, err := lunatask.ParseTaskStatus(*input.Status); err != nil {
71 return shared.ErrorResult("invalid status: must be later, next, started, waiting, or completed"), ListOutput{}, nil
72 }
73 }
74
75 tasks, err := h.client.ListTasks(ctx, nil)
76 if err != nil {
77 return shared.ErrorResult(err.Error()), ListOutput{}, nil
78 }
79
80 opts := &lunatask.TaskFilterOptions{
81 AreaID: input.AreaID,
82 IncludeCompleted: input.IncludeCompleted != nil && *input.IncludeCompleted,
83 Today: time.Now(),
84 }
85
86 if input.Status != nil {
87 s := lunatask.TaskStatus(*input.Status)
88 opts.Status = &s
89 }
90
91 filtered := lunatask.FilterTasks(tasks, opts)
92 summaries := buildSummaries(filtered)
93
94 return nil, ListOutput{
95 Tasks: summaries,
96 Count: len(summaries),
97 }, nil
98}
99
100func buildSummaries(tasks []lunatask.Task) []Summary {
101 summaries := make([]Summary, 0, len(tasks))
102
103 for _, task := range tasks {
104 summary := Summary{
105 ID: task.ID,
106 CreatedAt: task.CreatedAt.Format(time.RFC3339),
107 AreaID: task.AreaID,
108 GoalID: task.GoalID,
109 }
110
111 summary.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
112
113 if task.Status != nil {
114 s := string(*task.Status)
115 summary.Status = &s
116 }
117
118 if task.Priority != nil {
119 p := int(*task.Priority)
120 summary.Priority = &p
121 }
122
123 if task.ScheduledOn != nil {
124 s := task.ScheduledOn.Format("2006-01-02")
125 summary.ScheduledOn = &s
126 }
127
128 summaries = append(summaries, summary)
129 }
130
131 return summaries
132}