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