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}