1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package tools
  6
  7import (
  8	"context"
  9	"fmt"
 10	"strings"
 11	"time"
 12
 13	"github.com/ijt/go-anytime"
 14	"github.com/mark3labs/mcp-go/mcp"
 15)
 16
 17// AreaProvider defines the interface for accessing area data.
 18type AreaProvider interface {
 19	GetName() string
 20	GetID() string
 21	GetGoals() []GoalProvider
 22}
 23
 24// GoalProvider defines the interface for accessing goal data.
 25type GoalProvider interface {
 26	GetName() string
 27	GetID() string
 28}
 29
 30// HabitProvider defines the interface for accessing habit data.
 31type HabitProvider interface {
 32	GetName() string
 33	GetID() string
 34}
 35
 36// HandlerConfig holds the necessary configuration for tool handlers.
 37type HandlerConfig struct {
 38	AccessToken string
 39	Timezone    string
 40	Areas       []AreaProvider
 41	Habits      []HabitProvider
 42}
 43
 44// Handlers provides methods for handling MCP tool calls.
 45type Handlers struct {
 46	config HandlerConfig
 47}
 48
 49// NewHandlers creates a new Handlers instance.
 50func NewHandlers(config HandlerConfig) *Handlers {
 51	return &Handlers{config: config}
 52}
 53
 54// reportMCPError creates an MCP error result.
 55func reportMCPError(msg string) (*mcp.CallToolResult, error) {
 56	return &mcp.CallToolResult{
 57		IsError: true,
 58		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: msg}},
 59	}, nil
 60}
 61
 62// LoadLocation loads a timezone location string, returning a *time.Location or error
 63func LoadLocation(timezone string) (*time.Location, error) {
 64	if timezone == "" {
 65		return nil, fmt.Errorf("timezone is not configured; please set the 'timezone' value in your config file (e.g. 'UTC' or 'America/New_York')")
 66	}
 67	loc, err := time.LoadLocation(timezone)
 68	if err != nil {
 69		return nil, fmt.Errorf("could not load timezone '%s': %v", timezone, err)
 70	}
 71	return loc, nil
 72}
 73
 74// HandleGetTimestamp handles the get_timestamp tool call.
 75func (h *Handlers) HandleGetTimestamp(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 76	natLangDate, ok := request.Params.Arguments["natural_language_date"].(string)
 77	if !ok || natLangDate == "" {
 78		return reportMCPError("Missing or invalid required argument: natural_language_date")
 79	}
 80	loc, err := LoadLocation(h.config.Timezone)
 81	if err != nil {
 82		return reportMCPError(err.Error())
 83	}
 84	parsedTime, err := anytime.Parse(natLangDate, time.Now().In(loc))
 85	if err != nil {
 86		return reportMCPError(fmt.Sprintf("Could not parse natural language date: %v", err))
 87	}
 88	return &mcp.CallToolResult{
 89		Content: []mcp.Content{
 90			mcp.TextContent{
 91				Type: "text",
 92				Text: parsedTime.Format(time.RFC3339),
 93			},
 94		},
 95	}, nil
 96}
 97
 98// HandleListAreasAndGoals handles the list_areas_and_goals tool call.
 99func (h *Handlers) HandleListAreasAndGoals(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
100	var b strings.Builder
101	for _, area := range h.config.Areas {
102		fmt.Fprintf(&b, "- %s: %s\n", area.GetName(), area.GetID())
103		for _, goal := range area.GetGoals() {
104			fmt.Fprintf(&b, "  - %s: %s\n", goal.GetName(), goal.GetID())
105		}
106	}
107	return &mcp.CallToolResult{
108		Content: []mcp.Content{
109			mcp.TextContent{
110				Type: "text",
111				Text: b.String(),
112			},
113		},
114	}, nil
115}