rootly-mcp-server 2.1.2__py3-none-any.whl → 2.1.3__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.
@@ -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
@@ -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
 
@@ -3155,6 +3157,303 @@ Updated: {attributes.get("updated_at", "N/A")}"""
3155
3157
  "mimeType": "text/plain",
3156
3158
  }
3157
3159
 
3160
+ @mcp.tool()
3161
+ async def check_oncall_burnout_risk(
3162
+ start_date: Annotated[
3163
+ str,
3164
+ Field(description="Start date for the on-call period (ISO 8601, e.g., '2026-02-09')"),
3165
+ ],
3166
+ end_date: Annotated[
3167
+ str,
3168
+ Field(description="End date for the on-call period (ISO 8601, e.g., '2026-02-15')"),
3169
+ ],
3170
+ och_analysis_id: Annotated[
3171
+ int | None,
3172
+ Field(
3173
+ description="On-Call Health analysis ID. If not provided, uses the latest analysis"
3174
+ ),
3175
+ ] = None,
3176
+ och_threshold: Annotated[
3177
+ float,
3178
+ Field(description="OCH score threshold for at-risk classification (default: 50.0)"),
3179
+ ] = 50.0,
3180
+ include_replacements: Annotated[
3181
+ bool,
3182
+ Field(description="Include recommended replacement responders (default: true)"),
3183
+ ] = True,
3184
+ ) -> dict:
3185
+ """Check if any at-risk responders (based on On-Call Health burnout analysis) are scheduled for on-call.
3186
+
3187
+ Integrates with On-Call Health (oncallhealth.ai) to identify responders at risk of burnout
3188
+ and checks if they are scheduled during the specified period. Optionally recommends
3189
+ safe replacement responders.
3190
+
3191
+ Requires ONCALLHEALTH_API_KEY environment variable.
3192
+ """
3193
+ try:
3194
+ # Validate OCH API key is configured
3195
+ if not os.environ.get("ONCALLHEALTH_API_KEY"):
3196
+ raise PermissionError(
3197
+ "ONCALLHEALTH_API_KEY environment variable required. "
3198
+ "Get your key from oncallhealth.ai/settings/api-keys"
3199
+ )
3200
+
3201
+ och_client = OnCallHealthClient()
3202
+
3203
+ # 1. Get OCH analysis (by ID or latest)
3204
+ try:
3205
+ if och_analysis_id:
3206
+ analysis = await och_client.get_analysis(och_analysis_id)
3207
+ else:
3208
+ analysis = await och_client.get_latest_analysis()
3209
+ och_analysis_id = analysis.get("id")
3210
+ except httpx.HTTPStatusError as e:
3211
+ raise ConnectionError(f"Failed to fetch On-Call Health data: {e}")
3212
+ except ValueError as e:
3213
+ raise ValueError(str(e))
3214
+
3215
+ # 2. Extract at-risk and safe users
3216
+ at_risk_users, safe_users = och_client.extract_at_risk_users(
3217
+ analysis, threshold=och_threshold
3218
+ )
3219
+
3220
+ if not at_risk_users:
3221
+ return {
3222
+ "period": {"start": start_date, "end": end_date},
3223
+ "och_analysis_id": och_analysis_id,
3224
+ "och_threshold": och_threshold,
3225
+ "at_risk_scheduled": [],
3226
+ "at_risk_not_scheduled": [],
3227
+ "recommended_replacements": [],
3228
+ "summary": {
3229
+ "total_at_risk": 0,
3230
+ "at_risk_scheduled": 0,
3231
+ "action_required": False,
3232
+ "message": "No users above burnout threshold.",
3233
+ },
3234
+ }
3235
+
3236
+ # 3. Get shifts for the period
3237
+ all_shifts = []
3238
+ users_map = {}
3239
+ schedules_map = {}
3240
+
3241
+ # Fetch lookup maps
3242
+ lookup_users, lookup_schedules, lookup_teams = await _fetch_users_and_schedules_maps()
3243
+ users_map.update({str(k): v for k, v in lookup_users.items()})
3244
+ schedules_map.update({str(k): v for k, v in lookup_schedules.items()})
3245
+
3246
+ # Fetch shifts
3247
+ page = 1
3248
+ while page <= 10:
3249
+ shifts_response = await make_authenticated_request(
3250
+ "GET",
3251
+ "/v1/shifts",
3252
+ params={
3253
+ "filter[starts_at_lte]": (
3254
+ end_date if "T" in end_date else f"{end_date}T23:59:59Z"
3255
+ ),
3256
+ "filter[ends_at_gte]": (
3257
+ start_date if "T" in start_date else f"{start_date}T00:00:00Z"
3258
+ ),
3259
+ "page[size]": 100,
3260
+ "page[number]": page,
3261
+ "include": "user,schedule",
3262
+ },
3263
+ )
3264
+ if shifts_response is None:
3265
+ break
3266
+ shifts_response.raise_for_status()
3267
+ shifts_data = shifts_response.json()
3268
+
3269
+ shifts = shifts_data.get("data", [])
3270
+ included = shifts_data.get("included", [])
3271
+
3272
+ for resource in included:
3273
+ if resource.get("type") == "users":
3274
+ users_map[str(resource.get("id"))] = resource
3275
+ elif resource.get("type") == "schedules":
3276
+ schedules_map[str(resource.get("id"))] = resource
3277
+
3278
+ if not shifts:
3279
+ break
3280
+
3281
+ all_shifts.extend(shifts)
3282
+
3283
+ meta = shifts_data.get("meta", {})
3284
+ total_pages = meta.get("total_pages", 1)
3285
+ if page >= total_pages:
3286
+ break
3287
+ page += 1
3288
+
3289
+ # 5. Correlate: which at-risk users are scheduled?
3290
+ at_risk_scheduled = []
3291
+ at_risk_not_scheduled = []
3292
+
3293
+ for user in at_risk_users:
3294
+ rootly_id = user.get("rootly_user_id")
3295
+ if not rootly_id:
3296
+ continue
3297
+
3298
+ rootly_id_str = str(rootly_id)
3299
+
3300
+ # Find shifts for this user
3301
+ user_shifts = []
3302
+ for shift in all_shifts:
3303
+ relationships = shift.get("relationships", {})
3304
+ user_rel = relationships.get("user", {}).get("data") or {}
3305
+ shift_user_id = str(user_rel.get("id", ""))
3306
+
3307
+ if shift_user_id == rootly_id_str:
3308
+ attrs = shift.get("attributes", {})
3309
+ schedule_rel = relationships.get("schedule", {}).get("data") or {}
3310
+ schedule_id = str(schedule_rel.get("id", ""))
3311
+ schedule_info = schedules_map.get(schedule_id, {})
3312
+ schedule_name = schedule_info.get("attributes", {}).get("name", "Unknown")
3313
+
3314
+ starts_at = attrs.get("starts_at")
3315
+ ends_at = attrs.get("ends_at")
3316
+ hours = 0.0
3317
+ if starts_at and ends_at:
3318
+ try:
3319
+ start_dt = datetime.fromisoformat(starts_at.replace("Z", "+00:00"))
3320
+ end_dt = datetime.fromisoformat(ends_at.replace("Z", "+00:00"))
3321
+ hours = (end_dt - start_dt).total_seconds() / 3600
3322
+ except (ValueError, AttributeError):
3323
+ pass
3324
+
3325
+ user_shifts.append(
3326
+ {
3327
+ "schedule_id": schedule_id,
3328
+ "schedule_name": schedule_name,
3329
+ "starts_at": starts_at,
3330
+ "ends_at": ends_at,
3331
+ "hours": round(hours, 1),
3332
+ }
3333
+ )
3334
+
3335
+ if user_shifts:
3336
+ total_hours = sum(s["hours"] for s in user_shifts)
3337
+ at_risk_scheduled.append(
3338
+ {
3339
+ "user_name": user["user_name"],
3340
+ "user_id": int(rootly_id),
3341
+ "och_score": user["och_score"],
3342
+ "risk_level": user["risk_level"],
3343
+ "burnout_score": user["burnout_score"],
3344
+ "total_hours": round(total_hours, 1),
3345
+ "shifts": user_shifts,
3346
+ }
3347
+ )
3348
+ else:
3349
+ at_risk_not_scheduled.append(
3350
+ {
3351
+ "user_name": user["user_name"],
3352
+ "user_id": int(rootly_id) if rootly_id else None,
3353
+ "och_score": user["och_score"],
3354
+ "risk_level": user["risk_level"],
3355
+ }
3356
+ )
3357
+
3358
+ # 6. Get recommended replacements (if requested)
3359
+ recommended_replacements = []
3360
+ if include_replacements and safe_users:
3361
+ safe_rootly_ids = [
3362
+ str(u["rootly_user_id"]) for u in safe_users[:10] if u.get("rootly_user_id")
3363
+ ]
3364
+
3365
+ if safe_rootly_ids:
3366
+ # Calculate current hours for safe users
3367
+ for user in safe_users[:5]:
3368
+ rootly_id = user.get("rootly_user_id")
3369
+ if not rootly_id:
3370
+ continue
3371
+
3372
+ rootly_id_str = str(rootly_id)
3373
+ user_hours = 0.0
3374
+
3375
+ for shift in all_shifts:
3376
+ relationships = shift.get("relationships", {})
3377
+ user_rel = relationships.get("user", {}).get("data") or {}
3378
+ shift_user_id = str(user_rel.get("id", ""))
3379
+
3380
+ if shift_user_id == rootly_id_str:
3381
+ attrs = shift.get("attributes", {})
3382
+ starts_at = attrs.get("starts_at")
3383
+ ends_at = attrs.get("ends_at")
3384
+ if starts_at and ends_at:
3385
+ try:
3386
+ start_dt = datetime.fromisoformat(
3387
+ starts_at.replace("Z", "+00:00")
3388
+ )
3389
+ end_dt = datetime.fromisoformat(
3390
+ ends_at.replace("Z", "+00:00")
3391
+ )
3392
+ user_hours += (end_dt - start_dt).total_seconds() / 3600
3393
+ except (ValueError, AttributeError):
3394
+ pass
3395
+
3396
+ recommended_replacements.append(
3397
+ {
3398
+ "user_name": user["user_name"],
3399
+ "user_id": int(rootly_id),
3400
+ "och_score": user["och_score"],
3401
+ "risk_level": user["risk_level"],
3402
+ "current_hours_in_period": round(user_hours, 1),
3403
+ }
3404
+ )
3405
+
3406
+ # 7. Build summary
3407
+ total_scheduled_hours = sum(u["total_hours"] for u in at_risk_scheduled)
3408
+ action_required = len(at_risk_scheduled) > 0
3409
+
3410
+ if action_required:
3411
+ message = (
3412
+ f"{len(at_risk_scheduled)} at-risk user(s) scheduled for "
3413
+ f"{total_scheduled_hours} hours. Consider reassignment."
3414
+ )
3415
+ else:
3416
+ message = "No at-risk users are scheduled for the period."
3417
+
3418
+ return {
3419
+ "period": {"start": start_date, "end": end_date},
3420
+ "och_analysis_id": och_analysis_id,
3421
+ "och_threshold": och_threshold,
3422
+ "at_risk_scheduled": at_risk_scheduled,
3423
+ "at_risk_not_scheduled": at_risk_not_scheduled,
3424
+ "recommended_replacements": recommended_replacements,
3425
+ "summary": {
3426
+ "total_at_risk": len(at_risk_users),
3427
+ "at_risk_scheduled": len(at_risk_scheduled),
3428
+ "action_required": action_required,
3429
+ "message": message,
3430
+ },
3431
+ }
3432
+
3433
+ except PermissionError as e:
3434
+ return MCPError.tool_error(str(e), "permission_error")
3435
+ except ConnectionError as e:
3436
+ return MCPError.tool_error(str(e), "connection_error")
3437
+ except ValueError as e:
3438
+ return MCPError.tool_error(str(e), "validation_error")
3439
+ except Exception as e:
3440
+ import traceback
3441
+
3442
+ error_type, error_message = MCPError.categorize_error(e)
3443
+ return MCPError.tool_error(
3444
+ f"Failed to check burnout risk: {error_message}",
3445
+ error_type,
3446
+ details={
3447
+ "params": {
3448
+ "start_date": start_date,
3449
+ "end_date": end_date,
3450
+ "och_analysis_id": och_analysis_id,
3451
+ },
3452
+ "exception_type": type(e).__name__,
3453
+ "traceback": traceback.format_exc(),
3454
+ },
3455
+ )
3456
+
3158
3457
  # Log server creation (tool count will be shown when tools are accessed)
3159
3458
  logger.info("Created Rootly MCP Server successfully")
3160
3459
  return mcp
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.1.2
3
+ Version: 2.1.3
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
- ## Developer Setup & Troubleshooting
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
- **For developers:** Additional testing tools are available in the `tests/` directory.
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=IM_0HQZdWB9PmMc9Si4OKW4MFbzijvtrXQpFdFjleI0,150221
9
+ rootly_mcp_server/server.py,sha256=9XcQ5_yOv4ofMRjXoDg3LsPFb8qDiOHEFz9dPNfwl7o,163221
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.2.dist-info/METADATA,sha256=6HU6hf7I1n90bKQDVx1s84fGSC91DCss4D4TZxMtNKc,13560
15
- rootly_mcp_server-2.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
- rootly_mcp_server-2.1.2.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
17
- rootly_mcp_server-2.1.2.dist-info/licenses/LICENSE,sha256=c9w9ZZGl14r54tsP40oaq5adTVX_HMNHozPIH2ymzmw,11341
18
- rootly_mcp_server-2.1.2.dist-info/RECORD,,
15
+ rootly_mcp_server-2.1.3.dist-info/METADATA,sha256=321Q8VyO2kJGCHbpEyf1NT73jZpB7Eb-KxbECDZcnVs,9892
16
+ rootly_mcp_server-2.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ rootly_mcp_server-2.1.3.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
18
+ rootly_mcp_server-2.1.3.dist-info/licenses/LICENSE,sha256=c9w9ZZGl14r54tsP40oaq5adTVX_HMNHozPIH2ymzmw,11341
19
+ rootly_mcp_server-2.1.3.dist-info/RECORD,,