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// ShowToolName is the name of the show task tool.
19const ShowToolName = "show_task"
20
21// ShowToolDescription describes the show task tool for LLMs.
22const ShowToolDescription = `Shows details of a specific task from Lunatask.
23
24Required:
25- id: Task UUID or lunatask:// deep link
26
27Note: Due to end-to-end encryption, task name and note content are not available.
28Only metadata (ID, status, dates, priority, etc.) is returned.`
29
30// ShowInput is the input schema for showing a task.
31type ShowInput struct {
32 ID string `json:"id" jsonschema:"required"`
33}
34
35// ShowOutput is the output schema for showing a task.
36type ShowOutput struct {
37 DeepLink string `json:"deep_link"`
38 Status *string `json:"status,omitempty"`
39 Priority *int `json:"priority,omitempty"`
40 Estimate *int `json:"estimate,omitempty"`
41 ScheduledOn *string `json:"scheduled_on,omitempty"`
42 CompletedAt *string `json:"completed_at,omitempty"`
43 CreatedAt string `json:"created_at"`
44 UpdatedAt string `json:"updated_at"`
45 AreaID *string `json:"area_id,omitempty"`
46 GoalID *string `json:"goal_id,omitempty"`
47 Important *bool `json:"important,omitempty"`
48 Urgent *bool `json:"urgent,omitempty"`
49}
50
51// HandleShow shows a task's details.
52func (h *Handler) HandleShow(
53 ctx context.Context,
54 _ *mcp.CallToolRequest,
55 input ShowInput,
56) (*mcp.CallToolResult, ShowOutput, error) {
57 _, id, err := lunatask.ParseReference(input.ID)
58 if err != nil {
59 return shared.ErrorResult("invalid ID: expected UUID or lunatask:// deep link"), ShowOutput{}, nil
60 }
61
62 task, err := h.client.GetTask(ctx, id)
63 if err != nil {
64 return shared.ErrorResult(err.Error()), ShowOutput{}, nil
65 }
66
67 output := ShowOutput{
68 CreatedAt: task.CreatedAt.Format(time.RFC3339),
69 UpdatedAt: task.UpdatedAt.Format(time.RFC3339),
70 AreaID: task.AreaID,
71 GoalID: task.GoalID,
72 }
73
74 output.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
75
76 if task.Status != nil {
77 s := string(*task.Status)
78 output.Status = &s
79 }
80
81 if task.Priority != nil {
82 p := int(*task.Priority)
83 output.Priority = &p
84 }
85
86 if task.Estimate != nil {
87 output.Estimate = task.Estimate
88 }
89
90 if task.ScheduledOn != nil {
91 s := task.ScheduledOn.Format("2006-01-02")
92 output.ScheduledOn = &s
93 }
94
95 if task.CompletedAt != nil {
96 s := task.CompletedAt.Format(time.RFC3339)
97 output.CompletedAt = &s
98 }
99
100 if task.Eisenhower != nil {
101 important := task.Eisenhower.IsImportant()
102 urgent := task.Eisenhower.IsUrgent()
103 output.Important = &important
104 output.Urgent = &urgent
105 }
106
107 text := formatShowText(output)
108
109 return &mcp.CallToolResult{
110 Content: []mcp.Content{&mcp.TextContent{Text: text}},
111 }, output, nil
112}
113
114func formatShowText(output ShowOutput) string {
115 var builder strings.Builder
116
117 builder.WriteString(fmt.Sprintf("Task: %s\n", output.DeepLink))
118 writeOptionalField(&builder, "Status", output.Status)
119 writeOptionalIntField(&builder, "Priority", output.Priority)
120 writeOptionalField(&builder, "Scheduled", output.ScheduledOn)
121 writeOptionalMinutesField(&builder, "Estimate", output.Estimate)
122 writeEisenhowerField(&builder, output.Important, output.Urgent)
123 builder.WriteString(fmt.Sprintf("Created: %s\n", output.CreatedAt))
124 builder.WriteString("Updated: " + output.UpdatedAt)
125 writeOptionalField(&builder, "\nCompleted", output.CompletedAt)
126
127 return builder.String()
128}
129
130func writeOptionalField(builder *strings.Builder, label string, value *string) {
131 if value != nil {
132 fmt.Fprintf(builder, "%s: %s\n", label, *value)
133 }
134}
135
136func writeOptionalIntField(builder *strings.Builder, label string, value *int) {
137 if value != nil {
138 fmt.Fprintf(builder, "%s: %d\n", label, *value)
139 }
140}
141
142func writeOptionalMinutesField(builder *strings.Builder, label string, value *int) {
143 if value != nil {
144 fmt.Fprintf(builder, "%s: %d min\n", label, *value)
145 }
146}
147
148func writeEisenhowerField(builder *strings.Builder, important, urgent *bool) {
149 var parts []string
150
151 if important != nil && *important {
152 parts = append(parts, "important")
153 }
154
155 if urgent != nil && *urgent {
156 parts = append(parts, "urgent")
157 }
158
159 if len(parts) > 0 {
160 fmt.Fprintf(builder, "Eisenhower: %s\n", strings.Join(parts, ", "))
161 }
162}