rootly-mcp-server 2.1.0__tar.gz → 2.1.1__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 (70) hide show
  1. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/#Dockerfile# +1 -1
  2. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/beads.db +0 -0
  3. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/beads.db-shm +0 -0
  4. rootly_mcp_server-2.1.1/.beads/beads.db-wal +0 -0
  5. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.claude/settings.local.json +8 -1
  6. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.github/workflows/ci.yml +13 -13
  7. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.github/workflows/lint.yml +2 -2
  8. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/PKG-INFO +1 -1
  9. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/pyproject.toml +1 -1
  10. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/server.py +8 -130
  11. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_authentication.py +49 -0
  12. rootly_mcp_server-2.1.0/.beads/beads.db-wal +0 -0
  13. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/.gitignore +0 -0
  14. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/.local_version +0 -0
  15. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/.sync.lock +0 -0
  16. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/README.md +0 -0
  17. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/config.yaml +0 -0
  18. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/daemon.lock +0 -0
  19. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/daemon.pid +0 -0
  20. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/interactions.jsonl +0 -0
  21. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/issues.jsonl +0 -0
  22. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/last-touched +0 -0
  23. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/metadata.json +0 -0
  24. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.beads/sync_base.jsonl +0 -0
  25. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.gitattributes +0 -0
  26. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.github/dependabot.yml +0 -0
  27. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.github/workflows/pypi-release.yml +0 -0
  28. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.github/workflows/test.yml +0 -0
  29. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.gitignore +0 -0
  30. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.mcp.json +0 -0
  31. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.semaphore/deploy.yml +0 -0
  32. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.semaphore/semaphore.yml +0 -0
  33. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/.semaphore/update-task-definition.sh +0 -0
  34. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/2026-01-27-MCP.txt +0 -0
  35. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/2026-01-27-claude-code-v2120.txt +0 -0
  36. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/Dockerfile +0 -0
  37. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/LICENSE +0 -0
  38. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/README.md +0 -0
  39. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/examples/skills/README.md +0 -0
  40. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/rootly-mcp-server-demo.gif +0 -0
  41. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/rootly_openapi.json +0 -0
  42. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/scripts/setup-hooks.sh +0 -0
  43. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/__init__.py +0 -0
  44. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/__main__.py +0 -0
  45. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/client.py +0 -0
  46. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/data/__init__.py +0 -0
  47. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/exceptions.py +0 -0
  48. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/monitoring.py +0 -0
  49. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/pagination.py +0 -0
  50. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/security.py +0 -0
  51. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/smart_utils.py +0 -0
  52. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/texttest.json +0 -0
  53. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/utils.py +0 -0
  54. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/src/rootly_mcp_server/validators.py +0 -0
  55. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/README.md +0 -0
  56. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/conftest.py +0 -0
  57. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/integration/local/test_basic.py +0 -0
  58. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/integration/local/test_smart_tools.py +0 -0
  59. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/integration/remote/test_essential.py +0 -0
  60. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/test_client.py +0 -0
  61. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_exceptions.py +0 -0
  62. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_oncall_handoff.py +0 -0
  63. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_oncall_metrics.py +0 -0
  64. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_security.py +0 -0
  65. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_server.py +0 -0
  66. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_smart_utils.py +0 -0
  67. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_tools.py +0 -0
  68. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_utils.py +0 -0
  69. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/tests/unit/test_validators.py +0 -0
  70. {rootly_mcp_server-2.1.0 → rootly_mcp_server-2.1.1}/uv.lock +0 -0
@@ -1,5 +1,5 @@
1
1
  # Use Python 3.12 slim image as base
2
- FROM python:3.12-slim
2
+ FROM python:3.14-slim
3
3
 
4
4
  # Set working directory
5
5
  WORKDIR /app
@@ -63,7 +63,14 @@
63
63
  "Bash(git rm:*)",
64
64
  "Bash(black:*)",
65
65
  "Bash(isort:*)",
66
- "Bash(python -m isort:*)"
66
+ "Bash(python -m isort:*)",
67
+ "Bash(pip install:*)",
68
+ "Bash(gh pr close:*)",
69
+ "Bash(git fetch:*)",
70
+ "Bash(git cherry-pick:*)",
71
+ "Bash(git revert:*)",
72
+ "mcp__rootly__search_incidents",
73
+ "Bash(uv build:*)"
67
74
  ],
68
75
  "deny": []
69
76
  }
@@ -16,15 +16,15 @@ jobs:
16
16
 
17
17
  steps:
18
18
  - name: Checkout code
19
- uses: actions/checkout@v4
19
+ uses: actions/checkout@v6
20
20
 
21
21
  - name: Set up Python ${{ matrix.python-version }}
22
- uses: actions/setup-python@v5
22
+ uses: actions/setup-python@v6
23
23
  with:
24
24
  python-version: ${{ matrix.python-version }}
25
25
 
26
26
  - name: Install uv
27
- uses: astral-sh/setup-uv@v4
27
+ uses: astral-sh/setup-uv@v7
28
28
 
29
29
  - name: Install dependencies
30
30
  run: |
@@ -36,7 +36,7 @@ jobs:
36
36
  uv run pytest tests/ -v --cov=src/rootly_mcp_server --cov-report=term --cov-report=xml
37
37
 
38
38
  - name: Upload coverage to Codecov
39
- uses: codecov/codecov-action@v4
39
+ uses: codecov/codecov-action@v5
40
40
  with:
41
41
  file: ./coverage.xml
42
42
  flags: unittests
@@ -48,15 +48,15 @@ jobs:
48
48
 
49
49
  steps:
50
50
  - name: Checkout code
51
- uses: actions/checkout@v4
51
+ uses: actions/checkout@v6
52
52
 
53
53
  - name: Set up Python
54
- uses: actions/setup-python@v5
54
+ uses: actions/setup-python@v6
55
55
  with:
56
56
  python-version: "3.12"
57
57
 
58
58
  - name: Install uv
59
- uses: astral-sh/setup-uv@v4
59
+ uses: astral-sh/setup-uv@v7
60
60
 
61
61
  - name: Install dependencies
62
62
  run: |
@@ -85,10 +85,10 @@ jobs:
85
85
 
86
86
  steps:
87
87
  - name: Checkout code
88
- uses: actions/checkout@v4
88
+ uses: actions/checkout@v6
89
89
 
90
90
  - name: Set up Python
91
- uses: actions/setup-python@v5
91
+ uses: actions/setup-python@v6
92
92
  with:
93
93
  python-version: "3.12"
94
94
 
@@ -113,22 +113,22 @@ jobs:
113
113
 
114
114
  steps:
115
115
  - name: Checkout code
116
- uses: actions/checkout@v4
116
+ uses: actions/checkout@v6
117
117
 
118
118
  - name: Set up Python
119
- uses: actions/setup-python@v5
119
+ uses: actions/setup-python@v6
120
120
  with:
121
121
  python-version: "3.12"
122
122
 
123
123
  - name: Install uv
124
- uses: astral-sh/setup-uv@v4
124
+ uses: astral-sh/setup-uv@v7
125
125
 
126
126
  - name: Build package
127
127
  run: |
128
128
  uv build
129
129
 
130
130
  - name: Upload build artifacts
131
- uses: actions/upload-artifact@v4
131
+ uses: actions/upload-artifact@v6
132
132
  with:
133
133
  name: dist
134
134
  path: dist/
@@ -11,10 +11,10 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
 
13
13
  steps:
14
- - uses: actions/checkout@v4
14
+ - uses: actions/checkout@v6
15
15
 
16
16
  - name: Set up Python
17
- uses: actions/setup-python@v5
17
+ uses: actions/setup-python@v6
18
18
  with:
19
19
  python-version: '3.12'
20
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.1.0
3
+ Version: 2.1.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.1.0"
3
+ version = "2.1.1"
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.10"
@@ -163,131 +163,9 @@ def strip_heavy_nested_data(data: dict[str, Any]) -> dict[str, Any]:
163
163
  # Replace with just count
164
164
  rels[rel_key] = {"count": len(rels[rel_key]["data"])}
165
165
 
166
- # Process "included" section (common in shifts/alerts with user data)
167
- if "included" in data and isinstance(data["included"], list):
168
- for item in data["included"]:
169
- if item.get("type") == "users":
170
- # Keep only essential user fields
171
- if "attributes" in item:
172
- attrs = item["attributes"]
173
- keep_fields = {"name", "email", "phone", "time_zone", "full_name"}
174
- item["attributes"] = {k: v for k, v in attrs.items() if k in keep_fields}
175
- # Strip heavy relationships
176
- if "relationships" in item:
177
- for rel_key in [
178
- "schedules",
179
- "notification_rules",
180
- "teams",
181
- "devices",
182
- "email_addresses",
183
- "phone_numbers",
184
- ]:
185
- if rel_key in item["relationships"]:
186
- rel_data = item["relationships"][rel_key]
187
- if isinstance(rel_data, dict) and "data" in rel_data:
188
- data_list = rel_data.get("data", [])
189
- if isinstance(data_list, list):
190
- item["relationships"][rel_key] = {"count": len(data_list)}
191
-
192
- # Process alerts in data list
193
- if "data" in data and isinstance(data["data"], list):
194
- for item in data["data"]:
195
- if item.get("type") == "alerts":
196
- # Strip heavy attributes from alerts
197
- if "attributes" in item:
198
- attrs = item["attributes"]
199
- # Remove heavy fields - raw data, embedded objects, integration fields
200
- heavy_fields = [
201
- "data", # Raw alert payload from source - very large
202
- "labels",
203
- "external_url",
204
- "pagerduty_incident_id",
205
- "pagerduty_incident_url",
206
- "opsgenie_alert_id",
207
- "opsgenie_alert_url",
208
- "deduplication_key",
209
- ]
210
- for field in heavy_fields:
211
- attrs.pop(field, None)
212
-
213
- # Simplify embedded objects to just IDs/counts
214
- # groups - keep only group_ids
215
- if "groups" in attrs:
216
- attrs.pop("groups", None)
217
- # environments - keep only environment_ids
218
- if "environments" in attrs:
219
- attrs.pop("environments", None)
220
- # services - keep only service_ids
221
- if "services" in attrs:
222
- attrs.pop("services", None)
223
- # incidents - embedded incident objects
224
- if "incidents" in attrs:
225
- attrs.pop("incidents", None)
226
- # responders - embedded responder objects
227
- if "responders" in attrs:
228
- attrs.pop("responders", None)
229
- # notified_users - embedded user objects
230
- if "notified_users" in attrs:
231
- attrs.pop("notified_users", None)
232
- # alerting_targets - embedded target objects
233
- if "alerting_targets" in attrs:
234
- attrs.pop("alerting_targets", None)
235
- # alert_urgency - keep only alert_urgency_id
236
- if "alert_urgency" in attrs:
237
- attrs.pop("alert_urgency", None)
238
- # alert_field_values - embedded custom field values
239
- if "alert_field_values" in attrs:
240
- attrs.pop("alert_field_values", None)
241
-
242
- # Strip heavy relationships
243
- if "relationships" in item:
244
- rels = item["relationships"]
245
- for rel_key in ["events", "subscribers", "alerts"]:
246
- if (
247
- rel_key in rels
248
- and isinstance(rels[rel_key], dict)
249
- and "data" in rels[rel_key]
250
- ):
251
- data_list = rels[rel_key].get("data", [])
252
- if isinstance(data_list, list):
253
- rels[rel_key] = {"count": len(data_list)}
254
-
255
166
  return data
256
167
 
257
168
 
258
- class ProcessedResponse:
259
- """Wrapper around httpx.Response that processes JSON to reduce payload size."""
260
-
261
- def __init__(self, response: httpx.Response):
262
- self._response = response
263
- self._processed_json = None
264
-
265
- def json(self, **kwargs):
266
- """Parse JSON and strip heavy nested data."""
267
- if self._processed_json is None:
268
- raw_data = self._response.json(**kwargs)
269
- self._processed_json = strip_heavy_nested_data(raw_data)
270
- return self._processed_json
271
-
272
- def __getattr__(self, name):
273
- """Delegate all other attributes to the wrapped response."""
274
- return getattr(self._response, name)
275
-
276
-
277
- class ResponseProcessingClient(httpx.AsyncClient):
278
- """AsyncClient subclass that wraps responses to reduce payload size.
279
-
280
- This is necessary because FastMCP.from_openapi() uses the client directly,
281
- bypassing any wrapper class. By subclassing httpx.AsyncClient, we ensure
282
- all responses go through our processing.
283
- """
284
-
285
- async def request(self, method, url, **kwargs):
286
- """Override request to wrap response with ProcessedResponse."""
287
- response = await super().request(method, url, **kwargs)
288
- return ProcessedResponse(response)
289
-
290
-
291
169
  class MCPError:
292
170
  """Enhanced error handling for MCP protocol compliance."""
293
171
 
@@ -468,7 +346,7 @@ class AuthenticatedHTTPXClient:
468
346
  if self._api_token:
469
347
  headers["Authorization"] = f"Bearer {self._api_token}"
470
348
 
471
- self.client = ResponseProcessingClient(
349
+ self.client = httpx.AsyncClient(
472
350
  base_url=base_url,
473
351
  headers=headers,
474
352
  timeout=30.0,
@@ -500,16 +378,13 @@ class AuthenticatedHTTPXClient:
500
378
  return transformed
501
379
 
502
380
  async def request(self, method: str, url: str, **kwargs):
503
- """Override request to transform parameters and wrap response for payload reduction."""
381
+ """Override request to transform parameters."""
504
382
  # Transform query parameters
505
383
  if "params" in kwargs:
506
384
  kwargs["params"] = self._transform_params(kwargs["params"])
507
385
 
508
- # Call the underlying client's request method
509
- response = await self.client.request(method, url, **kwargs)
510
-
511
- # Wrap response to process JSON and reduce payload size
512
- return ProcessedResponse(response)
386
+ # Call the underlying client's request method and let it handle everything
387
+ return await self.client.request(method, url, **kwargs)
513
388
 
514
389
  async def get(self, url: str, **kwargs):
515
390
  """Proxy to request with GET method."""
@@ -612,9 +487,12 @@ def create_rootly_mcp_server(
612
487
 
613
488
  # Create the MCP server using OpenAPI integration
614
489
  # By default, all routes become tools which is what we want
490
+ # NOTE: We pass http_client (the wrapper) instead of http_client.client (the inner httpx client)
491
+ # so that parameter transformation (e.g., filter_status -> filter[status]) is applied.
492
+ # The wrapper implements the same interface as httpx.AsyncClient (duck typing).
615
493
  mcp = FastMCP.from_openapi(
616
494
  openapi_spec=filtered_spec,
617
- client=http_client.client,
495
+ client=http_client, # type: ignore[arg-type]
618
496
  name=name,
619
497
  timeout=30.0,
620
498
  tags={"rootly", "incident-management"},
@@ -238,3 +238,52 @@ class TestAuthenticationModeComparison:
238
238
 
239
239
  assert local_client.hosted is False
240
240
  assert hosted_client.hosted is True
241
+
242
+
243
+ class TestParameterTransformation:
244
+ """Test suite for parameter transformation functionality."""
245
+
246
+ def test_transform_params_with_mapping(self):
247
+ """Test that parameters are transformed according to mapping."""
248
+ mapping = {
249
+ "filter_status": "filter[status]",
250
+ "filter_services": "filter[services]",
251
+ }
252
+ client = AuthenticatedHTTPXClient(parameter_mapping=mapping)
253
+
254
+ params = {"filter_status": "active", "filter_services": "api,web", "page": 1}
255
+ result = client._transform_params(params)
256
+
257
+ assert result is not None
258
+ assert result["filter[status]"] == "active"
259
+ assert result["filter[services]"] == "api,web"
260
+ assert result["page"] == 1
261
+ assert "filter_status" not in result
262
+ assert "filter_services" not in result
263
+
264
+ def test_transform_params_without_mapping(self):
265
+ """Test that params pass through unchanged when no mapping exists."""
266
+ client = AuthenticatedHTTPXClient(parameter_mapping={})
267
+
268
+ params = {"filter_status": "active", "page": 1}
269
+ result = client._transform_params(params)
270
+
271
+ assert result is not None
272
+ assert result["filter_status"] == "active"
273
+ assert result["page"] == 1
274
+
275
+ def test_transform_params_with_none(self):
276
+ """Test that None params return None."""
277
+ client = AuthenticatedHTTPXClient(parameter_mapping={"foo": "bar"})
278
+
279
+ result = client._transform_params(None)
280
+
281
+ assert result is None
282
+
283
+ def test_transform_params_with_empty_dict(self):
284
+ """Test that empty dict returns empty dict."""
285
+ client = AuthenticatedHTTPXClient(parameter_mapping={"foo": "bar"})
286
+
287
+ result = client._transform_params({})
288
+
289
+ assert result == {}
Binary file