Skip to content

Commit bccd33f

Browse files
committed
fix time server
1 parent 18f6ab6 commit bccd33f

File tree

5 files changed

+78
-56
lines changed

5 files changed

+78
-56
lines changed

src/time/pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ version = "0.5.1"
44
description = "A Model Context Protocol server providing tools for time queries and timezone conversions for LLMs"
55
readme = "README.md"
66
requires-python = ">=3.10"
7-
authors = [{ name = "Mariusz 'maledorak' Korzekwa", email = "[email protected]" }]
7+
authors = [
8+
{ name = "Mariusz 'maledorak' Korzekwa", email = "[email protected]" },
9+
]
810
keywords = ["time", "timezone", "mcp", "llm"]
911
license = { text = "MIT" }
1012
classifiers = [
@@ -17,8 +19,7 @@ classifiers = [
1719
dependencies = [
1820
"mcp>=1.0.0",
1921
"pydantic>=2.0.0",
20-
"pytz>=2024.2",
21-
"tzlocal>=5.2",
22+
"tzdata>=2024.2",
2223
]
2324

2425
[project.scripts]
@@ -33,4 +34,5 @@ dev-dependencies = [
3334
"freezegun>=1.5.1",
3435
"pyright>=1.1.389",
3536
"pytest>=8.3.3",
37+
"ruff>=0.8.1",
3638
]

src/time/src/mcp_server_time/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .server import serve
22

3+
34
def main():
45
"""MCP Time Server - Time and timezone conversion functionality for MCP"""
56
import argparse

src/time/src/mcp_server_time/server.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
from datetime import datetime
1+
from datetime import datetime, timedelta
22
from enum import Enum
33
import json
44
from typing import Sequence
55

6-
import pytz
7-
from tzlocal import get_localzone
6+
from zoneinfo import ZoneInfo
87
from mcp.server import Server
98
from mcp.server.stdio import stdio_server
109
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
10+
from mcp.shared.exceptions import McpError
1111

1212
from pydantic import BaseModel
1313

@@ -34,17 +34,29 @@ class TimeConversionInput(BaseModel):
3434
time: str
3535
target_tz_list: list[str]
3636

37-
def get_local_tz(local_tz_override: str | None = None) -> pytz.timezone:
38-
return pytz.timezone(local_tz_override) if local_tz_override else get_localzone()
37+
38+
def get_local_tz(local_tz_override: str | None = None) -> ZoneInfo:
39+
if local_tz_override:
40+
return ZoneInfo(local_tz_override)
41+
42+
# Get local timezone from datetime.now()
43+
tzinfo = datetime.now().astimezone(tz=None).tzinfo
44+
if tzinfo is not None:
45+
return ZoneInfo(str(tzinfo))
46+
raise McpError("Could not determine local timezone - tzinfo is None")
47+
48+
49+
def get_zoneinfo(timezone_name: str) -> ZoneInfo:
50+
try:
51+
return ZoneInfo(timezone_name)
52+
except Exception as e:
53+
raise McpError(f"Invalid timezone: {str(e)}")
54+
3955

4056
class TimeServer:
4157
def get_current_time(self, timezone_name: str) -> TimeResult:
4258
"""Get current time in specified timezone"""
43-
try:
44-
timezone = pytz.timezone(timezone_name)
45-
except pytz.exceptions.UnknownTimeZoneError as e:
46-
raise ValueError(f"Unknown timezone: {str(e)}")
47-
59+
timezone = get_zoneinfo(timezone_name)
4860
current_time = datetime.now(timezone)
4961

5062
return TimeResult(
@@ -57,30 +69,28 @@ def convert_time(
5769
self, source_tz: str, time_str: str, target_tz: str
5870
) -> TimeConversionResult:
5971
"""Convert time between timezones"""
60-
try:
61-
source_timezone = pytz.timezone(source_tz)
62-
except pytz.exceptions.UnknownTimeZoneError as e:
63-
raise ValueError(f"Unknown source timezone: {str(e)}")
64-
65-
try:
66-
target_timezone = pytz.timezone(target_tz)
67-
except pytz.exceptions.UnknownTimeZoneError as e:
68-
raise ValueError(f"Unknown target timezone: {str(e)}")
72+
source_timezone = get_zoneinfo(source_tz)
73+
target_timezone = get_zoneinfo(target_tz)
6974

7075
try:
7176
parsed_time = datetime.strptime(time_str, "%H:%M").time()
7277
except ValueError:
7378
raise ValueError("Invalid time format. Expected HH:MM [24-hour format]")
7479

7580
now = datetime.now(source_timezone)
76-
source_time = source_timezone.localize(
77-
datetime(now.year, now.month, now.day, parsed_time.hour, parsed_time.minute)
81+
source_time = datetime(
82+
now.year,
83+
now.month,
84+
now.day,
85+
parsed_time.hour,
86+
parsed_time.minute,
87+
tzinfo=source_timezone,
7888
)
7989

8090
target_time = source_time.astimezone(target_timezone)
81-
hours_difference = (
82-
target_time.utcoffset() - source_time.utcoffset()
83-
).total_seconds() / 3600
91+
source_offset = source_time.utcoffset() or timedelta()
92+
target_offset = target_time.utcoffset() or timedelta()
93+
hours_difference = (target_offset - source_offset).total_seconds() / 3600
8494

8595
if hours_difference.is_integer():
8696
time_diff_str = f"{hours_difference:+.1f}h"
@@ -114,7 +124,7 @@ async def list_tools() -> list[Tool]:
114124
return [
115125
Tool(
116126
name=TimeTools.GET_CURRENT_TIME.value,
117-
description=f"Get current time in a specific timezones",
127+
description="Get current time in a specific timezones",
118128
inputSchema={
119129
"type": "object",
120130
"properties": {
@@ -128,7 +138,7 @@ async def list_tools() -> list[Tool]:
128138
),
129139
Tool(
130140
name=TimeTools.CONVERT_TIME.value,
131-
description=f"Convert time between timezones",
141+
description="Convert time between timezones",
132142
inputSchema={
133143
"type": "object",
134144
"properties": {
@@ -179,7 +189,9 @@ async def call_tool(
179189
case _:
180190
raise ValueError(f"Unknown tool: {name}")
181191

182-
return [TextContent(type="text", text=json.dumps(result.model_dump(), indent=2))]
192+
return [
193+
TextContent(type="text", text=json.dumps(result.model_dump(), indent=2))
194+
]
183195

184196
except Exception as e:
185197
raise ValueError(f"Error processing mcp-server-time query: {str(e)}")

src/time/test/time_server_test.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import os
21

32
from freezegun import freeze_time
3+
from mcp.shared.exceptions import McpError
44
import pytest
55

6-
from mcp_server_time.server import TimeServer, serve
6+
from mcp_server_time.server import TimeServer
77

88

99
@pytest.mark.parametrize(
@@ -82,7 +82,10 @@ def test_get_current_time(test_time, timezone, expected):
8282

8383
def test_get_current_time_with_invalid_timezone():
8484
time_server = TimeServer()
85-
with pytest.raises(ValueError, match=r"Unknown timezone: 'Invalid/Timezone'"):
85+
with pytest.raises(
86+
McpError,
87+
match=r"Invalid timezone: 'No time zone found with key Invalid/Timezone'",
88+
):
8689
time_server.get_current_time("Invalid/Timezone")
8790

8891

@@ -93,13 +96,13 @@ def test_get_current_time_with_invalid_timezone():
9396
"invalid_tz",
9497
"12:00",
9598
"Europe/London",
96-
"Unknown source timezone: 'invalid_tz'",
99+
"Invalid timezone: 'No time zone found with key invalid_tz'",
97100
),
98101
(
99102
"Europe/Warsaw",
100103
"12:00",
101104
"invalid_tz",
102-
"Unknown target timezone: 'invalid_tz'",
105+
"Invalid timezone: 'No time zone found with key invalid_tz'",
103106
),
104107
(
105108
"Europe/Warsaw",
@@ -111,7 +114,7 @@ def test_get_current_time_with_invalid_timezone():
111114
)
112115
def test_convert_time_errors(source_tz, time_str, target_tz, expected_error):
113116
time_server = TimeServer()
114-
with pytest.raises(ValueError, match=expected_error):
117+
with pytest.raises((McpError, ValueError), match=expected_error):
115118
time_server.convert_time(source_tz, time_str, target_tz)
116119

117120

src/time/uv.lock

Lines changed: 24 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)