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.
Files changed (86) hide show
  1. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/workflows/dependency-review.yml +1 -1
  2. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.gitignore +1 -0
  3. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/CHANGELOG.md +36 -0
  4. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/PKG-INFO +3 -3
  5. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/pyproject.toml +3 -3
  6. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/server.py +50 -11
  7. rootly_mcp_server-2.3.8/src/rootly_mcp_server/tools/alerts.py +87 -0
  8. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/tools/oncall.py +280 -310
  9. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/transport.py +100 -1
  10. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/utils.py +31 -0
  11. rootly_mcp_server-2.3.8/tests/unit/test_alert_tools.py +135 -0
  12. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_oncall_handoff.py +364 -0
  13. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_transport_module.py +127 -3
  14. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/uv.lock +10 -10
  15. rootly_mcp_server-2.3.7/src/rootly_mcp_server/tools/alerts.py +0 -105
  16. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.dockerignore +0 -0
  17. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.gitattributes +0 -0
  18. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/dependabot.yml +0 -0
  19. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/openapi-audit-remote-baseline.json +0 -0
  20. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/workflows/ci.yml +0 -0
  21. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/workflows/openapi-audit.yml +0 -0
  22. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.github/workflows/pypi-release.yml +0 -0
  23. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.mcp.json +0 -0
  24. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.semaphore/deploy-staging.yml +0 -0
  25. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.semaphore/deploy.yml +0 -0
  26. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.semaphore/semaphore.yml +0 -0
  27. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/.semaphore/update-task-definition.sh +0 -0
  28. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/CONTRIBUTING.md +0 -0
  29. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/Dockerfile +0 -0
  30. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/LICENSE +0 -0
  31. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/README.md +0 -0
  32. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/examples/skills/README.md +0 -0
  33. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/gemini-extension.json +0 -0
  34. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/rootly-mcp-server-demo.gif +0 -0
  35. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/rootly_openapi.json +0 -0
  36. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/scripts/audit_openapi.py +0 -0
  37. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/scripts/setup-hooks.sh +0 -0
  38. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/server.json +0 -0
  39. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/__init__.py +0 -0
  40. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/__main__.py +0 -0
  41. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/audit.py +0 -0
  42. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/client.py +0 -0
  43. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/code_mode.py +0 -0
  44. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/data/__init__.py +0 -0
  45. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/exceptions.py +0 -0
  46. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/legacy_server.py +0 -0
  47. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/mcp_error.py +0 -0
  48. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/monitoring.py +0 -0
  49. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/och_client.py +0 -0
  50. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/pagination.py +0 -0
  51. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/payload_stripping.py +0 -0
  52. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/security.py +0 -0
  53. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/server_defaults.py +0 -0
  54. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/smart_utils.py +0 -0
  55. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/spec_transform.py +0 -0
  56. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/tools/__init__.py +0 -0
  57. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/tools/incidents.py +0 -0
  58. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/tools/resources.py +0 -0
  59. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/src/rootly_mcp_server/validators.py +0 -0
  60. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/README.md +0 -0
  61. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/conftest.py +0 -0
  62. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/integration/local/test_basic.py +0 -0
  63. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/integration/local/test_smart_tools.py +0 -0
  64. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/integration/local/test_tool_allowlists.py +0 -0
  65. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/integration/remote/test_essential.py +0 -0
  66. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/test_client.py +0 -0
  67. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_alert_stripping.py +0 -0
  68. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_authentication.py +0 -0
  69. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_code_mode_module.py +0 -0
  70. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_exceptions.py +0 -0
  71. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_http_headers.py +0 -0
  72. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_legacy_server_module.py +0 -0
  73. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_main_transport.py +0 -0
  74. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_mcp_error_module.py +0 -0
  75. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_och_client.py +0 -0
  76. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_oncall_metrics.py +0 -0
  77. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_oncall_new_tools.py +0 -0
  78. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_oncall_severity_normalization.py +0 -0
  79. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_payload_stripping_module.py +0 -0
  80. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_security.py +0 -0
  81. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_server.py +0 -0
  82. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_server_defaults_module.py +0 -0
  83. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_smart_utils.py +0 -0
  84. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_tools.py +0 -0
  85. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_utils.py +0 -0
  86. {rootly_mcp_server-2.3.7 → rootly_mcp_server-2.3.8}/tests/unit/test_validators.py +0 -0
@@ -12,4 +12,4 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
  steps:
14
14
  - name: Dependency Review
15
- uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4
15
+ uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v4
@@ -199,3 +199,4 @@ test_output/
199
199
  !CONTRIBUTING.md
200
200
  !CHANGELOG.md
201
201
  .beads/
202
+ .claude/
@@ -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.7
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.3
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.33.1
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.7"
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.33.1", # Pin to patched version
45
+ "requests==2.34.0", # Pin to patched version
46
46
  "httpx==0.28.1", # Pin to specific version
47
- "pydantic==2.13.3", # Pin to specific version
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
- raw = (auth_header or "").strip()
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
- "all",
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
+ )