diff --git a/pyproject.toml b/pyproject.toml index f1b4471..9a88816 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,14 +19,11 @@ classifiers = [ ] dependencies = [ - # Floor bumped to 1.0.0: aligns with the GA release of the Python SDK - # cut alongside this server's 1.0.0. The 1.0.0 SDK includes every - # contract the server currently relies on (`managedBy` on resource - # DTOs, the monitor validation contract — frequencySeconds bounds, - # dynamic region whitelist) and commits to semver going forward, so - # `>=1.0.0` lets us pull in compatible patch/minor bumps without - # explicit floor bumps each time. - "devhelm>=1.0.0", + # Floor bumped to 1.3.0: first SDK release with the full + # `client.services` catalog resource and the extended + # `client.dependencies` (component-level track + alert sensitivity) + # that the services/dependencies tool modules call directly. + "devhelm>=1.3.0", "fastmcp>=2.0.0", ] diff --git a/src/devhelm_mcp/server.py b/src/devhelm_mcp/server.py index d394091..87ba4d1 100644 --- a/src/devhelm_mcp/server.py +++ b/src/devhelm_mcp/server.py @@ -40,6 +40,7 @@ notification_policies, resource_groups, secrets, + services, status, status_pages, tags, @@ -77,8 +78,9 @@ def _package_version() -> str: "DevHelm MCP server for monitoring infrastructure. " "Use these tools to manage uptime monitors, incidents, alert channels, " "notification policies, environments, secrets, tags, resource groups, " - "webhooks, API keys, service dependencies, deploy locks, maintenance " - "windows, status pages, and view dashboard status. " + "webhooks, API keys, service dependencies, the third-party service " + "catalog (search services, live status, uptime, incidents), deploy " + "locks, maintenance windows, status pages, and view dashboard status. " "Authentication is handled at the transport layer: " "`Authorization: Bearer ` for the hosted endpoint " "(https://mcp.devhelm.io/mcp/), or the `DEVHELM_API_TOKEN` environment " @@ -100,6 +102,7 @@ def _package_version() -> str: webhooks, api_keys, dependencies, + services, deploy_lock, maintenance_windows, status, diff --git a/src/devhelm_mcp/tools/dependencies.py b/src/devhelm_mcp/tools/dependencies.py index 57ef233..83b3441 100644 --- a/src/devhelm_mcp/tools/dependencies.py +++ b/src/devhelm_mcp/tools/dependencies.py @@ -26,10 +26,46 @@ def get_dependency(dependency_id: str, api_token: str | None = None) -> ToolResu raise_tool_error(e) @mcp.tool() - def track_dependency(slug: str, api_token: str | None = None) -> ToolResult: - """Start tracking a service dependency by its slug (e.g. 'github', 'aws').""" + def track_dependency( + slug: str, + component_id: str | None = None, + alert_sensitivity: str | None = None, + api_token: str | None = None, + ) -> ToolResult: + """Start tracking a service dependency by its slug (e.g. 'github', 'aws'). + + Optionally track a single component via `component_id` (see + list_service_components) and set `alert_sensitivity`: AWARENESS + (silent tracking, default), INCIDENTS_ONLY, MAJOR_ONLY, or ALL.""" + try: + return serialize( + get_client(api_token).dependencies.track( + slug, + component_id=component_id, + alert_sensitivity=alert_sensitivity, + ) + ) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def update_dependency_alert_sensitivity( + subscription_id: str, + alert_sensitivity: str, + api_token: str | None = None, + ) -> ToolResult: + """Change how loudly a tracked dependency alerts you. + + Levels: AWARENESS (silent tracking, default — status visible on the + dashboard but no notifications), INCIDENTS_ONLY (notify on any + incident), MAJOR_ONLY (notify only on major/critical incidents), and + ALL (every status change, including maintenance).""" try: - return serialize(get_client(api_token).dependencies.track(slug)) + return serialize( + get_client(api_token).dependencies.update_alert_sensitivity( + subscription_id, alert_sensitivity + ) + ) except DevhelmError as e: raise_tool_error(e) diff --git a/src/devhelm_mcp/tools/services.py b/src/devhelm_mcp/tools/services.py new file mode 100644 index 0000000..b1083f7 --- /dev/null +++ b/src/devhelm_mcp/tools/services.py @@ -0,0 +1,182 @@ +"""Service catalog tools — browse third-party services and their status.""" + +from __future__ import annotations + +from devhelm import DevhelmError +from fastmcp import FastMCP + +from devhelm_mcp.client import ToolResult, get_client, raise_tool_error, serialize + + +def register(mcp: FastMCP) -> None: + @mcp.tool() + def search_services( + query: str | None = None, + category: str | None = None, + limit: int = 20, + api_token: str | None = None, + ) -> ToolResult: + """Search the catalog of third-party services (Stripe, GitHub, AWS, ...) + that can be tracked as dependencies. + + Use `query` for free-text search by name (e.g. 'stripe', 'cloudflare') + and `category` to filter by catalog category (see + list_service_categories). Results are paginated; raise `limit` + (default 20) for broader sweeps.""" + try: + return serialize( + get_client(api_token).services.list( + search=query, category=category, limit=limit + ) + ) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def get_service(slug: str, api_token: str | None = None) -> ToolResult: + """Get a catalog service's summary by slug (e.g. 'github'), including + its current status, categories, and component overview.""" + try: + return serialize(get_client(api_token).services.get(slug, summary=True)) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def get_service_live_status(slug: str, api_token: str | None = None) -> ToolResult: + """Get the live (real-time) operational status of a catalog service, + fetched from its upstream status page. Use this when freshness matters + more than latency — e.g. 'is Stripe down right now?'.""" + try: + return serialize(get_client(api_token).services.live_status(slug)) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def get_services_summary(api_token: str | None = None) -> ToolResult: + """Get the global status summary across the entire service catalog — + counts of operational / degraded / outage services. Use this for a + quick 'is anything broken on the internet right now?' overview before + drilling into a specific service.""" + try: + return serialize(get_client(api_token).services.summary()) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def list_service_categories(api_token: str | None = None) -> ToolResult: + """List all service catalog categories (e.g. cloud, payments, devtools) + usable as the `category` filter in search_services.""" + try: + return serialize(get_client(api_token).services.categories()) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def list_service_components(slug: str, api_token: str | None = None) -> ToolResult: + """List a catalog service's components (e.g. 'API', 'Dashboard', + 'Webhooks') with their individual statuses. Component IDs can be used + to track a single component via track_dependency.""" + try: + return serialize(get_client(api_token).services.components(slug)) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def get_service_uptime( + slug: str, period: str = "30d", api_token: str | None = None + ) -> ToolResult: + """Get historical uptime stats for a catalog service over a period + (e.g. '7d', '30d', '90d'; default '30d').""" + try: + return serialize(get_client(api_token).services.uptime(slug, period=period)) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def list_service_incidents( + slug: str | None = None, + status: str | None = None, + api_token: str | None = None, + ) -> ToolResult: + """List incidents for a catalog service, or across all services when + `slug` is omitted. Filter by `status` (e.g. 'active', 'resolved') to + answer questions like 'which of my dependencies have open incidents?'.""" + try: + return serialize( + get_client(api_token).services.incidents(slug_or_id=slug, status=status) + ) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def get_service_incident( + slug: str, incident_id: str, api_token: str | None = None + ) -> ToolResult: + """Get one vendor incident in full detail, including the vendor's + timeline of status updates (investigating → identified → resolved). + Get incident IDs from list_service_incidents.""" + try: + return serialize(get_client(api_token).services.incident(slug, incident_id)) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def get_service_day_rollup( + slug: str, date: str, api_token: str | None = None + ) -> ToolResult: + """Get a one-day rollup for a catalog service on a UTC calendar day + (ISO YYYY-MM-DD): aggregated uptime, per-component impact windows, + and the incidents that overlapped that day. Use this to answer + 'what happened to Stripe on 2026-06-01?'.""" + try: + return serialize(get_client(api_token).services.day(slug, date)) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def get_component_uptime( + slug: str, + component_id: str, + period: str = "30d", + api_token: str | None = None, + ) -> ToolResult: + """Get daily uptime history for a single component of a catalog + service (e.g. just the 'API' component of Stripe) over a period + ('7d', '30d', '90d', '1y'; default '30d'). Get component IDs from + list_service_components.""" + try: + return serialize( + get_client(api_token).services.component_uptime( + slug, component_id, period=period + ) + ) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def get_all_components_uptime( + slug: str, period: str = "30d", api_token: str | None = None + ) -> ToolResult: + """Get daily uptime history for every leaf component of a catalog + service in one call, keyed by component ID, over a period ('7d', + '30d', '90d', '1y'; default '30d'). Prefer this over repeated + get_component_uptime calls when comparing components.""" + try: + return serialize( + get_client(api_token).services.batch_component_uptime( + slug, period=period + ) + ) + except DevhelmError as e: + raise_tool_error(e) + + @mcp.tool() + def list_service_maintenances( + slug: str, api_token: str | None = None + ) -> ToolResult: + """List scheduled and past maintenance windows announced by a catalog + service (e.g. upcoming AWS maintenance that could affect you).""" + try: + return serialize(get_client(api_token).services.maintenances(slug)) + except DevhelmError as e: + raise_tool_error(e) diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 0000000..2236119 --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,412 @@ +"""Tests for service catalog MCP tools + dependency alert-sensitivity tools. + +The ``client.services`` SDK resource and the extended +``client.dependencies.track`` / ``update_alert_sensitivity`` signatures ship +in a parallel SDK release; these tests patch ``get_client`` in the tool +module's namespace (the same pattern as ``test_devex_round3_fixes.py``) so +the tools are exercised against the agreed SDK call contract without +requiring the new SDK methods to be installed. +""" + +from __future__ import annotations + +import asyncio +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest +from devhelm import DevhelmApiError + +from devhelm_mcp.server import _strip_internal_schema_fields, mcp + +RegisteredTools = dict[str, Any] + + +@pytest.fixture(scope="module") +def registered_tools() -> RegisteredTools: + asyncio.run(_strip_internal_schema_fields()) + tools = asyncio.run(mcp.list_tools()) + return {t.name: t for t in tools} + + +_SERVICE_TOOLS = [ + "search_services", + "get_service", + "get_service_live_status", + "get_services_summary", + "list_service_categories", + "list_service_components", + "get_service_uptime", + "list_service_incidents", + "get_service_incident", + "get_service_day_rollup", + "get_component_uptime", + "get_all_components_uptime", + "list_service_maintenances", +] + +_SAMPLE_SERVICE: dict[str, Any] = { + "slug": "stripe", + "name": "Stripe", + "category": "payments", + "status": "OPERATIONAL", +} + +_SAMPLE_PAGE: dict[str, Any] = { + "data": [_SAMPLE_SERVICE], + "nextCursor": None, + "hasMore": False, +} + + +def _call_tool(tool_name: str, arguments: dict[str, Any]) -> Any: + return asyncio.run(mcp.call_tool(tool_name, arguments)) + + +def _mock_services_client() -> MagicMock: + """SDK client stub whose ``services`` / ``dependencies`` methods return + JSON-safe dicts (``serialize`` rejects bare MagicMocks).""" + client = MagicMock() + client.services.list.return_value = _SAMPLE_PAGE + client.services.get.return_value = _SAMPLE_SERVICE + client.services.live_status.return_value = { + "slug": "stripe", + "status": "OPERATIONAL", + } + client.services.categories.return_value = [{"slug": "payments"}] + client.services.components.return_value = [{"id": "comp_1", "name": "API"}] + client.services.uptime.return_value = {"period": "30d", "uptime": 99.99} + client.services.incidents.return_value = [{"id": "inc_1", "status": "active"}] + client.services.summary.return_value = { + "totalServices": 1200, + "operationalCount": 1190, + } + client.services.incident.return_value = { + "id": "inc_1", + "status": "resolved", + "updates": [{"status": "investigating"}, {"status": "resolved"}], + } + client.services.day.return_value = { + "date": "2026-06-01", + "uptime": 99.5, + "incidents": [{"id": "inc_1"}], + } + client.services.component_uptime.return_value = [ + {"date": "2026-06-01", "uptime": 100.0} + ] + client.services.batch_component_uptime.return_value = { + "comp_1": [{"date": "2026-06-01", "uptime": 100.0}] + } + client.services.maintenances.return_value = [{"id": "mw_1"}] + client.dependencies.track.return_value = {"id": "sub_1", "slug": "stripe"} + client.dependencies.update_alert_sensitivity.return_value = { + "id": "sub_1", + "alertSensitivity": "MAJOR_ONLY", + } + return client + + +# --------------------------------------------------------------------------- # +# Registration + schema hygiene +# --------------------------------------------------------------------------- # + + +class TestServiceToolsRegistered: + @pytest.mark.parametrize("name", _SERVICE_TOOLS) + def test_tool_registered_with_description( + self, registered_tools: RegisteredTools, name: str + ) -> None: + assert name in registered_tools, f"Missing tool: {name}" + desc = registered_tools[name].description + assert desc and len(desc) >= 10, f"{name} description too short: {desc!r}" + + def test_search_services_mentions_example_services( + self, registered_tools: RegisteredTools + ) -> None: + # The docstring is the LLM's only hint about what lives in the + # catalog; the Stripe/GitHub/AWS examples anchor that. + desc = registered_tools["search_services"].description + for example in ("Stripe", "GitHub", "AWS"): + assert example in desc, f"search_services should mention {example}" + + @pytest.mark.parametrize("name", _SERVICE_TOOLS) + def test_api_token_hidden_from_input_schema( + self, registered_tools: RegisteredTools, name: str + ) -> None: + properties = registered_tools[name].parameters.get("properties", {}) + assert "api_token" not in properties, ( + f"{name} leaks api_token into the LLM-facing input schema" + ) + required = registered_tools[name].parameters.get("required", []) + assert "api_token" not in required + + def test_slug_required_on_per_service_tools( + self, registered_tools: RegisteredTools + ) -> None: + for name in [ + "get_service", + "get_service_live_status", + "list_service_components", + "get_service_uptime", + "get_service_incident", + "get_service_day_rollup", + "get_component_uptime", + "get_all_components_uptime", + "list_service_maintenances", + ]: + required = registered_tools[name].parameters.get("required", []) + assert "slug" in required, f"{name} should require slug" + + def test_cross_service_tools_have_optional_filters( + self, registered_tools: RegisteredTools + ) -> None: + # search_services, list_service_incidents, and get_services_summary + # work with no args at all (browse mode / cross-service sweeps / + # global overview). + for name in [ + "search_services", + "list_service_incidents", + "get_services_summary", + ]: + required = registered_tools[name].parameters.get("required", []) + assert required == [], f"{name} should have no required params" + + def test_id_params_required_on_detail_tools( + self, registered_tools: RegisteredTools + ) -> None: + required_incident = registered_tools["get_service_incident"].parameters.get( + "required", [] + ) + assert "incident_id" in required_incident + required_day = registered_tools["get_service_day_rollup"].parameters.get( + "required", [] + ) + assert "date" in required_day + required_comp = registered_tools["get_component_uptime"].parameters.get( + "required", [] + ) + assert "component_id" in required_comp + + +# --------------------------------------------------------------------------- # +# SDK call contract — each tool forwards args to the agreed SDK signature +# --------------------------------------------------------------------------- # + + +class TestServiceToolsSdkContract: + def test_search_services_forwards_query_category_limit(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool( + "search_services", + {"query": "stripe", "category": "payments", "limit": 5}, + ) + client.services.list.assert_called_once_with( + search="stripe", category="payments", limit=5 + ) + + def test_search_services_defaults(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("search_services", {}) + client.services.list.assert_called_once_with( + search=None, category=None, limit=20 + ) + + def test_get_service_uses_summary(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("get_service", {"slug": "stripe"}) + client.services.get.assert_called_once_with("stripe", summary=True) + + def test_get_service_live_status(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("get_service_live_status", {"slug": "stripe"}) + client.services.live_status.assert_called_once_with("stripe") + + def test_list_service_categories(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("list_service_categories", {}) + client.services.categories.assert_called_once_with() + + def test_list_service_components(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("list_service_components", {"slug": "stripe"}) + client.services.components.assert_called_once_with("stripe") + + def test_get_service_uptime_forwards_period(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("get_service_uptime", {"slug": "stripe", "period": "90d"}) + client.services.uptime.assert_called_once_with("stripe", period="90d") + + def test_get_service_uptime_default_period(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("get_service_uptime", {"slug": "stripe"}) + client.services.uptime.assert_called_once_with("stripe", period="30d") + + def test_list_service_incidents_per_service(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("list_service_incidents", {"slug": "stripe", "status": "active"}) + client.services.incidents.assert_called_once_with( + slug_or_id="stripe", status="active" + ) + + def test_list_service_incidents_cross_service(self) -> None: + # Omitting slug must hit the cross-service listing (slug_or_id=None). + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("list_service_incidents", {}) + client.services.incidents.assert_called_once_with(slug_or_id=None, status=None) + + def test_list_service_maintenances(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("list_service_maintenances", {"slug": "aws"}) + client.services.maintenances.assert_called_once_with("aws") + + def test_get_services_summary(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("get_services_summary", {}) + client.services.summary.assert_called_once_with() + + def test_get_service_incident(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool( + "get_service_incident", {"slug": "stripe", "incident_id": "inc_1"} + ) + client.services.incident.assert_called_once_with("stripe", "inc_1") + + def test_get_service_day_rollup(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool( + "get_service_day_rollup", {"slug": "stripe", "date": "2026-06-01"} + ) + client.services.day.assert_called_once_with("stripe", "2026-06-01") + + def test_get_component_uptime_forwards_period(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool( + "get_component_uptime", + {"slug": "stripe", "component_id": "comp_1", "period": "90d"}, + ) + client.services.component_uptime.assert_called_once_with( + "stripe", "comp_1", period="90d" + ) + + def test_get_component_uptime_default_period(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool( + "get_component_uptime", {"slug": "stripe", "component_id": "comp_1"} + ) + client.services.component_uptime.assert_called_once_with( + "stripe", "comp_1", period="30d" + ) + + def test_get_all_components_uptime_forwards_period(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("get_all_components_uptime", {"slug": "stripe", "period": "7d"}) + client.services.batch_component_uptime.assert_called_once_with( + "stripe", period="7d" + ) + + def test_get_all_components_uptime_default_period(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.services.get_client", return_value=client): + _call_tool("get_all_components_uptime", {"slug": "stripe"}) + client.services.batch_component_uptime.assert_called_once_with( + "stripe", period="30d" + ) + + +# --------------------------------------------------------------------------- # +# Dependencies — extended track + alert sensitivity update +# --------------------------------------------------------------------------- # + + +class TestDependencyTrackExtensions: + def test_track_dependency_defaults_pass_none(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.dependencies.get_client", return_value=client): + _call_tool("track_dependency", {"slug": "stripe"}) + client.dependencies.track.assert_called_once_with( + "stripe", component_id=None, alert_sensitivity=None + ) + + def test_track_dependency_forwards_component_and_sensitivity(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.dependencies.get_client", return_value=client): + _call_tool( + "track_dependency", + { + "slug": "stripe", + "component_id": "comp_1", + "alert_sensitivity": "INCIDENTS_ONLY", + }, + ) + client.dependencies.track.assert_called_once_with( + "stripe", component_id="comp_1", alert_sensitivity="INCIDENTS_ONLY" + ) + + def test_update_dependency_alert_sensitivity(self) -> None: + client = _mock_services_client() + with patch("devhelm_mcp.tools.dependencies.get_client", return_value=client): + _call_tool( + "update_dependency_alert_sensitivity", + {"subscription_id": "sub_1", "alert_sensitivity": "MAJOR_ONLY"}, + ) + client.dependencies.update_alert_sensitivity.assert_called_once_with( + "sub_1", "MAJOR_ONLY" + ) + + def test_update_sensitivity_docstring_lists_all_levels( + self, registered_tools: RegisteredTools + ) -> None: + desc = registered_tools["update_dependency_alert_sensitivity"].description + for level in ("AWARENESS", "INCIDENTS_ONLY", "MAJOR_ONLY", "ALL"): + assert level in desc, f"description should document level {level}" + + +# --------------------------------------------------------------------------- # +# Error surfacing — upstream failures must set isError=true +# --------------------------------------------------------------------------- # + + +class TestServiceToolErrorsBubbleUp: + def test_get_service_propagates_api_error(self) -> None: + async def go() -> Any: + from fastmcp import Client + + async with Client(mcp) as client: + return await client.call_tool( + "get_service", + {"slug": "nonexistent"}, + raise_on_error=False, + ) + + mock_client = MagicMock() + mock_client.services.get.side_effect = DevhelmApiError( + "Service not found", + status=404, + code="NOT_FOUND", + request_id="req_svc", + ) + + with patch("devhelm_mcp.tools.services.get_client", return_value=mock_client): + result = asyncio.run(go()) + + assert result.is_error is True + text = result.content[0].text + assert "ApiError (404 NOT_FOUND)" in text + assert "request_id=req_svc" in text diff --git a/tests/test_tools.py b/tests/test_tools.py index 9149293..4b7a806 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -81,7 +81,21 @@ "list_dependencies", "get_dependency", "track_dependency", + "update_dependency_alert_sensitivity", "delete_dependency", + "search_services", + "get_service", + "get_service_live_status", + "get_services_summary", + "list_service_categories", + "list_service_components", + "get_service_uptime", + "list_service_incidents", + "get_service_incident", + "get_service_day_rollup", + "get_component_uptime", + "get_all_components_uptime", + "list_service_maintenances", "acquire_deploy_lock", "get_current_deploy_lock", "release_deploy_lock", diff --git a/uv.lock b/uv.lock index c9ee2e8..270088d 100644 --- a/uv.lock +++ b/uv.lock @@ -388,20 +388,20 @@ wheels = [ [[package]] name = "devhelm" -version = "1.1.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "pydantic", extra = ["email"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/dc/b333a18a6077a3e80b4c9dfa15cda3b806b1bd7505514daab9f74febe449/devhelm-1.1.0.tar.gz", hash = "sha256:519ea146ac35bba37514984261d7f8edfff02e31c0849360a5a56c84b2991c87", size = 250122, upload-time = "2026-05-11T20:03:43.238Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/2e/317082ad26945cdf815040ce48c45ecde7537ccae89d8011bc02840c9325/devhelm-1.3.0.tar.gz", hash = "sha256:fb270af6919bf6bc426de850b0bbd6251ec72a061f6d3170a79e2ead271283cb", size = 261987, upload-time = "2026-06-10T14:00:21.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/63/fbe3bddafc8feb743378a47f9426631ff4def731320c37dbf35a31e8aefe/devhelm-1.1.0-py3-none-any.whl", hash = "sha256:45fcf3e31424bb87e8d56784deeb862b7015dbd84af8cdcb1852fdfd3130ee7b", size = 81109, upload-time = "2026-05-11T20:03:41.927Z" }, + { url = "https://files.pythonhosted.org/packages/c5/62/261bc0a6551d8098670e755e4a44a74ea8c032edda472364d9a6bd73992c/devhelm-1.3.0-py3-none-any.whl", hash = "sha256:19fac4faad7af71e384ebcb7afd63c8198ad03367c411f88769ac81a1bcbdea2", size = 88194, upload-time = "2026-06-10T14:00:20.547Z" }, ] [[package]] name = "devhelm-mcp-server" -version = "1.0.2" +version = "1.1.0" source = { editable = "." } dependencies = [ { name = "devhelm" }, @@ -419,7 +419,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "devhelm", specifier = ">=1.0.0" }, + { name = "devhelm", specifier = ">=1.3.0" }, { name = "fastmcp", specifier = ">=2.0.0" }, ]