1
- from datetime import datetime
1
+ from dataclasses import dataclass
2
+ from datetime import datetime , timedelta
3
+ from enum import Enum
2
4
import json
3
- from typing import Dict , Any , Optional , Sequence
5
+ from typing import Sequence
4
6
5
7
import pytz
6
8
from tzlocal import get_localzone
7
9
from mcp .server import Server
8
10
from mcp .server .stdio import stdio_server
9
11
from mcp .types import Tool , TextContent , ImageContent , EmbeddedResource
10
12
13
+ from pydantic import BaseModel
14
+
15
+
16
+ class TimeTools (str , Enum ):
17
+ GET_CURRENT_TIME = "get_current_time"
18
+ CONVERT_TIME = "convert_time"
19
+
20
+
21
+ class TimeResult (BaseModel ):
22
+ timezone : str
23
+ datetime : str
24
+ is_dst : bool
25
+
26
+
27
+ class TimeConversionResult (BaseModel ):
28
+ source : TimeResult
29
+ target : TimeResult
30
+ time_difference : str
31
+
32
+
33
+ class TimeConversionInput (BaseModel ):
34
+ source_tz : str
35
+ time : str
36
+ target_tz_list : list [str ]
37
+
11
38
12
39
class TimeServer :
13
- def __init__ (self , local_tz_override : Optional [str ] = None ):
14
- self .local_tz = pytz .timezone (local_tz_override ) if local_tz_override else get_localzone ()
40
+ def __init__ (self , local_tz_override : str | None = None ):
41
+ self .local_tz = (
42
+ pytz .timezone (local_tz_override ) if local_tz_override else get_localzone ()
43
+ )
44
+
45
+ def get_current_time (self , timezone_name : str ) -> TimeResult :
46
+ """Get current time in specified timezone"""
47
+ try :
48
+ timezone = pytz .timezone (timezone_name )
49
+ except pytz .exceptions .UnknownTimeZoneError as e :
50
+ raise ValueError (f"Unknown timezone: { str (e )} " )
15
51
16
- def get_current_time (self , timezone_name : str | None = None ) -> Dict [str , Any ]:
17
- """Get current time in specified timezone or local timezone if none specified"""
18
- timezone = pytz .timezone (timezone_name ) if timezone_name else self .local_tz
19
52
current_time = datetime .now (timezone )
20
-
21
- return {
22
- " timezone" : timezone_name or str ( self . local_tz ) ,
23
- "time" : current_time .strftime ( "%H:%M %Z " ),
24
- "date" : current_time .strftime ( "%Y-%m-%d" ),
25
- "full_datetime" : current_time . strftime ( "%Y-%m-%d %H:%M:%S %Z" ),
26
- "is_dst" : bool ( current_time . dst ())
27
- }
28
-
29
- def convert_time ( self , source_tz : str , time_str : str , target_tz : str ) -> Dict [ str , Any ] :
53
+
54
+ return TimeResult (
55
+ timezone = timezone_name ,
56
+ datetime = current_time .isoformat ( timespec = "seconds " ),
57
+ is_dst = bool ( current_time .dst () ),
58
+ )
59
+
60
+ def convert_time (
61
+ self , source_tz : str , time_str : str , target_tz : str
62
+ ) -> TimeConversionResult :
30
63
"""Convert time between timezones"""
31
64
try :
32
65
source_timezone = pytz .timezone (source_tz )
66
+ except pytz .exceptions .UnknownTimeZoneError as e :
67
+ raise ValueError (f"Unknown source timezone: { str (e )} " )
68
+
69
+ try :
33
70
target_timezone = pytz .timezone (target_tz )
34
-
35
- # Parse time
36
- hour , minute = map (int , time_str .split (":" ))
37
- if not (0 <= hour <= 23 and 0 <= minute <= 59 ):
38
- raise ValueError
39
71
except pytz .exceptions .UnknownTimeZoneError as e :
40
- raise ValueError (f"Unknown timezone: { str (e )} " )
41
- except :
42
- raise ValueError ("Invalid time format. Expected HH:MM (24-hour format)" )
43
-
44
- # Create time in source timezone
72
+ raise ValueError (f"Unknown target timezone: { str (e )} " )
73
+
74
+ try :
75
+ parsed_time = datetime .strptime (time_str , "%H:%M" ).time ()
76
+ except ValueError :
77
+ raise ValueError ("Invalid time format. Expected HH:MM [24-hour format]" )
78
+
45
79
now = datetime .now (source_timezone )
46
80
source_time = source_timezone .localize (
47
- datetime (now .year , now .month , now .day , hour , minute )
81
+ datetime (now .year , now .month , now .day , parsed_time . hour , parsed_time . minute )
48
82
)
49
-
50
- # Convert to target timezone
83
+
51
84
target_time = source_time .astimezone (target_timezone )
52
- date_changed = source_time .date () != target_time .date ()
53
-
54
- return {
55
- "source" : {
56
- "timezone" : str (source_timezone ),
57
- "time" : source_time .strftime ("%H:%M %Z" ),
58
- "date" : source_time .strftime ("%Y-%m-%d" ),
59
- "full_datetime" : source_time .strftime ("%Y-%m-%d %H:%M:%S %Z" )
60
- },
61
- "target" : {
62
- "timezone" : str (target_timezone ),
63
- "time" : target_time .strftime ("%H:%M %Z" ),
64
- "date" : target_time .strftime ("%Y-%m-%d" ),
65
- "full_datetime" : target_time .strftime ("%Y-%m-%d %H:%M:%S %Z" )
66
- },
67
- "time_difference" : f"{ (target_time .utcoffset () - source_time .utcoffset ()).total_seconds () / 3600 :+.1f} h" ,
68
- "date_changed" : date_changed ,
69
- "day_relation" : "next day" if date_changed and target_time .date () > source_time .date () else "previous day" if date_changed else "same day"
70
- }
71
-
72
-
73
- async def serve (local_timezone : Optional [str ] = None ) -> None :
85
+ hours_difference = (
86
+ target_time .utcoffset () - source_time .utcoffset ()
87
+ ).total_seconds () / 3600
88
+
89
+ if hours_difference .is_integer ():
90
+ time_diff_str = f"{ hours_difference :+.1f} h"
91
+ else :
92
+ # For fractional hours like Nepal's UTC+5:45
93
+ time_diff_str = f"{ hours_difference :+.2f} " .rstrip ("0" ).rstrip ("." ) + "h"
94
+
95
+ return TimeConversionResult (
96
+ source = TimeResult (
97
+ timezone = source_tz ,
98
+ datetime = source_time .isoformat (timespec = "seconds" ),
99
+ is_dst = bool (source_time .dst ()),
100
+ ),
101
+ target = TimeResult (
102
+ timezone = target_tz ,
103
+ datetime = target_time .isoformat (timespec = "seconds" ),
104
+ is_dst = bool (target_time .dst ()),
105
+ ),
106
+ time_difference = time_diff_str ,
107
+ )
108
+
109
+
110
+ async def serve (local_timezone : str | None = None ) -> None :
74
111
server = Server ("mcp-time" )
75
112
time_server = TimeServer (local_timezone )
76
113
local_tz = str (time_server .local_tz )
@@ -80,65 +117,76 @@ async def list_tools() -> list[Tool]:
80
117
"""List available time tools."""
81
118
return [
82
119
Tool (
83
- name = "get_current_time" ,
84
- description = f"Get current time in a specific timezone (current system timezone is { local_tz } ) " ,
120
+ name = TimeTools . GET_CURRENT_TIME . value ,
121
+ description = f"Get current time in a specific timezones " ,
85
122
inputSchema = {
86
123
"type" : "object" ,
87
124
"properties" : {
88
125
"timezone" : {
89
126
"type" : "string" ,
90
- "description" : "IANA timezone name (e.g., 'America/New_York', 'Europe/London', etc ). If not provided, uses system timezone"
127
+ "description" : f "IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use ' { local_tz } ' as local timezone if no timezone provided by the user." ,
91
128
}
92
- }
93
- }
129
+ },
130
+ "required" : ["timezone" ],
131
+ },
94
132
),
95
133
Tool (
96
- name = "convert_time" ,
97
- description = f"Convert time between timezones using IANA timezone names (system timezone is { local_tz } , can be used as source or target) " ,
134
+ name = TimeTools . CONVERT_TIME . value ,
135
+ description = f"Convert time between timezones" ,
98
136
inputSchema = {
99
137
"type" : "object" ,
100
138
"properties" : {
101
139
"source_timezone" : {
102
140
"type" : "string" ,
103
- "description" : f"Source IANA timezone name (e.g., '{ local_tz } ', 'America/New_York')"
141
+ "description" : f"Source IANA timezone name (e.g., 'America/New_York ', 'Europe/London'). Use ' { local_tz } ' as local timezone if no source timezone provided by the user." ,
104
142
},
105
143
"time" : {
106
144
"type" : "string" ,
107
- "description" : "Time in 24-hour format (HH:MM)"
145
+ "description" : "Time to convert in 24-hour format (HH:MM)" ,
108
146
},
109
147
"target_timezone" : {
110
148
"type" : "string" ,
111
- "description" : f"Target IANA timezone name (e.g., '{ local_tz } ', 'Asia/Tokyo')"
112
- }
149
+ "description" : f"Target IANA timezone name (e.g., 'Asia/Tokyo ', 'America/San_Francisco'). Use ' { local_tz } ' as local timezone if no target timezone provided by the user." ,
150
+ },
113
151
},
114
- "required" : ["source_timezone" , "time" , "target_timezone" ]
115
- }
116
- )
152
+ "required" : ["source_timezone" , "time" , "target_timezone" ],
153
+ },
154
+ ),
117
155
]
118
156
119
157
@server .call_tool ()
120
- async def call_tool (name : str , arguments : dict ) -> Sequence [TextContent | ImageContent | EmbeddedResource ]:
158
+ async def call_tool (
159
+ name : str , arguments : dict
160
+ ) -> Sequence [TextContent | ImageContent | EmbeddedResource ]:
121
161
"""Handle tool calls for time queries."""
122
162
try :
123
- if name == "get_current_time" :
124
- timezone = arguments .get ("timezone" )
125
- result = time_server .get_current_time (timezone )
126
- elif name == "convert_time" :
127
- if not all (k in arguments for k in ["source_timezone" , "time" , "target_timezone" ]):
128
- raise ValueError ("Missing required arguments" )
129
-
130
- result = time_server .convert_time (
131
- arguments ["source_timezone" ],
132
- arguments ["time" ],
133
- arguments ["target_timezone" ]
134
- )
135
- else :
136
- raise ValueError (f"Unknown tool: { name } " )
163
+ match name :
164
+ case TimeTools .GET_CURRENT_TIME .value :
165
+ timezone = arguments .get ("timezone" )
166
+ if not timezone :
167
+ raise ValueError ("Missing required argument: timezone" )
168
+
169
+ result = time_server .get_current_time (timezone )
170
+
171
+ case TimeTools .CONVERT_TIME .value :
172
+ if not all (
173
+ k in arguments
174
+ for k in ["source_timezone" , "time" , "target_timezone" ]
175
+ ):
176
+ raise ValueError ("Missing required arguments" )
177
+
178
+ result = time_server .convert_time (
179
+ arguments ["source_timezone" ],
180
+ arguments ["time" ],
181
+ arguments ["target_timezone" ],
182
+ )
183
+ case _:
184
+ raise ValueError (f"Unknown tool: { name } " )
137
185
138
186
return [TextContent (type = "text" , text = json .dumps (result , indent = 2 ))]
139
-
187
+
140
188
except Exception as e :
141
- raise ValueError (f"Error processing time query: { str (e )} " )
189
+ raise ValueError (f"Error processing mcp-server- time query: { str (e )} " )
142
190
143
191
options = server .create_initialization_options ()
144
192
async with stdio_server () as (read_stream , write_stream ):
0 commit comments