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}