rootly-mcp-server 2.2.10__tar.gz → 2.2.12__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 (84) hide show
  1. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.github/workflows/ci.yml +3 -0
  2. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.github/workflows/pypi-release.yml +4 -1
  3. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.gitignore +2 -1
  4. rootly_mcp_server-2.2.12/CHANGELOG.md +153 -0
  5. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/PKG-INFO +27 -3
  6. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/README.md +25 -1
  7. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/pyproject.toml +2 -2
  8. rootly_mcp_server-2.2.12/src/rootly_mcp_server/code_mode.py +165 -0
  9. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/tools/incidents.py +100 -2
  10. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/tools/oncall.py +117 -21
  11. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/transport.py +228 -0
  12. rootly_mcp_server-2.2.12/tests/unit/test_code_mode_module.py +159 -0
  13. rootly_mcp_server-2.2.12/tests/unit/test_oncall_handoff.py +475 -0
  14. rootly_mcp_server-2.2.12/tests/unit/test_tools.py +286 -0
  15. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_transport_module.py +126 -0
  16. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/uv.lock +56 -19
  17. rootly_mcp_server-2.2.10/.beads/issues.jsonl +0 -6
  18. rootly_mcp_server-2.2.10/src/rootly_mcp_server/code_mode.py +0 -91
  19. rootly_mcp_server-2.2.10/src/rootly_mcp_server/texttest.json +0 -3178
  20. rootly_mcp_server-2.2.10/tests/unit/test_code_mode_module.py +0 -74
  21. rootly_mcp_server-2.2.10/tests/unit/test_oncall_handoff.py +0 -69
  22. rootly_mcp_server-2.2.10/tests/unit/test_tools.py +0 -93
  23. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.dockerignore +0 -0
  24. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.gitattributes +0 -0
  25. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.github/dependabot.yml +0 -0
  26. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.github/workflows/dependency-review.yml +0 -0
  27. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.semaphore/deploy-staging.yml +0 -0
  28. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.semaphore/deploy.yml +0 -0
  29. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.semaphore/semaphore.yml +0 -0
  30. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/.semaphore/update-task-definition.sh +0 -0
  31. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/CONTRIBUTING.md +0 -0
  32. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/Dockerfile +0 -0
  33. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/LICENSE +0 -0
  34. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/examples/skills/README.md +0 -0
  35. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/gemini-extension.json +0 -0
  36. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/rootly-mcp-server-demo.gif +0 -0
  37. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/rootly_openapi.json +0 -0
  38. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/scripts/setup-hooks.sh +0 -0
  39. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/server.json +0 -0
  40. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/__init__.py +0 -0
  41. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/__main__.py +0 -0
  42. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/client.py +0 -0
  43. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/data/__init__.py +0 -0
  44. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/exceptions.py +0 -0
  45. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/legacy_server.py +0 -0
  46. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/mcp_error.py +0 -0
  47. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/monitoring.py +0 -0
  48. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/och_client.py +0 -0
  49. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/pagination.py +0 -0
  50. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/payload_stripping.py +0 -0
  51. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/security.py +0 -0
  52. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/server.py +0 -0
  53. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/server_defaults.py +0 -0
  54. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/smart_utils.py +0 -0
  55. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/spec_transform.py +0 -0
  56. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/tools/__init__.py +0 -0
  57. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/tools/alerts.py +0 -0
  58. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/tools/resources.py +0 -0
  59. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/utils.py +0 -0
  60. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/src/rootly_mcp_server/validators.py +0 -0
  61. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/README.md +0 -0
  62. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/conftest.py +0 -0
  63. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/integration/local/test_basic.py +0 -0
  64. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/integration/local/test_smart_tools.py +0 -0
  65. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/integration/remote/test_essential.py +0 -0
  66. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/test_client.py +0 -0
  67. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_alert_stripping.py +0 -0
  68. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_authentication.py +0 -0
  69. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_exceptions.py +0 -0
  70. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_http_headers.py +0 -0
  71. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_legacy_server_module.py +0 -0
  72. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_main_transport.py +0 -0
  73. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_mcp_error_module.py +0 -0
  74. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_och_client.py +0 -0
  75. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_oncall_metrics.py +0 -0
  76. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_oncall_new_tools.py +0 -0
  77. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_oncall_severity_normalization.py +0 -0
  78. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_payload_stripping_module.py +0 -0
  79. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_security.py +0 -0
  80. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_server.py +0 -0
  81. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_server_defaults_module.py +0 -0
  82. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_smart_utils.py +0 -0
  83. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_utils.py +0 -0
  84. {rootly_mcp_server-2.2.10 → rootly_mcp_server-2.2.12}/tests/unit/test_validators.py +0 -0
@@ -6,6 +6,9 @@ on:
6
6
  pull_request:
7
7
  branches: [main, staging]
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  concurrency:
10
13
  group: ci-${{ github.ref }}
11
14
  cancel-in-progress: true
@@ -5,6 +5,9 @@ on:
5
5
  tags:
6
6
  - '*'
7
7
 
8
+ permissions:
9
+ contents: read
10
+
8
11
  jobs:
9
12
  build-and-publish:
10
13
  runs-on: ubuntu-latest
@@ -31,4 +34,4 @@ jobs:
31
34
  TWINE_USERNAME: __token__
32
35
  TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
33
36
  run: |
34
- twine upload dist/*
37
+ twine upload dist/*
@@ -193,5 +193,6 @@ test_output/
193
193
  *.md
194
194
  !README.md
195
195
  !CONTRIBUTING.md
196
- !CHANGELOG.md.beads/
196
+ !CHANGELOG.md
197
+ .beads/
197
198
  .mcp.json
@@ -0,0 +1,153 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Rootly MCP Server will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Gemini CLI extension support via `gemini-extension.json` manifest
12
+ - Gemini CLI installation instructions in README
13
+
14
+ ## [2.1.1] - 2026-02-04
15
+
16
+ ### Fixed
17
+ - Fixed parameter transformation bug where filter parameters (e.g., `filter_status`, `filter_services`) were not being transformed back to their API format (`filter[status]`, `filter[services]`) when making requests to the Rootly API
18
+ - Root cause: The inner httpx client was being passed to FastMCP instead of the AuthenticatedHTTPXClient wrapper, bypassing the `_transform_params` method
19
+ - Thanks to @smoya for reporting this issue in PR #29
20
+
21
+ ## [2.1.0] - 2026-01-27
22
+
23
+ ### Added
24
+
25
+ #### Security Improvements
26
+ - Comprehensive security module (`security.py`) with:
27
+ - API token validation (prevents invalid/short tokens)
28
+ - HTTPS enforcement for all API calls (rejects HTTP URLs)
29
+ - Input sanitization (SQL injection and XSS prevention)
30
+ - Rate limiting using token bucket algorithm (default: 100 req/min)
31
+ - Error message sanitization (removes stack traces and file paths)
32
+ - Sensitive data masking for logs (tokens, passwords, secrets)
33
+ - URL validation with allowed domain checking
34
+
35
+ #### Exception Handling
36
+ - Custom exception hierarchy (`exceptions.py`) with 11 specific exception types:
37
+ - `RootlyAuthenticationError` - 401 authentication failures
38
+ - `RootlyAuthorizationError` - 403 access denied
39
+ - `RootlyNetworkError` - Network/connection issues
40
+ - `RootlyTimeoutError` - Request timeouts
41
+ - `RootlyValidationError` - Input validation failures
42
+ - `RootlyRateLimitError` - Rate limit exceeded (with retry_after)
43
+ - `RootlyAPIError` - Generic API errors
44
+ - `RootlyServerError` - 5xx server errors
45
+ - `RootlyClientError` - 4xx client errors
46
+ - `RootlyConfigurationError` - Missing/invalid configuration
47
+ - `RootlyResourceNotFoundError` - 404 not found
48
+ - Automatic exception categorization with `categorize_exception()`
49
+
50
+ #### Input Validation
51
+ - Input validation utilities (`validators.py`) with:
52
+ - Positive integer validation
53
+ - String validation with length and pattern checks
54
+ - Dictionary validation with required keys
55
+ - Enum value validation
56
+ - Pagination parameter validation
57
+
58
+ #### Monitoring & Observability
59
+ - Structured JSON logging with correlation IDs (`monitoring.py`)
60
+ - Request metrics tracking:
61
+ - Request counts by endpoint and status code
62
+ - Response latency percentiles (p50, p95, p99)
63
+ - Error rate tracking by type
64
+ - Active connection monitoring
65
+ - Health check support with `get_health_status()`
66
+ - Request/response logging decorator (automatically sanitizes sensitive data)
67
+ - Context manager for tracking request metrics
68
+
69
+ #### Helper Utilities
70
+ - Pagination helpers (`pagination.py`):
71
+ - Async pagination across multiple pages
72
+ - Pagination parameter building for Rootly API
73
+ - Pagination metadata extraction
74
+
75
+ #### Testing Infrastructure
76
+ - 66 comprehensive unit tests (100% passing)
77
+ - Test coverage >90% for all new modules
78
+ - Security-focused tests:
79
+ - SQL injection prevention
80
+ - XSS prevention
81
+ - Rate limiting behavior
82
+ - Token validation
83
+ - HTTPS enforcement
84
+ - Error message sanitization
85
+
86
+ #### CI/CD Pipeline
87
+ - GitHub Actions workflow (`.github/workflows/ci.yml`) with:
88
+ - Automated testing on Python 3.10, 3.11, 3.12
89
+ - Code coverage reporting (Codecov integration)
90
+ - Automated linting (ruff, black, isort, mypy)
91
+ - Security scanning (bandit, safety)
92
+ - Automated package building
93
+ - Runs on every push and pull request
94
+
95
+ ### Changed
96
+
97
+ #### Security Enhancements
98
+ - **BREAKING SECURITY FIX**: Removed all API token logging from `__main__.py` (line 100, 116)
99
+ - Changed from: `logger.debug(f"Token starts with: {api_token[:5]}...")`
100
+ - Changed to: `logger.info("ROOTLY_API_TOKEN is configured")`
101
+ - **SECURITY**: Updated `client.py` to use structured logging without exposing tokens
102
+ - **SECURITY**: All error messages now sanitized to remove stack traces
103
+ - Replaced generic `except Exception` with specific exception types in:
104
+ - `__main__.py` - Now catches `RootlyConfigurationError`, `RootlyMCPError`
105
+ - `client.py` - Now catches specific HTTP errors and categorizes them
106
+
107
+ #### API Client Improvements
108
+ - `RootlyClient.make_request()` now raises specific exceptions instead of returning JSON errors
109
+ - Added HTTPS enforcement to base URL validation
110
+ - Added 30-second timeout to all requests (already existed, now enforced everywhere)
111
+ - Better error categorization for HTTP status codes (401, 403, 404, 429, 4xx, 5xx)
112
+
113
+ #### Configuration Validation
114
+ - API token now validated on startup with `validate_api_token()`
115
+ - Better error messages for missing or invalid configuration
116
+
117
+ ### Fixed
118
+
119
+ - Security vulnerability: API tokens no longer logged (even partially)
120
+ - Security vulnerability: Stack traces no longer exposed in error responses
121
+ - Security vulnerability: HTTP URLs now rejected (HTTPS enforced)
122
+ - Generic exception handling replaced with specific exception types
123
+ - Error messages now user-friendly (sanitized of internal details)
124
+
125
+ ### Documentation
126
+
127
+ - Added `IMPLEMENTATION_REPORT.md` - Detailed implementation summary
128
+ - Added `GPT4O_REVIEW.md` - External review of improvements
129
+ - Added `IMPLEMENTATION_CHECKLIST.md` - Implementation progress tracking
130
+ - Updated `IMPROVEMENT_PLAN.md` with GPT-4o recommendations
131
+ - All new modules have comprehensive docstrings
132
+ - Updated package docstring with new features
133
+
134
+ ### Technical Details
135
+
136
+ - **Lines of Code Added**: ~1,500 lines production code, ~500 lines test code
137
+ - **Test Coverage**: >90% for new modules
138
+ - **Tests Passing**: 66/66 (100%)
139
+ - **Security Issues Fixed**: 6 critical vulnerabilities
140
+ - **Breaking Changes**: 0 (fully backward compatible)
141
+
142
+ ### Backward Compatibility
143
+
144
+ All changes are backward compatible:
145
+ - Existing API unchanged
146
+ - New modules are additive
147
+ - Exception hierarchy maintains base `Exception` compatibility
148
+ - Client behavior unchanged from external perspective
149
+ - No migration required for existing users
150
+
151
+ ## [2.0.15] - Previous Release
152
+
153
+ (Previous changelog entries would go here)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.2.10
3
+ Version: 2.2.12
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
@@ -30,7 +30,7 @@ Requires-Dist: requests==2.32.5
30
30
  Requires-Dist: scikit-learn>=1.3.0
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: bandit>=1.7.0; extra == 'dev'
33
- Requires-Dist: black>=23.0.0; extra == 'dev'
33
+ Requires-Dist: black>=26.3.1; extra == 'dev'
34
34
  Requires-Dist: isort>=5.0.0; extra == 'dev'
35
35
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
36
36
  Requires-Dist: pip-audit>=2.7.0; extra == 'dev'
@@ -55,6 +55,8 @@ The fastest way to get started is to connect to our hosted MCP server — no ins
55
55
 
56
56
  ### Claude Code
57
57
 
58
+ **Hosted Server (Recommended)**
59
+
58
60
  ```bash
59
61
  claude mcp add --transport http rootly https://mcp.rootly.com/mcp \
60
62
  --header "Authorization: Bearer YOUR_ROOTLY_API_TOKEN"
@@ -74,6 +76,26 @@ claude mcp add --transport sse rootly-sse https://mcp.rootly.com/sse \
74
76
  --header "Authorization: Bearer YOUR_ROOTLY_API_TOKEN"
75
77
  ```
76
78
 
79
+ **Manual Configuration**
80
+
81
+ Alternatively, create `.mcp.json` in your project root:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "rootly": {
87
+ "type": "sse",
88
+ "url": "https://mcp.rootly.com/sse",
89
+ "headers": {
90
+ "Authorization": "Bearer YOUR_ROOTLY_API_TOKEN"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ Then restart Claude Code so it reloads the updated configuration.
98
+
77
99
  ### Gemini CLI
78
100
 
79
101
  Install the extension:
@@ -295,7 +317,7 @@ docker run -p 8000:8000 \
295
317
 
296
318
  ## Supported Tools
297
319
 
298
- The default server configuration currently exposes **99 tools** (including custom agentic tools and OpenAPI-generated tools).
320
+ The default server configuration currently exposes **101 tools** (including custom agentic tools and OpenAPI-generated tools).
299
321
 
300
322
  ### Custom Agentic Tools
301
323
 
@@ -303,6 +325,7 @@ The default server configuration currently exposes **99 tools** (including custo
303
325
  - `check_responder_availability`
304
326
  - `create_override_recommendation`
305
327
  - `find_related_incidents`
328
+ - `getIncident` - retrieve a single incident for direct verification, including PIR-related fields
306
329
  - `get_alert_by_short_id`
307
330
  - `get_oncall_handoff_summary`
308
331
  - `get_oncall_schedule_summary`
@@ -313,6 +336,7 @@ The default server configuration currently exposes **99 tools** (including custo
313
336
  - `list_shifts`
314
337
  - `search_incidents`
315
338
  - `suggest_solutions`
339
+ - `updateIncident` - scoped incident update tool for `summary` and `retrospective_progress_status`
316
340
 
317
341
  ### OpenAPI-Generated Tools
318
342
 
@@ -16,6 +16,8 @@ The fastest way to get started is to connect to our hosted MCP server — no ins
16
16
 
17
17
  ### Claude Code
18
18
 
19
+ **Hosted Server (Recommended)**
20
+
19
21
  ```bash
20
22
  claude mcp add --transport http rootly https://mcp.rootly.com/mcp \
21
23
  --header "Authorization: Bearer YOUR_ROOTLY_API_TOKEN"
@@ -35,6 +37,26 @@ claude mcp add --transport sse rootly-sse https://mcp.rootly.com/sse \
35
37
  --header "Authorization: Bearer YOUR_ROOTLY_API_TOKEN"
36
38
  ```
37
39
 
40
+ **Manual Configuration**
41
+
42
+ Alternatively, create `.mcp.json` in your project root:
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "rootly": {
48
+ "type": "sse",
49
+ "url": "https://mcp.rootly.com/sse",
50
+ "headers": {
51
+ "Authorization": "Bearer YOUR_ROOTLY_API_TOKEN"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ Then restart Claude Code so it reloads the updated configuration.
59
+
38
60
  ### Gemini CLI
39
61
 
40
62
  Install the extension:
@@ -256,7 +278,7 @@ docker run -p 8000:8000 \
256
278
 
257
279
  ## Supported Tools
258
280
 
259
- The default server configuration currently exposes **99 tools** (including custom agentic tools and OpenAPI-generated tools).
281
+ The default server configuration currently exposes **101 tools** (including custom agentic tools and OpenAPI-generated tools).
260
282
 
261
283
  ### Custom Agentic Tools
262
284
 
@@ -264,6 +286,7 @@ The default server configuration currently exposes **99 tools** (including custo
264
286
  - `check_responder_availability`
265
287
  - `create_override_recommendation`
266
288
  - `find_related_incidents`
289
+ - `getIncident` - retrieve a single incident for direct verification, including PIR-related fields
267
290
  - `get_alert_by_short_id`
268
291
  - `get_oncall_handoff_summary`
269
292
  - `get_oncall_schedule_summary`
@@ -274,6 +297,7 @@ The default server configuration currently exposes **99 tools** (including custo
274
297
  - `list_shifts`
275
298
  - `search_incidents`
276
299
  - `suggest_solutions`
300
+ - `updateIncident` - scoped incident update tool for `summary` and `retrospective_progress_status`
277
301
 
278
302
  ### OpenAPI-Generated Tools
279
303
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.2.10"
3
+ version = "2.2.12"
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"
@@ -70,7 +70,7 @@ rootly-mcp-server = "rootly_mcp_server.__main__:main"
70
70
 
71
71
  [project.optional-dependencies]
72
72
  dev = [
73
- "black>=23.0.0",
73
+ "black>=26.3.1",
74
74
  "isort>=5.0.0",
75
75
  "mypy>=1.0.0",
76
76
  "types-requests>=2.32.0",
@@ -0,0 +1,165 @@
1
+ """Code Mode helpers for exposing a third MCP endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import os
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from fastmcp.experimental.transforms.code_mode import (
11
+ CodeMode,
12
+ GetSchemas,
13
+ GetTags,
14
+ ListTools,
15
+ MontySandboxProvider,
16
+ Search,
17
+ _ensure_async,
18
+ )
19
+
20
+ from .server import create_rootly_mcp_server
21
+
22
+ if TYPE_CHECKING:
23
+ from fastmcp import FastMCP
24
+
25
+
26
+ DEFAULT_CODE_MODE_PATH = "/mcp-codemode"
27
+
28
+
29
+ def _normalize_http_path(path: str) -> str:
30
+ """Normalize hosted HTTP path values for reliable comparisons."""
31
+ if not path:
32
+ return "/"
33
+ normalized = path if path.startswith("/") else f"/{path}"
34
+ if len(normalized) > 1:
35
+ normalized = normalized.rstrip("/")
36
+ return normalized
37
+
38
+
39
+ def normalize_code_mode_path(path: str) -> str:
40
+ """Normalize a hosted Code Mode path value."""
41
+ return _normalize_http_path(path)
42
+
43
+
44
+ def code_mode_enabled_from_env(default: bool = True) -> bool:
45
+ """Return whether hosted Code Mode exposure is enabled.
46
+
47
+ Code Mode defaults on for hosted dual-transport deployments unless explicitly disabled.
48
+ """
49
+ raw = os.getenv("ROOTLY_CODE_MODE_ENABLED")
50
+ if raw is None:
51
+ return default
52
+ return raw.lower() in ("1", "true", "yes")
53
+
54
+
55
+ def code_mode_path_from_env() -> str:
56
+ """Return the configured hosted Code Mode path."""
57
+ return normalize_code_mode_path(os.getenv("ROOTLY_CODE_MODE_PATH", DEFAULT_CODE_MODE_PATH))
58
+
59
+
60
+ class CompatibleMontySandboxProvider(MontySandboxProvider):
61
+ """Monty sandbox provider that tolerates older constructor signatures.
62
+
63
+ Some deployed environments can end up with a Monty runtime that supports
64
+ ``run_monty_async(..., external_functions=...)`` but still rejects the
65
+ newer ``Monty(..., external_functions=[...])`` constructor argument. This
66
+ provider falls back to the older constructor form so Code Mode execution
67
+ continues to work during mixed-version rollouts.
68
+ """
69
+
70
+ @staticmethod
71
+ def _build_monty_runner(
72
+ pydantic_monty: Any,
73
+ code: str,
74
+ *,
75
+ input_names: list[str],
76
+ external_function_names: list[str],
77
+ ) -> Any:
78
+ try:
79
+ return pydantic_monty.Monty(
80
+ code,
81
+ inputs=input_names,
82
+ external_functions=external_function_names,
83
+ )
84
+ except TypeError as exc:
85
+ if "external_functions" not in str(exc):
86
+ raise
87
+ return pydantic_monty.Monty(code, inputs=input_names)
88
+
89
+ async def run(
90
+ self,
91
+ code: str,
92
+ *,
93
+ inputs: dict[str, Any] | None = None,
94
+ external_functions: dict[str, Callable[..., Any]] | None = None,
95
+ ) -> Any:
96
+ try:
97
+ pydantic_monty = importlib.import_module("pydantic_monty")
98
+ except ModuleNotFoundError as exc:
99
+ raise ImportError(
100
+ "CodeMode requires pydantic-monty for the Monty sandbox provider. "
101
+ "Install it with `fastmcp[code-mode]` or pass a custom SandboxProvider."
102
+ ) from exc
103
+
104
+ inputs = inputs or {}
105
+ async_functions = {
106
+ key: _ensure_async(value)
107
+ for key, value in (external_functions or {}).items()
108
+ }
109
+
110
+ monty = self._build_monty_runner(
111
+ pydantic_monty,
112
+ code,
113
+ input_names=list(inputs.keys()),
114
+ external_function_names=list(async_functions.keys()),
115
+ )
116
+ run_kwargs: dict[str, Any] = {"external_functions": async_functions}
117
+ if inputs:
118
+ run_kwargs["inputs"] = inputs
119
+ if self.limits is not None:
120
+ run_kwargs["limits"] = self.limits
121
+ return await pydantic_monty.run_monty_async(monty, **run_kwargs)
122
+
123
+
124
+ def build_code_mode_transform() -> CodeMode:
125
+ """Build the shared Code Mode transform used by hosted deployments."""
126
+ return CodeMode(
127
+ sandbox_provider=CompatibleMontySandboxProvider(),
128
+ discovery_tools=[
129
+ ListTools(default_detail="brief"),
130
+ Search(name="tool_search", default_detail="detailed", default_limit=12),
131
+ GetSchemas(default_detail="detailed"),
132
+ GetTags(default_detail="brief"),
133
+ ],
134
+ execute_description=(
135
+ "Write a short async Python block and chain await call_tool(name, params) calls "
136
+ "to complete a Rootly workflow. Use tool_search only to discover tools and their "
137
+ "schemas, then call the actual Rootly tool with the exact parameter names shown in "
138
+ "the schema. For paginated data tools, pass pagination arguments like page_size, "
139
+ "page_number, and max_results exactly as documented by that tool instead of inventing "
140
+ "alternatives like per_page. Prefer Rootly's higher-level custom tools when they fit "
141
+ "the task, then fall back to lower-level API tools as needed. Example: "
142
+ "await call_tool('search_incidents', {'query': '', 'page_size': 1, 'page_number': 1, "
143
+ "'max_results': 1}). Use return to emit the final result."
144
+ ),
145
+ )
146
+
147
+
148
+ def create_rootly_codemode_server(
149
+ swagger_path: str | None = None,
150
+ name: str = "Rootly Code Mode",
151
+ allowed_paths: list[str] | None = None,
152
+ hosted: bool = False,
153
+ base_url: str | None = None,
154
+ ) -> FastMCP:
155
+ """Create a Rootly MCP server instance wrapped with Code Mode."""
156
+ mcp: FastMCP = create_rootly_mcp_server(
157
+ swagger_path=swagger_path,
158
+ name=name,
159
+ allowed_paths=allowed_paths,
160
+ hosted=hosted,
161
+ base_url=base_url,
162
+ transport="streamable-http",
163
+ )
164
+ mcp.add_transform(build_code_mode_transform())
165
+ return mcp
@@ -14,6 +14,11 @@ MakeAuthenticatedRequest = Callable[..., Awaitable[Any]]
14
14
  StripHeavyNestedData = Callable[[JsonDict], JsonDict]
15
15
  GenerateRecommendation = Callable[[JsonDict], str]
16
16
 
17
+ RETROSPECTIVE_PROGRESS_STATUSES = ("not_started", "active", "completed", "skipped")
18
+ INCIDENT_SEARCH_FIELDS = (
19
+ "id,title,summary,status,created_at,updated_at,url,started_at,retrospective_progress_status"
20
+ )
21
+
17
22
 
18
23
  def register_incident_tools(
19
24
  mcp: Any,
@@ -60,7 +65,7 @@ def register_incident_tools(
60
65
  "page[size]": page_size, # Use requested page size (already limited to max 20)
61
66
  "page[number]": page_number,
62
67
  "include": "",
63
- "fields[incidents]": "id,title,summary,status,created_at,updated_at,url,started_at",
68
+ "fields[incidents]": INCIDENT_SEARCH_FIELDS,
64
69
  }
65
70
  if query:
66
71
  params["filter[search]"] = query
@@ -85,7 +90,7 @@ def register_incident_tools(
85
90
  "page[size]": effective_page_size,
86
91
  "page[number]": current_page,
87
92
  "include": "",
88
- "fields[incidents]": "id,title,summary,status,created_at,updated_at,url,started_at",
93
+ "fields[incidents]": INCIDENT_SEARCH_FIELDS,
89
94
  }
90
95
  if query:
91
96
  params["filter[search]"] = query
@@ -155,6 +160,99 @@ def register_incident_tools(
155
160
  error_type, error_message = mcp_error.categorize_error(e)
156
161
  return cast(JsonDict, mcp_error.tool_error(error_message, error_type))
157
162
 
163
+ @mcp.tool(name="getIncident")
164
+ async def get_incident(
165
+ incident_id: Annotated[str, Field(description="Incident ID to retrieve")],
166
+ ) -> JsonDict:
167
+ """Retrieve a single incident with PIR-related fields for direct verification."""
168
+ try:
169
+ response = await make_authenticated_request("GET", f"/v1/incidents/{incident_id}")
170
+ response.raise_for_status()
171
+
172
+ response_data = response.json()
173
+ if isinstance(response_data.get("data"), dict):
174
+ stripped = strip_heavy_nested_data({"data": [response_data["data"]]})
175
+ response_data["data"] = stripped["data"][0]
176
+ return cast(JsonDict, response_data)
177
+ except Exception as e:
178
+ error_type, error_message = mcp_error.categorize_error(e)
179
+ return cast(
180
+ JsonDict,
181
+ mcp_error.tool_error(
182
+ f"Failed to retrieve incident: {error_message}",
183
+ error_type,
184
+ ),
185
+ )
186
+
187
+ @mcp.tool(name="updateIncident")
188
+ async def update_incident(
189
+ incident_id: Annotated[str, Field(description="Incident ID to update")],
190
+ retrospective_progress_status: Annotated[
191
+ str | None,
192
+ Field(
193
+ description="Retrospective/PIR status: one of not_started, active, completed, skipped"
194
+ ),
195
+ ] = None,
196
+ summary: Annotated[
197
+ str | None,
198
+ Field(description="Updated incident summary"),
199
+ ] = None,
200
+ ) -> JsonDict:
201
+ """Update scoped incident fields for PIR lifecycle automation."""
202
+ attributes: dict[str, Any] = {}
203
+
204
+ if retrospective_progress_status is not None:
205
+ if retrospective_progress_status not in RETROSPECTIVE_PROGRESS_STATUSES:
206
+ allowed = ", ".join(RETROSPECTIVE_PROGRESS_STATUSES)
207
+ return cast(
208
+ JsonDict,
209
+ mcp_error.tool_error(
210
+ f"retrospective_progress_status must be one of: {allowed}",
211
+ "validation_error",
212
+ ),
213
+ )
214
+ attributes["retrospective_progress_status"] = retrospective_progress_status
215
+
216
+ if summary is not None:
217
+ attributes["summary"] = summary
218
+
219
+ if not attributes:
220
+ return cast(
221
+ JsonDict,
222
+ mcp_error.tool_error(
223
+ "Must provide at least one of retrospective_progress_status or summary",
224
+ "validation_error",
225
+ ),
226
+ )
227
+
228
+ payload = {
229
+ "data": {
230
+ "type": "incidents",
231
+ "attributes": attributes,
232
+ }
233
+ }
234
+
235
+ try:
236
+ response = await make_authenticated_request(
237
+ "PUT", f"/v1/incidents/{incident_id}", json=payload
238
+ )
239
+ response.raise_for_status()
240
+
241
+ response_data = response.json()
242
+ if isinstance(response_data.get("data"), dict):
243
+ stripped = strip_heavy_nested_data({"data": [response_data["data"]]})
244
+ response_data["data"] = stripped["data"][0]
245
+ return cast(JsonDict, response_data)
246
+ except Exception as e:
247
+ error_type, error_message = mcp_error.categorize_error(e)
248
+ return cast(
249
+ JsonDict,
250
+ mcp_error.tool_error(
251
+ f"Failed to update incident: {error_message}",
252
+ error_type,
253
+ ),
254
+ )
255
+
158
256
  @mcp.tool()
159
257
  async def find_related_incidents(
160
258
  incident_id: str = "",