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
 18// AreaProvider defines the interface for accessing area data.
 19type AreaProvider interface {
 20	GetName() string
 21	GetID() string
 22	GetGoals() []GoalProvider
 23}
 24
 25// GoalProvider defines the interface for accessing goal data.
 26type GoalProvider interface {
 27	GetName() string
 28	GetID() string
 29}
 30
 31// HabitProvider defines the interface for accessing habit data.
 32type HabitProvider interface {
 33	GetName() string
 34	GetID() string
 35}
 36
 37// HandlerConfig holds the necessary configuration for tool handlers.
 38type HandlerConfig struct {
 39	AccessToken string
 40	Timezone    string
 41	Areas       []AreaProvider
 42	Habits      []HabitProvider
 43}
 44
 45// Handlers provides methods for handling MCP tool calls.
 46type Handlers struct {
 47	config HandlerConfig
 48}
 49
 50// NewHandlers creates a new Handlers instance.
 51func NewHandlers(config HandlerConfig) *Handlers {
 52	return &Handlers{config: config}
 53}
 54
 55// reportMCPError creates an MCP error result.
 56func reportMCPError(msg string) (*mcp.CallToolResult, error) {
 57	return &mcp.CallToolResult{
 58		IsError: true,
 59		Content: []mcp.Content{mcp.TextContent{Type: "text", Text: msg}},
 60	}, nil
 61}
 62
 63// LoadLocation loads a timezone location string, returning a *time.Location or error
 64func LoadLocation(timezone string) (*time.Location, error) {
 65	if timezone == "" {
 66		return nil, fmt.Errorf("timezone is not configured; please set the 'timezone' value in your config file (e.g. 'UTC' or 'America/New_York')")
 67	}
 68	loc, err := time.LoadLocation(timezone)
 69	if err != nil {
 70		return nil, fmt.Errorf("could not load timezone '%s': %v", timezone, err)
 71	}
 72	return loc, nil
 73}
 74
 75// HandleGetTimestamp handles the get_timestamp tool call.
 76func (h *Handlers) HandleGetTimestamp(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 77	natLangDate, ok := request.Params.Arguments["natural_language_date"].(string)
 78	if !ok || natLangDate == "" {
 79		return reportMCPError("Missing or invalid required argument: natural_language_date")
 80	}
 81	loc, err := LoadLocation(h.config.Timezone)
 82	if err != nil {
 83		return reportMCPError(err.Error())
 84	}
 85	parsedTime, err := anytime.Parse(natLangDate, time.Now().In(loc))
 86	if err != nil {
 87		return reportMCPError(fmt.Sprintf("Could not parse natural language date: %v", err))
 88	}
 89	return &mcp.CallToolResult{
 90		Content: []mcp.Content{
 91			mcp.TextContent{
 92				Type: "text",
 93				Text: parsedTime.Format(time.RFC3339),
 94			},
 95		},
 96	}, nil
 97}
 98
 99// HandleListAreasAndGoals handles the list_areas_and_goals tool call.
100func (h *Handlers) HandleListAreasAndGoals(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
101	var b strings.Builder
102	for _, area := range h.config.Areas {
103		fmt.Fprintf(&b, "- %s: %s\n", area.GetName(), area.GetID())
104		for _, goal := range area.GetGoals() {
105			fmt.Fprintf(&b, "  - %s: %s\n", goal.GetName(), goal.GetID())
106		}
107	}
108	return &mcp.CallToolResult{
109		Content: []mcp.Content{
110			mcp.TextContent{
111				Type: "text",
112				Text: b.String(),
113			},
114		},
115	}, nil
116}
117// HandleGetTimestamp handles the get_timestamp tool call.
118func (h *Handlers) HandleGetTimestamp(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
119	natLangDate, ok := request.Params.Arguments["natural_language_date"].(string)
120	if !ok || natLangDate == "" {
121		return reportMCPError("Missing or invalid required argument: natural_language_date")
122	}
123	loc, err := LoadLocation(h.config.Timezone)
124	if err != nil {
125		return reportMCPError(err.Error())
126	}
127	parsedTime, err := anytime.Parse(natLangDate, time.Now().In(loc))
128	if err != nil {
129		return reportMCPError(fmt.Sprintf("Could not parse natural language date: %v", err))
130	}
131	return &mcp.CallToolResult{
132		Content: []mcp.Content{
133			mcp.TextContent{
134				Type: "text",
135				Text: parsedTime.Format(time.RFC3339),
136			},
137		},
138	}, nil
139}
140
141// HandleListAreasAndGoals handles the list_areas_and_goals tool call.
142func (h *Handlers) HandleListAreasAndGoals(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
143	var b strings.Builder
144	for _, area := range h.config.Areas {
145		fmt.Fprintf(&b, "- %s: %s\n", area.GetName(), area.GetID())
146		for _, goal := range area.GetGoals() {
147			fmt.Fprintf(&b, "  - %s: %s\n", goal.GetName(), goal.GetID())
148		}
149	}
150	return &mcp.CallToolResult{
151		Content: []mcp.Content{
152			mcp.TextContent{
153				Type: "text",
154				Text: b.String(),
155			},
156		},
157	}, nil
158}