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.
@@ -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("mcp").setLevel(numeric_level)
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__)
@@ -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, swagger_path: Optional[str] = None, name: str = "Rootly", default_page_size: int = 10):
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
- This helps prevent context window overflow.
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
- logger.info(f"Initializing RootlyMCPServer with name: {name}")
50
- super().__init__(name)
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
- logger.info(f"Found {len(paths)} paths in Swagger spec")
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 paths.items():
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 paths.items():
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.info(f"Added default pagination (page[size]={self.default_page_size}) for incidents endpoint: {path}")
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
+ ![Demo GIF](rootly-mcp-server-demo.gif)
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
+ ![Rootly AI logo](https://github.com/Rootly-AI-Labs/EventOrOutage/raw/main/rootly-ai.png)
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,,