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