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.
@@ -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
 
@@ -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.2
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
- ## 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=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.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.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,,