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}