Skip to content

time: accept only exact IANA timezone keys on every platform - #5077

Merged
cliffhall merged 1 commit into
v2/mainfrom
v2/fix/5060-time-case-insensitive-tz
Oct 10, 2026
Merged

cliffhall merged 1 commit into
v2/mainfrom
v2/fix/5060-time-case-insensitive-tz

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #5060

Description

The time server now accepts a timezone only when it is an exact IANA key, case included, on every platform.

zoneinfo looks a key up as a file path. On macOS the tz database sits on a case-insensitive APFS volume, so ZoneInfo("europe/warsaw") resolved to Europe/Warsaw and the server answered, while Linux rejected the same key. That made test_get_current_time_errors[arguments5-expected5] fail on macOS, so npm run local:gate could not exit 0 there.

The issue offered two fixes: make the server consistent, or skip the case-sensitive expectation where the filesystem is case-insensitive. This PR does the first, because skipping the test would leave the server behaving differently by OS. A new load_zoneinfo() calls ZoneInfo(key) first, so a path, a directory or a null byte still gets zoneinfo's own error. It then checks the key against available_timezones(), which is read once and cached, and raises the same ZoneInfoNotFoundError message Linux already produced. Both the tool path (get_zoneinfo) and the --local-timezone check in serve() use it. Without the second, --local-timezone europe/warsaw would be accepted on macOS and put into the tool descriptions as the local zone, and the tools would then reject that same name.

Server Details

  • Server: time (mcp-server-time)
  • Changes to: get_current_time and convert_time timezone validation, --local-timezone validation, and the README

Motivation and Context

#5060. On Linux, which is the behavior CI tests, nothing changes for a canonical key or a case mismatch. On macOS a case-mismatched key is now rejected, as it already was on Linux. Keys that resolve as files but are not IANA zone names (posix/…, right/…) are now rejected too, because available_timezones() leaves them out.

The approach of checking available_timezones() after ZoneInfo() was suggested by @SuparvaCode in a comment on the issue.

How Has This Been Tested?

On macOS (APFS), with the Inspector CLI 2.9.0 over stdio, running the same request against v2/main and this branch.

Before, on v2/main: get_current_time with timezone=europe/warsaw succeeds and echoes the lowercase key:

{"result":{"content":[{"type":"text","text":"{\n  \"timezone\": \"europe/warsaw\",\n  \"datetime\": \"2026-10-10T05:45:41+02:00\", ..."}],"isError":false}}   exit 0

After, on this branch: the same call is a tool error, with the message Linux gives:

{"result":{"content":[{"type":"text","text":"Error processing mcp-server-time query: Invalid timezone: 'No time zone found with key europe/warsaw'"}],"isError":true}}   exit 5

timezone=Europe/Warsaw still succeeds ("timezone": "Europe/Warsaw", isError: false, exit 0).

--local-timezone europe/warsaw: on v2/main the server starts (exit 0 when stdin closes). On this branch it stops before the transport opens:

Error: invalid --local-timezone 'europe/warsaw': not a known IANA timezone name   exit 1

LLM client (Claude Code 2.1.296, claude -p --strict-mcp-config with only this build configured):

  • Prompt: "Call the get_current_time tool of the time server with the timezone argument set to exactly the lowercase string 'europe/warsaw' (do not correct it). Reply with exactly the text the tool returned." Result: Error processing mcp-server-time query: Invalid timezone: 'No time zone found with key europe/warsaw'
  • Prompt: "What time is it in Warsaw right now? Use the time server, and quote the timezone field it returned." The model called the tool with Europe/Warsaw and reported 05:46 on Saturday, 10 October 2026, "timezone": "Europe/Warsaw".

Protocol eras: these runs are the legacy (2025-11-25) era. The time server does not yet serve 2026-07-28 (#4853), so the modern era cannot be exercised. Claude Code has no era switch, so the era of the LLM run could not be selected.

Tests: uv run pytest passes 122 tests on macOS, including the case that failed before. New tests:

  • test_get_zoneinfo_rejects_a_key_the_tz_database_does_not_list patches the key list so a key ZoneInfo accepts is missing from it. This reproduces the macOS path on Linux CI.
  • europe/warsaw is added to the --local-timezone rejection cases in test_invalid_local_timezone_fails_before_the_transport_opens.

npm run local:gate exits 0 (run under Node 22).

Breaking Changes

None for a client that sends IANA names. On macOS, a client that was sending a case-mismatched name such as europe/warsaw now gets an isError result, as it already did on Linux. A --local-timezone that is not an exact key now stops the server at startup on macOS too.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follow MCP security best practices
  • I have updated the server's README accordingly
  • I have added a changeset (npm run changeset) if this changes what a TypeScript server publishes (not applicable: Python server)
  • I have tested this with an LLM client
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options (not applicable: no new variables or options)

Additional context

available_timezones() walks the tz database, so its result is cached with functools.cache and computed on the first lookup only.

🤖 Generated with Claude Code

On macOS, zoneinfo resolves a key such as europe/warsaw through the
case-insensitive APFS tz database, so the server accepted it where Linux
rejects it, and test_get_current_time_errors failed locally. Check each
key against available_timezones() in the tool path and in the
--local-timezone validation.

Closes #5060

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@changeset-bot

changeset-bot Bot commented Oct 10, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d52c223

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The shared validation addresses the platform inconsistency with appropriate tests and documentation.

0 open findings

What changed in this PR

Ensures timezone validation consistently requires exact IANA keys across platforms.

Changes:

  • Adds cached exact-key validation shared by tool calls and startup configuration.
  • Adds unit and protocol regression coverage.
  • Documents case-sensitive timezone requirements.
File Description
src/​time/​src/​mcp_server_time/​server.py Implements consistent timezone validation.
src/​time/​tests/​test_server.py Tests database key-list rejection.
src/​time/​tests/​test_protocol.py Tests lowercase startup rejection.
src/​time/​README.md Documents exact-key requirements.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 1: clean (0 findings, approval recommended). The review loop stops on the first clean round, so no further rounds were requested.

@cliffhall
cliffhall merged commit 8efea13 into v2/main Oct 10, 2026
31 checks passed
@cliffhall
cliffhall deleted the v2/fix/5060-time-case-insensitive-tz branch October 10, 2026 04:14
cliffhall added a commit that referenced this pull request Oct 10, 2026
…ase-cherry-picks

v1.0.0: cherry-pick #5077 and #5078 onto the release branch
spritstarx Bot pushed a commit to SpritStarX/servers that referenced this pull request Oct 11, 2026
The cherry-pick of d52c223 (modelcontextprotocol#5077) comes from the SDK v2 tree, where the exception is MCPError. On v2/release/1.0.0 the server raises mcp.shared.exceptions.McpError, already imported by this test module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

time: test_get_current_time_errors fails on macOS (case-insensitive tz database)

2 participants