rootly-mcp-server 2.2.19__tar.gz → 2.2.20__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 (81) hide show
  1. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.github/dependabot.yml +23 -7
  2. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.github/workflows/ci.yml +12 -12
  3. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.github/workflows/dependency-review.yml +1 -1
  4. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.github/workflows/openapi-audit.yml +4 -4
  5. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.github/workflows/pypi-release.yml +2 -2
  6. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.gitignore +4 -0
  7. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/CHANGELOG.md +15 -0
  8. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/PKG-INFO +4 -2
  9. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/pyproject.toml +4 -2
  10. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/__main__.py +9 -5
  11. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/code_mode.py +4 -6
  12. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/server.py +8 -9
  13. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/spec_transform.py +1 -2
  14. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/tools/incidents.py +23 -32
  15. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/tools/oncall.py +5 -2
  16. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/transport.py +7 -9
  17. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/integration/local/test_basic.py +3 -3
  18. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/integration/local/test_smart_tools.py +21 -21
  19. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/integration/remote/test_essential.py +6 -6
  20. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_code_mode_module.py +6 -2
  21. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_oncall_metrics.py +3 -3
  22. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_server.py +36 -19
  23. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_smart_utils.py +0 -1
  24. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_transport_module.py +21 -7
  25. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/uv.lock +27 -10
  26. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.dockerignore +0 -0
  27. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.gitattributes +0 -0
  28. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.github/openapi-audit-remote-baseline.json +0 -0
  29. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.semaphore/deploy-staging.yml +0 -0
  30. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.semaphore/deploy.yml +0 -0
  31. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.semaphore/semaphore.yml +0 -0
  32. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/.semaphore/update-task-definition.sh +0 -0
  33. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/CONTRIBUTING.md +0 -0
  34. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/Dockerfile +0 -0
  35. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/LICENSE +0 -0
  36. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/README.md +0 -0
  37. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/examples/skills/README.md +0 -0
  38. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/gemini-extension.json +0 -0
  39. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/rootly-mcp-server-demo.gif +0 -0
  40. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/rootly_openapi.json +0 -0
  41. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/scripts/audit_openapi.py +0 -0
  42. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/scripts/setup-hooks.sh +0 -0
  43. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/server.json +0 -0
  44. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/__init__.py +0 -0
  45. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/client.py +0 -0
  46. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/data/__init__.py +0 -0
  47. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/exceptions.py +0 -0
  48. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/legacy_server.py +0 -0
  49. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/mcp_error.py +0 -0
  50. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/monitoring.py +0 -0
  51. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/och_client.py +0 -0
  52. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/pagination.py +0 -0
  53. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/payload_stripping.py +0 -0
  54. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/security.py +0 -0
  55. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/server_defaults.py +0 -0
  56. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/smart_utils.py +0 -0
  57. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/tools/__init__.py +0 -0
  58. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/tools/alerts.py +0 -0
  59. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/tools/resources.py +0 -0
  60. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/utils.py +0 -0
  61. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/src/rootly_mcp_server/validators.py +0 -0
  62. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/README.md +0 -0
  63. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/conftest.py +0 -0
  64. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/test_client.py +0 -0
  65. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_alert_stripping.py +0 -0
  66. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_authentication.py +0 -0
  67. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_exceptions.py +0 -0
  68. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_http_headers.py +0 -0
  69. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_legacy_server_module.py +0 -0
  70. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_main_transport.py +0 -0
  71. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_mcp_error_module.py +0 -0
  72. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_och_client.py +0 -0
  73. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_oncall_handoff.py +0 -0
  74. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_oncall_new_tools.py +0 -0
  75. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_oncall_severity_normalization.py +0 -0
  76. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_payload_stripping_module.py +0 -0
  77. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_security.py +0 -0
  78. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_server_defaults_module.py +0 -0
  79. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_tools.py +0 -0
  80. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_utils.py +0 -0
  81. {rootly_mcp_server-2.2.19 → rootly_mcp_server-2.2.20}/tests/unit/test_validators.py +0 -0
@@ -1,35 +1,51 @@
1
1
  version: 2
2
+
2
3
  updates:
3
- - package-ecosystem: "github-actions"
4
+ - package-ecosystem: "pip"
4
5
  directory: "/"
5
6
  schedule:
6
7
  interval: "weekly"
8
+ open-pull-requests-limit: 20
7
9
  cooldown:
8
10
  default-days: 3
11
+ semver-major-days: 7
9
12
  groups:
10
- actions:
13
+ minor-and-patch:
14
+ applies-to: version-updates
15
+ update-types:
16
+ - "minor"
17
+ - "patch"
11
18
  patterns:
12
19
  - "*"
13
20
 
14
- - package-ecosystem: "pip"
21
+ - package-ecosystem: "docker"
15
22
  directory: "/"
16
23
  schedule:
17
24
  interval: "weekly"
18
- open-pull-requests-limit: 10
25
+ open-pull-requests-limit: 20
19
26
  cooldown:
20
27
  default-days: 3
21
- semver-major-days: 7
22
28
  groups:
23
29
  minor-and-patch:
24
30
  applies-to: version-updates
25
31
  update-types:
26
32
  - "minor"
27
33
  - "patch"
34
+ patterns:
35
+ - "*"
28
36
 
29
- - package-ecosystem: "docker"
37
+ - package-ecosystem: "github-actions"
30
38
  directory: "/"
31
39
  schedule:
32
40
  interval: "weekly"
41
+ open-pull-requests-limit: 20
33
42
  cooldown:
34
43
  default-days: 3
35
- semver-major-days: 7
44
+ groups:
45
+ minor-and-patch:
46
+ applies-to: version-updates
47
+ update-types:
48
+ - "minor"
49
+ - "patch"
50
+ patterns:
51
+ - "*"
@@ -19,10 +19,10 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  steps:
21
21
  - name: Checkout code
22
- uses: actions/checkout@v6
22
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
23
23
 
24
24
  - name: Set up Python
25
- uses: actions/setup-python@v6
25
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
26
26
  with:
27
27
  python-version: "3.12"
28
28
 
@@ -53,10 +53,10 @@ jobs:
53
53
 
54
54
  steps:
55
55
  - name: Checkout code
56
- uses: actions/checkout@v6
56
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
57
57
 
58
58
  - name: Set up Python ${{ matrix.python-version }}
59
- uses: actions/setup-python@v6
59
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
60
60
  with:
61
61
  python-version: ${{ matrix.python-version }}
62
62
 
@@ -87,13 +87,13 @@ jobs:
87
87
  name: Container Tests
88
88
  runs-on: ubuntu-latest
89
89
  needs: test
90
- if: ${{ secrets.ROOTLY_API_TOKEN != '' }}
90
+ if: ${{ github.repository == 'Rootly-AI-Labs/rootly-mcp-server' }}
91
91
  steps:
92
92
  - name: Checkout code
93
- uses: actions/checkout@v6
93
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
94
94
 
95
95
  - name: Set up Python
96
- uses: actions/setup-python@v6
96
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
97
97
  with:
98
98
  python-version: "3.12"
99
99
 
@@ -135,10 +135,10 @@ jobs:
135
135
  runs-on: ubuntu-latest
136
136
  steps:
137
137
  - name: Checkout code
138
- uses: actions/checkout@v6
138
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
139
139
 
140
140
  - name: Set up Python
141
- uses: actions/setup-python@v6
141
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
142
142
  with:
143
143
  python-version: "3.12"
144
144
 
@@ -161,10 +161,10 @@ jobs:
161
161
  needs: [quality, test, security]
162
162
  steps:
163
163
  - name: Checkout code
164
- uses: actions/checkout@v6
164
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
165
165
 
166
166
  - name: Set up Python
167
- uses: actions/setup-python@v6
167
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
168
168
  with:
169
169
  python-version: "3.12"
170
170
 
@@ -175,7 +175,7 @@ jobs:
175
175
  run: uv build
176
176
 
177
177
  - name: Upload build artifacts
178
- uses: actions/upload-artifact@v7
178
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
179
179
  with:
180
180
  name: dist
181
181
  path: dist/
@@ -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@v4
15
+ uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4
@@ -36,10 +36,10 @@ jobs:
36
36
  runs-on: ubuntu-latest
37
37
  steps:
38
38
  - name: Checkout code
39
- uses: actions/checkout@v6
39
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
40
40
 
41
41
  - name: Set up Python
42
- uses: actions/setup-python@v6
42
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
43
43
  with:
44
44
  python-version: "3.12"
45
45
 
@@ -58,10 +58,10 @@ jobs:
58
58
  if: ${{ github.event_name != 'pull_request' }}
59
59
  steps:
60
60
  - name: Checkout code
61
- uses: actions/checkout@v6
61
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
62
62
 
63
63
  - name: Set up Python
64
- uses: actions/setup-python@v6
64
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
65
65
  with:
66
66
  python-version: "3.12"
67
67
 
@@ -13,10 +13,10 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
15
  - name: Checkout code
16
- uses: actions/checkout@v6
16
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
17
17
 
18
18
  - name: Set up Python
19
- uses: actions/setup-python@v6
19
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
20
20
  with:
21
21
  python-version: '3.12'
22
22
 
@@ -1,3 +1,7 @@
1
+ # Benchmark and performance tracking files
2
+ *benchmark*.json
3
+ *_benchmark_*.json
4
+
1
5
  # Byte-compiled / optimized / DLL files
2
6
  __pycache__/
3
7
  *.py[cod]
@@ -5,6 +5,21 @@ 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.2.20] - Released 2026-04-21
9
+
10
+ ### Security
11
+
12
+ - **Critical Security Updates**: Upgraded vulnerable dependencies to address 3 security advisories
13
+ - **authlib**: Updated from `1.6.9` to `1.7.0` to fix CSRF protection vulnerability (GHSA-jj8c-mmj3-mmgv)
14
+ - **python-dotenv**: Updated from `1.1.0` to `1.2.2` to fix symlink attack vulnerability (GHSA-m8f7-34r5-grfg)
15
+ - **python-multipart**: Updated from `0.0.22` to `0.0.26` to fix denial of service vulnerability (CVE-2026-40347)
16
+ - **Dependabot Configuration**: Fixed unsupported `semver-major-days` property for docker and github-actions ecosystems
17
+
18
+ ### Dependencies
19
+
20
+ - **joserfc**: Added `1.6.4` as new dependency (required by updated authlib)
21
+ - **Security Scanning**: All known vulnerabilities resolved as confirmed by pip-audit
22
+
8
23
  ## [2.2.19] - Released 2026-04-17
9
24
 
10
25
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.2.19
3
+ Version: 2.2.20
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
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Build Tools
21
21
  Classifier: Topic :: System :: Monitoring
22
22
  Classifier: Topic :: System :: Systems Administration
23
23
  Requires-Python: >=3.12
24
- Requires-Dist: authlib>=1.6.9
24
+ Requires-Dist: authlib>=1.6.11
25
25
  Requires-Dist: brotli>=1.2.0
26
26
  Requires-Dist: cryptography>=46.0.7
27
27
  Requires-Dist: fastmcp[code-mode]==3.2.0
@@ -29,6 +29,8 @@ Requires-Dist: httpx==0.28.1
29
29
  Requires-Dist: numpy>=1.24.0
30
30
  Requires-Dist: pydantic==2.12.5
31
31
  Requires-Dist: pygments>=2.20.0
32
+ Requires-Dist: python-dotenv>=1.2.2
33
+ Requires-Dist: python-multipart>=0.0.26
32
34
  Requires-Dist: requests==2.33.1
33
35
  Requires-Dist: scikit-learn>=1.3.0
34
36
  Provides-Extra: dev
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.2.19"
3
+ version = "2.2.20"
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"
@@ -37,7 +37,9 @@ classifiers = [
37
37
  ]
38
38
  dependencies = [
39
39
  "fastmcp[code-mode]==3.2.0", # Pin to patched version with Code Mode support
40
- "authlib>=1.6.9", # Override vulnerable transitive authlib from fastmcp
40
+ "authlib>=1.6.11", # Override vulnerable transitive authlib (GHSA-jj8c-mmj3-mmgv)
41
+ "python-dotenv>=1.2.2", # Override vulnerable transitive python-dotenv (GHSA-m8f7-34r5-grfg)
42
+ "python-multipart>=0.0.26", # Override vulnerable transitive python-multipart (CVE-2026-40347)
41
43
  "cryptography>=46.0.7", # Override vulnerable transitive cryptography
42
44
  "Pygments>=2.20.0", # Override vulnerable transitive Pygments
43
45
  "requests==2.33.1", # Pin to patched version
@@ -50,9 +50,7 @@ def normalize_transport(value: str) -> TransportName:
50
50
  return mapped
51
51
 
52
52
 
53
- def normalize_transport_or_default(
54
- value: str, default: TransportName = "stdio"
55
- ) -> TransportName:
53
+ def normalize_transport_or_default(value: str, default: TransportName = "stdio") -> TransportName:
56
54
  """Normalize transport value, falling back to default when invalid."""
57
55
  try:
58
56
  return normalize_transport(value)
@@ -275,7 +273,9 @@ def run_dual_http_server(
275
273
  stateless=stateless_http,
276
274
  )
277
275
  code_mode_http_app = StreamableHTTPASGIApp(code_mode_session_manager)
278
- routes.append(Route(code_mode_path, endpoint=code_mode_http_app, methods=streamable_methods))
276
+ routes.append(
277
+ Route(code_mode_path, endpoint=code_mode_http_app, methods=streamable_methods)
278
+ )
279
279
 
280
280
  routes.extend(server._get_additional_http_routes()) # noqa: SLF001
281
281
 
@@ -373,7 +373,11 @@ def main():
373
373
  # argparse already normalizes/validates --transport via type=normalize_transport
374
374
  normalized_transport = args.transport
375
375
  code_mode_enabled = args.enable_code_mode or code_mode_enabled_from_env(default=True)
376
- code_mode_path = normalize_code_mode_path(args.code_mode_path) if args.code_mode_path else code_mode_path_from_env()
376
+ code_mode_path = (
377
+ normalize_code_mode_path(args.code_mode_path)
378
+ if args.code_mode_path
379
+ else code_mode_path_from_env()
380
+ )
377
381
  server = create_rootly_mcp_server(
378
382
  swagger_path=args.swagger_path,
379
383
  name=args.name,
@@ -172,8 +172,7 @@ class CompatibleMontySandboxProvider(MontySandboxProvider):
172
172
 
173
173
  inputs = inputs or {}
174
174
  async_functions = {
175
- key: _ensure_async(value)
176
- for key, value in (external_functions or {}).items()
175
+ key: _ensure_async(value) for key, value in (external_functions or {}).items()
177
176
  }
178
177
 
179
178
  monty = self._build_monty_runner(
@@ -219,10 +218,9 @@ class RootlyCodeMode(CodeMode):
219
218
  async def call_tool(tool_name: str, params: dict[str, Any]) -> Any:
220
219
  resolved_name = _normalize_execute_tool_name(tool_name)
221
220
  backend_tools = await _get_cached_tools()
222
- tool = (
223
- transform._find_tool(resolved_name, transform._build_discovery_tools())
224
- or transform._find_tool(resolved_name, backend_tools)
225
- )
221
+ tool = transform._find_tool(
222
+ resolved_name, transform._build_discovery_tools()
223
+ ) or transform._find_tool(resolved_name, backend_tools)
226
224
  if tool is None:
227
225
  raise NotFoundError(f"Unknown tool: {tool_name}")
228
226
 
@@ -201,16 +201,14 @@ def _log_tool_usage_event(
201
201
  event["error_type"] = error_type
202
202
  if error_context:
203
203
  event.update(
204
- {
205
- key: value
206
- for key, value in error_context.items()
207
- if value not in ("", [], None, {})
208
- }
204
+ {key: value for key, value in error_context.items() if value not in ("", [], None, {})}
209
205
  )
210
206
 
211
207
  _configure_tool_usage_json_logger()
212
208
  _tool_usage_json_logger.info(
213
- json.dumps({k: v for k, v in event.items() if v not in ("", [], None)}, separators=(",", ":"))
209
+ json.dumps(
210
+ {k: v for k, v in event.items() if v not in ("", [], None)}, separators=(",", ":")
211
+ )
214
212
  )
215
213
 
216
214
 
@@ -405,8 +403,7 @@ def create_rootly_mcp_server(
405
403
  f"/v1{path}" if not path.startswith("/v1") else path for path in allowed_paths
406
404
  ]
407
405
  delete_allowed_paths_v1 = [
408
- f"/v1{path}" if not path.startswith("/v1") else path
409
- for path in delete_allowed_paths
406
+ f"/v1{path}" if not path.startswith("/v1") else path for path in delete_allowed_paths
410
407
  ]
411
408
 
412
409
  logger.info(f"Creating Rootly MCP Server with allowed paths: {allowed_paths_v1}")
@@ -518,7 +515,9 @@ def create_rootly_mcp_server(
518
515
  logger.debug(
519
516
  f"make_authenticated_request: client_ip={client_ip}, headers_keys={list(request_headers.keys()) if request_headers else []}"
520
517
  )
521
- direct_auth_header = request_headers.get("authorization", "") if request_headers else ""
518
+ direct_auth_header = (
519
+ request_headers.get("authorization", "") if request_headers else ""
520
+ )
522
521
  effective_auth_header = direct_auth_header or _session_auth_token.get("")
523
522
  if direct_auth_header:
524
523
  logger.debug("make_authenticated_request: Found auth header, adding to request")
@@ -164,8 +164,7 @@ def _filter_openapi_spec(
164
164
  if not allow_delete:
165
165
  path_item.pop("delete", None)
166
166
  if not any(
167
- method.lower() in ["get", "post", "put", "patch", "delete"]
168
- for method in path_item
167
+ method.lower() in ["get", "post", "put", "patch", "delete"] for method in path_item
169
168
  ):
170
169
  paths_to_remove.append(path)
171
170
  for path in paths_to_remove:
@@ -119,10 +119,13 @@ def register_incident_tools(
119
119
 
120
120
  for candidate in response.json().get("data", []):
121
121
  attrs = candidate.get("attributes", {})
122
- candidate_value = attrs.get("slug") if filter_key == "filter[slug]" else attrs.get(
123
- "name"
122
+ candidate_value = (
123
+ attrs.get("slug") if filter_key == "filter[slug]" else attrs.get("name")
124
124
  )
125
- if isinstance(candidate_value, str) and candidate_value.lower() == expected_value.lower():
125
+ if (
126
+ isinstance(candidate_value, str)
127
+ and candidate_value.lower() == expected_value.lower()
128
+ ):
126
129
  matched_id = str(candidate.get("id"))
127
130
  break
128
131
 
@@ -137,8 +140,7 @@ def register_incident_tools(
137
140
 
138
141
  if unresolved_teams:
139
142
  raise ValueError(
140
- "Could not resolve team names/slugs to team IDs: "
141
- + ", ".join(unresolved_teams)
143
+ "Could not resolve team names/slugs to team IDs: " + ", ".join(unresolved_teams)
142
144
  )
143
145
 
144
146
  return ",".join(dict.fromkeys(resolved_team_ids)), resolved_team_lookup
@@ -214,9 +216,7 @@ def register_incident_tools(
214
216
  async def list_incidents(
215
217
  query: Annotated[
216
218
  str,
217
- Field(
218
- description="Optional free-text search across incident titles and summaries"
219
- ),
219
+ Field(description="Optional free-text search across incident titles and summaries"),
220
220
  ] = "",
221
221
  teams: Annotated[
222
222
  str,
@@ -238,9 +238,7 @@ def register_incident_tools(
238
238
  ] = "",
239
239
  severity: Annotated[
240
240
  str,
241
- Field(
242
- description="Optional severity filter (e.g., critical, high, medium, low)"
243
- ),
241
+ Field(description="Optional severity filter (e.g., critical, high, medium, low)"),
244
242
  ] = "",
245
243
  status: Annotated[
246
244
  str,
@@ -250,15 +248,11 @@ def register_incident_tools(
250
248
  ] = "",
251
249
  started_after: Annotated[
252
250
  str,
253
- Field(
254
- description="Filter incidents that started at or after this ISO 8601 timestamp"
255
- ),
251
+ Field(description="Filter incidents that started at or after this ISO 8601 timestamp"),
256
252
  ] = "",
257
253
  started_before: Annotated[
258
254
  str,
259
- Field(
260
- description="Filter incidents that started at or before this ISO 8601 timestamp"
261
- ),
255
+ Field(description="Filter incidents that started at or before this ISO 8601 timestamp"),
262
256
  ] = "",
263
257
  custom_field_selected_option_ids: Annotated[
264
258
  str,
@@ -340,9 +334,7 @@ def register_incident_tools(
340
334
  async def collect_incidents(
341
335
  query: Annotated[
342
336
  str,
343
- Field(
344
- description="Optional free-text search across incident titles and summaries"
345
- ),
337
+ Field(description="Optional free-text search across incident titles and summaries"),
346
338
  ] = "",
347
339
  teams: Annotated[
348
340
  str,
@@ -364,9 +356,7 @@ def register_incident_tools(
364
356
  ] = "",
365
357
  severity: Annotated[
366
358
  str,
367
- Field(
368
- description="Optional severity filter (e.g., critical, high, medium, low)"
369
- ),
359
+ Field(description="Optional severity filter (e.g., critical, high, medium, low)"),
370
360
  ] = "",
371
361
  status: Annotated[
372
362
  str,
@@ -376,15 +366,11 @@ def register_incident_tools(
376
366
  ] = "",
377
367
  started_after: Annotated[
378
368
  str,
379
- Field(
380
- description="Filter incidents that started at or after this ISO 8601 timestamp"
381
- ),
369
+ Field(description="Filter incidents that started at or after this ISO 8601 timestamp"),
382
370
  ] = "",
383
371
  started_before: Annotated[
384
372
  str,
385
- Field(
386
- description="Filter incidents that started at or before this ISO 8601 timestamp"
387
- ),
373
+ Field(description="Filter incidents that started at or before this ISO 8601 timestamp"),
388
374
  ] = "",
389
375
  custom_field_selected_option_ids: Annotated[
390
376
  str,
@@ -452,7 +438,9 @@ def register_incident_tools(
452
438
  page_params["page[size]"] = batch_size
453
439
  page_params["page[number]"] = page_number
454
440
 
455
- response = await make_authenticated_request("GET", "/v1/incidents", params=page_params)
441
+ response = await make_authenticated_request(
442
+ "GET", "/v1/incidents", params=page_params
443
+ )
456
444
  response.raise_for_status()
457
445
 
458
446
  response_data = strip_heavy_nested_data(response.json())
@@ -483,7 +471,9 @@ def register_incident_tools(
483
471
  results_truncated = True
484
472
 
485
473
  return {
486
- "incidents": [_summarize_incident_record(incident) for incident in collected_incidents],
474
+ "incidents": [
475
+ _summarize_incident_record(incident) for incident in collected_incidents
476
+ ],
487
477
  "returned_incidents": len(collected_incidents),
488
478
  "collection": {
489
479
  "max_results": max_results,
@@ -861,7 +851,8 @@ def register_incident_tools(
861
851
  return cast(
862
852
  JsonDict,
863
853
  mcp_error.tool_error(
864
- "Must provide either incident_id or incident_description", "validation_error"
854
+ "Must provide either incident_id or incident_description",
855
+ "validation_error",
865
856
  ),
866
857
  )
867
858
 
@@ -36,7 +36,7 @@ def _truncate_text(value: Any, max_length: int = 280) -> str | None:
36
36
  return None
37
37
 
38
38
  if len(value) <= max_length:
39
- return value
39
+ return str(value)
40
40
 
41
41
  return f"{value[: max_length - 1].rstrip()}…"
42
42
 
@@ -100,6 +100,7 @@ def register_oncall_tools(
100
100
  mcp_error: MCPErrorLike,
101
101
  ) -> None:
102
102
  """Register on-call analysis and scheduling tools on the MCP server."""
103
+
103
104
  @mcp.tool()
104
105
  async def get_oncall_shift_metrics(
105
106
  start_date: Annotated[
@@ -1229,7 +1230,9 @@ def register_oncall_tools(
1229
1230
  _lookup_maps_lock = asyncio.Lock()
1230
1231
 
1231
1232
  # Helper function to fetch users and schedules for enrichment
1232
- async def _fetch_users_and_schedules_maps() -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
1233
+ async def _fetch_users_and_schedules_maps() -> tuple[
1234
+ dict[str, Any], dict[str, Any], dict[str, Any]
1235
+ ]:
1233
1236
  """Fetch all users, schedules, and teams to build lookup maps.
1234
1237
 
1235
1238
  Results are cached for 5 minutes to avoid repeated API calls.
@@ -49,7 +49,7 @@ def _sanitize_log_excerpt(value: Any, max_length: int = _MAX_LOG_EXCERPT_CHARS)
49
49
 
50
50
  text = str(value).replace("\r\n", "\n").strip()
51
51
  text = re.sub(r"/[\w/.-]+\.py", "[file]", text)
52
- text = re.sub(r'C:\\[\w\\.-]+\.py', "[file]", text)
52
+ text = re.sub(r"C:\\[\w\\.-]+\.py", "[file]", text)
53
53
  text = re.sub(r'File "[^"]+"', 'File "[file]"', text)
54
54
  text = re.sub(r"Bearer\s+[A-Za-z0-9._~\-]+", "Bearer ***REDACTED***", text, flags=re.I)
55
55
  text = re.sub(r"\brootly_[A-Za-z0-9]+\b", "rootly_***REDACTED***", text)
@@ -73,7 +73,7 @@ def _sanitize_error_context_value(value: Any) -> Any:
73
73
  normalized = _sanitize_error_context_value(parsed)
74
74
  if isinstance(normalized, dict | list):
75
75
  return json.dumps(normalized, separators=(",", ":"))
76
- except Exception:
76
+ except Exception: # nosec B110 - Safe fallback for malformed JSON in log sanitization
77
77
  pass
78
78
  return _sanitize_log_excerpt(value)
79
79
 
@@ -260,9 +260,7 @@ class AuthCaptureMiddleware:
260
260
  self.app = app
261
261
  self._sse_path = _normalize_path(os.getenv("FASTMCP_SSE_PATH", "/sse"))
262
262
  self._message_path = _normalize_path(os.getenv("FASTMCP_MESSAGE_PATH", "/messages"))
263
- self._streamable_path = _normalize_path(
264
- os.getenv("FASTMCP_STREAMABLE_HTTP_PATH", "/mcp")
265
- )
263
+ self._streamable_path = _normalize_path(os.getenv("FASTMCP_STREAMABLE_HTTP_PATH", "/mcp"))
266
264
  self._code_mode_path = _normalize_path(os.getenv("ROOTLY_CODE_MODE_PATH", "/mcp-codemode"))
267
265
  self._capture_paths = {
268
266
  self._sse_path,
@@ -509,7 +507,9 @@ def strip_heavy_shift_data(data: dict[str, Any]) -> dict[str, Any]:
509
507
  kept_relationships = {}
510
508
  for rel_key in ("user", "shift_override"):
511
509
  if rel_key in relationships:
512
- kept_relationships[rel_key] = _collapse_relationship_data(relationships[rel_key])
510
+ kept_relationships[rel_key] = _collapse_relationship_data(
511
+ relationships[rel_key]
512
+ )
513
513
  shift["relationships"] = kept_relationships
514
514
 
515
515
  if isinstance(data["data"], list):
@@ -857,9 +857,7 @@ class AuthenticatedHTTPXClient:
857
857
  _record_upstream_response_context(request.method, response)
858
858
 
859
859
  response = self._maybe_strip_alert_response(request.method, str(request.url), response)
860
- response = self._maybe_strip_collection_response(
861
- request.method, str(request.url), response
862
- )
860
+ response = self._maybe_strip_collection_response(request.method, str(request.url), response)
863
861
  return response
864
862
 
865
863
  async def __aenter__(self):
@@ -205,9 +205,9 @@ class TestLocalWithRealToken:
205
205
  token = os.getenv("ROOTLY_API_TOKEN")
206
206
 
207
207
  # Basic format validation
208
- assert token is not None and token.startswith(
209
- "rootly_"
210
- ), "Token should start with 'rootly_'"
208
+ assert token is not None and token.startswith("rootly_"), (
209
+ "Token should start with 'rootly_'"
210
+ )
211
211
  assert token is not None and len(token) > 20, "Token should be reasonably long"
212
212
  assert token is not None and "_" in token, "Token should contain underscores"
213
213