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}