rootly-mcp-server 2.3.7__tar.gz → 2.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/workflows/dependency-review.yml +1 -1
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.gitignore +1 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/CHANGELOG.md +36 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/PKG-INFO +3 -3
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/pyproject.toml +3 -3
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/server.py +50 -11
- rootly_mcp_server-2.3.8/src/rootly_mcp_server/tools/alerts.py +87 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/tools/oncall.py +280 -310
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/transport.py +100 -1
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/utils.py +31 -0
- rootly_mcp_server-2.3.8/tests/unit/test_alert_tools.py +135 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_oncall_handoff.py +364 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_transport_module.py +127 -3
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/uv.lock +10 -10
- rootly_mcp_server-2.3.7/src/rootly_mcp_server/tools/alerts.py +0 -105
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.dockerignore +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.gitattributes +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/dependabot.yml +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/openapi-audit-remote-baseline.json +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/workflows/ci.yml +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/workflows/openapi-audit.yml +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/workflows/pypi-release.yml +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.mcp.json +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.semaphore/deploy-staging.yml +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.semaphore/deploy.yml +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.semaphore/semaphore.yml +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.semaphore/update-task-definition.sh +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/CONTRIBUTING.md +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/Dockerfile +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/LICENSE +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/README.md +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/examples/skills/README.md +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/gemini-extension.json +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/rootly-mcp-server-demo.gif +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/rootly_openapi.json +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/scripts/audit_openapi.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/scripts/setup-hooks.sh +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/server.json +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/__init__.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/__main__.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/audit.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/client.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/code_mode.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/data/__init__.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/exceptions.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/legacy_server.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/mcp_error.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/monitoring.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/och_client.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/pagination.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/payload_stripping.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/security.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/server_defaults.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/smart_utils.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/spec_transform.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/tools/__init__.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/tools/incidents.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/tools/resources.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/validators.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/README.md +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/conftest.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/integration/local/test_basic.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/integration/local/test_smart_tools.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/integration/local/test_tool_allowlists.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/integration/remote/test_essential.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/test_client.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_alert_stripping.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_authentication.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_code_mode_module.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_exceptions.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_http_headers.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_legacy_server_module.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_main_transport.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_mcp_error_module.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_och_client.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_oncall_metrics.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_oncall_new_tools.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_oncall_severity_normalization.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_payload_stripping_module.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_security.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_server.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_server_defaults_module.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_smart_utils.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_tools.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_utils.py +0 -0
- {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_validators.py +0 -0
|
@@ -5,6 +5,42 @@ All notable changes to the Rootly MCP Server will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.8] - Released 2026-05-19
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **Bearer Auth Enforcement on Hosted Transports**: Required Bearer authentication on MCP transport paths in hosted mode so unauthenticated requests are rejected before any tool dispatch
|
|
13
|
+
- **Upstream Token Validation**: Validated Bearer tokens against the upstream Rootly API before granting MCP access, surfacing malformed or expired credentials at the edge with clearer errors
|
|
14
|
+
- **Dependency Advisories**: Bumped `urllib3` (≥2.7.0), `python-multipart` (≥0.0.27), and `pip` (≥26.1) to resolve five open Dependabot advisories (urllib3 CVE-2026-44431/44432, python-multipart CVE-2026-42561, pip CVE-2026-6357/3219)
|
|
15
|
+
|
|
16
|
+
### Performance
|
|
17
|
+
|
|
18
|
+
- **`get_alert_by_short_id` Now O(1)**: Replaced the up-to-20-page sequential scan with a single direct `GET /v1/alerts/{short_id}` point lookup. P95 spikes of 41s on this tool collapse to ~200ms; not-found responses no longer cost 10+ seconds; workspaces with >2,000 alerts no longer silently fail
|
|
19
|
+
- **`get_oncall_handoff_summary` Fan-Out Parallelized**: Replaced sequential per-schedule shift and incident fetches with `asyncio.gather` capped by a `Semaphore(10)`. Worst-case p95 of ~15s on populated workspaces drops to ~1–2s
|
|
20
|
+
- **Lookup-Maps Helper Parallelized**: `_fetch_users_and_schedules_maps` (used by `list_shifts`, `check_responder_availability`, `check_oncall_health_risk`, `create_override_recommendation`, `find_related_incidents`) now runs the three independent resource fetches concurrently with intra-resource page fan-out. Cold-cache worst case drops from ~15s to ~1s — meaningfully more impactful since v2.3.7 made sessions stateless and the in-memory cache is repaid more often
|
|
21
|
+
- **Shared `_fetch_all_pages` Helper Across Five Shift Tools**: `list_shifts`, `get_oncall_schedule_summary`, `check_responder_availability`, `create_override_recommendation`, and `check_oncall_health_risk` now all route through the same paginated, concurrency-capped helper instead of duplicating sequential pagination loops. Each tool saves 2–5s worst case under load
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **`get_oncall_shift_metrics` Silent Data Loss**: The function previously fetched only page 1 of schedules and teams (no pagination loop), causing team-id filtering to silently drop schedules whose teams lived past page 1. Now uses the shared paginated, cached helper so all schedules are considered
|
|
26
|
+
- **OAuth Authorization Server Discovery**: Derived the OAuth authorization server URL from the API base URL so discovery works against staging and self-hosted Rootly instances
|
|
27
|
+
- **OAuth Protected Resource Metadata**: Removed `scopes_supported` from the protected resource metadata, advertised only the granular scopes the resource actually accepts, and included resource-specific scopes for clients that key off them
|
|
28
|
+
- **Pagination Resilience**: `asyncio.gather` callsites that fan out pages now use `return_exceptions=True` with an `isinstance(_, BaseException)` guard so a single transient httpx error on one page no longer aborts the entire handler
|
|
29
|
+
- **Missing `total_pages` Metadata Fallback**: When upstream omits `meta.total_pages`, the helper now falls back to fetching up to `max_pages` (preserving the legacy "keep going until a short page" semantics) instead of silently truncating to page 1
|
|
30
|
+
|
|
31
|
+
### Dependencies
|
|
32
|
+
|
|
33
|
+
- **Routine Refresh**: Pulled in the latest minor and patch dependency updates from Dependabot
|
|
34
|
+
- **CI Action Bump**: Updated `actions/dependency-review-action` from 4.9.0 to 5.0.0
|
|
35
|
+
|
|
36
|
+
### Repo
|
|
37
|
+
|
|
38
|
+
- **`.claude/` Ignored**: Stopped tracking transient `.claude/worktrees/` directories and added `.claude/` to `.gitignore` so local Claude Code state no longer pollutes diffs
|
|
39
|
+
|
|
40
|
+
### Testing
|
|
41
|
+
|
|
42
|
+
- New unit tests for the parallelized fan-out paths, the missing-`total_pages` fallback, exception-tolerance in `asyncio.gather`, the `get_oncall_shift_metrics` pagination regression, and the direct-GET path for `get_alert_by_short_id`
|
|
43
|
+
|
|
8
44
|
## [2.3.7] - Released 2026-05-08
|
|
9
45
|
|
|
10
46
|
### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rootly-mcp-server
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.8
|
|
4
4
|
Summary: Secure Model Context Protocol server for Rootly APIs with AI SRE capabilities, comprehensive error handling, and input validation
|
|
5
5
|
Project-URL: Homepage, https://github.com/Rootly-AI-Labs/Rootly-MCP-server
|
|
6
6
|
Project-URL: Issues, https://github.com/Rootly-AI-Labs/Rootly-MCP-server/issues
|
|
@@ -27,11 +27,11 @@ Requires-Dist: cryptography>=46.0.7
|
|
|
27
27
|
Requires-Dist: fastmcp[code-mode]==3.2.4
|
|
28
28
|
Requires-Dist: httpx==0.28.1
|
|
29
29
|
Requires-Dist: numpy>=1.24.0
|
|
30
|
-
Requires-Dist: pydantic==2.13.
|
|
30
|
+
Requires-Dist: pydantic==2.13.4
|
|
31
31
|
Requires-Dist: pygments>=2.20.0
|
|
32
32
|
Requires-Dist: python-dotenv>=1.2.2
|
|
33
33
|
Requires-Dist: python-multipart>=0.0.26
|
|
34
|
-
Requires-Dist: requests==2.
|
|
34
|
+
Requires-Dist: requests==2.34.0
|
|
35
35
|
Requires-Dist: scikit-learn>=1.3.0
|
|
36
36
|
Provides-Extra: dev
|
|
37
37
|
Requires-Dist: bandit>=1.7.0; extra == 'dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rootly-mcp-server"
|
|
3
|
-
version = "2.3.
|
|
3
|
+
version = "2.3.8"
|
|
4
4
|
description = "Secure Model Context Protocol server for Rootly APIs with AI SRE capabilities, comprehensive error handling, and input validation"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -42,9 +42,9 @@ dependencies = [
|
|
|
42
42
|
"python-multipart>=0.0.26", # Override vulnerable transitive python-multipart (CVE-2026-40347)
|
|
43
43
|
"cryptography>=46.0.7", # Override vulnerable transitive cryptography
|
|
44
44
|
"Pygments>=2.20.0", # Override vulnerable transitive Pygments
|
|
45
|
-
"requests==2.
|
|
45
|
+
"requests==2.34.0", # Pin to patched version
|
|
46
46
|
"httpx==0.28.1", # Pin to specific version
|
|
47
|
-
"pydantic==2.13.
|
|
47
|
+
"pydantic==2.13.4", # Pin to specific version
|
|
48
48
|
"brotli>=1.2.0", # For Brotli compression support in httpx
|
|
49
49
|
"scikit-learn>=1.3.0", # For text similarity and ML utilities
|
|
50
50
|
"numpy>=1.24.0", # For numerical operations
|
|
@@ -27,6 +27,8 @@ from .tools.oncall import register_oncall_tools
|
|
|
27
27
|
from .tools.resources import register_resource_handlers
|
|
28
28
|
from .utils import (
|
|
29
29
|
OAUTH_PROTECTED_RESOURCE_PATH,
|
|
30
|
+
auth_header_state,
|
|
31
|
+
derive_oauth_server_url,
|
|
30
32
|
is_mcp_server_url_static,
|
|
31
33
|
resolve_mcp_server_url,
|
|
32
34
|
sanitize_parameters_in_spec,
|
|
@@ -89,15 +91,7 @@ def _fingerprint_auth_header(auth_header: str) -> str:
|
|
|
89
91
|
|
|
90
92
|
def _auth_header_state(auth_header: str) -> str:
|
|
91
93
|
"""Classify Authorization header shape without exposing token contents."""
|
|
92
|
-
|
|
93
|
-
if not raw:
|
|
94
|
-
return "missing"
|
|
95
|
-
parts = raw.split(None, 1)
|
|
96
|
-
if not parts or parts[0].lower() != "bearer":
|
|
97
|
-
return "invalid_format"
|
|
98
|
-
if len(parts) == 1 or not parts[1].strip():
|
|
99
|
-
return "missing_token"
|
|
100
|
-
return "bearer"
|
|
94
|
+
return auth_header_state(auth_header)
|
|
101
95
|
|
|
102
96
|
|
|
103
97
|
def _validate_bearer_auth_header(auth_header: str) -> str:
|
|
@@ -558,12 +552,57 @@ def create_rootly_mcp_server(
|
|
|
558
552
|
return JSONResponse(
|
|
559
553
|
{
|
|
560
554
|
"resource": mcp_server_url,
|
|
561
|
-
"authorization_servers": [base_url],
|
|
555
|
+
"authorization_servers": [derive_oauth_server_url(base_url)],
|
|
562
556
|
"scopes_supported": [
|
|
563
557
|
"openid",
|
|
564
558
|
"profile",
|
|
565
559
|
"email",
|
|
566
|
-
"
|
|
560
|
+
"ir.incidents:read",
|
|
561
|
+
"ir.incidents:write",
|
|
562
|
+
"ir.services:read",
|
|
563
|
+
"ir.services:write",
|
|
564
|
+
"ir.environments:read",
|
|
565
|
+
"ir.environments:write",
|
|
566
|
+
"ir.functionalities:read",
|
|
567
|
+
"ir.functionalities:write",
|
|
568
|
+
"ir.severities:read",
|
|
569
|
+
"ir.severities:write",
|
|
570
|
+
"ir.incident_types:read",
|
|
571
|
+
"ir.incident_types:write",
|
|
572
|
+
"ir.incident_roles:read",
|
|
573
|
+
"ir.incident_roles:write",
|
|
574
|
+
"ir.workflows:read",
|
|
575
|
+
"ir.workflows:write",
|
|
576
|
+
"ir.catalogs:read",
|
|
577
|
+
"ir.catalogs:write",
|
|
578
|
+
"ir.groups:read",
|
|
579
|
+
"ir.groups:write",
|
|
580
|
+
"ir.playbooks:read",
|
|
581
|
+
"ir.playbooks:write",
|
|
582
|
+
"ir.retrospectives:read",
|
|
583
|
+
"ir.retrospectives:write",
|
|
584
|
+
"ir.status_pages:read",
|
|
585
|
+
"ir.status_pages:write",
|
|
586
|
+
"ir.form_fields:read",
|
|
587
|
+
"ir.form_fields:write",
|
|
588
|
+
"ir.pulses:read",
|
|
589
|
+
"ir.pulses:write",
|
|
590
|
+
"oc.alerts:read",
|
|
591
|
+
"oc.alerts:write",
|
|
592
|
+
"oc.schedules:read",
|
|
593
|
+
"oc.schedules:write",
|
|
594
|
+
"oc.escalation_policies:read",
|
|
595
|
+
"oc.escalation_policies:write",
|
|
596
|
+
"oc.alert_routing_rules:read",
|
|
597
|
+
"oc.alert_routing_rules:write",
|
|
598
|
+
"oc.heartbeats:read",
|
|
599
|
+
"oc.heartbeats:write",
|
|
600
|
+
"oc.alert_sources:read",
|
|
601
|
+
"oc.alert_sources:write",
|
|
602
|
+
"oc.live_call_routing:read",
|
|
603
|
+
"oc.live_call_routing:write",
|
|
604
|
+
"oc.shift_overrides:read",
|
|
605
|
+
"oc.shift_overrides:write",
|
|
567
606
|
],
|
|
568
607
|
"bearer_methods_supported": ["header"],
|
|
569
608
|
},
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Alert tool registration for Rootly MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from typing import Annotated, Any, Protocol
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
|
|
11
|
+
JsonDict = dict[str, Any]
|
|
12
|
+
MakeAuthenticatedRequest = Callable[..., Awaitable[Any]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MCPErrorLike(Protocol):
|
|
16
|
+
"""Protocol for MCP error helpers used by alert tools."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def tool_error(
|
|
20
|
+
error_message: str,
|
|
21
|
+
error_type: str = "execution_error",
|
|
22
|
+
details: dict[str, Any] | None = None,
|
|
23
|
+
) -> JsonDict: ...
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def categorize_error(exception: Exception) -> tuple[str, str]: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_alert_tools(
|
|
30
|
+
mcp: Any,
|
|
31
|
+
make_authenticated_request: MakeAuthenticatedRequest,
|
|
32
|
+
mcp_error: MCPErrorLike,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Register alert tools on the MCP server."""
|
|
35
|
+
|
|
36
|
+
@mcp.tool()
|
|
37
|
+
async def get_alert_by_short_id(
|
|
38
|
+
short_id: Annotated[
|
|
39
|
+
str,
|
|
40
|
+
Field(
|
|
41
|
+
description="The alert short_id (e.g., 'PhIQtP') or full alert URL (e.g., 'https://rootly.com/account/alerts/PhIQtP')"
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
) -> JsonDict:
|
|
45
|
+
"""Get alert details by short_id or alert URL. Use this when a user pastes an alert URL or short_id from a pager notification and wants to investigate the alert."""
|
|
46
|
+
try:
|
|
47
|
+
alert_short_id = short_id.strip()
|
|
48
|
+
if "/" in alert_short_id:
|
|
49
|
+
alert_short_id = alert_short_id.rstrip("/").split("/")[-1]
|
|
50
|
+
|
|
51
|
+
if not alert_short_id:
|
|
52
|
+
return mcp_error.tool_error("short_id is required", "validation_error")
|
|
53
|
+
|
|
54
|
+
# GET /v1/alerts/{id} accepts the short_id as well as the UUID
|
|
55
|
+
# (undocumented but supported), so a single point lookup avoids
|
|
56
|
+
# listing/filtering altogether.
|
|
57
|
+
response = await make_authenticated_request(
|
|
58
|
+
"GET", f"/v1/alerts/{quote(alert_short_id, safe='')}"
|
|
59
|
+
)
|
|
60
|
+
if response.status_code == 404:
|
|
61
|
+
return mcp_error.tool_error(
|
|
62
|
+
f"Alert with short_id '{alert_short_id}' not found",
|
|
63
|
+
"not_found",
|
|
64
|
+
)
|
|
65
|
+
response.raise_for_status()
|
|
66
|
+
|
|
67
|
+
data = response.json().get("data", {})
|
|
68
|
+
attrs = data.get("attributes", {})
|
|
69
|
+
return {
|
|
70
|
+
"id": data.get("id"),
|
|
71
|
+
"short_id": attrs.get("short_id"),
|
|
72
|
+
"summary": attrs.get("summary"),
|
|
73
|
+
"status": attrs.get("status"),
|
|
74
|
+
"source": attrs.get("source"),
|
|
75
|
+
"description": attrs.get("description"),
|
|
76
|
+
"started_at": attrs.get("started_at"),
|
|
77
|
+
"ended_at": attrs.get("ended_at"),
|
|
78
|
+
"noise": attrs.get("noise"),
|
|
79
|
+
"url": attrs.get("url"),
|
|
80
|
+
"created_at": attrs.get("created_at"),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
error_type, error_message = mcp_error.categorize_error(e)
|
|
85
|
+
return mcp_error.tool_error(
|
|
86
|
+
f"Failed to get alert by short_id: {error_message}", error_type
|
|
87
|
+
)
|