mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2024-12-01 18:58:34 +03:00
fix time server
This commit is contained in:
@@ -4,7 +4,9 @@ version = "0.5.1"
|
||||
description = "A Model Context Protocol server providing tools for time queries and timezone conversions for LLMs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
authors = [{ name = "Mariusz 'maledorak' Korzekwa", email = "mariusz@korzekwa.dev" }]
|
||||
authors = [
|
||||
{ name = "Mariusz 'maledorak' Korzekwa", email = "mariusz@korzekwa.dev" },
|
||||
]
|
||||
keywords = ["time", "timezone", "mcp", "llm"]
|
||||
license = { text = "MIT" }
|
||||
classifiers = [
|
||||
@@ -17,8 +19,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"mcp>=1.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
"pytz>=2024.2",
|
||||
"tzlocal>=5.2",
|
||||
"tzdata>=2024.2",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -33,4 +34,5 @@ dev-dependencies = [
|
||||
"freezegun>=1.5.1",
|
||||
"pyright>=1.1.389",
|
||||
"pytest>=8.3.3",
|
||||
"ruff>=0.8.1",
|
||||
]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .server import serve
|
||||
|
||||
|
||||
def main():
|
||||
"""MCP Time Server - Time and timezone conversion functionality for MCP"""
|
||||
import argparse
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
import json
|
||||
from typing import Sequence
|
||||
|
||||
import pytz
|
||||
from tzlocal import get_localzone
|
||||
from zoneinfo import ZoneInfo
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
|
||||
from mcp.shared.exceptions import McpError
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -34,17 +34,29 @@ class TimeConversionInput(BaseModel):
|
||||
time: str
|
||||
target_tz_list: list[str]
|
||||
|
||||
def get_local_tz(local_tz_override: str | None = None) -> pytz.timezone:
|
||||
return pytz.timezone(local_tz_override) if local_tz_override else get_localzone()
|
||||
|
||||
def get_local_tz(local_tz_override: str | None = None) -> ZoneInfo:
|
||||
if local_tz_override:
|
||||
return ZoneInfo(local_tz_override)
|
||||
|
||||
# Get local timezone from datetime.now()
|
||||
tzinfo = datetime.now().astimezone(tz=None).tzinfo
|
||||
if tzinfo is not None:
|
||||
return ZoneInfo(str(tzinfo))
|
||||
raise McpError("Could not determine local timezone - tzinfo is None")
|
||||
|
||||
|
||||
def get_zoneinfo(timezone_name: str) -> ZoneInfo:
|
||||
try:
|
||||
return ZoneInfo(timezone_name)
|
||||
except Exception as e:
|
||||
raise McpError(f"Invalid timezone: {str(e)}")
|
||||
|
||||
|
||||
class TimeServer:
|
||||
def get_current_time(self, timezone_name: str) -> TimeResult:
|
||||
"""Get current time in specified timezone"""
|
||||
try:
|
||||
timezone = pytz.timezone(timezone_name)
|
||||
except pytz.exceptions.UnknownTimeZoneError as e:
|
||||
raise ValueError(f"Unknown timezone: {str(e)}")
|
||||
|
||||
timezone = get_zoneinfo(timezone_name)
|
||||
current_time = datetime.now(timezone)
|
||||
|
||||
return TimeResult(
|
||||
@@ -57,15 +69,8 @@ class TimeServer:
|
||||
self, source_tz: str, time_str: str, target_tz: str
|
||||
) -> TimeConversionResult:
|
||||
"""Convert time between timezones"""
|
||||
try:
|
||||
source_timezone = pytz.timezone(source_tz)
|
||||
except pytz.exceptions.UnknownTimeZoneError as e:
|
||||
raise ValueError(f"Unknown source timezone: {str(e)}")
|
||||
|
||||
try:
|
||||
target_timezone = pytz.timezone(target_tz)
|
||||
except pytz.exceptions.UnknownTimeZoneError as e:
|
||||
raise ValueError(f"Unknown target timezone: {str(e)}")
|
||||
source_timezone = get_zoneinfo(source_tz)
|
||||
target_timezone = get_zoneinfo(target_tz)
|
||||
|
||||
try:
|
||||
parsed_time = datetime.strptime(time_str, "%H:%M").time()
|
||||
@@ -73,14 +78,19 @@ class TimeServer:
|
||||
raise ValueError("Invalid time format. Expected HH:MM [24-hour format]")
|
||||
|
||||
now = datetime.now(source_timezone)
|
||||
source_time = source_timezone.localize(
|
||||
datetime(now.year, now.month, now.day, parsed_time.hour, parsed_time.minute)
|
||||
source_time = datetime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
parsed_time.hour,
|
||||
parsed_time.minute,
|
||||
tzinfo=source_timezone,
|
||||
)
|
||||
|
||||
target_time = source_time.astimezone(target_timezone)
|
||||
hours_difference = (
|
||||
target_time.utcoffset() - source_time.utcoffset()
|
||||
).total_seconds() / 3600
|
||||
source_offset = source_time.utcoffset() or timedelta()
|
||||
target_offset = target_time.utcoffset() or timedelta()
|
||||
hours_difference = (target_offset - source_offset).total_seconds() / 3600
|
||||
|
||||
if hours_difference.is_integer():
|
||||
time_diff_str = f"{hours_difference:+.1f}h"
|
||||
@@ -114,7 +124,7 @@ async def serve(local_timezone: str | None = None) -> None:
|
||||
return [
|
||||
Tool(
|
||||
name=TimeTools.GET_CURRENT_TIME.value,
|
||||
description=f"Get current time in a specific timezones",
|
||||
description="Get current time in a specific timezones",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -128,7 +138,7 @@ async def serve(local_timezone: str | None = None) -> None:
|
||||
),
|
||||
Tool(
|
||||
name=TimeTools.CONVERT_TIME.value,
|
||||
description=f"Convert time between timezones",
|
||||
description="Convert time between timezones",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -179,7 +189,9 @@ async def serve(local_timezone: str | None = None) -> None:
|
||||
case _:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
return [TextContent(type="text", text=json.dumps(result.model_dump(), indent=2))]
|
||||
return [
|
||||
TextContent(type="text", text=json.dumps(result.model_dump(), indent=2))
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error processing mcp-server-time query: {str(e)}")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import os
|
||||
|
||||
from freezegun import freeze_time
|
||||
from mcp.shared.exceptions import McpError
|
||||
import pytest
|
||||
|
||||
from mcp_server_time.server import TimeServer, serve
|
||||
from mcp_server_time.server import TimeServer
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -82,7 +82,10 @@ def test_get_current_time(test_time, timezone, expected):
|
||||
|
||||
def test_get_current_time_with_invalid_timezone():
|
||||
time_server = TimeServer()
|
||||
with pytest.raises(ValueError, match=r"Unknown timezone: 'Invalid/Timezone'"):
|
||||
with pytest.raises(
|
||||
McpError,
|
||||
match=r"Invalid timezone: 'No time zone found with key Invalid/Timezone'",
|
||||
):
|
||||
time_server.get_current_time("Invalid/Timezone")
|
||||
|
||||
|
||||
@@ -93,13 +96,13 @@ def test_get_current_time_with_invalid_timezone():
|
||||
"invalid_tz",
|
||||
"12:00",
|
||||
"Europe/London",
|
||||
"Unknown source timezone: 'invalid_tz'",
|
||||
"Invalid timezone: 'No time zone found with key invalid_tz'",
|
||||
),
|
||||
(
|
||||
"Europe/Warsaw",
|
||||
"12:00",
|
||||
"invalid_tz",
|
||||
"Unknown target timezone: 'invalid_tz'",
|
||||
"Invalid timezone: 'No time zone found with key invalid_tz'",
|
||||
),
|
||||
(
|
||||
"Europe/Warsaw",
|
||||
@@ -111,7 +114,7 @@ def test_get_current_time_with_invalid_timezone():
|
||||
)
|
||||
def test_convert_time_errors(source_tz, time_str, target_tz, expected_error):
|
||||
time_server = TimeServer()
|
||||
with pytest.raises(ValueError, match=expected_error):
|
||||
with pytest.raises((McpError, ValueError), match=expected_error):
|
||||
time_server.convert_time(source_tz, time_str, target_tz)
|
||||
|
||||
|
||||
|
||||
44
src/time/uv.lock
generated
44
src/time/uv.lock
generated
@@ -165,8 +165,7 @@ source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "mcp" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pytz" },
|
||||
{ name = "tzlocal" },
|
||||
{ name = "tzdata" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -174,14 +173,14 @@ dev = [
|
||||
{ name = "freezegun" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "mcp", specifier = ">=1.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.0.0" },
|
||||
{ name = "pytz", specifier = ">=2024.2" },
|
||||
{ name = "tzlocal", specifier = ">=5.2" },
|
||||
{ name = "tzdata", specifier = ">=2024.2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -189,6 +188,7 @@ dev = [
|
||||
{ name = "freezegun", specifier = ">=1.5.1" },
|
||||
{ name = "pyright", specifier = ">=1.1.389" },
|
||||
{ name = "pytest", specifier = ">=8.3.3" },
|
||||
{ name = "ruff", specifier = ">=0.8.1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -350,12 +350,28 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2024.2"
|
||||
name = "ruff"
|
||||
version = "0.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/d0/8ff5b189d125f4260f2255d143bf2fa413b69c2610c405ace7a0a8ec81ec/ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", size = 3313222 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/d6/1a6314e568db88acdbb5121ed53e2c52cebf3720d3437a76f82f923bf171/ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5", size = 10532605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a8/a957a8812e31facffb6a26a30be0b5b4af000a6e30c7d43a22a5232a3398/ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", size = 10278243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/23/9db40fa19c453fabf94f7a35c61c58f20e8200b4734a20839515a19da790/ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", size = 9917739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a0/6ee2d949835d5701d832fc5acd05c0bfdad5e89cfdd074a171411f5ccad5/ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", size = 10779153 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/25/9c11dca9404ef1eb24833f780146236131a3c7941de394bc356912ef1041/ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", size = 10304387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/b9/84c323780db1b06feae603a707d82dbbd85955c8c917738571c65d7d5aff/ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", size = 11360351 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e1/9d4bbb2ace7aad14ded20e4674a48cda5b902aed7a1b14e6b028067060c4/ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", size = 12022879 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/28/752ff6120c0e7f9981bc4bc275d540c7f36db1379ba9db9142f69c88db21/ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", size = 11610354 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/967b61c2cc8ebd1df877607fbe462bc1e1220b4a30ae3352648aec8c24bd/ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", size = 12813976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/29/e059f945d6bd2d90213387b8c360187f2fefc989ddcee6bbf3c241329b92/ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", size = 11154564 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/47/cbd05e5a62f3fb4c072bc65c1e8fd709924cad1c7ec60a1000d1e4ee8307/ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", size = 10760604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ee/4c3981c47147c72647a198a94202633130cfda0fc95cd863a553b6f65c6a/ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", size = 10391071 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e6/083eb61300214590b188616a8ac6ae1ef5730a0974240fb4bec9c17de78b/ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", size = 10896657 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/bd/aacdb8285d10f1b943dbeb818968efca35459afc29f66ae3bd4596fbf954/ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", size = 11228362 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/72/fcb7ad41947f38b4eaa702aca0a361af0e9c2bf671d7fd964480670c297e/ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", size = 8803476 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ea/cae9aeb0f4822c44651c8407baacdb2e5b4dcd7b31a84e1c5df33aa2cc20/ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", size = 9614463 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/76/fbb4bd23dfb48fa7758d35b744413b650a9fd2ddd93bca77e30376864414/ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", size = 8959621 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -459,18 +475,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/ab/7e5f53c3b9d14972843a647d8d7a853969a58aecc7559cb3267302c94774/tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd", size = 346586 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/d3/c19d65ae67636fe63953b20c2e4a8ced4497ea232c43ff8d01db16de8dc0/tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e", size = 30201 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/3f/c4c51c55ff8487f2e6d0e618dba917e3c3ee2caae6cf0fbb59c9b1876f2e/tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8", size = 17859 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.32.1"
|
||||
|
||||
Reference in New Issue
Block a user