1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package task provides the MCP resource handler for individual Lunatask tasks.
6package task
7
8import (
9 "context"
10 "encoding/json"
11 "fmt"
12 "time"
13
14 "git.secluded.site/go-lunatask"
15 "github.com/modelcontextprotocol/go-sdk/mcp"
16)
17
18// ResourceTemplate is the URI template for task resources.
19const ResourceTemplate = "lunatask://task/{id}"
20
21// ResourceDescription describes the task resource for LLMs.
22const ResourceDescription = `Reads metadata for a specific Lunatask task by ID or deep link.
23
24Due to end-to-end encryption, task name and note content are not available.
25Returns metadata including status, priority, dates, area, and goal.
26
27Use list_tasks tool to discover task IDs, then read individual tasks here.`
28
29// taskInfo represents task metadata in the resource response.
30type taskInfo struct {
31 DeepLink string `json:"deep_link"`
32 Status *string `json:"status,omitempty"`
33 Priority *int `json:"priority,omitempty"`
34 Estimate *int `json:"estimate,omitempty"`
35 ScheduledOn *string `json:"scheduled_on,omitempty"`
36 CompletedAt *string `json:"completed_at,omitempty"`
37 CreatedAt string `json:"created_at"`
38 UpdatedAt string `json:"updated_at"`
39 AreaID *string `json:"area_id,omitempty"`
40 GoalID *string `json:"goal_id,omitempty"`
41 Important *bool `json:"important,omitempty"`
42 Urgent *bool `json:"urgent,omitempty"`
43}
44
45// Handler handles task resource requests.
46type Handler struct {
47 client *lunatask.Client
48}
49
50// NewHandler creates a new task resource handler.
51func NewHandler(accessToken string) *Handler {
52 return &Handler{
53 client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
54 }
55}
56
57// HandleRead returns metadata for a specific task.
58func (h *Handler) HandleRead(
59 ctx context.Context,
60 req *mcp.ReadResourceRequest,
61) (*mcp.ReadResourceResult, error) {
62 _, id, err := lunatask.ParseReference(req.Params.URI)
63 if err != nil {
64 return nil, fmt.Errorf("invalid URI %q: %w", req.Params.URI, mcp.ResourceNotFoundError(req.Params.URI))
65 }
66
67 task, err := h.client.GetTask(ctx, id)
68 if err != nil {
69 return nil, fmt.Errorf("fetching task: %w", err)
70 }
71
72 info := buildTaskInfo(task)
73
74 data, err := json.MarshalIndent(info, "", " ")
75 if err != nil {
76 return nil, fmt.Errorf("marshaling task: %w", err)
77 }
78
79 return &mcp.ReadResourceResult{
80 Contents: []*mcp.ResourceContents{{
81 URI: req.Params.URI,
82 MIMEType: "application/json",
83 Text: string(data),
84 }},
85 }, nil
86}
87
88func buildTaskInfo(task *lunatask.Task) taskInfo {
89 info := taskInfo{
90 CreatedAt: task.CreatedAt.Format(time.RFC3339),
91 UpdatedAt: task.UpdatedAt.Format(time.RFC3339),
92 AreaID: task.AreaID,
93 GoalID: task.GoalID,
94 }
95
96 info.DeepLink, _ = lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
97
98 if task.Status != nil {
99 s := string(*task.Status)
100 info.Status = &s
101 }
102
103 if task.Priority != nil {
104 p := int(*task.Priority)
105 info.Priority = &p
106 }
107
108 if task.Estimate != nil {
109 info.Estimate = task.Estimate
110 }
111
112 if task.ScheduledOn != nil {
113 s := task.ScheduledOn.Format("2006-01-02")
114 info.ScheduledOn = &s
115 }
116
117 if task.CompletedAt != nil {
118 s := task.CompletedAt.Format(time.RFC3339)
119 info.CompletedAt = &s
120 }
121
122 if task.Eisenhower != nil {
123 important := task.Eisenhower.IsImportant()
124 urgent := task.Eisenhower.IsUrgent()
125 info.Important = &important
126 info.Urgent = &urgent
127 }
128
129 return info
130}