Skip to content

Commit 91f1ab7

Browse files
authored
Merge pull request #5364 from modelcontextprotocol/v2/feat/2853-time-default-timezone
feat(time): default omitted timezone arguments to the local timezone
2 parents 593cb09 + bd3d538 commit 91f1ab7

3 files changed

Lines changed: 94 additions & 28 deletions

File tree

‎src/time/README.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ Requires MCP Python SDK 2.x (`mcp>=2.3,<3`).
1111
### Available Tools
1212

1313
- `get_current_time` - Get current time in a specific timezone or system timezone.
14-
- Required arguments:
15-
- `timezone` (string): IANA timezone name (e.g., 'America/New_York', 'Europe/London')
14+
- Optional arguments:
15+
- `timezone` (string): IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Defaults to the server's local timezone.
1616

1717
- `convert_time` - Convert time between timezones.
1818
- Required arguments:
19-
- `source_timezone` (string): Source IANA timezone name
2019
- `time` (string): Time in 24-hour format (HH:MM)
21-
- `target_timezone` (string): Target IANA timezone name
20+
- Optional arguments:
21+
- `source_timezone` (string): Source IANA timezone name. Defaults to the server's local timezone.
22+
- `target_timezone` (string): Target IANA timezone name. Defaults to the server's local timezone.
2223

2324
A timezone name, here and in `--local-timezone`, must be an exact IANA key, case included: `Europe/Warsaw` is accepted and `europe/warsaw` is rejected on every platform, including macOS, whose case-insensitive filesystem would otherwise resolve it.
2425

@@ -208,7 +209,7 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace
208209

209210
### Customization - System Timezone
210211

211-
By default, the server automatically detects your system's timezone. You can override this by adding the argument `--local-timezone` to the `args` list in the configuration.
212+
By default, the server automatically detects your system's timezone. You can override this by adding the argument `--local-timezone` to the `args` list in the configuration. This local timezone is also what a tool uses for a timezone argument the call omits, and each tool's input schema names it as that argument's `default`. An explicitly empty timezone is still an error.
212213

213214
Example:
214215
```json
@@ -288,7 +289,7 @@ npx @modelcontextprotocol/inspector uv run mcp-server-time
288289

289290
## Examples of Questions for Claude
290291

291-
1. "What time is it now?" (will use system timezone)
292+
1. "What time is it now?" (the timezone can be omitted: the server uses its local timezone)
292293
2. "What time is it in Tokyo?"
293294
3. "When it's 4 PM in New York, what time is it in London?"
294295
4. "Convert 9:30 AM Tokyo time to New York time"

‎src/time/src/mcp_server_time/server.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,9 @@ def get_current_time(
230230
timezone: Annotated[
231231
str,
232232
Field(
233-
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."
233+
description=f"IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Optional: defaults to the server's local timezone, '{local_tz}', when omitted."
234234
),
235-
],
235+
] = local_tz,
236236
) -> TimeResult:
237237
require("timezone", timezone)
238238
return time_server.get_current_time(timezone)
@@ -243,21 +243,24 @@ def get_current_time(
243243
annotations=READ_ONLY,
244244
)
245245
def convert_time(
246+
# Keyword-only, so the required `time` can keep its place between the
247+
# two optional timezones and the schema keeps its property order.
248+
*,
246249
source_timezone: Annotated[
247250
str,
248251
Field(
249-
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."
252+
description=f"Source IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Optional: defaults to the server's local timezone, '{local_tz}', when omitted."
250253
),
251-
],
254+
] = local_tz,
252255
time: Annotated[
253256
str, Field(description="Time to convert in 24-hour format (HH:MM)")
254257
],
255258
target_timezone: Annotated[
256259
str,
257260
Field(
258-
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."
261+
description=f"Target IANA timezone name (e.g., 'Asia/Tokyo', 'America/San_Francisco'). Optional: defaults to the server's local timezone, '{local_tz}', when omitted."
259262
),
260-
],
263+
] = local_tz,
261264
) -> TimeConversionResult:
262265
require("source_timezone", source_timezone)
263266
require("target_timezone", target_timezone)

‎src/time/tests/test_protocol.py‎

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,12 @@ def expected_tools(local_tz: str) -> list[dict[str, Any]]:
205205
"title": "Timezone",
206206
"description": (
207207
"IANA timezone name (e.g., 'America/New_York', "
208-
f"'Europe/London'). Use '{local_tz}' as local timezone "
209-
"if no timezone provided by the user."
208+
"'Europe/London'). Optional: defaults to the "
209+
f"server's local timezone, '{local_tz}', when omitted."
210210
),
211+
"default": local_tz,
211212
}
212213
},
213-
"required": ["timezone"],
214214
},
215215
"outputSchema": TIME_RESULT_SCHEMA,
216216
"annotations": READ_ONLY_ANNOTATIONS,
@@ -227,9 +227,10 @@ def expected_tools(local_tz: str) -> list[dict[str, Any]]:
227227
"title": "Source Timezone",
228228
"description": (
229229
"Source IANA timezone name (e.g., 'America/New_York', "
230-
f"'Europe/London'). Use '{local_tz}' as local timezone "
231-
"if no source timezone provided by the user."
230+
"'Europe/London'). Optional: defaults to the "
231+
f"server's local timezone, '{local_tz}', when omitted."
232232
),
233+
"default": local_tz,
233234
},
234235
"time": {
235236
"type": "string",
@@ -241,12 +242,13 @@ def expected_tools(local_tz: str) -> list[dict[str, Any]]:
241242
"title": "Target Timezone",
242243
"description": (
243244
"Target IANA timezone name (e.g., 'Asia/Tokyo', "
244-
f"'America/San_Francisco'). Use '{local_tz}' as local "
245-
"timezone if no target timezone provided by the user."
245+
"'America/San_Francisco'). Optional: defaults to the "
246+
f"server's local timezone, '{local_tz}', when omitted."
246247
),
248+
"default": local_tz,
247249
},
248250
},
249-
"required": ["source_timezone", "time", "target_timezone"],
251+
"required": ["time"],
250252
},
251253
"outputSchema": {
252254
"type": "object",
@@ -512,21 +514,80 @@ async def test_get_current_time_ignores_extra_arguments() -> None:
512514
assert result["datetime"] == "2024-01-01T12:00:00+00:00"
513515

514516

515-
async def test_get_current_time_does_not_default_to_the_local_timezone() -> None:
516-
# The local timezone only appears in the schema descriptions; the server
517-
# never substitutes it for an empty argument.
517+
async def test_get_current_time_does_not_default_an_empty_timezone() -> None:
518+
# Only an omitted timezone takes the local default (#2853). An explicitly
519+
# empty one is a mistake, reported as missing rather than guessed at.
518520
async with connected("Europe/Warsaw") as session:
519521
result = await call(session, "get_current_time", {"timezone": ""})
520522
assert result == tool_error(
521523
"get_current_time", "Missing required argument: timezone"
522524
)
523525

524526

527+
async def test_get_current_time_defaults_to_the_local_timezone() -> None:
528+
# An omitted timezone is the server's local one (#2853): the
529+
# --local-timezone override here.
530+
async with connected("Asia/Kolkata") as session:
531+
with frozen_at("2024-01-01 12:00:00+00:00"):
532+
result = await call_json(session, "get_current_time", {})
533+
assert result == {
534+
"timezone": "Asia/Kolkata",
535+
"datetime": "2024-01-01T17:30:00+05:30",
536+
"day_of_week": "Monday",
537+
"is_dst": False,
538+
}
539+
540+
541+
async def test_get_current_time_defaults_to_the_detected_local_timezone() -> None:
542+
# With no --local-timezone, the default is the detected zone, the same one
543+
# the argument's description names.
544+
with patch("mcp_server_time.server.get_localzone_name", return_value="Asia/Tokyo"):
545+
async with connected(None) as session:
546+
with frozen_at("2024-01-01 12:00:00+00:00"):
547+
result = await call_json(session, "get_current_time", {})
548+
assert result["timezone"] == "Asia/Tokyo"
549+
assert result["datetime"] == "2024-01-01T21:00:00+09:00"
550+
551+
552+
@pytest.mark.parametrize(
553+
"arguments,source,target",
554+
[
555+
# Either timezone, or both, may be omitted (#2853); each omitted one
556+
# is the local timezone, Europe/Warsaw here.
557+
(
558+
{"time": "12:00", "target_timezone": "Asia/Tokyo"},
559+
("Europe/Warsaw", "2024-01-01T12:00:00+01:00"),
560+
("Asia/Tokyo", "2024-01-01T20:00:00+09:00"),
561+
),
562+
(
563+
{"source_timezone": "Asia/Tokyo", "time": "12:00"},
564+
("Asia/Tokyo", "2024-01-01T12:00:00+09:00"),
565+
("Europe/Warsaw", "2024-01-01T04:00:00+01:00"),
566+
),
567+
(
568+
{"time": "12:00"},
569+
("Europe/Warsaw", "2024-01-01T12:00:00+01:00"),
570+
("Europe/Warsaw", "2024-01-01T12:00:00+01:00"),
571+
),
572+
],
573+
)
574+
async def test_convert_time_defaults_omitted_timezones_to_the_local_timezone(
575+
arguments: dict[str, Any],
576+
source: tuple[str, str],
577+
target: tuple[str, str],
578+
) -> None:
579+
async with connected("Europe/Warsaw") as session:
580+
with frozen_at("2024-01-01 06:00:00+00:00"):
581+
result = await call_json(session, "convert_time", arguments)
582+
assert (result["source"]["timezone"], result["source"]["datetime"]) == source
583+
assert (result["target"]["timezone"], result["target"]["datetime"]) == target
584+
585+
525586
@pytest.mark.parametrize(
526587
"arguments,needles",
527588
[
528589
# Rejected by MCPServer against the tool's signature, before it runs.
529-
({}, ["timezone\n Field required [type=missing"]),
590+
# An omitted timezone is not an error: it defaults (#2853).
530591
({"timezone": 5}, ["timezone\n Input should be a valid string"]),
531592
({"timezone": None}, ["timezone\n Input should be a valid string"]),
532593
],
@@ -867,13 +928,14 @@ async def test_convert_time_ignores_extra_arguments() -> None:
867928
"arguments,needles",
868929
[
869930
# Rejected by MCPServer against the tool's signature, before it runs.
931+
# `time` stays required; the timezones default when omitted (#2853).
870932
(
871-
{"source_timezone": "UTC", "time": "12:00"},
872-
["target_timezone\n Field required [type=missing"],
933+
{"source_timezone": "UTC", "target_timezone": "UTC"},
934+
["time\n Field required [type=missing"],
873935
),
874936
(
875-
{"time": "12:00", "target_timezone": "UTC"},
876-
["source_timezone\n Field required [type=missing"],
937+
{"source_timezone": 5, "time": "12:00"},
938+
["source_timezone\n Input should be a valid string"],
877939
),
878940
(
879941
{"source_timezone": "UTC", "time": 1200, "target_timezone": "UTC"},

0 commit comments

Comments
 (0)