Datetime MCP Server

""" Acceptance tests for the datetime_mcp_server. These tests verify the basic functionality of the datetime MCP server by testing resources, prompts, and tools. """ import asyncio import datetime import json from typing import Any, Dict, List, Optional, Union, cast from typing import TYPE_CHECKING import pytest from pydantic import AnyUrl import mcp.types as types from mcp.server import NotificationOptions, Server from mcp.server.models import InitializationOptions from datetime_mcp_server.server import server, notes, handle_list_resources, handle_read_resource, \ handle_list_prompts, handle_get_prompt, handle_list_tools, handle_call_tool if TYPE_CHECKING: from _pytest.capture import CaptureFixture from _pytest.fixtures import FixtureRequest from _pytest.logging import LogCaptureFixture from _pytest.monkeypatch import MonkeyPatch from pytest_mock.plugin import MockerFixture @pytest.fixture def reset_server_state() -> None: """ Reset the server state before each test. This ensures tests don't affect each other by clearing the notes dictionary. """ # Clear all notes notes.clear() # Add some test notes for the tests notes["test1"] = "This is a test note" notes["test2"] = "This is another test note" @pytest.mark.asyncio async def test_list_resources(reset_server_state: None) -> None: """ Test that the server correctly lists all resources. This test verifies that both note and datetime resources are returned. Args: reset_server_state: Fixture to reset the server state before the test. """ resources = await handle_list_resources() # Check that we have both note and datetime resources assert len(resources) >= 5 # 2 notes + 3 datetime resources # Check that we have the expected datetime resources datetime_uris = [str(r.uri) for r in resources if r.uri.scheme == "datetime"] assert "datetime://current" in datetime_uris assert "datetime://today" in datetime_uris assert "datetime://time" in datetime_uris # Check that we have the expected note resources note_uris = [str(r.uri) for r in resources if r.uri.scheme == "note"] assert "note://internal/test1" in note_uris assert "note://internal/test2" in note_uris @pytest.mark.asyncio async def test_read_note_resource(reset_server_state: None) -> None: """ Test that the server correctly reads a note resource. Args: reset_server_state: Fixture to reset the server state before the test. """ content = await handle_read_resource(AnyUrl("note://internal/test1")) assert content == "This is a test note" @pytest.mark.asyncio async def test_read_nonexistent_note_resource(reset_server_state: None) -> None: """ Test that the server correctly handles reading a nonexistent note resource. Args: reset_server_state: Fixture to reset the server state before the test. """ with pytest.raises(ValueError, match="Note not found: nonexistent"): await handle_read_resource(AnyUrl("note://internal/nonexistent")) @pytest.mark.asyncio async def test_read_datetime_resources(reset_server_state: None) -> None: """ Test that the server correctly reads datetime resources. Args: reset_server_state: Fixture to reset the server state before the test. """ # Test current datetime content = await handle_read_resource(AnyUrl("datetime://current")) # Verify format, but not exact time since it will change assert len(content.split()) == 2 # Date and time parts # Test today's date content = await handle_read_resource(AnyUrl("datetime://today")) # Verify format, but not exact date assert len(content.split("-")) == 3 # Year, month, day parts # Test current time content = await handle_read_resource(AnyUrl("datetime://time")) # Verify format, but not exact time assert len(content.split(":")) == 3 # Hour, minute, second parts @pytest.mark.asyncio async def test_read_invalid_datetime_resource(reset_server_state: None) -> None: """ Test that the server correctly handles reading an invalid datetime resource. Args: reset_server_state: Fixture to reset the server state before the test. """ with pytest.raises(ValueError, match="Unknown datetime resource: nonexistent"): await handle_read_resource(AnyUrl("datetime://nonexistent")) @pytest.mark.asyncio async def test_read_unsupported_uri_scheme(reset_server_state: None) -> None: """ Test that the server correctly handles reading a resource with an unsupported URI scheme. Args: reset_server_state: Fixture to reset the server state before the test. """ with pytest.raises(ValueError, match="Unsupported URI scheme: file"): await handle_read_resource(AnyUrl("file:///path/to/file")) @pytest.mark.asyncio async def test_list_prompts(reset_server_state: None) -> None: """ Test that the server correctly lists all prompts. Args: reset_server_state: Fixture to reset the server state before the test. """ prompts = await handle_list_prompts() # Check that we have the expected prompts prompt_names = [p.name for p in prompts] assert "summarize-notes" in prompt_names assert "schedule-event" in prompt_names # Check specific prompts schedule_event = next(p for p in prompts if p.name == "schedule-event") assert schedule_event.description == "Helps schedule an event at a specific time" assert len(schedule_event.arguments) == 2 # Check arguments for schedule-event arg_names = [a.name for a in schedule_event.arguments] assert "event" in arg_names assert "time" in arg_names @pytest.mark.asyncio async def test_get_summarize_notes_prompt(reset_server_state: None) -> None: """ Test that the server correctly generates the summarize-notes prompt. Args: reset_server_state: Fixture to reset the server state before the test. """ result = await handle_get_prompt("summarize-notes", None) assert result.description == "Summarize the current notes" assert len(result.messages) == 1 message = result.messages[0] assert message.role == "user" assert "test1: This is a test note" in cast(types.TextContent, message.content).text assert "test2: This is another test note" in cast(types.TextContent, message.content).text @pytest.mark.asyncio async def test_get_schedule_event_prompt(reset_server_state: None) -> None: """ Test that the server correctly generates the schedule-event prompt. Args: reset_server_state: Fixture to reset the server state before the test. """ arguments = {"event": "Meeting", "time": "15:00"} result = await handle_get_prompt("schedule-event", arguments) assert result.description == "Schedule an event at a specific time" assert len(result.messages) == 1 message = result.messages[0] assert message.role == "user" content_text = cast(types.TextContent, message.content).text assert "Please schedule an event named 'Meeting'" in content_text assert "at 15:00" in content_text @pytest.mark.asyncio async def test_get_schedule_event_prompt_missing_args(reset_server_state: None) -> None: """ Test that the server correctly handles missing arguments for schedule-event prompt. Args: reset_server_state: Fixture to reset the server state before the test. """ with pytest.raises(ValueError, match="Missing required arguments for schedule-event prompt"): await handle_get_prompt("schedule-event", None) with pytest.raises(ValueError, match="Missing required event or time argument"): await handle_get_prompt("schedule-event", {"event": "Meeting"}) @pytest.mark.asyncio async def test_get_unknown_prompt(reset_server_state: None) -> None: """ Test that the server correctly handles an unknown prompt name. Args: reset_server_state: Fixture to reset the server state before the test. """ with pytest.raises(ValueError, match="Unknown prompt: unknown-prompt"): await handle_get_prompt("unknown-prompt", None) @pytest.mark.asyncio async def test_list_tools(reset_server_state: None) -> None: """ Test that the server correctly lists all tools. Args: reset_server_state: Fixture to reset the server state before the test. """ tools = await handle_list_tools() # Check that we have the expected tools tool_names = [t.name for t in tools] assert "add-note" in tool_names assert "get-current-time" in tool_names assert "format-date" in tool_names # Check specific tools get_current_time = next(t for t in tools if t.name == "get-current-time") assert get_current_time.description == "Get the current time in various formats" # Check input schema for get-current-time schema = get_current_time.inputSchema assert "format" in schema["properties"] assert "timezone" in schema["properties"] assert "format" in schema["required"] @pytest.mark.asyncio async def test_call_add_note_tool(reset_server_state: None) -> None: """ Test that the server correctly handles the add-note tool. Args: reset_server_state: Fixture to reset the server state before the test. """ # Save the initial state of notes initial_notes = notes.copy() arguments = {"name": "new-note", "content": "This is a new note"} result = await handle_call_tool("add-note", arguments) # Check that the note was added assert "new-note" in notes assert notes["new-note"] == "This is a new note" assert len(notes) == len(initial_notes) + 1 # Check the result assert len(result) == 1 assert result[0].type == "text" assert "Added note 'new-note'" in result[0].text @pytest.mark.asyncio async def test_call_add_note_tool_missing_args(reset_server_state: None) -> None: """ Test that the server correctly handles missing arguments for add-note tool. Args: reset_server_state: Fixture to reset the server state before the test. """ with pytest.raises(ValueError, match="Missing arguments"): await handle_call_tool("add-note", None) with pytest.raises(ValueError, match="Missing name or content"): await handle_call_tool("add-note", {"name": "new-note"}) @pytest.mark.asyncio async def test_call_get_current_time_tool(reset_server_state: None) -> None: """ Test that the server correctly handles the get-current-time tool. Args: reset_server_state: Fixture to reset the server state before the test. """ # Test with ISO format arguments = {"format": "iso"} result = await handle_call_tool("get-current-time", arguments) assert len(result) == 1 assert result[0].type == "text" # The result should be a valid ISO format datetime try: datetime.datetime.fromisoformat(result[0].text) is_valid_iso = True except ValueError: is_valid_iso = False assert is_valid_iso # Test with readable format arguments = {"format": "readable"} result = await handle_call_tool("get-current-time", arguments) assert len(result) == 1 assert result[0].type == "text" # The result should be in format YYYY-MM-DD HH:MM:SS time_str = result[0].text assert len(time_str.split()) == 2 assert len(time_str.split()[0].split("-")) == 3 assert len(time_str.split()[1].split(":")) == 3 @pytest.mark.asyncio async def test_call_format_date_tool(reset_server_state: None) -> None: """ Test that the server correctly handles the format-date tool. Args: reset_server_state: Fixture to reset the server state before the test. """ # Test with custom format today = datetime.datetime.now().strftime("%Y-%m-%d") arguments = {"date": today, "format": "%d/%m/%Y"} result = await handle_call_tool("format-date", arguments) assert len(result) == 1 assert result[0].type == "text" # The result should be in format DD/MM/YYYY date_parts = result[0].text.split("/") assert len(date_parts) == 3 # Test with default date (today) arguments = {"format": "%B %d, %Y"} result = await handle_call_tool("format-date", arguments) assert len(result) == 1 assert result[0].type == "text" # The result should be in format Month DD, YYYY today = datetime.datetime.now() expected = today.strftime("%B %d, %Y") assert result[0].text == expected @pytest.mark.asyncio async def test_call_format_date_tool_invalid_date(reset_server_state: None) -> None: """ Test that the server correctly handles invalid date for format-date tool. Args: reset_server_state: Fixture to reset the server state before the test. """ arguments = {"date": "invalid-date", "format": "%Y-%m-%d"} result = await handle_call_tool("format-date", arguments) assert len(result) == 1 assert result[0].type == "text" assert "Could not parse date string: invalid-date" in result[0].text @pytest.mark.asyncio async def test_call_format_date_tool_invalid_format(reset_server_state: None) -> None: """ Test that the server correctly handles invalid format for format-date tool. Args: reset_server_state: Fixture to reset the server state before the test. """ arguments = {"date": "2023-01-01", "format": "%invalid"} result = await handle_call_tool("format-date", arguments) assert len(result) == 1 assert result[0].type == "text" assert "invalid" in result[0].text.lower() @pytest.mark.asyncio async def test_call_unknown_tool(reset_server_state: None) -> None: """ Test that the server correctly handles an unknown tool name. Args: reset_server_state: Fixture to reset the server state before the test. """ with pytest.raises(ValueError, match="Unknown tool: unknown-tool"): await handle_call_tool("unknown-tool", {}) @pytest.mark.asyncio async def test_call_get_current_time_with_timezone(reset_server_state: None, monkeypatch: "MonkeyPatch") -> None: """ Test that the server correctly handles timezones in the get-current-time tool. This test handles both the case when pytz is available and when it's not. Args: reset_server_state: Fixture to reset the server state before the test. monkeypatch: Pytest monkeypatch fixture. """ try: import pytz has_pytz = True except ImportError: has_pytz = False # Test with timezone argument arguments = {"format": "readable", "timezone": "America/New_York"} result = await handle_call_tool("get-current-time", arguments) assert len(result) >= 1 assert result[-1].type == "text" if not has_pytz: # If pytz is not available, there should be a warning message assert len(result) == 2 assert "pytz library is not available" in result[0].text # The result should be a readable datetime string time_str = result[-1].text assert len(time_str.split()) == 2 assert len(time_str.split()[0].split("-")) == 3 assert len(time_str.split()[1].split(":")) == 3 # Test with invalid timezone if has_pytz: arguments = {"format": "readable", "timezone": "Invalid/Timezone"} result = await handle_call_tool("get-current-time", arguments) assert len(result) == 2 assert "Error with timezone" in result[0].text # Despite the error, there should still be a result time_str = result[1].text assert len(time_str.split()) == 2