aiomcp-server-time 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiomcp_server_time-0.0.1/PKG-INFO +13 -0
- aiomcp_server_time-0.0.1/aiomcp_server_time/__main__.py +29 -0
- aiomcp_server_time-0.0.1/aiomcp_server_time/server.py +165 -0
- aiomcp_server_time-0.0.1/aiomcp_server_time.egg-info/PKG-INFO +13 -0
- aiomcp_server_time-0.0.1/aiomcp_server_time.egg-info/SOURCES.txt +10 -0
- aiomcp_server_time-0.0.1/aiomcp_server_time.egg-info/dependency_links.txt +1 -0
- aiomcp_server_time-0.0.1/aiomcp_server_time.egg-info/entry_points.txt +2 -0
- aiomcp_server_time-0.0.1/aiomcp_server_time.egg-info/requires.txt +7 -0
- aiomcp_server_time-0.0.1/aiomcp_server_time.egg-info/top_level.txt +1 -0
- aiomcp_server_time-0.0.1/pyproject.toml +28 -0
- aiomcp_server_time-0.0.1/setup.cfg +4 -0
- aiomcp_server_time-0.0.1/tests/test_time_server.py +223 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiomcp-server-time
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Time and timezone conversion MCP server powered by aiomcp
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: aiomcp
|
|
9
|
+
Requires-Dist: pydantic
|
|
10
|
+
Requires-Dist: tzlocal
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest; extra == "test"
|
|
13
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
def main() -> None:
|
|
2
|
+
import argparse
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from aiomcp_server_time.server import host_http, host_stdio
|
|
6
|
+
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
description="Time and timezone conversion MCP server."
|
|
9
|
+
)
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"--http",
|
|
12
|
+
metavar="URL",
|
|
13
|
+
help="Host an HTTP MCP endpoint at URL instead of using stdio.",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument("--local-timezone", type=str, help="Override local timezone")
|
|
16
|
+
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
if args.http:
|
|
21
|
+
asyncio.run(host_http(args.http, local_timezone=args.local_timezone))
|
|
22
|
+
else:
|
|
23
|
+
asyncio.run(host_stdio(local_timezone=args.local_timezone))
|
|
24
|
+
except KeyboardInterrupt:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Annotated, Any
|
|
3
|
+
|
|
4
|
+
from aiomcp import McpServer
|
|
5
|
+
from aiomcp.transports.stdio import McpStdioServerTransport
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from tzlocal import get_localzone_name
|
|
8
|
+
from zoneinfo import ZoneInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TimeResult(BaseModel):
|
|
12
|
+
timezone: str
|
|
13
|
+
datetime: str
|
|
14
|
+
day_of_week: str
|
|
15
|
+
is_dst: bool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TimeConversionResult(BaseModel):
|
|
19
|
+
source: TimeResult
|
|
20
|
+
target: TimeResult
|
|
21
|
+
time_difference: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
TIME_TOOL_ANNOTATIONS = {
|
|
25
|
+
"readOnlyHint": True,
|
|
26
|
+
"destructiveHint": False,
|
|
27
|
+
"idempotentHint": True,
|
|
28
|
+
"openWorldHint": False,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
SERVER_NAME = "aiomcp-time-server"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_timezone(timezone_name: str) -> ZoneInfo:
|
|
36
|
+
try:
|
|
37
|
+
return ZoneInfo(timezone_name)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
raise ValueError(f"Invalid timezone: {exc}") from exc
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_local_timezone(local_timezone_override: str | None = None) -> ZoneInfo:
|
|
43
|
+
if local_timezone_override:
|
|
44
|
+
return get_timezone(local_timezone_override)
|
|
45
|
+
|
|
46
|
+
local_timezone_name = get_localzone_name()
|
|
47
|
+
if local_timezone_name is not None:
|
|
48
|
+
return get_timezone(local_timezone_name)
|
|
49
|
+
return ZoneInfo("UTC")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_current_time(
|
|
53
|
+
timezone: Annotated[
|
|
54
|
+
str,
|
|
55
|
+
Field(
|
|
56
|
+
description="IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '{CURRENT_TIMEZONE}' as local timezone if no timezone provided by the user."
|
|
57
|
+
),
|
|
58
|
+
],
|
|
59
|
+
) -> Any:
|
|
60
|
+
resolved_timezone = get_timezone(timezone)
|
|
61
|
+
current_time = datetime.now(resolved_timezone)
|
|
62
|
+
|
|
63
|
+
return TimeResult(
|
|
64
|
+
timezone=timezone,
|
|
65
|
+
datetime=current_time.isoformat(timespec="seconds"),
|
|
66
|
+
day_of_week=current_time.strftime("%A"),
|
|
67
|
+
is_dst=bool(current_time.dst()),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def convert_time(
|
|
72
|
+
source_timezone: Annotated[
|
|
73
|
+
str,
|
|
74
|
+
Field(
|
|
75
|
+
description="Source IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '{CURRENT_TIMEZONE}' as local timezone if no source timezone provided by the user."
|
|
76
|
+
),
|
|
77
|
+
],
|
|
78
|
+
time: Annotated[
|
|
79
|
+
str,
|
|
80
|
+
Field(description="Time to convert in 24-hour format (HH:MM)"),
|
|
81
|
+
],
|
|
82
|
+
target_timezone: Annotated[
|
|
83
|
+
str,
|
|
84
|
+
Field(
|
|
85
|
+
description="Target IANA timezone name (e.g., 'Asia/Tokyo', 'America/San_Francisco'). Use '{CURRENT_TIMEZONE}' as local timezone if no target timezone provided by the user."
|
|
86
|
+
),
|
|
87
|
+
],
|
|
88
|
+
) -> Any:
|
|
89
|
+
resolved_source_timezone = get_timezone(source_timezone)
|
|
90
|
+
resolved_target_timezone = get_timezone(target_timezone)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
parsed_time = datetime.strptime(time, "%H:%M").time()
|
|
94
|
+
except ValueError as exc:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"Invalid time format. Expected HH:MM [24-hour format]"
|
|
97
|
+
) from exc
|
|
98
|
+
|
|
99
|
+
now = datetime.now(resolved_source_timezone)
|
|
100
|
+
source_time = datetime(
|
|
101
|
+
now.year,
|
|
102
|
+
now.month,
|
|
103
|
+
now.day,
|
|
104
|
+
parsed_time.hour,
|
|
105
|
+
parsed_time.minute,
|
|
106
|
+
tzinfo=resolved_source_timezone,
|
|
107
|
+
)
|
|
108
|
+
target_time = source_time.astimezone(resolved_target_timezone)
|
|
109
|
+
|
|
110
|
+
source_offset = source_time.utcoffset() or timedelta()
|
|
111
|
+
target_offset = target_time.utcoffset() or timedelta()
|
|
112
|
+
hours_difference = (target_offset - source_offset).total_seconds() / 3600
|
|
113
|
+
if hours_difference.is_integer():
|
|
114
|
+
time_difference = f"{hours_difference:+.1f}h"
|
|
115
|
+
else:
|
|
116
|
+
time_difference = f"{hours_difference:+.2f}".rstrip("0").rstrip(".") + "h"
|
|
117
|
+
|
|
118
|
+
return TimeConversionResult(
|
|
119
|
+
source=TimeResult(
|
|
120
|
+
timezone=source_timezone,
|
|
121
|
+
datetime=source_time.isoformat(timespec="seconds"),
|
|
122
|
+
day_of_week=source_time.strftime("%A"),
|
|
123
|
+
is_dst=bool(source_time.dst()),
|
|
124
|
+
),
|
|
125
|
+
target=TimeResult(
|
|
126
|
+
timezone=target_timezone,
|
|
127
|
+
datetime=target_time.isoformat(timespec="seconds"),
|
|
128
|
+
day_of_week=target_time.strftime("%A"),
|
|
129
|
+
is_dst=bool(target_time.dst()),
|
|
130
|
+
),
|
|
131
|
+
time_difference=time_difference,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def register_tools(server: McpServer, local_timezone: str | None = None) -> None:
|
|
136
|
+
resolved_local_timezone = str(get_local_timezone(local_timezone))
|
|
137
|
+
format_map = {"CURRENT_TIMEZONE": resolved_local_timezone}
|
|
138
|
+
|
|
139
|
+
await server.register_tool(
|
|
140
|
+
func=get_current_time,
|
|
141
|
+
description="Get current time in a specific timezones",
|
|
142
|
+
annotations=TIME_TOOL_ANNOTATIONS,
|
|
143
|
+
format_map=format_map,
|
|
144
|
+
)
|
|
145
|
+
await server.register_tool(
|
|
146
|
+
func=convert_time,
|
|
147
|
+
description="Convert time between timezones",
|
|
148
|
+
annotations=TIME_TOOL_ANNOTATIONS,
|
|
149
|
+
format_map=format_map,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def host_stdio(local_timezone: str | None = None) -> None:
|
|
154
|
+
server = McpServer(SERVER_NAME)
|
|
155
|
+
await register_tools(server, local_timezone=local_timezone)
|
|
156
|
+
|
|
157
|
+
transport = McpStdioServerTransport()
|
|
158
|
+
await server.host(transport)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def host_http(url: str, local_timezone: str | None = None) -> None:
|
|
162
|
+
server = McpServer(SERVER_NAME)
|
|
163
|
+
await register_tools(server, local_timezone=local_timezone)
|
|
164
|
+
|
|
165
|
+
await server.host(url)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiomcp-server-time
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Time and timezone conversion MCP server powered by aiomcp
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: aiomcp
|
|
9
|
+
Requires-Dist: pydantic
|
|
10
|
+
Requires-Dist: tzlocal
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest; extra == "test"
|
|
13
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
aiomcp_server_time/__main__.py
|
|
3
|
+
aiomcp_server_time/server.py
|
|
4
|
+
aiomcp_server_time.egg-info/PKG-INFO
|
|
5
|
+
aiomcp_server_time.egg-info/SOURCES.txt
|
|
6
|
+
aiomcp_server_time.egg-info/dependency_links.txt
|
|
7
|
+
aiomcp_server_time.egg-info/entry_points.txt
|
|
8
|
+
aiomcp_server_time.egg-info/requires.txt
|
|
9
|
+
aiomcp_server_time.egg-info/top_level.txt
|
|
10
|
+
tests/test_time_server.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiomcp_server_time
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aiomcp-server-time"
|
|
3
|
+
description = "Time and timezone conversion MCP server powered by aiomcp"
|
|
4
|
+
version = "0.0.1"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
"aiomcp",
|
|
10
|
+
"pydantic",
|
|
11
|
+
"tzlocal",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
test = [
|
|
16
|
+
"pytest",
|
|
17
|
+
"pytest-asyncio",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
aiomcp-server-time = "aiomcp_server_time.__main__:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
include = ["aiomcp_server_time*"]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from aiomcp import McpClient, McpServer
|
|
9
|
+
from aiomcp_server_time.server import (
|
|
10
|
+
SERVER_NAME,
|
|
11
|
+
convert_time,
|
|
12
|
+
get_current_time,
|
|
13
|
+
get_local_timezone,
|
|
14
|
+
register_tools,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def freeze_time(test_time: str):
|
|
19
|
+
frozen_time = datetime.fromisoformat(test_time)
|
|
20
|
+
|
|
21
|
+
class FrozenDateTime(datetime):
|
|
22
|
+
@classmethod
|
|
23
|
+
def now(cls, timezone=None):
|
|
24
|
+
if timezone is None:
|
|
25
|
+
return frozen_time.replace(tzinfo=None)
|
|
26
|
+
return frozen_time.astimezone(timezone)
|
|
27
|
+
|
|
28
|
+
return patch("aiomcp_server_time.server.datetime", FrozenDateTime)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def create_test_server(local_timezone: str | None = None) -> McpServer:
|
|
32
|
+
server = McpServer(SERVER_NAME)
|
|
33
|
+
await register_tools(server, local_timezone=local_timezone)
|
|
34
|
+
return server
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parametrize(
|
|
38
|
+
("test_time", "timezone", "expected"),
|
|
39
|
+
[
|
|
40
|
+
(
|
|
41
|
+
"2024-01-01 12:00:00+00:00",
|
|
42
|
+
"Europe/Warsaw",
|
|
43
|
+
{
|
|
44
|
+
"timezone": "Europe/Warsaw",
|
|
45
|
+
"datetime": "2024-01-01T13:00:00+01:00",
|
|
46
|
+
"day_of_week": "Monday",
|
|
47
|
+
"is_dst": False,
|
|
48
|
+
},
|
|
49
|
+
),
|
|
50
|
+
(
|
|
51
|
+
"2024-03-31 12:00:00+00:00",
|
|
52
|
+
"America/New_York",
|
|
53
|
+
{
|
|
54
|
+
"timezone": "America/New_York",
|
|
55
|
+
"datetime": "2024-03-31T08:00:00-04:00",
|
|
56
|
+
"day_of_week": "Sunday",
|
|
57
|
+
"is_dst": True,
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
def test_get_current_time(test_time, timezone, expected):
|
|
63
|
+
with freeze_time(test_time):
|
|
64
|
+
result = get_current_time(timezone)
|
|
65
|
+
|
|
66
|
+
assert result.model_dump() == expected
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize(
|
|
70
|
+
("test_time", "source_timezone", "time_str", "target_timezone", "expected"),
|
|
71
|
+
[
|
|
72
|
+
(
|
|
73
|
+
"2024-01-01 00:00:00+00:00",
|
|
74
|
+
"Europe/London",
|
|
75
|
+
"12:00",
|
|
76
|
+
"Europe/Warsaw",
|
|
77
|
+
{
|
|
78
|
+
"source": {
|
|
79
|
+
"timezone": "Europe/London",
|
|
80
|
+
"datetime": "2024-01-01T12:00:00+00:00",
|
|
81
|
+
"day_of_week": "Monday",
|
|
82
|
+
"is_dst": False,
|
|
83
|
+
},
|
|
84
|
+
"target": {
|
|
85
|
+
"timezone": "Europe/Warsaw",
|
|
86
|
+
"datetime": "2024-01-01T13:00:00+01:00",
|
|
87
|
+
"day_of_week": "Monday",
|
|
88
|
+
"is_dst": False,
|
|
89
|
+
},
|
|
90
|
+
"time_difference": "+1.0h",
|
|
91
|
+
},
|
|
92
|
+
),
|
|
93
|
+
(
|
|
94
|
+
"2024-01-01 00:00:00+00:00",
|
|
95
|
+
"Europe/Warsaw",
|
|
96
|
+
"12:00",
|
|
97
|
+
"Asia/Kathmandu",
|
|
98
|
+
{
|
|
99
|
+
"source": {
|
|
100
|
+
"timezone": "Europe/Warsaw",
|
|
101
|
+
"datetime": "2024-01-01T12:00:00+01:00",
|
|
102
|
+
"day_of_week": "Monday",
|
|
103
|
+
"is_dst": False,
|
|
104
|
+
},
|
|
105
|
+
"target": {
|
|
106
|
+
"timezone": "Asia/Kathmandu",
|
|
107
|
+
"datetime": "2024-01-01T16:45:00+05:45",
|
|
108
|
+
"day_of_week": "Monday",
|
|
109
|
+
"is_dst": False,
|
|
110
|
+
},
|
|
111
|
+
"time_difference": "+4.75h",
|
|
112
|
+
},
|
|
113
|
+
),
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
def test_convert_time(test_time, source_timezone, time_str, target_timezone, expected):
|
|
117
|
+
with freeze_time(test_time):
|
|
118
|
+
result = convert_time(source_timezone, time_str, target_timezone)
|
|
119
|
+
|
|
120
|
+
assert result.model_dump() == expected
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.parametrize(
|
|
124
|
+
("source_timezone", "time_str", "target_timezone", "expected_error"),
|
|
125
|
+
[
|
|
126
|
+
(
|
|
127
|
+
"invalid_timezone",
|
|
128
|
+
"12:00",
|
|
129
|
+
"Europe/London",
|
|
130
|
+
"Invalid timezone: 'No time zone found with key invalid_timezone'",
|
|
131
|
+
),
|
|
132
|
+
(
|
|
133
|
+
"Europe/Warsaw",
|
|
134
|
+
"25:00",
|
|
135
|
+
"Europe/London",
|
|
136
|
+
"Invalid time format. Expected HH:MM [24-hour format]",
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
def test_convert_time_errors(
|
|
141
|
+
source_timezone, time_str, target_timezone, expected_error
|
|
142
|
+
):
|
|
143
|
+
with pytest.raises(ValueError, match=re.escape(expected_error)):
|
|
144
|
+
convert_time(source_timezone, time_str, target_timezone)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_get_local_timezone_with_override():
|
|
148
|
+
assert str(get_local_timezone("America/New_York")) == "America/New_York"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@patch("aiomcp_server_time.server.get_localzone_name")
|
|
152
|
+
def test_get_local_timezone_defaults_to_utc(mock_get_localzone):
|
|
153
|
+
mock_get_localzone.return_value = None
|
|
154
|
+
|
|
155
|
+
assert str(get_local_timezone()) == "UTC"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.asyncio
|
|
159
|
+
async def test_aiomcp_server_exposes_reference_tools():
|
|
160
|
+
server = await create_test_server("Europe/Warsaw")
|
|
161
|
+
tools = await server.list_tools()
|
|
162
|
+
tool_names = {tool.name for tool in tools}
|
|
163
|
+
|
|
164
|
+
assert tool_names == {"get_current_time", "convert_time"}
|
|
165
|
+
|
|
166
|
+
current_time_tool = next(tool for tool in tools if tool.name == "get_current_time")
|
|
167
|
+
assert current_time_tool.description == "Get current time in a specific timezones"
|
|
168
|
+
assert current_time_tool.outputSchema is None
|
|
169
|
+
current_time_input_schema = current_time_tool.inputSchema.model_dump(
|
|
170
|
+
exclude_none=True
|
|
171
|
+
)
|
|
172
|
+
assert current_time_input_schema["properties"]["timezone"]["description"] == (
|
|
173
|
+
"IANA timezone name (e.g., 'America/New_York', 'Europe/London'). "
|
|
174
|
+
"Use 'Europe/Warsaw' as local timezone if no timezone provided by the user."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
convert_tool = next(tool for tool in tools if tool.name == "convert_time")
|
|
178
|
+
assert convert_tool.outputSchema is None
|
|
179
|
+
input_schema = convert_tool.inputSchema.model_dump(exclude_none=True)
|
|
180
|
+
annotations = convert_tool.annotations.model_dump(exclude_none=True)
|
|
181
|
+
assert input_schema["required"] == ["source_timezone", "time", "target_timezone"]
|
|
182
|
+
assert input_schema["properties"]["source_timezone"]["description"] == (
|
|
183
|
+
"Source IANA timezone name (e.g., 'America/New_York', 'Europe/London'). "
|
|
184
|
+
"Use 'Europe/Warsaw' as local timezone if no source timezone provided by the user."
|
|
185
|
+
)
|
|
186
|
+
assert input_schema["properties"]["time"]["description"] == (
|
|
187
|
+
"Time to convert in 24-hour format (HH:MM)"
|
|
188
|
+
)
|
|
189
|
+
assert input_schema["properties"]["target_timezone"]["description"] == (
|
|
190
|
+
"Target IANA timezone name (e.g., 'Asia/Tokyo', 'America/San_Francisco'). "
|
|
191
|
+
"Use 'Europe/Warsaw' as local timezone if no target timezone provided by the user."
|
|
192
|
+
)
|
|
193
|
+
assert annotations == {
|
|
194
|
+
"readOnlyHint": True,
|
|
195
|
+
"destructiveHint": False,
|
|
196
|
+
"idempotentHint": True,
|
|
197
|
+
"openWorldHint": False,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.asyncio
|
|
202
|
+
async def test_aiomcp_client_can_call_convert_time():
|
|
203
|
+
server = await create_test_server()
|
|
204
|
+
client = McpClient()
|
|
205
|
+
await client.initialize(server)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
with freeze_time("2024-01-01 00:00:00+00:00"):
|
|
209
|
+
result = await client.invoke(
|
|
210
|
+
"convert_time",
|
|
211
|
+
{
|
|
212
|
+
"source_timezone": "Europe/Warsaw",
|
|
213
|
+
"time": "12:00",
|
|
214
|
+
"target_timezone": "Asia/Kathmandu",
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
finally:
|
|
218
|
+
await client.close()
|
|
219
|
+
|
|
220
|
+
assert result[0]["type"] == "text"
|
|
221
|
+
payload = json.loads(result[0]["text"])
|
|
222
|
+
assert payload["target"]["datetime"] == "2024-01-01T16:45:00+05:45"
|
|
223
|
+
assert payload["time_difference"] == "+4.75h"
|