Skip to content
Merged
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
15 changes: 14 additions & 1 deletion src/time/src/mcp_server_time/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from importlib.metadata import version
from enum import Enum
import json
import sys
from typing import Sequence

from zoneinfo import ZoneInfo
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from tzlocal import get_localzone_name # ← returns "Europe/Paris", etc.

from mcp.server import Server
Expand Down Expand Up @@ -139,6 +140,18 @@ def convert_time(
async def serve(local_timezone: str | None = None) -> None:
server = Server("mcp-time", version=SERVER_VERSION)
time_server = TimeServer()
if local_timezone:
# Fail before the transport opens, with one line naming the bad value
# rather than a traceback. zoneinfo raises a different type per kind of
# bad key (unknown name, a tzdata directory, a path, a null byte), and
# its messages can carry a filesystem path, so none of them is shown.
try:
ZoneInfo(local_timezone)
except (ZoneInfoNotFoundError, ValueError, OSError):
sys.exit(
f"Error: invalid --local-timezone {local_timezone!r}: "
"not a known IANA timezone name"
)
local_tz = str(get_local_tz(local_timezone))

@server.list_tools()
Expand Down
20 changes: 20 additions & 0 deletions src/time/tests/test_entrypoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import runpy
import shutil
import subprocess
import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
Expand Down Expand Up @@ -114,3 +115,22 @@ async def test_console_script_boots_over_stdio() -> None:
)
assert [tool.name for tool in tools.tools] == ["get_current_time", "convert_time"]
assert result.model_dump(by_alias=True, mode="json")["isError"] is False


def test_console_script_rejects_an_invalid_local_timezone() -> None:
# #5001: one line on stderr naming the value, a non-zero exit, and no
# traceback.
script = shutil.which("mcp-server-time", path=str(Path(sys.executable).parent))
assert script is not None, "the console script is not installed in this venv"
proc = subprocess.run(
[script, "--local-timezone", "Not/AZone"],
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
timeout=60,
)
assert proc.returncode == 1
assert proc.stdout == ""
assert proc.stderr.splitlines() == [
"Error: invalid --local-timezone 'Not/AZone': not a known IANA timezone name"
]
28 changes: 21 additions & 7 deletions src/time/tests/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
from mcp import ClientSession
from mcp.shared.memory import create_client_server_memory_streams
from pydantic import BaseModel
from zoneinfo import ZoneInfoNotFoundError

from mcp_server_time.server import serve

Expand Down Expand Up @@ -236,16 +235,31 @@ async def test_empty_local_timezone_override_is_treated_as_absent() -> None:
assert result == {"tools": expected_tools("Africa/Cairo")}


# KNOWN BUG #5001: an invalid --local-timezone crashes serve() with a traceback instead of a clean error; the fix changes this assertion.
async def test_invalid_local_timezone_fails_before_the_transport_opens() -> None:
# An unknown `--local-timezone` raises out of `serve()` itself, before
# stdio is opened: the process dies with a traceback instead of starting.
@pytest.mark.parametrize(
"local_timezone",
[
"Not/AZone", # unknown name: ZoneInfoNotFoundError
"America", # a tzdata directory, not a zone: OSError
"../etc/passwd", # outside TZPATH: ValueError
],
)
async def test_invalid_local_timezone_fails_before_the_transport_opens(
local_timezone: str,
) -> None:
# An invalid `--local-timezone` stops `serve()` before stdio is opened,
# with a one-line message naming the value and a non-zero exit, rather
# than a traceback (#5001). A `SystemExit` whose code is a string prints
# that string to stderr and exits 1.
def unexpected_stdio_server():
raise AssertionError("stdio_server must not be reached")

with patch("mcp_server_time.server.stdio_server", unexpected_stdio_server):
with pytest.raises(ZoneInfoNotFoundError, match="Not/AZone"):
await serve("Not/AZone")
with pytest.raises(SystemExit) as exit_info:
await serve(local_timezone)
assert exit_info.value.code == (
f"Error: invalid --local-timezone {local_timezone!r}: "
"not a known IANA timezone name"
)


# ---------------------------------------------------------------------------
Expand Down
Loading