diff --git a/src/hyperping/_async_mcp_client.py b/src/hyperping/_async_mcp_client.py index 6440c94..2718983 100644 --- a/src/hyperping/_async_mcp_client.py +++ b/src/hyperping/_async_mcp_client.py @@ -21,7 +21,7 @@ from hyperping._async_mcp_transport import AsyncMcpTransport from hyperping.endpoints import MCP_URL from hyperping.models._integration_models import Integration -from hyperping.models._monitor_models import Monitor +from hyperping.models._monitor_models import Monitor, MonitorCreate from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember from hyperping.models._outage_models import OutageTimeline @@ -270,3 +270,52 @@ async def search_monitors_by_name(self, query: str) -> list[Monitor]: data = await self._call("search_monitors_by_name", {"query": query}) raw = data if isinstance(data, list) else [] return [Monitor.model_validate(m) for m in raw] + + async def create_monitor(self, monitor: MonitorCreate) -> Monitor: + """Create a new monitor. + + Args: + monitor: Monitor configuration. + """ + return Monitor.model_validate( + await self._call("create_monitor", monitor.model_dump(exclude_none=True)) + ) + + async def update_monitor(self, monitor_uuid: str, **kwargs: Any) -> Monitor: + """Update an existing monitor. + + Args: + monitor_uuid: Monitor UUID. + **kwargs: Fields to update. + """ + return Monitor.model_validate( + await self._call("update_monitor", {"uuid": monitor_uuid, **kwargs}) + ) + + async def pause_monitor(self, monitor_uuid: str) -> Monitor: + """Pause a monitor. + + Args: + monitor_uuid: Monitor UUID. + """ + return Monitor.model_validate( + await self._call("pause_monitor", {"uuid": monitor_uuid}) + ) + + async def resume_monitor(self, monitor_uuid: str) -> Monitor: + """Resume a paused monitor. + + Args: + monitor_uuid: Monitor UUID. + """ + return Monitor.model_validate( + await self._call("resume_monitor", {"uuid": monitor_uuid}) + ) + + async def delete_monitor(self, monitor_uuid: str) -> None: + """Delete a monitor. + + Args: + monitor_uuid: Monitor UUID. + """ + await self._call("delete_monitor", {"uuid": monitor_uuid}) diff --git a/src/hyperping/mcp_client.py b/src/hyperping/mcp_client.py index 70de527..3d6e063 100644 --- a/src/hyperping/mcp_client.py +++ b/src/hyperping/mcp_client.py @@ -21,7 +21,7 @@ from hyperping._mcp_transport import McpTransport from hyperping.endpoints import MCP_URL from hyperping.models._integration_models import Integration -from hyperping.models._monitor_models import Monitor +from hyperping.models._monitor_models import Monitor, MonitorCreate from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember from hyperping.models._outage_models import OutageTimeline @@ -267,3 +267,52 @@ def search_monitors_by_name(self, query: str) -> list[Monitor]: data = self._call("search_monitors_by_name", {"query": query}) raw = data if isinstance(data, list) else [] return [Monitor.model_validate(m) for m in raw] + + def create_monitor(self, monitor: MonitorCreate) -> Monitor: + """Create a new monitor. + + Args: + monitor: Monitor configuration. + """ + return Monitor.model_validate( + self._call("create_monitor", monitor.model_dump(exclude_none=True)) + ) + + def update_monitor(self, monitor_uuid: str, **kwargs: Any) -> Monitor: + """Update an existing monitor. + + Args: + monitor_uuid: Monitor UUID. + **kwargs: Fields to update. + """ + return Monitor.model_validate( + self._call("update_monitor", {"uuid": monitor_uuid, **kwargs}) + ) + + def pause_monitor(self, monitor_uuid: str) -> Monitor: + """Pause a monitor. + + Args: + monitor_uuid: Monitor UUID. + """ + return Monitor.model_validate( + self._call("pause_monitor", {"uuid": monitor_uuid}) + ) + + def resume_monitor(self, monitor_uuid: str) -> Monitor: + """Resume a paused monitor. + + Args: + monitor_uuid: Monitor UUID. + """ + return Monitor.model_validate( + self._call("resume_monitor", {"uuid": monitor_uuid}) + ) + + def delete_monitor(self, monitor_uuid: str) -> None: + """Delete a monitor. + + Args: + monitor_uuid: Monitor UUID. + """ + self._call("delete_monitor", {"uuid": monitor_uuid}) diff --git a/tests/unit/test_async_mcp_client.py b/tests/unit/test_async_mcp_client.py index 3c76a2d..937f97d 100644 --- a/tests/unit/test_async_mcp_client.py +++ b/tests/unit/test_async_mcp_client.py @@ -10,7 +10,7 @@ from hyperping._async_mcp_transport import MCP_URL from hyperping.exceptions import HyperpingRateLimitError from hyperping.models._integration_models import Integration -from hyperping.models._monitor_models import Monitor +from hyperping.models._monitor_models import Monitor, MonitorCreate from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember from hyperping.models._outage_models import OutageTimeline @@ -328,3 +328,84 @@ async def test_ensure_initialized_real_transport_is_idempotent(): await client.ensure_initialized() assert route.call_count == 2 await client.close() + + +# -- MCP write tools (ticket #139561) ---------------------------------------- + + +_MONITOR_PAYLOAD = { + "uuid": "mon_abc", + "name": "My API", + "url": "https://api.example.com", + "protocol": "http", +} + + +@pytest.mark.asyncio +async def test_create_monitor(): + client = make_client() + client._transport.call_tool.return_value = _MONITOR_PAYLOAD + monitor = MonitorCreate(name="My API", url="https://api.example.com") + result = await client.create_monitor(monitor) + assert isinstance(result, Monitor) + assert result.uuid == "mon_abc" + client._transport.call_tool.assert_called_once_with( + "create_monitor", monitor.model_dump(exclude_none=True) + ) + + +@pytest.mark.asyncio +async def test_update_monitor(): + client = make_client() + client._transport.call_tool.return_value = _MONITOR_PAYLOAD + result = await client.update_monitor("mon_abc", name="New Name", check_frequency=60) + assert isinstance(result, Monitor) + client._transport.call_tool.assert_called_once_with( + "update_monitor", {"uuid": "mon_abc", "name": "New Name", "check_frequency": 60} + ) + + +@pytest.mark.asyncio +async def test_update_monitor_no_kwargs(): + client = make_client() + client._transport.call_tool.return_value = _MONITOR_PAYLOAD + result = await client.update_monitor("mon_abc") + assert isinstance(result, Monitor) + client._transport.call_tool.assert_called_once_with( + "update_monitor", {"uuid": "mon_abc"} + ) + + +@pytest.mark.asyncio +async def test_pause_monitor(): + client = make_client() + client._transport.call_tool.return_value = {**_MONITOR_PAYLOAD, "paused": True} + result = await client.pause_monitor("mon_abc") + assert isinstance(result, Monitor) + assert result.paused is True + client._transport.call_tool.assert_called_once_with( + "pause_monitor", {"uuid": "mon_abc"} + ) + + +@pytest.mark.asyncio +async def test_resume_monitor(): + client = make_client() + client._transport.call_tool.return_value = {**_MONITOR_PAYLOAD, "paused": False} + result = await client.resume_monitor("mon_abc") + assert isinstance(result, Monitor) + assert result.paused is False + client._transport.call_tool.assert_called_once_with( + "resume_monitor", {"uuid": "mon_abc"} + ) + + +@pytest.mark.asyncio +async def test_delete_monitor(): + client = make_client() + client._transport.call_tool.return_value = None + result = await client.delete_monitor("mon_abc") + assert result is None + client._transport.call_tool.assert_called_once_with( + "delete_monitor", {"uuid": "mon_abc"} + ) diff --git a/tests/unit/test_mcp_client.py b/tests/unit/test_mcp_client.py index b55d608..ea617cd 100644 --- a/tests/unit/test_mcp_client.py +++ b/tests/unit/test_mcp_client.py @@ -11,7 +11,7 @@ from hyperping.exceptions import HyperpingRateLimitError from hyperping.mcp_client import HyperpingMcpClient from hyperping.models._integration_models import Integration -from hyperping.models._monitor_models import Monitor +from hyperping.models._monitor_models import Monitor, MonitorCreate from hyperping.models._observability_models import MonitorAnomaly, ProbeLogResponse from hyperping.models._oncall_models import EscalationPolicy, OnCallSchedule, TeamMember from hyperping.models._outage_models import OutageTimeline @@ -334,3 +334,78 @@ def test_changelog_documents_mcp_rate_limit_work(): assert "rate limit" in changelog.lower(), ( "CHANGELOG must mention rate-limit handling somewhere" ) + + +# -- MCP write tools (ticket #139561) ---------------------------------------- + + +_MONITOR_PAYLOAD = { + "uuid": "mon_abc", + "name": "My API", + "url": "https://api.example.com", + "protocol": "http", +} + + +def test_create_monitor(): + client = make_client() + client._transport.call_tool.return_value = _MONITOR_PAYLOAD + monitor = MonitorCreate(name="My API", url="https://api.example.com") + result = client.create_monitor(monitor) + assert isinstance(result, Monitor) + assert result.uuid == "mon_abc" + client._transport.call_tool.assert_called_once_with( + "create_monitor", monitor.model_dump(exclude_none=True) + ) + + +def test_update_monitor(): + client = make_client() + client._transport.call_tool.return_value = _MONITOR_PAYLOAD + result = client.update_monitor("mon_abc", name="New Name", check_frequency=60) + assert isinstance(result, Monitor) + client._transport.call_tool.assert_called_once_with( + "update_monitor", {"uuid": "mon_abc", "name": "New Name", "check_frequency": 60} + ) + + +def test_update_monitor_no_kwargs(): + client = make_client() + client._transport.call_tool.return_value = _MONITOR_PAYLOAD + result = client.update_monitor("mon_abc") + assert isinstance(result, Monitor) + client._transport.call_tool.assert_called_once_with( + "update_monitor", {"uuid": "mon_abc"} + ) + + +def test_pause_monitor(): + client = make_client() + client._transport.call_tool.return_value = {**_MONITOR_PAYLOAD, "paused": True} + result = client.pause_monitor("mon_abc") + assert isinstance(result, Monitor) + assert result.paused is True + client._transport.call_tool.assert_called_once_with( + "pause_monitor", {"uuid": "mon_abc"} + ) + + +def test_resume_monitor(): + client = make_client() + client._transport.call_tool.return_value = {**_MONITOR_PAYLOAD, "paused": False} + result = client.resume_monitor("mon_abc") + assert isinstance(result, Monitor) + assert result.paused is False + client._transport.call_tool.assert_called_once_with( + "resume_monitor", {"uuid": "mon_abc"} + ) + + +def test_delete_monitor(): + client = make_client() + client._transport.call_tool.return_value = None + result = client.delete_monitor("mon_abc") + assert result is None + client._transport.call_tool.assert_called_once_with( + "delete_monitor", {"uuid": "mon_abc"} + ) diff --git a/uv.lock b/uv.lock index f7589d6..827b823 100644 --- a/uv.lock +++ b/uv.lock @@ -335,7 +335,7 @@ wheels = [ [[package]] name = "hyperping" -version = "1.7.0" +version = "1.8.0" source = { editable = "." } dependencies = [ { name = "httpx" },