rootly-mcp-server 2.1.2__py3-none-any.whl → 2.1.4__py3-none-any.whl
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.
- rootly_mcp_server/och_client.py +71 -0
- rootly_mcp_server/server.py +310 -1
- {rootly_mcp_server-2.1.2.dist-info → rootly_mcp_server-2.1.4.dist-info}/METADATA +38 -164
- {rootly_mcp_server-2.1.2.dist-info → rootly_mcp_server-2.1.4.dist-info}/RECORD +7 -6
- {rootly_mcp_server-2.1.2.dist-info → rootly_mcp_server-2.1.4.dist-info}/WHEEL +0 -0
- {rootly_mcp_server-2.1.2.dist-info → rootly_mcp_server-2.1.4.dist-info}/entry_points.txt +0 -0
- {rootly_mcp_server-2.1.2.dist-info → rootly_mcp_server-2.1.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""On-Call Health API client for burnout risk analysis."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OnCallHealthClient:
|
|
10
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None):
|
|
11
|
+
self.api_key: str = api_key or os.environ.get("ONCALLHEALTH_API_KEY", "")
|
|
12
|
+
self.base_url: str = base_url or os.environ.get(
|
|
13
|
+
"ONCALLHEALTH_API_URL", "https://api.oncallhealth.ai"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
async def get_analysis(self, analysis_id: int) -> dict[str, Any]:
|
|
17
|
+
"""Fetch analysis by ID."""
|
|
18
|
+
async with httpx.AsyncClient() as client:
|
|
19
|
+
response = await client.get(
|
|
20
|
+
f"{self.base_url}/analyses/{analysis_id}",
|
|
21
|
+
headers={"X-API-Key": self.api_key},
|
|
22
|
+
timeout=30.0,
|
|
23
|
+
)
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
return response.json()
|
|
26
|
+
|
|
27
|
+
async def get_latest_analysis(self) -> dict[str, Any]:
|
|
28
|
+
"""Fetch most recent analysis."""
|
|
29
|
+
async with httpx.AsyncClient() as client:
|
|
30
|
+
response = await client.get(
|
|
31
|
+
f"{self.base_url}/analyses",
|
|
32
|
+
headers={"X-API-Key": self.api_key},
|
|
33
|
+
params={"limit": 1},
|
|
34
|
+
timeout=30.0,
|
|
35
|
+
)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
data = response.json()
|
|
38
|
+
analyses = data.get("analyses", [])
|
|
39
|
+
if not analyses:
|
|
40
|
+
raise ValueError("No analyses found")
|
|
41
|
+
return analyses[0]
|
|
42
|
+
|
|
43
|
+
def extract_at_risk_users(
|
|
44
|
+
self, analysis: dict[str, Any], threshold: float = 50.0
|
|
45
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
46
|
+
"""Extract at-risk and safe users from analysis."""
|
|
47
|
+
members = analysis.get("analysis_data", {}).get("team_analysis", {}).get("members", [])
|
|
48
|
+
|
|
49
|
+
at_risk: list[dict[str, Any]] = []
|
|
50
|
+
safe: list[dict[str, Any]] = []
|
|
51
|
+
|
|
52
|
+
for member in members:
|
|
53
|
+
user_data: dict[str, Any] = {
|
|
54
|
+
"user_name": member.get("user_name"),
|
|
55
|
+
"rootly_user_id": member.get("rootly_user_id"),
|
|
56
|
+
"och_score": member.get("och_score", 0),
|
|
57
|
+
"risk_level": member.get("risk_level", "unknown"),
|
|
58
|
+
"burnout_score": member.get("burnout_score", 0),
|
|
59
|
+
"incident_count": member.get("incident_count", 0),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if user_data["och_score"] >= threshold:
|
|
63
|
+
at_risk.append(user_data)
|
|
64
|
+
elif user_data["och_score"] < 20: # Safe threshold
|
|
65
|
+
safe.append(user_data)
|
|
66
|
+
|
|
67
|
+
# Sort at-risk by score descending, safe by score ascending
|
|
68
|
+
at_risk.sort(key=lambda x: x["och_score"], reverse=True)
|
|
69
|
+
safe.sort(key=lambda x: x["och_score"])
|
|
70
|
+
|
|
71
|
+
return at_risk, safe
|
rootly_mcp_server/server.py
CHANGED
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
12
|
from copy import deepcopy
|
|
13
|
+
from datetime import datetime
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Annotated, Any
|
|
15
16
|
|
|
@@ -18,6 +19,7 @@ import requests
|
|
|
18
19
|
from fastmcp import FastMCP
|
|
19
20
|
from pydantic import Field
|
|
20
21
|
|
|
22
|
+
from .och_client import OnCallHealthClient
|
|
21
23
|
from .smart_utils import SolutionExtractor, TextSimilarityAnalyzer
|
|
22
24
|
from .utils import sanitize_parameters_in_spec
|
|
23
25
|
|
|
@@ -379,11 +381,21 @@ class AuthenticatedHTTPXClient:
|
|
|
379
381
|
return transformed
|
|
380
382
|
|
|
381
383
|
async def request(self, method: str, url: str, **kwargs):
|
|
382
|
-
"""Override request to transform parameters."""
|
|
384
|
+
"""Override request to transform parameters and ensure correct headers."""
|
|
383
385
|
# Transform query parameters
|
|
384
386
|
if "params" in kwargs:
|
|
385
387
|
kwargs["params"] = self._transform_params(kwargs["params"])
|
|
386
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
|
|
398
|
+
|
|
387
399
|
# Call the underlying client's request method and let it handle everything
|
|
388
400
|
return await self.client.request(method, url, **kwargs)
|
|
389
401
|
|
|
@@ -3155,6 +3167,303 @@ Updated: {attributes.get("updated_at", "N/A")}"""
|
|
|
3155
3167
|
"mimeType": "text/plain",
|
|
3156
3168
|
}
|
|
3157
3169
|
|
|
3170
|
+
@mcp.tool()
|
|
3171
|
+
async def check_oncall_burnout_risk(
|
|
3172
|
+
start_date: Annotated[
|
|
3173
|
+
str,
|
|
3174
|
+
Field(description="Start date for the on-call period (ISO 8601, e.g., '2026-02-09')"),
|
|
3175
|
+
],
|
|
3176
|
+
end_date: Annotated[
|
|
3177
|
+
str,
|
|
3178
|
+
Field(description="End date for the on-call period (ISO 8601, e.g., '2026-02-15')"),
|
|
3179
|
+
],
|
|
3180
|
+
och_analysis_id: Annotated[
|
|
3181
|
+
int | None,
|
|
3182
|
+
Field(
|
|
3183
|
+
description="On-Call Health analysis ID. If not provided, uses the latest analysis"
|
|
3184
|
+
),
|
|
3185
|
+
] = None,
|
|
3186
|
+
och_threshold: Annotated[
|
|
3187
|
+
float,
|
|
3188
|
+
Field(description="OCH score threshold for at-risk classification (default: 50.0)"),
|
|
3189
|
+
] = 50.0,
|
|
3190
|
+
include_replacements: Annotated[
|
|
3191
|
+
bool,
|
|
3192
|
+
Field(description="Include recommended replacement responders (default: true)"),
|
|
3193
|
+
] = True,
|
|
3194
|
+
) -> dict:
|
|
3195
|
+
"""Check if any at-risk responders (based on On-Call Health burnout analysis) are scheduled for on-call.
|
|
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.
|
|
3200
|
+
|
|
3201
|
+
Requires ONCALLHEALTH_API_KEY environment variable.
|
|
3202
|
+
"""
|
|
3203
|
+
try:
|
|
3204
|
+
# Validate OCH API key is configured
|
|
3205
|
+
if not os.environ.get("ONCALLHEALTH_API_KEY"):
|
|
3206
|
+
raise PermissionError(
|
|
3207
|
+
"ONCALLHEALTH_API_KEY environment variable required. "
|
|
3208
|
+
"Get your key from oncallhealth.ai/settings/api-keys"
|
|
3209
|
+
)
|
|
3210
|
+
|
|
3211
|
+
och_client = OnCallHealthClient()
|
|
3212
|
+
|
|
3213
|
+
# 1. Get OCH analysis (by ID or latest)
|
|
3214
|
+
try:
|
|
3215
|
+
if och_analysis_id:
|
|
3216
|
+
analysis = await och_client.get_analysis(och_analysis_id)
|
|
3217
|
+
else:
|
|
3218
|
+
analysis = await och_client.get_latest_analysis()
|
|
3219
|
+
och_analysis_id = analysis.get("id")
|
|
3220
|
+
except httpx.HTTPStatusError as e:
|
|
3221
|
+
raise ConnectionError(f"Failed to fetch On-Call Health data: {e}")
|
|
3222
|
+
except ValueError as e:
|
|
3223
|
+
raise ValueError(str(e))
|
|
3224
|
+
|
|
3225
|
+
# 2. Extract at-risk and safe users
|
|
3226
|
+
at_risk_users, safe_users = och_client.extract_at_risk_users(
|
|
3227
|
+
analysis, threshold=och_threshold
|
|
3228
|
+
)
|
|
3229
|
+
|
|
3230
|
+
if not at_risk_users:
|
|
3231
|
+
return {
|
|
3232
|
+
"period": {"start": start_date, "end": end_date},
|
|
3233
|
+
"och_analysis_id": och_analysis_id,
|
|
3234
|
+
"och_threshold": och_threshold,
|
|
3235
|
+
"at_risk_scheduled": [],
|
|
3236
|
+
"at_risk_not_scheduled": [],
|
|
3237
|
+
"recommended_replacements": [],
|
|
3238
|
+
"summary": {
|
|
3239
|
+
"total_at_risk": 0,
|
|
3240
|
+
"at_risk_scheduled": 0,
|
|
3241
|
+
"action_required": False,
|
|
3242
|
+
"message": "No users above burnout threshold.",
|
|
3243
|
+
},
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
# 3. Get shifts for the period
|
|
3247
|
+
all_shifts = []
|
|
3248
|
+
users_map = {}
|
|
3249
|
+
schedules_map = {}
|
|
3250
|
+
|
|
3251
|
+
# Fetch lookup maps
|
|
3252
|
+
lookup_users, lookup_schedules, lookup_teams = await _fetch_users_and_schedules_maps()
|
|
3253
|
+
users_map.update({str(k): v for k, v in lookup_users.items()})
|
|
3254
|
+
schedules_map.update({str(k): v for k, v in lookup_schedules.items()})
|
|
3255
|
+
|
|
3256
|
+
# Fetch shifts
|
|
3257
|
+
page = 1
|
|
3258
|
+
while page <= 10:
|
|
3259
|
+
shifts_response = await make_authenticated_request(
|
|
3260
|
+
"GET",
|
|
3261
|
+
"/v1/shifts",
|
|
3262
|
+
params={
|
|
3263
|
+
"filter[starts_at_lte]": (
|
|
3264
|
+
end_date if "T" in end_date else f"{end_date}T23:59:59Z"
|
|
3265
|
+
),
|
|
3266
|
+
"filter[ends_at_gte]": (
|
|
3267
|
+
start_date if "T" in start_date else f"{start_date}T00:00:00Z"
|
|
3268
|
+
),
|
|
3269
|
+
"page[size]": 100,
|
|
3270
|
+
"page[number]": page,
|
|
3271
|
+
"include": "user,schedule",
|
|
3272
|
+
},
|
|
3273
|
+
)
|
|
3274
|
+
if shifts_response is None:
|
|
3275
|
+
break
|
|
3276
|
+
shifts_response.raise_for_status()
|
|
3277
|
+
shifts_data = shifts_response.json()
|
|
3278
|
+
|
|
3279
|
+
shifts = shifts_data.get("data", [])
|
|
3280
|
+
included = shifts_data.get("included", [])
|
|
3281
|
+
|
|
3282
|
+
for resource in included:
|
|
3283
|
+
if resource.get("type") == "users":
|
|
3284
|
+
users_map[str(resource.get("id"))] = resource
|
|
3285
|
+
elif resource.get("type") == "schedules":
|
|
3286
|
+
schedules_map[str(resource.get("id"))] = resource
|
|
3287
|
+
|
|
3288
|
+
if not shifts:
|
|
3289
|
+
break
|
|
3290
|
+
|
|
3291
|
+
all_shifts.extend(shifts)
|
|
3292
|
+
|
|
3293
|
+
meta = shifts_data.get("meta", {})
|
|
3294
|
+
total_pages = meta.get("total_pages", 1)
|
|
3295
|
+
if page >= total_pages:
|
|
3296
|
+
break
|
|
3297
|
+
page += 1
|
|
3298
|
+
|
|
3299
|
+
# 5. Correlate: which at-risk users are scheduled?
|
|
3300
|
+
at_risk_scheduled = []
|
|
3301
|
+
at_risk_not_scheduled = []
|
|
3302
|
+
|
|
3303
|
+
for user in at_risk_users:
|
|
3304
|
+
rootly_id = user.get("rootly_user_id")
|
|
3305
|
+
if not rootly_id:
|
|
3306
|
+
continue
|
|
3307
|
+
|
|
3308
|
+
rootly_id_str = str(rootly_id)
|
|
3309
|
+
|
|
3310
|
+
# Find shifts for this user
|
|
3311
|
+
user_shifts = []
|
|
3312
|
+
for shift in all_shifts:
|
|
3313
|
+
relationships = shift.get("relationships", {})
|
|
3314
|
+
user_rel = relationships.get("user", {}).get("data") or {}
|
|
3315
|
+
shift_user_id = str(user_rel.get("id", ""))
|
|
3316
|
+
|
|
3317
|
+
if shift_user_id == rootly_id_str:
|
|
3318
|
+
attrs = shift.get("attributes", {})
|
|
3319
|
+
schedule_rel = relationships.get("schedule", {}).get("data") or {}
|
|
3320
|
+
schedule_id = str(schedule_rel.get("id", ""))
|
|
3321
|
+
schedule_info = schedules_map.get(schedule_id, {})
|
|
3322
|
+
schedule_name = schedule_info.get("attributes", {}).get("name", "Unknown")
|
|
3323
|
+
|
|
3324
|
+
starts_at = attrs.get("starts_at")
|
|
3325
|
+
ends_at = attrs.get("ends_at")
|
|
3326
|
+
hours = 0.0
|
|
3327
|
+
if starts_at and ends_at:
|
|
3328
|
+
try:
|
|
3329
|
+
start_dt = datetime.fromisoformat(starts_at.replace("Z", "+00:00"))
|
|
3330
|
+
end_dt = datetime.fromisoformat(ends_at.replace("Z", "+00:00"))
|
|
3331
|
+
hours = (end_dt - start_dt).total_seconds() / 3600
|
|
3332
|
+
except (ValueError, AttributeError):
|
|
3333
|
+
pass
|
|
3334
|
+
|
|
3335
|
+
user_shifts.append(
|
|
3336
|
+
{
|
|
3337
|
+
"schedule_id": schedule_id,
|
|
3338
|
+
"schedule_name": schedule_name,
|
|
3339
|
+
"starts_at": starts_at,
|
|
3340
|
+
"ends_at": ends_at,
|
|
3341
|
+
"hours": round(hours, 1),
|
|
3342
|
+
}
|
|
3343
|
+
)
|
|
3344
|
+
|
|
3345
|
+
if user_shifts:
|
|
3346
|
+
total_hours = sum(s["hours"] for s in user_shifts)
|
|
3347
|
+
at_risk_scheduled.append(
|
|
3348
|
+
{
|
|
3349
|
+
"user_name": user["user_name"],
|
|
3350
|
+
"user_id": int(rootly_id),
|
|
3351
|
+
"och_score": user["och_score"],
|
|
3352
|
+
"risk_level": user["risk_level"],
|
|
3353
|
+
"burnout_score": user["burnout_score"],
|
|
3354
|
+
"total_hours": round(total_hours, 1),
|
|
3355
|
+
"shifts": user_shifts,
|
|
3356
|
+
}
|
|
3357
|
+
)
|
|
3358
|
+
else:
|
|
3359
|
+
at_risk_not_scheduled.append(
|
|
3360
|
+
{
|
|
3361
|
+
"user_name": user["user_name"],
|
|
3362
|
+
"user_id": int(rootly_id) if rootly_id else None,
|
|
3363
|
+
"och_score": user["och_score"],
|
|
3364
|
+
"risk_level": user["risk_level"],
|
|
3365
|
+
}
|
|
3366
|
+
)
|
|
3367
|
+
|
|
3368
|
+
# 6. Get recommended replacements (if requested)
|
|
3369
|
+
recommended_replacements = []
|
|
3370
|
+
if include_replacements and safe_users:
|
|
3371
|
+
safe_rootly_ids = [
|
|
3372
|
+
str(u["rootly_user_id"]) for u in safe_users[:10] if u.get("rootly_user_id")
|
|
3373
|
+
]
|
|
3374
|
+
|
|
3375
|
+
if safe_rootly_ids:
|
|
3376
|
+
# Calculate current hours for safe users
|
|
3377
|
+
for user in safe_users[:5]:
|
|
3378
|
+
rootly_id = user.get("rootly_user_id")
|
|
3379
|
+
if not rootly_id:
|
|
3380
|
+
continue
|
|
3381
|
+
|
|
3382
|
+
rootly_id_str = str(rootly_id)
|
|
3383
|
+
user_hours = 0.0
|
|
3384
|
+
|
|
3385
|
+
for shift in all_shifts:
|
|
3386
|
+
relationships = shift.get("relationships", {})
|
|
3387
|
+
user_rel = relationships.get("user", {}).get("data") or {}
|
|
3388
|
+
shift_user_id = str(user_rel.get("id", ""))
|
|
3389
|
+
|
|
3390
|
+
if shift_user_id == rootly_id_str:
|
|
3391
|
+
attrs = shift.get("attributes", {})
|
|
3392
|
+
starts_at = attrs.get("starts_at")
|
|
3393
|
+
ends_at = attrs.get("ends_at")
|
|
3394
|
+
if starts_at and ends_at:
|
|
3395
|
+
try:
|
|
3396
|
+
start_dt = datetime.fromisoformat(
|
|
3397
|
+
starts_at.replace("Z", "+00:00")
|
|
3398
|
+
)
|
|
3399
|
+
end_dt = datetime.fromisoformat(
|
|
3400
|
+
ends_at.replace("Z", "+00:00")
|
|
3401
|
+
)
|
|
3402
|
+
user_hours += (end_dt - start_dt).total_seconds() / 3600
|
|
3403
|
+
except (ValueError, AttributeError):
|
|
3404
|
+
pass
|
|
3405
|
+
|
|
3406
|
+
recommended_replacements.append(
|
|
3407
|
+
{
|
|
3408
|
+
"user_name": user["user_name"],
|
|
3409
|
+
"user_id": int(rootly_id),
|
|
3410
|
+
"och_score": user["och_score"],
|
|
3411
|
+
"risk_level": user["risk_level"],
|
|
3412
|
+
"current_hours_in_period": round(user_hours, 1),
|
|
3413
|
+
}
|
|
3414
|
+
)
|
|
3415
|
+
|
|
3416
|
+
# 7. Build summary
|
|
3417
|
+
total_scheduled_hours = sum(u["total_hours"] for u in at_risk_scheduled)
|
|
3418
|
+
action_required = len(at_risk_scheduled) > 0
|
|
3419
|
+
|
|
3420
|
+
if action_required:
|
|
3421
|
+
message = (
|
|
3422
|
+
f"{len(at_risk_scheduled)} at-risk user(s) scheduled for "
|
|
3423
|
+
f"{total_scheduled_hours} hours. Consider reassignment."
|
|
3424
|
+
)
|
|
3425
|
+
else:
|
|
3426
|
+
message = "No at-risk users are scheduled for the period."
|
|
3427
|
+
|
|
3428
|
+
return {
|
|
3429
|
+
"period": {"start": start_date, "end": end_date},
|
|
3430
|
+
"och_analysis_id": och_analysis_id,
|
|
3431
|
+
"och_threshold": och_threshold,
|
|
3432
|
+
"at_risk_scheduled": at_risk_scheduled,
|
|
3433
|
+
"at_risk_not_scheduled": at_risk_not_scheduled,
|
|
3434
|
+
"recommended_replacements": recommended_replacements,
|
|
3435
|
+
"summary": {
|
|
3436
|
+
"total_at_risk": len(at_risk_users),
|
|
3437
|
+
"at_risk_scheduled": len(at_risk_scheduled),
|
|
3438
|
+
"action_required": action_required,
|
|
3439
|
+
"message": message,
|
|
3440
|
+
},
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
except PermissionError as e:
|
|
3444
|
+
return MCPError.tool_error(str(e), "permission_error")
|
|
3445
|
+
except ConnectionError as e:
|
|
3446
|
+
return MCPError.tool_error(str(e), "connection_error")
|
|
3447
|
+
except ValueError as e:
|
|
3448
|
+
return MCPError.tool_error(str(e), "validation_error")
|
|
3449
|
+
except Exception as e:
|
|
3450
|
+
import traceback
|
|
3451
|
+
|
|
3452
|
+
error_type, error_message = MCPError.categorize_error(e)
|
|
3453
|
+
return MCPError.tool_error(
|
|
3454
|
+
f"Failed to check burnout risk: {error_message}",
|
|
3455
|
+
error_type,
|
|
3456
|
+
details={
|
|
3457
|
+
"params": {
|
|
3458
|
+
"start_date": start_date,
|
|
3459
|
+
"end_date": end_date,
|
|
3460
|
+
"och_analysis_id": och_analysis_id,
|
|
3461
|
+
},
|
|
3462
|
+
"exception_type": type(e).__name__,
|
|
3463
|
+
"traceback": traceback.format_exc(),
|
|
3464
|
+
},
|
|
3465
|
+
)
|
|
3466
|
+
|
|
3158
3467
|
# Log server creation (tool count will be shown when tools are accessed)
|
|
3159
3468
|
logger.info("Created Rootly MCP Server successfully")
|
|
3160
3469
|
return mcp
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rootly-mcp-server
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
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
|
|
@@ -113,27 +113,6 @@ Configure your MCP-compatible editor (tested with Cursor) with one of the config
|
|
|
113
113
|
}
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
To customize `allowed_paths` and access additional Rootly API paths, clone the repository and use this configuration:
|
|
117
|
-
|
|
118
|
-
```json
|
|
119
|
-
{
|
|
120
|
-
"mcpServers": {
|
|
121
|
-
"rootly": {
|
|
122
|
-
"command": "uv",
|
|
123
|
-
"args": [
|
|
124
|
-
"run",
|
|
125
|
-
"--directory",
|
|
126
|
-
"/path/to/rootly-mcp-server",
|
|
127
|
-
"rootly-mcp-server"
|
|
128
|
-
],
|
|
129
|
-
"env": {
|
|
130
|
-
"ROOTLY_API_TOKEN": "<YOUR_ROOTLY_API_TOKEN>"
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
116
|
### Connect to Hosted MCP Server
|
|
138
117
|
|
|
139
118
|
Alternatively, connect directly to our hosted MCP server:
|
|
@@ -168,6 +147,41 @@ Alternatively, connect directly to our hosted MCP server:
|
|
|
168
147
|
- **`suggest_solutions`**: Mines past incident resolutions to recommend actionable solutions
|
|
169
148
|
- **MCP Resources**: Exposes incident and team data as structured resources for easy AI reference
|
|
170
149
|
- **Intelligent Pattern Recognition**: Automatically identifies services, error types, and resolution patterns
|
|
150
|
+
- **On-Call Health Integration**: Detects burnout risk in scheduled responders
|
|
151
|
+
|
|
152
|
+
## On-Call Health Integration
|
|
153
|
+
|
|
154
|
+
Rootly MCP integrates with [On-Call Health](https://oncallhealth.ai) to detect burnout risk in scheduled responders.
|
|
155
|
+
|
|
156
|
+
### Setup
|
|
157
|
+
|
|
158
|
+
Set the `ONCALLHEALTH_API_KEY` environment variable:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"mcpServers": {
|
|
163
|
+
"rootly": {
|
|
164
|
+
"command": "uvx",
|
|
165
|
+
"args": ["rootly-mcp-server"],
|
|
166
|
+
"env": {
|
|
167
|
+
"ROOTLY_API_TOKEN": "your_rootly_token",
|
|
168
|
+
"ONCALLHEALTH_API_KEY": "och_live_your_key"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Usage
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
check_oncall_burnout_risk(
|
|
179
|
+
start_date="2026-02-09",
|
|
180
|
+
end_date="2026-02-15"
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Returns at-risk users who are scheduled, recommended safe replacements, and action summaries.
|
|
171
185
|
|
|
172
186
|
## Example Skills
|
|
173
187
|
|
|
@@ -196,103 +210,6 @@ cp examples/skills/rootly-incident-responder.md .claude/skills/
|
|
|
196
210
|
|
|
197
211
|
This skill demonstrates a complete incident response workflow using Rootly's intelligent tools combined with GitHub integration for code correlation.
|
|
198
212
|
|
|
199
|
-
### Available Tools
|
|
200
|
-
|
|
201
|
-
**Alerts**
|
|
202
|
-
- `listIncidentAlerts`
|
|
203
|
-
- `listAlerts`
|
|
204
|
-
- `attachAlert`
|
|
205
|
-
- `createAlert`
|
|
206
|
-
|
|
207
|
-
**Environments**
|
|
208
|
-
- `listEnvironments`
|
|
209
|
-
- `createEnvironment`
|
|
210
|
-
|
|
211
|
-
**Functionalities**
|
|
212
|
-
- `listFunctionalities`
|
|
213
|
-
- `createFunctionality`
|
|
214
|
-
|
|
215
|
-
**Workflows**
|
|
216
|
-
- `listWorkflows`
|
|
217
|
-
- `createWorkflow`
|
|
218
|
-
|
|
219
|
-
**Incidents**
|
|
220
|
-
- `listIncidentActionItems`
|
|
221
|
-
- `createIncidentActionItem`
|
|
222
|
-
- `listIncident_Types`
|
|
223
|
-
- `createIncidentType`
|
|
224
|
-
- `search_incidents`
|
|
225
|
-
- `find_related_incidents`
|
|
226
|
-
- `suggest_solutions`
|
|
227
|
-
|
|
228
|
-
**On-Call**
|
|
229
|
-
- `get_oncall_shift_metrics`
|
|
230
|
-
- `get_oncall_handoff_summary`
|
|
231
|
-
- `get_shift_incidents`
|
|
232
|
-
|
|
233
|
-
**Services & Severities**
|
|
234
|
-
- `listServices`
|
|
235
|
-
- `createService`
|
|
236
|
-
- `listSeverities`
|
|
237
|
-
- `createSeverity`
|
|
238
|
-
|
|
239
|
-
**Teams & Users**
|
|
240
|
-
- `listTeams`
|
|
241
|
-
- `createTeam`
|
|
242
|
-
- `listUsers`
|
|
243
|
-
- `getCurrentUser`
|
|
244
|
-
|
|
245
|
-
**Meta**
|
|
246
|
-
- `list_endpoints`
|
|
247
|
-
|
|
248
|
-
### Why Path Limiting?
|
|
249
|
-
|
|
250
|
-
We limit exposed API paths for two key reasons:
|
|
251
|
-
|
|
252
|
-
1. **Context Management**: Rootly's comprehensive API can overwhelm AI agents, affecting their ability to perform simple tasks effectively
|
|
253
|
-
2. **Security**: Controls which information and actions are accessible through the MCP server
|
|
254
|
-
|
|
255
|
-
To expose additional paths, modify the `allowed_paths` variable in `src/rootly_mcp_server/server.py`.
|
|
256
|
-
|
|
257
|
-
### Smart Analysis Tools
|
|
258
|
-
|
|
259
|
-
The MCP server includes intelligent tools that analyze historical incident data to provide actionable insights:
|
|
260
|
-
|
|
261
|
-
#### `find_related_incidents`
|
|
262
|
-
Finds historically similar incidents using text similarity analysis:
|
|
263
|
-
```
|
|
264
|
-
find_related_incidents(incident_id="12345", similarity_threshold=0.15, max_results=5)
|
|
265
|
-
```
|
|
266
|
-
- **Input**: Incident ID, similarity threshold (0.0-1.0), max results
|
|
267
|
-
- **Output**: Similar incidents with confidence scores, matched services, and resolution times
|
|
268
|
-
- **Use Case**: Get context from past incidents to understand patterns and solutions
|
|
269
|
-
|
|
270
|
-
#### `suggest_solutions`
|
|
271
|
-
Recommends solutions by analyzing how similar incidents were resolved:
|
|
272
|
-
```
|
|
273
|
-
suggest_solutions(incident_id="12345", max_solutions=3)
|
|
274
|
-
# OR for new incidents:
|
|
275
|
-
suggest_solutions(incident_title="Payment API errors", incident_description="Users getting 500 errors during checkout")
|
|
276
|
-
```
|
|
277
|
-
- **Input**: Either incident ID OR title/description text
|
|
278
|
-
- **Output**: Actionable solution recommendations with confidence scores and time estimates
|
|
279
|
-
- **Use Case**: Get intelligent suggestions based on successful past resolutions
|
|
280
|
-
|
|
281
|
-
#### How It Works
|
|
282
|
-
- **Text Similarity**: Uses TF-IDF vectorization and cosine similarity (scikit-learn)
|
|
283
|
-
- **Service Detection**: Automatically identifies affected services from incident text
|
|
284
|
-
- **Pattern Recognition**: Finds common error types, resolution patterns, and time estimates
|
|
285
|
-
- **Fallback Mode**: Works without ML libraries using keyword-based similarity
|
|
286
|
-
- **Solution Mining**: Extracts actionable steps from resolution summaries
|
|
287
|
-
|
|
288
|
-
#### Data Requirements
|
|
289
|
-
For optimal results, ensure your Rootly incidents have descriptive:
|
|
290
|
-
- **Titles**: Clear, specific incident descriptions
|
|
291
|
-
- **Summaries**: Detailed resolution steps when closing incidents
|
|
292
|
-
- **Service Tags**: Proper service identification
|
|
293
|
-
|
|
294
|
-
Example good resolution summary: `"Restarted auth-service, cleared Redis cache, and increased connection pool from 10 to 50"`
|
|
295
|
-
|
|
296
213
|
### On-Call Shift Metrics
|
|
297
214
|
|
|
298
215
|
Get on-call shift metrics for any time period, grouped by user, team, or schedule. Includes primary/secondary role tracking, shift counts, hours, and days on-call.
|
|
@@ -344,52 +261,9 @@ get_shift_incidents(
|
|
|
344
261
|
Returns: `incidents` list + `summary` (counts, avg resolution time, grouping)
|
|
345
262
|
|
|
346
263
|
|
|
347
|
-
##
|
|
348
|
-
|
|
349
|
-
### Prerequisites
|
|
350
|
-
- Python 3.12 or higher
|
|
351
|
-
- [`uv`](https://github.com/astral-sh/uv) for dependency management
|
|
352
|
-
|
|
353
|
-
### 1. Set Up Virtual Environment
|
|
354
|
-
|
|
355
|
-
Create and activate a virtual environment:
|
|
356
|
-
|
|
357
|
-
```bash
|
|
358
|
-
uv venv .venv
|
|
359
|
-
source .venv/bin/activate # Always activate before running scripts
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### 2. Install Dependencies
|
|
363
|
-
|
|
364
|
-
Install all project dependencies:
|
|
365
|
-
|
|
366
|
-
```bash
|
|
367
|
-
uv pip install .
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
To add new dependencies during development:
|
|
371
|
-
```bash
|
|
372
|
-
uv pip install <package>
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### 3. Set Up Git Hooks (Recommended for Contributors)
|
|
376
|
-
|
|
377
|
-
Install pre-commit hooks to automatically run linting and tests before commits:
|
|
378
|
-
|
|
379
|
-
```bash
|
|
380
|
-
./scripts/setup-hooks.sh
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
This ensures code quality by running:
|
|
384
|
-
- Ruff linting
|
|
385
|
-
- Pyright type checking
|
|
386
|
-
- Unit tests
|
|
387
|
-
|
|
388
|
-
### 4. Verify Installation
|
|
389
|
-
|
|
390
|
-
The server should now be ready to use with your MCP-compatible editor.
|
|
264
|
+
## Contributing
|
|
391
265
|
|
|
392
|
-
|
|
266
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for developer setup and guidelines.
|
|
393
267
|
|
|
394
268
|
## Play with it on Postman
|
|
395
269
|
[<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://god.gw.postman.com/run-collection/45004446-1074ba3c-44fe-40e3-a932-af7c071b96eb?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D45004446-1074ba3c-44fe-40e3-a932-af7c071b96eb%26entityType%3Dcollection%26workspaceId%3D4bec6e3c-50a0-4746-85f1-00a703c32f24)
|
|
@@ -3,16 +3,17 @@ rootly_mcp_server/__main__.py,sha256=mt74vaOpfHnX5rTO0CFAeulatR_9K3NBNCaLAhBLxlc
|
|
|
3
3
|
rootly_mcp_server/client.py,sha256=Qca2R9cgBxXcyobQj4RHl8gdxLB4Jphq0RIr61DAVKw,6542
|
|
4
4
|
rootly_mcp_server/exceptions.py,sha256=67J_wlfOICg87eUipbkARzn_6u_Io82L-5cVnk2UPr0,4504
|
|
5
5
|
rootly_mcp_server/monitoring.py,sha256=k1X7vK65FOTrCrOsLUXrFm6AJxKpXt_a0PzL6xdPuVU,11681
|
|
6
|
+
rootly_mcp_server/och_client.py,sha256=tz_Qw8NwAmrlSjtl-V6WNVChiIofyur06YJZRfYotKE,2737
|
|
6
7
|
rootly_mcp_server/pagination.py,sha256=2hZSO4DLUEJZbdF8oDfIt2_7X_XGBG1jIxN8VGmeJBE,2420
|
|
7
8
|
rootly_mcp_server/security.py,sha256=YkMoVALZ3XaKnMu3yF5kVf3SW_jdKHllSMwVLk1OlX0,11556
|
|
8
|
-
rootly_mcp_server/server.py,sha256=
|
|
9
|
+
rootly_mcp_server/server.py,sha256=FymvgcoKBCj_ULM1fw8fSd6vXq71ys1Tg1kihb1fjT4,163851
|
|
9
10
|
rootly_mcp_server/smart_utils.py,sha256=c7S-8H151GfmDw6dZBDdLH_cCmR1qiXkKEYSKc0WwUY,23481
|
|
10
11
|
rootly_mcp_server/texttest.json,sha256=KV9m13kWugmW1VEpU80Irp50uCcLgJtV1YT-JzMogQg,154182
|
|
11
12
|
rootly_mcp_server/utils.py,sha256=TWG1MaaFKrU1phRhU6FgHuZAEv91JOe_1w0L2OrPJMY,4406
|
|
12
13
|
rootly_mcp_server/validators.py,sha256=z1Lvel2SpOFLo1cPdQGSrX2ySt6zqR42w0R6QV9c2Cc,4092
|
|
13
14
|
rootly_mcp_server/data/__init__.py,sha256=KdWD6hiRssHXt0Ywgj3wjNHY1sx-XSPEqVHqrTArf54,143
|
|
14
|
-
rootly_mcp_server-2.1.
|
|
15
|
-
rootly_mcp_server-2.1.
|
|
16
|
-
rootly_mcp_server-2.1.
|
|
17
|
-
rootly_mcp_server-2.1.
|
|
18
|
-
rootly_mcp_server-2.1.
|
|
15
|
+
rootly_mcp_server-2.1.4.dist-info/METADATA,sha256=SmppK2hXkVEav1lWhENMOv-LVsiRkOkbgeGIz9TVskk,9892
|
|
16
|
+
rootly_mcp_server-2.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
rootly_mcp_server-2.1.4.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
|
|
18
|
+
rootly_mcp_server-2.1.4.dist-info/licenses/LICENSE,sha256=c9w9ZZGl14r54tsP40oaq5adTVX_HMNHozPIH2ymzmw,11341
|
|
19
|
+
rootly_mcp_server-2.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|