Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/time/src/mcp_server_time/server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from enum import Enum
import json
from typing import Sequence
Expand Down Expand Up @@ -92,6 +92,23 @@ def convert_time(
tzinfo=source_timezone,
)

# Attaching ZoneInfo does not validate local wall times. During a
# spring-forward transition, times in the skipped interval are silently
# assigned the pre-transition offset even though they never occurred.
# A UTC round trip normalizes such a value to the first valid local time,
# allowing us to reject the invalid input instead of returning a
# misleading conversion.
round_tripped_source_time = source_time.astimezone(timezone.utc).astimezone(
source_timezone
)
if round_tripped_source_time.replace(tzinfo=None) != source_time.replace(
tzinfo=None
):
raise ValueError(
f"Time {time_str} does not exist in {source_tz} on "
f"{source_time.date()} due to a UTC offset transition"
)

target_time = source_time.astimezone(target_timezone)
source_offset = source_time.utcoffset() or timedelta()
target_offset = target_time.utcoffset() or timedelta()
Expand Down
31 changes: 31 additions & 0 deletions src/time/test/time_server_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,37 @@ def test_convert_time_errors(source_tz, time_str, target_tz, expected_error):
time_server.convert_time(source_tz, time_str, target_tz)


def test_convert_time_rejects_nonexistent_dst_time():
# New York advances from 01:59 to 03:00 on this date, so 02:30 never
# occurs. ZoneInfo otherwise silently assigns it the earlier UTC-05:00
# offset and produces a conversion for a nonexistent wall time.
with freeze_time("2026-03-08 12:00:00+00:00"):
time_server = TimeServer()

with pytest.raises(
ValueError,
match=(
r"Time 02:30 does not exist in America/New_York on 2026-03-08 "
r"due to a UTC offset transition"
),
):
time_server.convert_time("America/New_York", "02:30", "UTC")


@pytest.mark.parametrize(
"time_str,expected_utc",
[
("01:30", "2026-03-08T06:30:00+00:00"),
("03:30", "2026-03-08T07:30:00+00:00"),
],
)
def test_convert_time_accepts_valid_times_around_dst_gap(time_str, expected_utc):
with freeze_time("2026-03-08 12:00:00+00:00"):
result = TimeServer().convert_time("America/New_York", time_str, "UTC")

assert result.target.datetime == expected_utc


@pytest.mark.parametrize(
"test_time,source_tz,time_str,target_tz,expected",
[
Expand Down