handler.go

  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}