1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package habit provides MCP tools for Lunatask habit operations.
6package habit
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// TrackToolName is the name of the track habit tool.
18const TrackToolName = "track_habit"
19
20// TrackToolDescription describes the track habit tool for LLMs.
21const TrackToolDescription = `Records that a habit was performed on a specific date.
22
23Required:
24- habit_id: Habit UUID, deep link, or config key
25
26Optional:
27- performed_on: Date performed (YYYY-MM-DD or natural language, default: today)
28
29Use the lunatask://habits resource to discover valid habit IDs.`
30
31// TrackInput is the input schema for tracking a habit.
32type TrackInput struct {
33 HabitID string `json:"habit_id" jsonschema:"required"`
34 PerformedOn *string `json:"performed_on,omitempty"`
35}
36
37// TrackOutput is the output schema for tracking a habit.
38type TrackOutput struct {
39 Success bool `json:"success"`
40 HabitID string `json:"habit_id"`
41 PerformedOn string `json:"performed_on"`
42}
43
44// Handler handles habit-related MCP tool requests.
45type Handler struct {
46 client *lunatask.Client
47 habits []shared.HabitProvider
48}
49
50// NewHandler creates a new habit handler.
51func NewHandler(accessToken string, habits []shared.HabitProvider) *Handler {
52 return &Handler{
53 client: lunatask.NewClient(accessToken, lunatask.UserAgent("lune-mcp/1.0")),
54 habits: habits,
55 }
56}
57
58// HandleTrack records a habit activity.
59func (h *Handler) HandleTrack(
60 ctx context.Context,
61 _ *mcp.CallToolRequest,
62 input TrackInput,
63) (*mcp.CallToolResult, TrackOutput, error) {
64 habitID := h.resolveHabitRef(input.HabitID)
65 if habitID == "" {
66 return shared.ErrorResult("unknown habit: " + input.HabitID), TrackOutput{}, nil
67 }
68
69 dateStr := ""
70 if input.PerformedOn != nil {
71 dateStr = *input.PerformedOn
72 }
73
74 performedOn, err := dateutil.Parse(dateStr)
75 if err != nil {
76 return shared.ErrorResult(err.Error()), TrackOutput{}, nil
77 }
78
79 req := &lunatask.TrackHabitActivityRequest{
80 PerformedOn: performedOn,
81 }
82
83 _, err = h.client.TrackHabitActivity(ctx, habitID, req)
84 if err != nil {
85 return shared.ErrorResult(err.Error()), TrackOutput{}, nil
86 }
87
88 return nil, TrackOutput{
89 Success: true,
90 HabitID: habitID,
91 PerformedOn: performedOn.Format("2006-01-02"),
92 }, nil
93}
94
95// resolveHabitRef resolves a habit reference to a UUID.
96// Accepts config key, UUID, or deep link.
97func (h *Handler) resolveHabitRef(input string) string {
98 // Try UUID or deep link first
99 if _, id, err := lunatask.ParseReference(input); err == nil {
100 return id
101 }
102
103 // Try config key lookup
104 for _, habit := range h.habits {
105 if habit.Key == input {
106 return habit.ID
107 }
108 }
109
110 return ""
111}