tools.go

  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	"errors"
 10	"fmt"
 11	"strings"
 12	"time"
 13
 14	"github.com/ijt/go-anytime"
 15	"github.com/mark3labs/mcp-go/mcp"
 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, errors.New("timezone is not configured; please set the 'timezone' value in your config file (e.g. 'UTC' or 'America/New_York')")
 67	}
 68
 69	loc, err := time.LoadLocation(timezone)
 70	if err != nil {
 71		return nil, fmt.Errorf("could not load timezone '%s': %w", timezone, err)
 72	}
 73
 74	return loc, nil
 75}
 76
 77// HandleGetTimestamp handles the get_timestamp tool call.
 78func (h *Handlers) HandleGetTimestamp(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 79	natLangDate, ok := request.Params.Arguments["natural_language_date"].(string)
 80	if !ok || natLangDate == "" {
 81		return reportMCPError("Missing or invalid required argument: natural_language_date")
 82	}
 83
 84	loc, err := LoadLocation(h.config.Timezone)
 85	if err != nil {
 86		return reportMCPError(err.Error())
 87	}
 88
 89	parsedTime, err := anytime.Parse(natLangDate, time.Now().In(loc))
 90	if err != nil {
 91		return reportMCPError(fmt.Sprintf("Could not parse natural language date: %v", err))
 92	}
 93
 94	return &mcp.CallToolResult{
 95		Content: []mcp.Content{
 96			mcp.TextContent{
 97				Type: "text",
 98				Text: parsedTime.Format(time.RFC3339),
 99			},
100		},
101	}, nil
102}
103
104// HandleListAreasAndGoals handles the list_areas_and_goals tool call.
105func (h *Handlers) HandleListAreasAndGoals(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
106	var b strings.Builder
107	for _, area := range h.config.Areas {
108		fmt.Fprintf(&b, "- %s: %s\n", area.GetName(), area.GetID())
109
110		for _, goal := range area.GetGoals() {
111			fmt.Fprintf(&b, "  - %s: %s\n", goal.GetName(), goal.GetID())
112		}
113	}
114
115	return &mcp.CallToolResult{
116		Content: []mcp.Content{
117			mcp.TextContent{
118				Type: "text",
119				Text: b.String(),
120			},
121		},
122	}, nil
123}