rootly-mcp-server 2.1.4__tar.gz → 2.2.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 (55) hide show
  1. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.gitignore +1 -0
  2. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/PKG-INFO +4 -4
  3. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/README.md +3 -3
  4. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/pyproject.toml +1 -1
  5. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/och_client.py +5 -2
  6. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/server.py +17 -17
  7. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_och_client.py +172 -15
  8. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/uv.lock +1 -1
  9. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.beads/issues.jsonl +0 -0
  10. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.gitattributes +0 -0
  11. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.github/dependabot.yml +0 -0
  12. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.github/workflows/ci.yml +0 -0
  13. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.github/workflows/lint.yml +0 -0
  14. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.github/workflows/pypi-release.yml +0 -0
  15. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.github/workflows/test.yml +0 -0
  16. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.semaphore/deploy.yml +0 -0
  17. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.semaphore/semaphore.yml +0 -0
  18. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/.semaphore/update-task-definition.sh +0 -0
  19. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/CONTRIBUTING.md +0 -0
  20. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/Dockerfile +0 -0
  21. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/LICENSE +0 -0
  22. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/examples/skills/README.md +0 -0
  23. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/rootly-mcp-server-demo.gif +0 -0
  24. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/rootly_openapi.json +0 -0
  25. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/scripts/setup-hooks.sh +0 -0
  26. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/__init__.py +0 -0
  27. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/__main__.py +0 -0
  28. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/client.py +0 -0
  29. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/data/__init__.py +0 -0
  30. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/exceptions.py +0 -0
  31. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/monitoring.py +0 -0
  32. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/pagination.py +0 -0
  33. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/security.py +0 -0
  34. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/smart_utils.py +0 -0
  35. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/texttest.json +0 -0
  36. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/utils.py +0 -0
  37. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/src/rootly_mcp_server/validators.py +0 -0
  38. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/README.md +0 -0
  39. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/conftest.py +0 -0
  40. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/integration/local/test_basic.py +0 -0
  41. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/integration/local/test_smart_tools.py +0 -0
  42. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/integration/remote/test_essential.py +0 -0
  43. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/test_client.py +0 -0
  44. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_authentication.py +0 -0
  45. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_exceptions.py +0 -0
  46. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_http_headers.py +0 -0
  47. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_oncall_handoff.py +0 -0
  48. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_oncall_metrics.py +0 -0
  49. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_oncall_new_tools.py +0 -0
  50. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_security.py +0 -0
  51. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_server.py +0 -0
  52. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_smart_utils.py +0 -0
  53. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_tools.py +0 -0
  54. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_utils.py +0 -0
  55. {rootly_mcp_server-2.1.4 → rootly_mcp_server-2.2.1}/tests/unit/test_validators.py +0 -0
@@ -194,3 +194,4 @@ test_output/
194
194
  !README.md
195
195
  !CONTRIBUTING.md
196
196
  !CHANGELOG.md.beads/
197
+ .mcp.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.1.4
3
+ Version: 2.2.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
@@ -147,11 +147,11 @@ Alternatively, connect directly to our hosted MCP server:
147
147
  - **`suggest_solutions`**: Mines past incident resolutions to recommend actionable solutions
148
148
  - **MCP Resources**: Exposes incident and team data as structured resources for easy AI reference
149
149
  - **Intelligent Pattern Recognition**: Automatically identifies services, error types, and resolution patterns
150
- - **On-Call Health Integration**: Detects burnout risk in scheduled responders
150
+ - **On-Call Health Integration**: Detects workload health risk in scheduled responders
151
151
 
152
152
  ## On-Call Health Integration
153
153
 
154
- Rootly MCP integrates with [On-Call Health](https://oncallhealth.ai) to detect burnout risk in scheduled responders.
154
+ Rootly MCP integrates with [On-Call Health](https://oncallhealth.ai) to detect workload health risk in scheduled responders.
155
155
 
156
156
  ### Setup
157
157
 
@@ -175,7 +175,7 @@ Set the `ONCALLHEALTH_API_KEY` environment variable:
175
175
  ### Usage
176
176
 
177
177
  ```
178
- check_oncall_burnout_risk(
178
+ check_oncall_health_risk(
179
179
  start_date="2026-02-09",
180
180
  end_date="2026-02-15"
181
181
  )
@@ -108,11 +108,11 @@ Alternatively, connect directly to our hosted MCP server:
108
108
  - **`suggest_solutions`**: Mines past incident resolutions to recommend actionable solutions
109
109
  - **MCP Resources**: Exposes incident and team data as structured resources for easy AI reference
110
110
  - **Intelligent Pattern Recognition**: Automatically identifies services, error types, and resolution patterns
111
- - **On-Call Health Integration**: Detects burnout risk in scheduled responders
111
+ - **On-Call Health Integration**: Detects workload health risk in scheduled responders
112
112
 
113
113
  ## On-Call Health Integration
114
114
 
115
- Rootly MCP integrates with [On-Call Health](https://oncallhealth.ai) to detect burnout risk in scheduled responders.
115
+ Rootly MCP integrates with [On-Call Health](https://oncallhealth.ai) to detect workload health risk in scheduled responders.
116
116
 
117
117
  ### Setup
118
118
 
@@ -136,7 +136,7 @@ Set the `ONCALLHEALTH_API_KEY` environment variable:
136
136
  ### Usage
137
137
 
138
138
  ```
139
- check_oncall_burnout_risk(
139
+ check_oncall_health_risk(
140
140
  start_date="2026-02-09",
141
141
  end_date="2026-02-15"
142
142
  )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.1.4"
3
+ version = "2.2.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"
@@ -1,10 +1,13 @@
1
- """On-Call Health API client for burnout risk analysis."""
1
+ """On-Call Health API client for workload health risk analysis."""
2
2
 
3
3
  import os
4
4
  from typing import Any
5
5
 
6
6
  import httpx
7
7
 
8
+ # External API field mapping (On-Call Health API response field)
9
+ _OCH_RISK_SCORE_FIELD = "burnout_score"
10
+
8
11
 
9
12
  class OnCallHealthClient:
10
13
  def __init__(self, api_key: str | None = None, base_url: str | None = None):
@@ -55,7 +58,7 @@ class OnCallHealthClient:
55
58
  "rootly_user_id": member.get("rootly_user_id"),
56
59
  "och_score": member.get("och_score", 0),
57
60
  "risk_level": member.get("risk_level", "unknown"),
58
- "burnout_score": member.get("burnout_score", 0),
61
+ "health_risk_score": member.get(_OCH_RISK_SCORE_FIELD, 0),
59
62
  "incident_count": member.get("incident_count", 0),
60
63
  }
61
64
 
@@ -386,15 +386,15 @@ class AuthenticatedHTTPXClient:
386
386
  if "params" in kwargs:
387
387
  kwargs["params"] = self._transform_params(kwargs["params"])
388
388
 
389
- # Ensure Content-Type and Accept headers are always set correctly for Rootly API
390
- # This is critical because FastMCP may pass headers from the MCP client request
391
- # (e.g., Content-Type: application/json from SSE) which would override our defaults
392
- if "headers" in kwargs:
393
- headers = dict(kwargs["headers"]) if kwargs["headers"] else {}
394
- # Always use JSON-API content type for Rootly API
395
- headers["Content-Type"] = "application/vnd.api+json"
396
- headers["Accept"] = "application/vnd.api+json"
397
- kwargs["headers"] = headers
389
+ # ALWAYS ensure Content-Type and Accept headers are set correctly for Rootly API
390
+ # This is critical because:
391
+ # 1. FastMCP may pass headers from the MCP client request (e.g., Content-Type: application/json)
392
+ # 2. Per-request headers override httpx client defaults
393
+ # 3. We must force JSON-API content type regardless of what's passed in
394
+ headers = dict(kwargs.get("headers") or {})
395
+ headers["Content-Type"] = "application/vnd.api+json"
396
+ headers["Accept"] = "application/vnd.api+json"
397
+ kwargs["headers"] = headers
398
398
 
399
399
  # Call the underlying client's request method and let it handle everything
400
400
  return await self.client.request(method, url, **kwargs)
@@ -3168,7 +3168,7 @@ Updated: {attributes.get("updated_at", "N/A")}"""
3168
3168
  }
3169
3169
 
3170
3170
  @mcp.tool()
3171
- async def check_oncall_burnout_risk(
3171
+ async def check_oncall_health_risk(
3172
3172
  start_date: Annotated[
3173
3173
  str,
3174
3174
  Field(description="Start date for the on-call period (ISO 8601, e.g., '2026-02-09')"),
@@ -3192,11 +3192,11 @@ Updated: {attributes.get("updated_at", "N/A")}"""
3192
3192
  Field(description="Include recommended replacement responders (default: true)"),
3193
3193
  ] = True,
3194
3194
  ) -> dict:
3195
- """Check if any at-risk responders (based on On-Call Health burnout analysis) are scheduled for on-call.
3195
+ """Check if any at-risk responders (based on On-Call Health analysis) are scheduled for on-call.
3196
3196
 
3197
- Integrates with On-Call Health (oncallhealth.ai) to identify responders at risk of burnout
3198
- and checks if they are scheduled during the specified period. Optionally recommends
3199
- safe replacement responders.
3197
+ Integrates with On-Call Health (oncallhealth.ai) to identify responders with elevated
3198
+ workload health risk and checks if they are scheduled during the specified period.
3199
+ Optionally recommends safe replacement responders.
3200
3200
 
3201
3201
  Requires ONCALLHEALTH_API_KEY environment variable.
3202
3202
  """
@@ -3239,7 +3239,7 @@ Updated: {attributes.get("updated_at", "N/A")}"""
3239
3239
  "total_at_risk": 0,
3240
3240
  "at_risk_scheduled": 0,
3241
3241
  "action_required": False,
3242
- "message": "No users above burnout threshold.",
3242
+ "message": "No users above health risk threshold.",
3243
3243
  },
3244
3244
  }
3245
3245
 
@@ -3350,7 +3350,7 @@ Updated: {attributes.get("updated_at", "N/A")}"""
3350
3350
  "user_id": int(rootly_id),
3351
3351
  "och_score": user["och_score"],
3352
3352
  "risk_level": user["risk_level"],
3353
- "burnout_score": user["burnout_score"],
3353
+ "health_risk_score": user["health_risk_score"],
3354
3354
  "total_hours": round(total_hours, 1),
3355
3355
  "shifts": user_shifts,
3356
3356
  }
@@ -3451,7 +3451,7 @@ Updated: {attributes.get("updated_at", "N/A")}"""
3451
3451
 
3452
3452
  error_type, error_message = MCPError.categorize_error(e)
3453
3453
  return MCPError.tool_error(
3454
- f"Failed to check burnout risk: {error_message}",
3454
+ f"Failed to check health risk: {error_message}",
3455
3455
  error_type,
3456
3456
  details={
3457
3457
  "params": {
@@ -1,12 +1,16 @@
1
1
  """
2
- Unit tests for On-Call Health client and burnout risk tool.
2
+ Unit tests for On-Call Health client and health risk tool.
3
3
 
4
4
  Tests cover:
5
5
  - OnCallHealthClient initialization
6
6
  - extract_at_risk_users logic
7
- - check_oncall_burnout_risk tool behavior
7
+ - check_oncall_health_risk tool behavior
8
+ - Field mapping from external API to internal names
9
+ - Tool registration verification
8
10
  """
9
11
 
12
+ from unittest.mock import patch
13
+
10
14
  import pytest
11
15
 
12
16
 
@@ -40,7 +44,7 @@ class TestExtractAtRiskUsers:
40
44
 
41
45
  @pytest.fixture
42
46
  def mock_analysis(self):
43
- """Mock OCH analysis response."""
47
+ """Mock OCH analysis response (uses external API field names)."""
44
48
  return {
45
49
  "id": 1226,
46
50
  "analysis_data": {
@@ -51,7 +55,7 @@ class TestExtractAtRiskUsers:
51
55
  "rootly_user_id": "2381",
52
56
  "och_score": 86.5,
53
57
  "risk_level": "high",
54
- "burnout_score": 3.5,
58
+ "burnout_score": 3.5, # External API field name
55
59
  "incident_count": 15,
56
60
  },
57
61
  {
@@ -59,7 +63,7 @@ class TestExtractAtRiskUsers:
59
63
  "rootly_user_id": "94178",
60
64
  "och_score": 55.0,
61
65
  "risk_level": "medium",
62
- "burnout_score": 2.0,
66
+ "burnout_score": 2.0, # External API field name
63
67
  "incident_count": 8,
64
68
  },
65
69
  {
@@ -67,7 +71,7 @@ class TestExtractAtRiskUsers:
67
71
  "rootly_user_id": "62208",
68
72
  "och_score": 15.0,
69
73
  "risk_level": "low",
70
- "burnout_score": 0.5,
74
+ "burnout_score": 0.5, # External API field name
71
75
  "incident_count": 2,
72
76
  },
73
77
  {
@@ -75,7 +79,7 @@ class TestExtractAtRiskUsers:
75
79
  "rootly_user_id": "12345",
76
80
  "och_score": 35.0,
77
81
  "risk_level": "moderate",
78
- "burnout_score": 1.0,
82
+ "burnout_score": 1.0, # External API field name
79
83
  "incident_count": 5,
80
84
  },
81
85
  ]
@@ -134,7 +138,7 @@ class TestExtractAtRiskUsers:
134
138
  "rootly_user_id": "99999",
135
139
  "och_score": 5.0,
136
140
  "risk_level": "low",
137
- "burnout_score": 0.1,
141
+ "burnout_score": 0.1, # External API field name
138
142
  "incident_count": 0,
139
143
  },
140
144
  ]
@@ -158,7 +162,7 @@ class TestExtractAtRiskUsers:
158
162
  assert "rootly_user_id" in user
159
163
  assert "och_score" in user
160
164
  assert "risk_level" in user
161
- assert "burnout_score" in user
165
+ assert "health_risk_score" in user
162
166
  assert "incident_count" in user
163
167
 
164
168
  def test_empty_members_list(self):
@@ -186,8 +190,8 @@ class TestExtractAtRiskUsers:
186
190
  assert safe == []
187
191
 
188
192
 
189
- class TestCheckOncallBurnoutRiskLogic:
190
- """Tests for check_oncall_burnout_risk tool logic."""
193
+ class TestCheckOncallHealthRiskLogic:
194
+ """Tests for check_oncall_health_risk tool logic."""
191
195
 
192
196
  def test_no_at_risk_users_returns_empty(self):
193
197
  """Test response when no users are above threshold."""
@@ -204,7 +208,7 @@ class TestCheckOncallBurnoutRiskLogic:
204
208
  "rootly_user_id": "123",
205
209
  "och_score": 10.0,
206
210
  "risk_level": "low",
207
- "burnout_score": 0.1,
211
+ "burnout_score": 0.1, # External API field name
208
212
  "incident_count": 1,
209
213
  }
210
214
  ]
@@ -231,7 +235,7 @@ class TestCheckOncallBurnoutRiskLogic:
231
235
  "rootly_user_id": None,
232
236
  "och_score": 80.0,
233
237
  "risk_level": "high",
234
- "burnout_score": 3.0,
238
+ "burnout_score": 3.0, # External API field name
235
239
  "incident_count": 10,
236
240
  }
237
241
  ]
@@ -246,8 +250,8 @@ class TestCheckOncallBurnoutRiskLogic:
246
250
  assert at_risk[0]["rootly_user_id"] is None
247
251
 
248
252
 
249
- class TestBurnoutRiskSummaryLogic:
250
- """Tests for burnout risk summary generation logic."""
253
+ class TestHealthRiskSummaryLogic:
254
+ """Tests for health risk summary generation logic."""
251
255
 
252
256
  def test_action_required_when_at_risk_scheduled(self):
253
257
  """Test that action_required is True when at-risk users are scheduled."""
@@ -280,3 +284,156 @@ class TestBurnoutRiskSummaryLogic:
280
284
  assert "2 at-risk user(s)" in message
281
285
  assert "64 hours" in message
282
286
  assert "Consider reassignment" in message
287
+
288
+
289
+ class TestToolRegistration:
290
+ """Tests for tool registration with correct naming."""
291
+
292
+ def test_health_risk_tool_is_registered(self):
293
+ """Verify check_oncall_health_risk tool is registered."""
294
+ with patch("rootly_mcp_server.server._load_swagger_spec") as mock_load_spec:
295
+ mock_spec = {
296
+ "openapi": "3.0.0",
297
+ "info": {"title": "Test API", "version": "1.0.0"},
298
+ "paths": {},
299
+ "components": {"schemas": {}},
300
+ }
301
+ mock_load_spec.return_value = mock_spec
302
+
303
+ from rootly_mcp_server.server import create_rootly_mcp_server
304
+
305
+ server = create_rootly_mcp_server()
306
+ assert server is not None
307
+
308
+ # Get all registered tools
309
+ tools = server._tool_manager._tools
310
+ tool_names = list(tools.keys())
311
+
312
+ # Verify new tool name exists
313
+ assert "check_oncall_health_risk" in tool_names
314
+
315
+ def test_old_burnout_tool_name_not_registered(self):
316
+ """Verify old check_oncall_burnout_risk tool name does NOT exist."""
317
+ with patch("rootly_mcp_server.server._load_swagger_spec") as mock_load_spec:
318
+ mock_spec = {
319
+ "openapi": "3.0.0",
320
+ "info": {"title": "Test API", "version": "1.0.0"},
321
+ "paths": {},
322
+ "components": {"schemas": {}},
323
+ }
324
+ mock_load_spec.return_value = mock_spec
325
+
326
+ from rootly_mcp_server.server import create_rootly_mcp_server
327
+
328
+ server = create_rootly_mcp_server()
329
+ tools = server._tool_manager._tools
330
+ tool_names = list(tools.keys())
331
+
332
+ # Verify old tool name does NOT exist
333
+ assert "check_oncall_burnout_risk" not in tool_names
334
+
335
+
336
+ class TestFieldMapping:
337
+ """Tests for field name mapping from external API to internal names."""
338
+
339
+ def test_external_api_field_mapped_to_health_risk_score(self):
340
+ """Verify external API's field is mapped to health_risk_score."""
341
+ from rootly_mcp_server.och_client import OnCallHealthClient
342
+
343
+ # Simulate external API response with their field name
344
+ api_response = {
345
+ "analysis_data": {
346
+ "team_analysis": {
347
+ "members": [
348
+ {
349
+ "user_name": "Test User",
350
+ "rootly_user_id": "123",
351
+ "och_score": 75.0,
352
+ "risk_level": "high",
353
+ "burnout_score": 3.2, # External API field
354
+ "incident_count": 10,
355
+ }
356
+ ]
357
+ }
358
+ }
359
+ }
360
+
361
+ client = OnCallHealthClient(api_key="test")
362
+ at_risk, _ = client.extract_at_risk_users(api_response)
363
+
364
+ # Verify the extracted data uses our field name
365
+ assert len(at_risk) == 1
366
+ assert "health_risk_score" in at_risk[0]
367
+ assert at_risk[0]["health_risk_score"] == 3.2
368
+
369
+ # Verify old field name is NOT in output
370
+ assert "burnout_score" not in at_risk[0]
371
+
372
+ def test_response_schema_uses_health_risk_score(self):
373
+ """Verify all extracted users have health_risk_score field."""
374
+ from rootly_mcp_server.och_client import OnCallHealthClient
375
+
376
+ api_response = {
377
+ "analysis_data": {
378
+ "team_analysis": {
379
+ "members": [
380
+ {
381
+ "user_name": "User 1",
382
+ "rootly_user_id": "1",
383
+ "och_score": 80.0,
384
+ "risk_level": "high",
385
+ "burnout_score": 3.5,
386
+ "incident_count": 15,
387
+ },
388
+ {
389
+ "user_name": "User 2",
390
+ "rootly_user_id": "2",
391
+ "och_score": 10.0,
392
+ "risk_level": "low",
393
+ "burnout_score": 0.5,
394
+ "incident_count": 2,
395
+ },
396
+ ]
397
+ }
398
+ }
399
+ }
400
+
401
+ client = OnCallHealthClient(api_key="test")
402
+ at_risk, safe = client.extract_at_risk_users(api_response)
403
+
404
+ # Check at-risk users
405
+ for user in at_risk:
406
+ assert "health_risk_score" in user
407
+ assert "burnout_score" not in user
408
+
409
+ # Check safe users
410
+ for user in safe:
411
+ assert "health_risk_score" in user
412
+ assert "burnout_score" not in user
413
+
414
+ def test_missing_external_field_defaults_to_zero(self):
415
+ """Verify missing external API field defaults to 0."""
416
+ from rootly_mcp_server.och_client import OnCallHealthClient
417
+
418
+ api_response = {
419
+ "analysis_data": {
420
+ "team_analysis": {
421
+ "members": [
422
+ {
423
+ "user_name": "User Without Score",
424
+ "rootly_user_id": "999",
425
+ "och_score": 60.0,
426
+ "risk_level": "medium",
427
+ # No burnout_score field
428
+ "incident_count": 5,
429
+ }
430
+ ]
431
+ }
432
+ }
433
+ }
434
+
435
+ client = OnCallHealthClient(api_key="test")
436
+ at_risk, _ = client.extract_at_risk_users(api_response)
437
+
438
+ assert len(at_risk) == 1
439
+ assert at_risk[0]["health_risk_score"] == 0
@@ -1658,7 +1658,7 @@ wheels = [
1658
1658
 
1659
1659
  [[package]]
1660
1660
  name = "rootly-mcp-server"
1661
- version = "2.1.0"
1661
+ version = "2.2.0"
1662
1662
  source = { editable = "." }
1663
1663
  dependencies = [
1664
1664
  { name = "brotli" },