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