1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package habits provides MCP tools for habit tracking in Lunatask.
6package habits
7
8import (
9 "context"
10 "encoding/json"
11 "fmt"
12
13 "git.secluded.site/go-lunatask"
14 "github.com/modelcontextprotocol/go-sdk/mcp"
15
16 "git.secluded.site/lunatask-mcp-server/tools/shared"
17)
18
19// Handler handles habit-related MCP tool calls.
20type Handler struct {
21 client *lunatask.Client
22 habits []shared.HabitProvider
23}
24
25// NewHandler creates a new habits Handler for tool operations.
26func NewHandler(accessToken string, habits []shared.HabitProvider) *Handler {
27 return &Handler{
28 client: lunatask.NewClient(accessToken),
29 habits: habits,
30 }
31}
32
33// ResourceHandler handles habit-related MCP resource requests.
34type ResourceHandler struct {
35 habits []shared.HabitProvider
36}
37
38// NewResourceHandler creates a new habits ResourceHandler for resource reads.
39func NewResourceHandler(habits []shared.HabitProvider) *ResourceHandler {
40 return &ResourceHandler{habits: habits}
41}
42
43// HandleTrack handles the track_habit_activity tool call.
44func (h *Handler) HandleTrack(
45 ctx context.Context,
46 _ *mcp.CallToolRequest,
47 input TrackInput,
48) (*mcp.CallToolResult, TrackOutput, error) {
49 // Resolve habit by ID or key
50 habit := shared.FindHabit(h.habits, input.HabitID)
51 if habit == nil {
52 return nil, TrackOutput{}, fmt.Errorf("habit not found: %s", input.HabitID)
53 }
54
55 performedOn, err := lunatask.ParseDate(input.PerformedOn)
56 if err != nil {
57 return nil, TrackOutput{}, fmt.Errorf(
58 "invalid format for performed_on %q: must be YYYY-MM-DD",
59 input.PerformedOn,
60 )
61 }
62
63 resp, err := h.client.TrackHabitActivity(ctx, habit.GetID(), &lunatask.TrackHabitActivityRequest{
64 PerformedOn: performedOn,
65 })
66 if err != nil {
67 return nil, TrackOutput{}, fmt.Errorf("failed to track habit activity: %w", err)
68 }
69
70 return nil, TrackOutput{
71 Status: resp.Status,
72 Message: resp.Message,
73 }, nil
74}
75
76// ResourceURI is the URI for the habits resource.
77const ResourceURI = "lunatask://habits"
78
79// HabitInfo represents a habit for JSON serialization.
80type HabitInfo struct {
81 Key string `json:"key"`
82 Name string `json:"name"`
83 ID string `json:"id"`
84}
85
86// HandleRead handles the habits resource read request.
87func (h *ResourceHandler) HandleRead(
88 _ context.Context,
89 _ *mcp.ReadResourceRequest,
90) (*mcp.ReadResourceResult, error) {
91 habitsInfo := make([]HabitInfo, 0, len(h.habits))
92
93 for _, habit := range h.habits {
94 habitsInfo = append(habitsInfo, HabitInfo{
95 Key: habit.GetKey(),
96 Name: habit.GetName(),
97 ID: habit.GetID(),
98 })
99 }
100
101 data, err := json.MarshalIndent(habitsInfo, "", " ")
102 if err != nil {
103 return nil, err
104 }
105
106 return &mcp.ReadResourceResult{
107 Contents: []*mcp.ResourceContents{{
108 URI: ResourceURI,
109 MIMEType: "application/json",
110 Text: string(data),
111 }},
112 }, nil
113}