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
23 changes: 16 additions & 7 deletions src/hyperping/mcp_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP

from hyperping._async_client import AsyncHyperpingClient
from hyperping._async_mcp_client import AsyncHyperpingMcpClient
from hyperping.client import HyperpingClient
from hyperping.mcp_client import HyperpingMcpClient


def create_mcp_server(
api_key: str | None = None,
client: HyperpingClient | None = None,
mcp_client: HyperpingMcpClient | None = None,
client: HyperpingClient | AsyncHyperpingClient | None = None,
mcp_client: HyperpingMcpClient | AsyncHyperpingMcpClient | None = None,
tools: list[str] | None = None,
name: str = "hyperping",
) -> FastMCP:
Expand All @@ -35,12 +37,19 @@ def create_mcp_server(
Args:
api_key: Hyperping API key. Used to create internal REST and MCP
clients when *client* and *mcp_client* are not supplied.
client: Pre-configured :class:`~hyperping.client.HyperpingClient`.
Takes precedence over *api_key* for REST operations.
mcp_client: Pre-configured :class:`~hyperping.mcp_client.HyperpingMcpClient`.
When only *api_key* is provided, sync clients are created.
client: Pre-configured :class:`~hyperping.client.HyperpingClient` or
:class:`~hyperping._async_client.AsyncHyperpingClient`.
Takes precedence over *api_key* for REST operations. When an
async client is passed, all REST tools are registered as
coroutine functions so FastMCP can run them on the event loop.
mcp_client: Pre-configured :class:`~hyperping.mcp_client.HyperpingMcpClient`
or :class:`~hyperping._async_mcp_client.AsyncHyperpingMcpClient`.
Required for the observability tool group. When ``None`` and
*api_key* is provided, one is created internally. When ``None``
and only *client* is provided, the observability group is skipped.
*api_key* is provided, a sync client is created internally. When
``None`` and only *client* is provided, the observability group
is skipped. When an async client is passed, observability tools
are registered as coroutines.
tools: List of tool group names to register. ``None`` (default)
registers all groups. Valid group names: ``monitors``,
``incidents``, ``maintenance``, ``outages``, ``statuspages``,
Expand Down
6 changes: 4 additions & 2 deletions src/hyperping/mcp_server/_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP

from hyperping._async_client import AsyncHyperpingClient
from hyperping._async_mcp_client import AsyncHyperpingMcpClient
from hyperping.client import HyperpingClient
from hyperping.mcp_client import HyperpingMcpClient

Expand All @@ -30,8 +32,8 @@

def register_tools(
mcp: FastMCP,
client: HyperpingClient,
mcp_client: HyperpingMcpClient | None,
client: HyperpingClient | AsyncHyperpingClient,
mcp_client: HyperpingMcpClient | AsyncHyperpingMcpClient | None,
groups: list[str] | None,
) -> None:
"""Register tool groups on *mcp*.
Expand Down
213 changes: 146 additions & 67 deletions src/hyperping/mcp_server/_tools_healthchecks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,74 +11,153 @@
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP

from hyperping._async_client import AsyncHyperpingClient
from hyperping.client import HyperpingClient


def register_healthcheck_tools(mcp: FastMCP, client: HyperpingClient) -> None:
def register_healthcheck_tools(
mcp: FastMCP,
client: HyperpingClient | AsyncHyperpingClient,
) -> None:
"""Register healthcheck tools on *mcp*."""

@mcp.tool()
def list_healthchecks() -> list[dict[str, Any]]:
"""List all healthchecks (push-based cron/heartbeat monitors)."""
return [h.model_dump() for h in client.list_healthchecks()]

@mcp.tool()
def get_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Get a single healthcheck by UUID."""
return client.get_healthcheck(healthcheck_id).model_dump()

@mcp.tool()
def create_healthcheck(
name: str,
period: int,
grace: int,
escalation_policy: str | None = None,
project_uuid: str | None = None,
) -> dict[str, Any]:
"""This will create a new healthcheck. period and grace are in seconds."""
from hyperping.models import HealthcheckCreate

fields: dict[str, Any] = {"name": name, "period": period, "grace": grace}
if escalation_policy is not None:
fields["escalation_policy"] = escalation_policy
if project_uuid is not None:
fields["project_uuid"] = project_uuid
return client.create_healthcheck(HealthcheckCreate(**fields)).model_dump()

@mcp.tool()
def update_healthcheck(
healthcheck_id: str,
name: str | None = None,
period: int | None = None,
grace: int | None = None,
escalation_policy: str | None = None,
) -> dict[str, Any]:
"""Update an existing healthcheck. Only supplied fields are changed."""
from hyperping.models import HealthcheckUpdate

fields: dict[str, Any] = {}
if name is not None:
fields["name"] = name
if period is not None:
fields["period"] = period
if grace is not None:
fields["grace"] = grace
if escalation_policy is not None:
fields["escalation_policy"] = escalation_policy
return client.update_healthcheck(healthcheck_id, HealthcheckUpdate(**fields)).model_dump()

@mcp.tool()
def delete_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""This will permanently delete a healthcheck."""
client.delete_healthcheck(healthcheck_id)
return {"success": True}

@mcp.tool()
def pause_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Pause a healthcheck so it stops alerting on missed pings."""
return client.pause_healthcheck(healthcheck_id).model_dump()

@mcp.tool()
def resume_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Resume a paused healthcheck."""
return client.resume_healthcheck(healthcheck_id).model_dump()
from hyperping._async_client import AsyncHyperpingClient

if isinstance(client, AsyncHyperpingClient):

@mcp.tool()
async def list_healthchecks() -> list[dict[str, Any]]:
"""List all healthchecks (push-based cron/heartbeat monitors)."""
return [h.model_dump() for h in await client.list_healthchecks()]

@mcp.tool()
async def get_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Get a single healthcheck by UUID."""
return (await client.get_healthcheck(healthcheck_id)).model_dump()

@mcp.tool()
async def create_healthcheck(
name: str,
period: int,
grace: int,
escalation_policy: str | None = None,
project_uuid: str | None = None,
) -> dict[str, Any]:
"""This will create a new healthcheck. period and grace are in seconds."""
from hyperping.models import HealthcheckCreate

fields: dict[str, Any] = {"name": name, "period": period, "grace": grace}
if escalation_policy is not None:
fields["escalation_policy"] = escalation_policy
if project_uuid is not None:
fields["project_uuid"] = project_uuid
return (await client.create_healthcheck(HealthcheckCreate(**fields))).model_dump()

@mcp.tool()
async def update_healthcheck(
healthcheck_id: str,
name: str | None = None,
period: int | None = None,
grace: int | None = None,
escalation_policy: str | None = None,
) -> dict[str, Any]:
"""Update an existing healthcheck. Only supplied fields are changed."""
from hyperping.models import HealthcheckUpdate

fields: dict[str, Any] = {}
if name is not None:
fields["name"] = name
if period is not None:
fields["period"] = period
if grace is not None:
fields["grace"] = grace
if escalation_policy is not None:
fields["escalation_policy"] = escalation_policy
return (
await client.update_healthcheck(healthcheck_id, HealthcheckUpdate(**fields))
).model_dump()

@mcp.tool()
async def delete_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""This will permanently delete a healthcheck."""
await client.delete_healthcheck(healthcheck_id)
return {"success": True}

@mcp.tool()
async def pause_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Pause a healthcheck so it stops alerting on missed pings."""
return (await client.pause_healthcheck(healthcheck_id)).model_dump()

@mcp.tool()
async def resume_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Resume a paused healthcheck."""
return (await client.resume_healthcheck(healthcheck_id)).model_dump()

else:

@mcp.tool()
def list_healthchecks() -> list[dict[str, Any]]:
"""List all healthchecks (push-based cron/heartbeat monitors)."""
return [h.model_dump() for h in client.list_healthchecks()]

@mcp.tool()
def get_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Get a single healthcheck by UUID."""
return client.get_healthcheck(healthcheck_id).model_dump()

@mcp.tool()
def create_healthcheck(
name: str,
period: int,
grace: int,
escalation_policy: str | None = None,
project_uuid: str | None = None,
) -> dict[str, Any]:
"""This will create a new healthcheck. period and grace are in seconds."""
from hyperping.models import HealthcheckCreate

fields: dict[str, Any] = {"name": name, "period": period, "grace": grace}
if escalation_policy is not None:
fields["escalation_policy"] = escalation_policy
if project_uuid is not None:
fields["project_uuid"] = project_uuid
return client.create_healthcheck(HealthcheckCreate(**fields)).model_dump()

@mcp.tool()
def update_healthcheck(
healthcheck_id: str,
name: str | None = None,
period: int | None = None,
grace: int | None = None,
escalation_policy: str | None = None,
) -> dict[str, Any]:
"""Update an existing healthcheck. Only supplied fields are changed."""
from hyperping.models import HealthcheckUpdate

fields: dict[str, Any] = {}
if name is not None:
fields["name"] = name
if period is not None:
fields["period"] = period
if grace is not None:
fields["grace"] = grace
if escalation_policy is not None:
fields["escalation_policy"] = escalation_policy
return client.update_healthcheck(
healthcheck_id, HealthcheckUpdate(**fields)
).model_dump()

@mcp.tool()
def delete_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""This will permanently delete a healthcheck."""
client.delete_healthcheck(healthcheck_id)
return {"success": True}

@mcp.tool()
def pause_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Pause a healthcheck so it stops alerting on missed pings."""
return client.pause_healthcheck(healthcheck_id).model_dump()

@mcp.tool()
def resume_healthcheck(healthcheck_id: str) -> dict[str, Any]:
"""Resume a paused healthcheck."""
return client.resume_healthcheck(healthcheck_id).model_dump()
Loading