main
  1package time
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"strconv"
  7	"strings"
  8	"time"
  9
 10	"github.com/xlgmokha/mcp/pkg/mcp"
 11)
 12
 13// TimeResult represents the result of a time operation
 14type TimeResult struct {
 15	Timezone string `json:"timezone"`
 16	Datetime string `json:"datetime"`
 17	IsDST    bool   `json:"is_dst"`
 18}
 19
 20// TimeConversionResult represents the result of a time conversion
 21type TimeConversionResult struct {
 22	Source         TimeResult `json:"source"`
 23	Target         TimeResult `json:"target"`
 24	TimeDifference string     `json:"time_difference"`
 25}
 26
 27// TimeOperations provides time operations and timezone handling
 28type TimeOperations struct {
 29	localTimezone string
 30}
 31
 32// NewTimeOperations creates a new TimeOperations helper
 33func NewTimeOperations() *TimeOperations {
 34	return &TimeOperations{
 35		localTimezone: getLocalTimezone(),
 36	}
 37}
 38
 39// New creates a new Time MCP server
 40func New() *mcp.Server {
 41	timeOps := NewTimeOperations()
 42	builder := mcp.NewServerBuilder("mcp-time", "1.0.0")
 43
 44	// Add get_current_time tool
 45	builder.AddTool(mcp.NewTool("get_current_time", "Get current time in a specific timezone", map[string]interface{}{
 46		"type": "object",
 47		"properties": map[string]interface{}{
 48			"timezone": map[string]interface{}{
 49				"type":        "string",
 50				"description": fmt.Sprintf("IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '%s' as local timezone if no timezone provided by the user.", timeOps.localTimezone),
 51			},
 52		},
 53		"required": []string{"timezone"},
 54	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 55		timezone, ok := req.Arguments["timezone"].(string)
 56		if !ok {
 57			return mcp.NewToolError("timezone is required"), nil
 58		}
 59
 60		result, err := timeOps.getCurrentTime(timezone)
 61		if err != nil {
 62			return mcp.NewToolError(err.Error()), nil
 63		}
 64
 65		jsonResult, err := json.MarshalIndent(result, "", "  ")
 66		if err != nil {
 67			return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
 68		}
 69
 70		return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
 71	}))
 72
 73	// Add convert_time tool
 74	builder.AddTool(mcp.NewTool("convert_time", "Convert time between timezones", map[string]interface{}{
 75		"type": "object",
 76		"properties": map[string]interface{}{
 77			"source_timezone": map[string]interface{}{
 78				"type":        "string",
 79				"description": fmt.Sprintf("Source IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '%s' as local timezone if no source timezone provided by the user.", timeOps.localTimezone),
 80			},
 81			"time": map[string]interface{}{
 82				"type":        "string",
 83				"description": "Time to convert in 24-hour format (HH:MM)",
 84			},
 85			"target_timezone": map[string]interface{}{
 86				"type":        "string",
 87				"description": fmt.Sprintf("Target IANA timezone name (e.g., 'Asia/Tokyo', 'America/San_Francisco'). Use '%s' as local timezone if no target timezone provided by the user.", timeOps.localTimezone),
 88			},
 89		},
 90		"required": []string{"source_timezone", "time", "target_timezone"},
 91	}, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
 92		sourceTimezone, ok := req.Arguments["source_timezone"].(string)
 93		if !ok {
 94			return mcp.NewToolError("source_timezone is required"), nil
 95		}
 96
 97		timeStr, ok := req.Arguments["time"].(string)
 98		if !ok {
 99			return mcp.NewToolError("time is required"), nil
100		}
101
102		targetTimezone, ok := req.Arguments["target_timezone"].(string)
103		if !ok {
104			return mcp.NewToolError("target_timezone is required"), nil
105		}
106
107		result, err := timeOps.convertTime(sourceTimezone, timeStr, targetTimezone)
108		if err != nil {
109			return mcp.NewToolError(err.Error()), nil
110		}
111
112		jsonResult, err := json.MarshalIndent(result, "", "  ")
113		if err != nil {
114			return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
115		}
116
117		return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
118	}))
119
120	return builder.Build()
121}
122
123func (timeOps *TimeOperations) getCurrentTime(timezone string) (*TimeResult, error) {
124	loc, err := time.LoadLocation(timezone)
125	if err != nil {
126		return nil, fmt.Errorf("Invalid timezone: %v", err)
127	}
128
129	currentTime := time.Now().In(loc)
130
131	return &TimeResult{
132		Timezone: timezone,
133		Datetime: currentTime.Format("2006-01-02T15:04:05-07:00"),
134		IsDST:    isDST(currentTime),
135	}, nil
136}
137
138func (timeOps *TimeOperations) convertTime(sourceTimezone, timeStr, targetTimezone string) (*TimeConversionResult, error) {
139	sourceLoc, err := time.LoadLocation(sourceTimezone)
140	if err != nil {
141		return nil, fmt.Errorf("Invalid source timezone: %v", err)
142	}
143
144	targetLoc, err := time.LoadLocation(targetTimezone)
145	if err != nil {
146		return nil, fmt.Errorf("Invalid target timezone: %v", err)
147	}
148
149	parts := strings.Split(timeStr, ":")
150	if len(parts) != 2 {
151		return nil, fmt.Errorf("Invalid time format. Expected HH:MM [24-hour format]")
152	}
153
154	hour, err := strconv.Atoi(parts[0])
155	if err != nil || hour < 0 || hour > 23 {
156		return nil, fmt.Errorf("Invalid time format. Expected HH:MM [24-hour format]")
157	}
158
159	minute, err := strconv.Atoi(parts[1])
160	if err != nil || minute < 0 || minute > 59 {
161		return nil, fmt.Errorf("Invalid time format. Expected HH:MM [24-hour format]")
162	}
163
164	now := time.Now().In(sourceLoc)
165	sourceTime := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, sourceLoc)
166
167	targetTime := sourceTime.In(targetLoc)
168	sourceOffset := getTimezoneOffset(sourceTime)
169	targetOffset := getTimezoneOffset(targetTime)
170	hoursDifference := float64(targetOffset-sourceOffset) / 3600
171
172	timeDiffStr := formatTimeDifference(hoursDifference)
173
174	return &TimeConversionResult{
175		Source: TimeResult{
176			Timezone: sourceTimezone,
177			Datetime: sourceTime.Format("2006-01-02T15:04:05-07:00"),
178			IsDST:    isDST(sourceTime),
179		},
180		Target: TimeResult{
181			Timezone: targetTimezone,
182			Datetime: targetTime.Format("2006-01-02T15:04:05-07:00"),
183			IsDST:    isDST(targetTime),
184		},
185		TimeDifference: timeDiffStr,
186	}, nil
187}
188
189func getLocalTimezone() string {
190	now := time.Now()
191	zone, _ := now.Zone()
192	if zone == "" {
193		return "UTC"
194	}
195
196	loc := now.Location()
197	if loc != nil && loc.String() != "" && loc.String() != "Local" {
198		return loc.String()
199	}
200
201	return "UTC"
202}
203
204func isDST(t time.Time) bool {
205	_, offset := t.Zone()
206
207	jan := time.Date(t.Year(), 1, 1, 12, 0, 0, 0, t.Location())
208	jul := time.Date(t.Year(), 7, 1, 12, 0, 0, 0, t.Location())
209
210	_, janOffset := jan.Zone()
211	_, julOffset := jul.Zone()
212
213	if janOffset != julOffset {
214		if janOffset < julOffset {
215			return offset == julOffset
216		} else {
217			return offset == janOffset
218		}
219	}
220
221	return false
222}
223
224func getTimezoneOffset(t time.Time) int {
225	_, offset := t.Zone()
226	return offset
227}
228
229func formatTimeDifference(hours float64) string {
230	if hours == float64(int(hours)) {
231		return fmt.Sprintf("%+.1fh", hours)
232	} else {
233		formatted := fmt.Sprintf("%+.2f", hours)
234		formatted = strings.TrimRight(formatted, "0")
235		formatted = strings.TrimRight(formatted, ".")
236		return formatted + "h"
237	}
238}