rootly-mcp-server 0.0.3__py3-none-any.whl → 0.0.5__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/__main__.py +5 -1
- rootly_mcp_server/server.py +104 -83
- rootly_mcp_server-0.0.5.dist-info/METADATA +103 -0
- rootly_mcp_server-0.0.5.dist-info/RECORD +12 -0
- rootly_mcp_server-0.0.5.dist-info/licenses/LICENSE +202 -0
- rootly_mcp_server-0.0.3.dist-info/METADATA +0 -121
- rootly_mcp_server-0.0.3.dist-info/RECORD +0 -12
- rootly_mcp_server-0.0.3.dist-info/licenses/LICENSE +0 -674
- {rootly_mcp_server-0.0.3.dist-info → rootly_mcp_server-0.0.5.dist-info}/WHEEL +0 -0
- {rootly_mcp_server-0.0.3.dist-info → rootly_mcp_server-0.0.5.dist-info}/entry_points.txt +0 -0
rootly_mcp_server/__main__.py
CHANGED
|
@@ -67,7 +67,11 @@ def setup_logging(log_level, debug=False):
|
|
|
67
67
|
|
|
68
68
|
# Set specific logger levels
|
|
69
69
|
logging.getLogger("rootly_mcp_server").setLevel(numeric_level)
|
|
70
|
-
logging.getLogger("
|
|
70
|
+
logging.getLogger("rootly_mcp_server.server").setLevel(logging.WARNING) # Reduce server-specific logs
|
|
71
|
+
|
|
72
|
+
# Always set MCP logger to ERROR level to fix Cline UI issue
|
|
73
|
+
# This prevents INFO logs from causing problems with Cline tool display
|
|
74
|
+
logging.getLogger("mcp").setLevel(logging.ERROR)
|
|
71
75
|
|
|
72
76
|
# Log the configuration
|
|
73
77
|
logger = logging.getLogger(__name__)
|
rootly_mcp_server/server.py
CHANGED
|
@@ -30,51 +30,69 @@ SWAGGER_URL = "https://rootly-heroku.s3.amazonaws.com/swagger/v1/swagger.json"
|
|
|
30
30
|
class RootlyMCPServer(FastMCP):
|
|
31
31
|
"""
|
|
32
32
|
A Model Context Protocol server for Rootly API integration.
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
This server dynamically generates MCP tools based on the Rootly API's
|
|
35
35
|
OpenAPI (Swagger) specification.
|
|
36
36
|
"""
|
|
37
|
-
|
|
38
|
-
def __init__(self,
|
|
37
|
+
|
|
38
|
+
def __init__(self,
|
|
39
|
+
swagger_path: Optional[str] = None,
|
|
40
|
+
name: str = "Rootly",
|
|
41
|
+
default_page_size: int = 10,
|
|
42
|
+
allowed_paths: Optional[List[str]] = None):
|
|
39
43
|
"""
|
|
40
44
|
Initialize the Rootly MCP Server.
|
|
41
|
-
|
|
45
|
+
|
|
42
46
|
Args:
|
|
43
47
|
swagger_path: Path to the Swagger JSON file. If None, will look for
|
|
44
48
|
swagger.json in the current directory and parent directories.
|
|
45
49
|
name: Name of the MCP server.
|
|
46
50
|
default_page_size: Default number of items to return per page for paginated endpoints.
|
|
47
|
-
|
|
51
|
+
allowed_paths: List of API paths to load. If None, all paths will be loaded.
|
|
52
|
+
Paths should be specified without the /v1 prefix.
|
|
53
|
+
Example: ["/incidents", "/incidents/{incident_id}/alerts"]
|
|
48
54
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
# Set default allowed paths if none provided
|
|
56
|
+
self.allowed_paths = allowed_paths or [
|
|
57
|
+
"/incidents",
|
|
58
|
+
"/incidents/{incident_id}/alerts"
|
|
59
|
+
]
|
|
60
|
+
# Add /v1 prefix to paths if not present
|
|
61
|
+
self.allowed_paths = [
|
|
62
|
+
f"/v1{path}" if not path.startswith("/v1") else path
|
|
63
|
+
for path in self.allowed_paths
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
logger.info(f"Initializing RootlyMCPServer with allowed paths: {self.allowed_paths}")
|
|
67
|
+
# Initialize FastMCP with ERROR log level to fix Cline UI issue
|
|
68
|
+
super().__init__(name, log_level="ERROR")
|
|
69
|
+
|
|
52
70
|
# Initialize the Rootly API client
|
|
53
71
|
self.client = RootlyClient()
|
|
54
|
-
|
|
72
|
+
|
|
55
73
|
# Store default page size
|
|
56
74
|
self.default_page_size = default_page_size
|
|
57
75
|
logger.info(f"Using default page size: {default_page_size}")
|
|
58
|
-
|
|
76
|
+
|
|
59
77
|
# Load the Swagger specification
|
|
60
78
|
logger.info("Loading Swagger specification")
|
|
61
79
|
self.swagger_spec = self._load_swagger_spec(swagger_path)
|
|
62
|
-
logger.info(f"Loaded Swagger spec with {len(self.swagger_spec.get('paths', {}))} paths")
|
|
63
|
-
|
|
80
|
+
logger.info(f"Loaded Swagger spec with {len(self.swagger_spec.get('paths', {}))} total paths")
|
|
81
|
+
|
|
64
82
|
# Register tools based on the Swagger spec
|
|
65
83
|
logger.info("Registering tools based on Swagger spec")
|
|
66
84
|
self._register_tools()
|
|
67
|
-
|
|
85
|
+
|
|
68
86
|
def _fetch_swagger_from_url(self, url: str = SWAGGER_URL) -> Dict[str, Any]:
|
|
69
87
|
"""
|
|
70
88
|
Fetch the Swagger specification from the specified URL.
|
|
71
|
-
|
|
89
|
+
|
|
72
90
|
Args:
|
|
73
91
|
url: URL of the Swagger JSON file.
|
|
74
|
-
|
|
92
|
+
|
|
75
93
|
Returns:
|
|
76
94
|
The Swagger specification as a dictionary.
|
|
77
|
-
|
|
95
|
+
|
|
78
96
|
Raises:
|
|
79
97
|
Exception: If the request fails or the response is not valid JSON.
|
|
80
98
|
"""
|
|
@@ -89,18 +107,18 @@ class RootlyMCPServer(FastMCP):
|
|
|
89
107
|
except json.JSONDecodeError as e:
|
|
90
108
|
logger.error(f"Failed to parse Swagger spec: {e}")
|
|
91
109
|
raise Exception(f"Failed to parse Swagger specification: {e}")
|
|
92
|
-
|
|
110
|
+
|
|
93
111
|
def _load_swagger_spec(self, swagger_path: Optional[str] = None) -> Dict[str, Any]:
|
|
94
112
|
"""
|
|
95
113
|
Load the Swagger specification from a file.
|
|
96
|
-
|
|
114
|
+
|
|
97
115
|
Args:
|
|
98
116
|
swagger_path: Path to the Swagger JSON file. If None, will look for
|
|
99
117
|
swagger.json in the following locations (in order):
|
|
100
118
|
1. package data directory
|
|
101
119
|
2. current directory and parent directories
|
|
102
120
|
3. download from the URL
|
|
103
|
-
|
|
121
|
+
|
|
104
122
|
Returns:
|
|
105
123
|
The Swagger specification as a dictionary.
|
|
106
124
|
"""
|
|
@@ -121,18 +139,18 @@ class RootlyMCPServer(FastMCP):
|
|
|
121
139
|
return json.load(f)
|
|
122
140
|
except Exception as e:
|
|
123
141
|
logger.debug(f"Could not load Swagger file from package data: {e}")
|
|
124
|
-
|
|
142
|
+
|
|
125
143
|
# Then, look for swagger.json in the current directory and parent directories
|
|
126
144
|
logger.info("Looking for swagger.json in current directory and parent directories")
|
|
127
145
|
current_dir = Path.cwd()
|
|
128
|
-
|
|
146
|
+
|
|
129
147
|
# Check current directory first
|
|
130
148
|
swagger_path = current_dir / "swagger.json"
|
|
131
149
|
if swagger_path.is_file():
|
|
132
150
|
logger.info(f"Found Swagger file at {swagger_path}")
|
|
133
151
|
with open(swagger_path, "r") as f:
|
|
134
152
|
return json.load(f)
|
|
135
|
-
|
|
153
|
+
|
|
136
154
|
# Check parent directories
|
|
137
155
|
for parent in current_dir.parents:
|
|
138
156
|
swagger_path = parent / "swagger.json"
|
|
@@ -140,11 +158,11 @@ class RootlyMCPServer(FastMCP):
|
|
|
140
158
|
logger.info(f"Found Swagger file at {swagger_path}")
|
|
141
159
|
with open(swagger_path, "r") as f:
|
|
142
160
|
return json.load(f)
|
|
143
|
-
|
|
161
|
+
|
|
144
162
|
# If the file wasn't found, fetch it from the URL and save it
|
|
145
163
|
logger.info("Swagger file not found locally, fetching from URL")
|
|
146
164
|
swagger_spec = self._fetch_swagger_from_url()
|
|
147
|
-
|
|
165
|
+
|
|
148
166
|
# Save the fetched spec to the current directory
|
|
149
167
|
swagger_path = current_dir / "swagger.json"
|
|
150
168
|
logger.info(f"Saving Swagger file to {swagger_path}")
|
|
@@ -154,32 +172,38 @@ class RootlyMCPServer(FastMCP):
|
|
|
154
172
|
logger.info(f"Saved Swagger file to {swagger_path}")
|
|
155
173
|
except Exception as e:
|
|
156
174
|
logger.warning(f"Failed to save Swagger file: {e}")
|
|
157
|
-
|
|
175
|
+
|
|
158
176
|
return swagger_spec
|
|
159
|
-
|
|
177
|
+
|
|
160
178
|
def _register_tools(self) -> None:
|
|
161
179
|
"""
|
|
162
180
|
Register MCP tools based on the Swagger specification.
|
|
163
|
-
|
|
164
|
-
This method iterates through the paths and operations in the Swagger spec
|
|
165
|
-
and creates corresponding MCP tools.
|
|
181
|
+
Only registers tools for paths specified in allowed_paths.
|
|
166
182
|
"""
|
|
167
183
|
paths = self.swagger_spec.get("paths", {})
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
|
|
185
|
+
# Filter paths based on allowed_paths
|
|
186
|
+
filtered_paths = {
|
|
187
|
+
path: path_item
|
|
188
|
+
for path, path_item in paths.items()
|
|
189
|
+
if path in self.allowed_paths
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
logger.info(f"Registering {len(filtered_paths)} paths out of {len(paths)} total paths")
|
|
193
|
+
|
|
170
194
|
# Register the list_endpoints tool
|
|
171
195
|
@self.tool()
|
|
172
196
|
def list_endpoints() -> str:
|
|
173
197
|
"""List all available Rootly API endpoints."""
|
|
174
198
|
endpoints = []
|
|
175
|
-
for path, path_item in
|
|
199
|
+
for path, path_item in filtered_paths.items():
|
|
176
200
|
for method, operation in path_item.items():
|
|
177
201
|
if method.lower() not in ["get", "post", "put", "delete", "patch"]:
|
|
178
202
|
continue
|
|
179
|
-
|
|
203
|
+
|
|
180
204
|
summary = operation.get("summary", "")
|
|
181
205
|
description = operation.get("description", "")
|
|
182
|
-
|
|
206
|
+
|
|
183
207
|
endpoints.append({
|
|
184
208
|
"path": path,
|
|
185
209
|
"method": method.upper(),
|
|
@@ -187,92 +211,89 @@ class RootlyMCPServer(FastMCP):
|
|
|
187
211
|
"description": description,
|
|
188
212
|
"tool_name": self._create_tool_name(path, method)
|
|
189
213
|
})
|
|
190
|
-
|
|
214
|
+
|
|
191
215
|
return json.dumps(endpoints, indent=2)
|
|
192
|
-
|
|
216
|
+
|
|
193
217
|
# Register a tool for each endpoint
|
|
194
218
|
tool_count = 0
|
|
195
|
-
|
|
196
|
-
for path, path_item in
|
|
219
|
+
|
|
220
|
+
for path, path_item in filtered_paths.items():
|
|
197
221
|
# Skip path parameters
|
|
198
222
|
if "parameters" in path_item:
|
|
199
223
|
path_item = {k: v for k, v in path_item.items() if k != "parameters"}
|
|
200
|
-
|
|
224
|
+
|
|
201
225
|
for method, operation in path_item.items():
|
|
202
226
|
if method.lower() not in ["get", "post", "put", "delete", "patch"]:
|
|
203
227
|
continue
|
|
204
|
-
|
|
228
|
+
|
|
205
229
|
# Create a tool name based on the path and method
|
|
206
230
|
tool_name = self._create_tool_name(path, method)
|
|
207
|
-
|
|
231
|
+
|
|
208
232
|
# Create a tool description
|
|
209
233
|
description = operation.get("summary", "") or operation.get("description", "")
|
|
210
234
|
if not description:
|
|
211
235
|
description = f"{method.upper()} {path}"
|
|
212
|
-
|
|
213
|
-
# Create the input schema
|
|
214
|
-
input_schema = self._create_input_schema(path, operation)
|
|
215
|
-
|
|
236
|
+
|
|
216
237
|
# Register the tool using the direct method
|
|
217
238
|
try:
|
|
218
239
|
# Define the tool function
|
|
219
240
|
def create_tool_fn(p=path, m=method, op=operation):
|
|
220
241
|
def tool_fn(**kwargs):
|
|
221
242
|
return self._handle_api_request(p, m, op, **kwargs)
|
|
222
|
-
|
|
243
|
+
|
|
223
244
|
# Set the function name and docstring
|
|
224
245
|
tool_fn.__name__ = tool_name
|
|
225
246
|
tool_fn.__doc__ = description
|
|
226
247
|
return tool_fn
|
|
227
|
-
|
|
248
|
+
|
|
228
249
|
# Create the tool function
|
|
229
250
|
tool_fn = create_tool_fn()
|
|
230
|
-
|
|
251
|
+
|
|
231
252
|
# Register the tool with FastMCP
|
|
232
253
|
self.add_tool(
|
|
233
254
|
name=tool_name,
|
|
234
255
|
description=description,
|
|
235
256
|
fn=tool_fn
|
|
236
257
|
)
|
|
237
|
-
|
|
258
|
+
|
|
238
259
|
tool_count += 1
|
|
239
260
|
logger.info(f"Registered tool: {tool_name}")
|
|
240
261
|
except Exception as e:
|
|
241
262
|
logger.error(f"Error registering tool {tool_name}: {e}")
|
|
242
|
-
|
|
243
|
-
logger.info(f"Registered {tool_count} tools in total")
|
|
244
|
-
|
|
263
|
+
|
|
264
|
+
logger.info(f"Registered {tool_count} tools in total. Modify allowed_paths to register more paths from the Rootly API.")
|
|
265
|
+
|
|
245
266
|
def _create_tool_name(self, path: str, method: str) -> str:
|
|
246
267
|
"""
|
|
247
268
|
Create a tool name based on the path and method.
|
|
248
|
-
|
|
269
|
+
|
|
249
270
|
Args:
|
|
250
271
|
path: The API path.
|
|
251
272
|
method: The HTTP method.
|
|
252
|
-
|
|
273
|
+
|
|
253
274
|
Returns:
|
|
254
275
|
A tool name string.
|
|
255
276
|
"""
|
|
256
277
|
# Remove the /v1 prefix if present
|
|
257
278
|
if path.startswith("/v1"):
|
|
258
279
|
path = path[3:]
|
|
259
|
-
|
|
280
|
+
|
|
260
281
|
# Replace path parameters with "by_id"
|
|
261
282
|
path = re.sub(r"\{([^}]+)\}", r"by_\1", path)
|
|
262
|
-
|
|
283
|
+
|
|
263
284
|
# Replace slashes with underscores and remove leading/trailing underscores
|
|
264
285
|
path = path.replace("/", "_").strip("_")
|
|
265
|
-
|
|
286
|
+
|
|
266
287
|
return f"{path}_{method.lower()}"
|
|
267
|
-
|
|
288
|
+
|
|
268
289
|
def _create_input_schema(self, path: str, operation: Dict[str, Any]) -> Dict[str, Any]:
|
|
269
290
|
"""
|
|
270
291
|
Create an input schema for the tool.
|
|
271
|
-
|
|
292
|
+
|
|
272
293
|
Args:
|
|
273
294
|
path: The API path.
|
|
274
295
|
operation: The Swagger operation object.
|
|
275
|
-
|
|
296
|
+
|
|
276
297
|
Returns:
|
|
277
298
|
An input schema dictionary.
|
|
278
299
|
"""
|
|
@@ -283,7 +304,7 @@ class RootlyMCPServer(FastMCP):
|
|
|
283
304
|
"required": [],
|
|
284
305
|
"additionalProperties": False
|
|
285
306
|
}
|
|
286
|
-
|
|
307
|
+
|
|
287
308
|
# Extract path parameters
|
|
288
309
|
path_params = re.findall(r"\{([^}]+)\}", path)
|
|
289
310
|
for param in path_params:
|
|
@@ -292,96 +313,96 @@ class RootlyMCPServer(FastMCP):
|
|
|
292
313
|
"description": f"Path parameter: {param}"
|
|
293
314
|
}
|
|
294
315
|
schema["required"].append(param)
|
|
295
|
-
|
|
316
|
+
|
|
296
317
|
# Add operation parameters
|
|
297
318
|
for param in operation.get("parameters", []):
|
|
298
319
|
param_name = param.get("name")
|
|
299
320
|
param_in = param.get("in")
|
|
300
|
-
|
|
321
|
+
|
|
301
322
|
if param_in in ["query", "header"]:
|
|
302
323
|
param_schema = param.get("schema", {})
|
|
303
324
|
param_type = param_schema.get("type", "string")
|
|
304
|
-
|
|
325
|
+
|
|
305
326
|
schema["properties"][param_name] = {
|
|
306
327
|
"type": param_type,
|
|
307
328
|
"description": param.get("description", f"{param_in} parameter: {param_name}")
|
|
308
329
|
}
|
|
309
|
-
|
|
330
|
+
|
|
310
331
|
if param.get("required", False):
|
|
311
332
|
schema["required"].append(param_name)
|
|
312
|
-
|
|
333
|
+
|
|
313
334
|
# Add request body for POST, PUT, PATCH methods
|
|
314
335
|
if "requestBody" in operation:
|
|
315
336
|
content = operation["requestBody"].get("content", {})
|
|
316
337
|
if "application/json" in content:
|
|
317
338
|
body_schema = content["application/json"].get("schema", {})
|
|
318
|
-
|
|
339
|
+
|
|
319
340
|
if "properties" in body_schema:
|
|
320
341
|
for prop_name, prop_schema in body_schema["properties"].items():
|
|
321
342
|
schema["properties"][prop_name] = {
|
|
322
343
|
"type": prop_schema.get("type", "string"),
|
|
323
344
|
"description": prop_schema.get("description", f"Body parameter: {prop_name}")
|
|
324
345
|
}
|
|
325
|
-
|
|
346
|
+
|
|
326
347
|
if "required" in body_schema:
|
|
327
348
|
schema["required"].extend(body_schema["required"])
|
|
328
|
-
|
|
349
|
+
|
|
329
350
|
return schema
|
|
330
|
-
|
|
351
|
+
|
|
331
352
|
def _handle_api_request(self, path: str, method: str, operation: Dict[str, Any], **kwargs) -> str:
|
|
332
353
|
"""
|
|
333
354
|
Handle an API request to the Rootly API.
|
|
334
|
-
|
|
355
|
+
|
|
335
356
|
Args:
|
|
336
357
|
path: The API path.
|
|
337
358
|
method: The HTTP method.
|
|
338
359
|
operation: The Swagger operation object.
|
|
339
360
|
**kwargs: The parameters for the request.
|
|
340
|
-
|
|
361
|
+
|
|
341
362
|
Returns:
|
|
342
363
|
The API response as a JSON string.
|
|
343
364
|
"""
|
|
344
365
|
logger.debug(f"Handling API request: {method} {path}")
|
|
345
366
|
logger.debug(f"Request parameters: {kwargs}")
|
|
346
|
-
|
|
367
|
+
|
|
347
368
|
# Extract path parameters
|
|
348
369
|
path_params = re.findall(r"\{([^}]+)\}", path)
|
|
349
370
|
actual_path = path
|
|
350
|
-
|
|
371
|
+
|
|
351
372
|
# Replace path parameters in the URL
|
|
352
373
|
for param in path_params:
|
|
353
374
|
if param in kwargs:
|
|
354
375
|
actual_path = actual_path.replace(f"{{{param}}}", str(kwargs.pop(param)))
|
|
355
|
-
|
|
376
|
+
|
|
356
377
|
# Separate query parameters and body parameters
|
|
357
378
|
query_params = {}
|
|
358
379
|
body_params = {}
|
|
359
|
-
|
|
380
|
+
|
|
360
381
|
if method.lower() == "get":
|
|
361
382
|
# For GET requests, all remaining parameters are query parameters
|
|
362
383
|
query_params = kwargs
|
|
363
|
-
|
|
384
|
+
|
|
364
385
|
# Add default pagination for incident-related endpoints
|
|
365
386
|
if "incidents" in path and method.lower() == "get":
|
|
366
387
|
# Check if pagination parameters are already specified
|
|
367
388
|
has_pagination = any(param.startswith("page[") for param in query_params.keys())
|
|
368
|
-
|
|
389
|
+
|
|
369
390
|
# If no pagination parameters are specified, add default page size
|
|
370
391
|
if not has_pagination:
|
|
371
392
|
query_params["page[size]"] = self.default_page_size
|
|
372
|
-
logger.
|
|
393
|
+
logger.debug(f"Added default pagination (page[size]={self.default_page_size}) for incidents endpoint: {path}")
|
|
373
394
|
else:
|
|
374
395
|
# For other methods, check which parameters are query parameters
|
|
375
396
|
for param in operation.get("parameters", []):
|
|
376
397
|
param_name = param.get("name")
|
|
377
398
|
param_in = param.get("in")
|
|
378
|
-
|
|
399
|
+
|
|
379
400
|
if param_in == "query" and param_name in kwargs:
|
|
380
401
|
query_params[param_name] = kwargs.pop(param_name)
|
|
381
|
-
|
|
402
|
+
|
|
382
403
|
# All remaining parameters go in the request body
|
|
383
404
|
body_params = kwargs
|
|
384
|
-
|
|
405
|
+
|
|
385
406
|
# Make the API request
|
|
386
407
|
try:
|
|
387
408
|
response = self.client.make_request(
|
|
@@ -393,4 +414,4 @@ class RootlyMCPServer(FastMCP):
|
|
|
393
414
|
return response
|
|
394
415
|
except Exception as e:
|
|
395
416
|
logger.error(f"Error calling Rootly API: {e}")
|
|
396
|
-
return json.dumps({"error": str(e)})
|
|
417
|
+
return json.dumps({"error": str(e)})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rootly-mcp-server
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: A Model Context Protocol server for Rootly APIs using OpenAPI spec
|
|
5
|
+
Project-URL: Homepage, https://github.com/Rootly-AI-Labs/Rootly-MCP-server
|
|
6
|
+
Project-URL: Issues, https://github.com/Rootly-AI-Labs/Rootly-MCP-server/issues
|
|
7
|
+
Author-email: Rootly AI Labs <support@rootly.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: automation,incidents,llm,mcp,rootly
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: mcp>=1.1.2
|
|
18
|
+
Requires-Dist: pydantic>=2.0.0
|
|
19
|
+
Requires-Dist: requests>=2.28.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: isort>=5.0.0; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Rootly MCP Server
|
|
27
|
+
|
|
28
|
+
An MCP server for [Rootly API](https://docs.rootly.com/api-reference/overview) that you can plug into your favorite MCP-compatible editors like Cursor, Windsurf, and Claude. Resolve production incidents in under a minute without leaving your IDE.
|
|
29
|
+
<br>
|
|
30
|
+
<br>
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Prerequisites
|
|
36
|
+
|
|
37
|
+
- Python 3.12 or higher
|
|
38
|
+
- `uv` package manager
|
|
39
|
+
```bash
|
|
40
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
41
|
+
```
|
|
42
|
+
- [Rootly API token](https://docs.rootly.com/api-reference/overview#how-to-generate-an-api-key%3F)
|
|
43
|
+
|
|
44
|
+
## Run it in your IDE
|
|
45
|
+
Install with our [PyPi package](https://pypi.org/project/rootly-mcp-server/) or by cloning this repo.
|
|
46
|
+
|
|
47
|
+
To set it up in your favorite MCP-compatible editor (we tested it with Cursor and Windsurf), here is the config :
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"rootly": {
|
|
52
|
+
"command": "uvx",
|
|
53
|
+
"args": [
|
|
54
|
+
"--from",
|
|
55
|
+
"rootly-mcp-server",
|
|
56
|
+
"rootly-mcp-server"
|
|
57
|
+
],
|
|
58
|
+
"env": {
|
|
59
|
+
"ROOTLY_API_TOKEN": "<YOUR_ROOTLY_API_TOKEN>"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
If you want to customize `allowed_paths` to access more Rootly API paths, clone the package and use this config.
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"rootly": {
|
|
70
|
+
"command": "uv",
|
|
71
|
+
"args": [
|
|
72
|
+
"run",
|
|
73
|
+
"--directory",
|
|
74
|
+
"/path/to/rootly-mcp-server",
|
|
75
|
+
"rootly-mcp-server"
|
|
76
|
+
],
|
|
77
|
+
"env": {
|
|
78
|
+
"ROOTLY_API_TOKEN": "<YOUR_ROOTLY_API_TOKEN>"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Features
|
|
86
|
+
This server dynamically generates MCP resources based on Rootly's OpenAPI (Swagger) specification:
|
|
87
|
+
- Dynamically generated MCP tools based on Rootly's OpenAPI specification
|
|
88
|
+
- Default pagination (10 items) for incident endpoints to prevent context window overflow
|
|
89
|
+
- Limits the number of API paths exposed to the AI agent
|
|
90
|
+
|
|
91
|
+
We limited the number of API paths exposed for 2 reasons
|
|
92
|
+
* Context size: because [Rootly's API](https://docs.rootly.com/api-reference/overview) is very rich in paths, AI agents can get overwhelmed and not perform simple actions properly. As of now we only expose the [/incidents](https://docs.rootly.com/api-reference/incidents/list-incidents) and [/incidents/{incident_id}/alerts](https://docs.rootly.com/api-reference/incidentevents/list-incident-events).
|
|
93
|
+
* Security: if you want to limit the type of information or actions that users can access through the MCP server
|
|
94
|
+
|
|
95
|
+
If you want to make more path available, edit the variable `allowed_paths` in `src/rootly_mcp_server/server.py`.
|
|
96
|
+
|
|
97
|
+
## Disclaimer
|
|
98
|
+
This project is a prototype and not intended for production use. If you have featured ideas or spotted some issues, feel free to submit a PR or open an issue.
|
|
99
|
+
|
|
100
|
+
## About the Rootly AI Labs
|
|
101
|
+
This project was developed by the [Rootly AI Labs](https://labs.rootly.ai/). The AI Labs is building the future of system reliability and operational excellence. We operate as an open-source incubator, sharing ideas, experimenting, and rapidly prototyping. We're committed to ensuring our research benefits the entire community.
|
|
102
|
+

|
|
103
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
rootly_mcp_server/__init__.py,sha256=n-YajkwxYg0eoVvtYfYTY6slaktHTYvQbasg15HwGKo,628
|
|
2
|
+
rootly_mcp_server/__main__.py,sha256=yVbn4s2WGDy7ASbcLULMi2ro4Qt6WgVZbHVD0p0ibrs,4311
|
|
3
|
+
rootly_mcp_server/client.py,sha256=vvaY_UaYLobeHbJwgsmFNX-2ABpYoKxMTie8DRBo_xk,3865
|
|
4
|
+
rootly_mcp_server/server.py,sha256=iYOSmS7UfxF28w09X3PjDI6he9VLE1h3U9U3qsJ8K5M,16273
|
|
5
|
+
rootly_mcp_server/test_client.py,sha256=xFQ4cfUpD6qs-aLidy56B3nnV38EFvUe_eBHOqZOC9o,2191
|
|
6
|
+
rootly_mcp_server/data/__init__.py,sha256=fO8a0bQnRVEoRMHKvhFzj10bhoaw7VsI51czc2MsUm4,143
|
|
7
|
+
rootly_mcp_server/data/swagger.json,sha256=8Ag4COTnS3WSC6vBaa2Q7hq3RxIQ8fGrmwsnNSnPheA,2046619
|
|
8
|
+
rootly_mcp_server-0.0.5.dist-info/METADATA,sha256=r9dctRmxTaFySuSdiyoELmxJT019AHoLbaO9dXbF_2g,4074
|
|
9
|
+
rootly_mcp_server-0.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
rootly_mcp_server-0.0.5.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
|
|
11
|
+
rootly_mcp_server-0.0.5.dist-info/licenses/LICENSE,sha256=c9w9ZZGl14r54tsP40oaq5adTVX_HMNHozPIH2ymzmw,11341
|
|
12
|
+
rootly_mcp_server-0.0.5.dist-info/RECORD,,
|