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// 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}
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}