Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion src/hyperping/_async_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})
51 changes: 50 additions & 1 deletion src/hyperping/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})
83 changes: 83 additions & 0 deletions tests/unit/test_async_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,86 @@ 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) ----------------------------------------

from hyperping.models._monitor_models import MonitorCreate # noqa: E402


_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"}
)
77 changes: 77 additions & 0 deletions tests/unit/test_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,80 @@ 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) ----------------------------------------

from hyperping.models._monitor_models import MonitorCreate # noqa: E402


_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"}
)
Loading