rootly-mcp-server 0.0.2__py3-none-any.whl → 0.0.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.
@@ -11,6 +11,7 @@ import re
11
11
  import logging
12
12
  from pathlib import Path
13
13
  import requests
14
+ import importlib.resources
14
15
  from typing import Any, Dict, List, Optional, Tuple, Union, Callable
15
16
 
16
17
  import mcp
@@ -29,51 +30,68 @@ SWAGGER_URL = "https://rootly-heroku.s3.amazonaws.com/swagger/v1/swagger.json"
29
30
  class RootlyMCPServer(FastMCP):
30
31
  """
31
32
  A Model Context Protocol server for Rootly API integration.
32
-
33
+
33
34
  This server dynamically generates MCP tools based on the Rootly API's
34
35
  OpenAPI (Swagger) specification.
35
36
  """
36
-
37
- 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):
38
43
  """
39
44
  Initialize the Rootly MCP Server.
40
-
45
+
41
46
  Args:
42
47
  swagger_path: Path to the Swagger JSON file. If None, will look for
43
48
  swagger.json in the current directory and parent directories.
44
49
  name: Name of the MCP server.
45
50
  default_page_size: Default number of items to return per page for paginated endpoints.
46
- 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"]
47
54
  """
48
- logger.info(f"Initializing RootlyMCPServer with name: {name}")
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}")
49
67
  super().__init__(name)
50
-
68
+
51
69
  # Initialize the Rootly API client
52
70
  self.client = RootlyClient()
53
-
71
+
54
72
  # Store default page size
55
73
  self.default_page_size = default_page_size
56
74
  logger.info(f"Using default page size: {default_page_size}")
57
-
75
+
58
76
  # Load the Swagger specification
59
77
  logger.info("Loading Swagger specification")
60
78
  self.swagger_spec = self._load_swagger_spec(swagger_path)
61
- logger.info(f"Loaded Swagger spec with {len(self.swagger_spec.get('paths', {}))} paths")
62
-
79
+ logger.info(f"Loaded Swagger spec with {len(self.swagger_spec.get('paths', {}))} total paths")
80
+
63
81
  # Register tools based on the Swagger spec
64
82
  logger.info("Registering tools based on Swagger spec")
65
83
  self._register_tools()
66
-
84
+
67
85
  def _fetch_swagger_from_url(self, url: str = SWAGGER_URL) -> Dict[str, Any]:
68
86
  """
69
87
  Fetch the Swagger specification from the specified URL.
70
-
88
+
71
89
  Args:
72
90
  url: URL of the Swagger JSON file.
73
-
91
+
74
92
  Returns:
75
93
  The Swagger specification as a dictionary.
76
-
94
+
77
95
  Raises:
78
96
  Exception: If the request fails or the response is not valid JSON.
79
97
  """
@@ -88,15 +106,18 @@ class RootlyMCPServer(FastMCP):
88
106
  except json.JSONDecodeError as e:
89
107
  logger.error(f"Failed to parse Swagger spec: {e}")
90
108
  raise Exception(f"Failed to parse Swagger specification: {e}")
91
-
109
+
92
110
  def _load_swagger_spec(self, swagger_path: Optional[str] = None) -> Dict[str, Any]:
93
111
  """
94
112
  Load the Swagger specification from a file.
95
-
113
+
96
114
  Args:
97
115
  swagger_path: Path to the Swagger JSON file. If None, will look for
98
- swagger.json in the current directory and parent directories.
99
-
116
+ swagger.json in the following locations (in order):
117
+ 1. package data directory
118
+ 2. current directory and parent directories
119
+ 3. download from the URL
120
+
100
121
  Returns:
101
122
  The Swagger specification as a dictionary.
102
123
  """
@@ -108,17 +129,27 @@ class RootlyMCPServer(FastMCP):
108
129
  with open(swagger_path, "r") as f:
109
130
  return json.load(f)
110
131
  else:
111
- # Look for swagger.json in the current directory and parent directories
132
+ # First, check in the package data directory
133
+ try:
134
+ package_data_path = Path(__file__).parent / "data" / "swagger.json"
135
+ if package_data_path.is_file():
136
+ logger.info(f"Found Swagger file in package data: {package_data_path}")
137
+ with open(package_data_path, "r") as f:
138
+ return json.load(f)
139
+ except Exception as e:
140
+ logger.debug(f"Could not load Swagger file from package data: {e}")
141
+
142
+ # Then, look for swagger.json in the current directory and parent directories
112
143
  logger.info("Looking for swagger.json in current directory and parent directories")
113
144
  current_dir = Path.cwd()
114
-
145
+
115
146
  # Check current directory first
116
147
  swagger_path = current_dir / "swagger.json"
117
148
  if swagger_path.is_file():
118
149
  logger.info(f"Found Swagger file at {swagger_path}")
119
150
  with open(swagger_path, "r") as f:
120
151
  return json.load(f)
121
-
152
+
122
153
  # Check parent directories
123
154
  for parent in current_dir.parents:
124
155
  swagger_path = parent / "swagger.json"
@@ -126,11 +157,11 @@ class RootlyMCPServer(FastMCP):
126
157
  logger.info(f"Found Swagger file at {swagger_path}")
127
158
  with open(swagger_path, "r") as f:
128
159
  return json.load(f)
129
-
160
+
130
161
  # If the file wasn't found, fetch it from the URL and save it
131
162
  logger.info("Swagger file not found locally, fetching from URL")
132
163
  swagger_spec = self._fetch_swagger_from_url()
133
-
164
+
134
165
  # Save the fetched spec to the current directory
135
166
  swagger_path = current_dir / "swagger.json"
136
167
  logger.info(f"Saving Swagger file to {swagger_path}")
@@ -140,32 +171,38 @@ class RootlyMCPServer(FastMCP):
140
171
  logger.info(f"Saved Swagger file to {swagger_path}")
141
172
  except Exception as e:
142
173
  logger.warning(f"Failed to save Swagger file: {e}")
143
-
174
+
144
175
  return swagger_spec
145
-
176
+
146
177
  def _register_tools(self) -> None:
147
178
  """
148
179
  Register MCP tools based on the Swagger specification.
149
-
150
- This method iterates through the paths and operations in the Swagger spec
151
- and creates corresponding MCP tools.
180
+ Only registers tools for paths specified in allowed_paths.
152
181
  """
153
182
  paths = self.swagger_spec.get("paths", {})
154
- logger.info(f"Found {len(paths)} paths in Swagger spec")
155
-
183
+
184
+ # Filter paths based on allowed_paths
185
+ filtered_paths = {
186
+ path: path_item
187
+ for path, path_item in paths.items()
188
+ if path in self.allowed_paths
189
+ }
190
+
191
+ logger.info(f"Registering {len(filtered_paths)} paths out of {len(paths)} total paths")
192
+
156
193
  # Register the list_endpoints tool
157
194
  @self.tool()
158
195
  def list_endpoints() -> str:
159
196
  """List all available Rootly API endpoints."""
160
197
  endpoints = []
161
- for path, path_item in paths.items():
198
+ for path, path_item in filtered_paths.items():
162
199
  for method, operation in path_item.items():
163
200
  if method.lower() not in ["get", "post", "put", "delete", "patch"]:
164
201
  continue
165
-
202
+
166
203
  summary = operation.get("summary", "")
167
204
  description = operation.get("description", "")
168
-
205
+
169
206
  endpoints.append({
170
207
  "path": path,
171
208
  "method": method.upper(),
@@ -173,92 +210,89 @@ class RootlyMCPServer(FastMCP):
173
210
  "description": description,
174
211
  "tool_name": self._create_tool_name(path, method)
175
212
  })
176
-
213
+
177
214
  return json.dumps(endpoints, indent=2)
178
-
215
+
179
216
  # Register a tool for each endpoint
180
217
  tool_count = 0
181
-
182
- for path, path_item in paths.items():
218
+
219
+ for path, path_item in filtered_paths.items():
183
220
  # Skip path parameters
184
221
  if "parameters" in path_item:
185
222
  path_item = {k: v for k, v in path_item.items() if k != "parameters"}
186
-
223
+
187
224
  for method, operation in path_item.items():
188
225
  if method.lower() not in ["get", "post", "put", "delete", "patch"]:
189
226
  continue
190
-
227
+
191
228
  # Create a tool name based on the path and method
192
229
  tool_name = self._create_tool_name(path, method)
193
-
230
+
194
231
  # Create a tool description
195
232
  description = operation.get("summary", "") or operation.get("description", "")
196
233
  if not description:
197
234
  description = f"{method.upper()} {path}"
198
-
199
- # Create the input schema
200
- input_schema = self._create_input_schema(path, operation)
201
-
235
+
202
236
  # Register the tool using the direct method
203
237
  try:
204
238
  # Define the tool function
205
239
  def create_tool_fn(p=path, m=method, op=operation):
206
240
  def tool_fn(**kwargs):
207
241
  return self._handle_api_request(p, m, op, **kwargs)
208
-
242
+
209
243
  # Set the function name and docstring
210
244
  tool_fn.__name__ = tool_name
211
245
  tool_fn.__doc__ = description
212
246
  return tool_fn
213
-
247
+
214
248
  # Create the tool function
215
249
  tool_fn = create_tool_fn()
216
-
250
+
217
251
  # Register the tool with FastMCP
218
252
  self.add_tool(
219
253
  name=tool_name,
220
254
  description=description,
221
255
  fn=tool_fn
222
256
  )
223
-
257
+
224
258
  tool_count += 1
225
259
  logger.info(f"Registered tool: {tool_name}")
226
260
  except Exception as e:
227
261
  logger.error(f"Error registering tool {tool_name}: {e}")
228
-
229
- logger.info(f"Registered {tool_count} tools in total")
230
-
262
+
263
+ logger.info(f"Registered {tool_count} tools in total. Modify allowed_paths to register more paths from the Rootly API.")
264
+
231
265
  def _create_tool_name(self, path: str, method: str) -> str:
232
266
  """
233
267
  Create a tool name based on the path and method.
234
-
268
+
235
269
  Args:
236
270
  path: The API path.
237
271
  method: The HTTP method.
238
-
272
+
239
273
  Returns:
240
274
  A tool name string.
241
275
  """
242
276
  # Remove the /v1 prefix if present
243
277
  if path.startswith("/v1"):
244
278
  path = path[3:]
245
-
279
+
246
280
  # Replace path parameters with "by_id"
247
281
  path = re.sub(r"\{([^}]+)\}", r"by_\1", path)
248
-
282
+
249
283
  # Replace slashes with underscores and remove leading/trailing underscores
250
284
  path = path.replace("/", "_").strip("_")
251
-
285
+
252
286
  return f"{path}_{method.lower()}"
253
-
287
+
254
288
  def _create_input_schema(self, path: str, operation: Dict[str, Any]) -> Dict[str, Any]:
255
289
  """
256
290
  Create an input schema for the tool.
257
-
291
+
258
292
  Args:
259
293
  path: The API path.
260
294
  operation: The Swagger operation object.
261
-
295
+
262
296
  Returns:
263
297
  An input schema dictionary.
264
298
  """
@@ -269,7 +303,7 @@ class RootlyMCPServer(FastMCP):
269
303
  "required": [],
270
304
  "additionalProperties": False
271
305
  }
272
-
306
+
273
307
  # Extract path parameters
274
308
  path_params = re.findall(r"\{([^}]+)\}", path)
275
309
  for param in path_params:
@@ -278,80 +312,80 @@ class RootlyMCPServer(FastMCP):
278
312
  "description": f"Path parameter: {param}"
279
313
  }
280
314
  schema["required"].append(param)
281
-
315
+
282
316
  # Add operation parameters
283
317
  for param in operation.get("parameters", []):
284
318
  param_name = param.get("name")
285
319
  param_in = param.get("in")
286
-
320
+
287
321
  if param_in in ["query", "header"]:
288
322
  param_schema = param.get("schema", {})
289
323
  param_type = param_schema.get("type", "string")
290
-
324
+
291
325
  schema["properties"][param_name] = {
292
326
  "type": param_type,
293
327
  "description": param.get("description", f"{param_in} parameter: {param_name}")
294
328
  }
295
-
329
+
296
330
  if param.get("required", False):
297
331
  schema["required"].append(param_name)
298
-
332
+
299
333
  # Add request body for POST, PUT, PATCH methods
300
334
  if "requestBody" in operation:
301
335
  content = operation["requestBody"].get("content", {})
302
336
  if "application/json" in content:
303
337
  body_schema = content["application/json"].get("schema", {})
304
-
338
+
305
339
  if "properties" in body_schema:
306
340
  for prop_name, prop_schema in body_schema["properties"].items():
307
341
  schema["properties"][prop_name] = {
308
342
  "type": prop_schema.get("type", "string"),
309
343
  "description": prop_schema.get("description", f"Body parameter: {prop_name}")
310
344
  }
311
-
345
+
312
346
  if "required" in body_schema:
313
347
  schema["required"].extend(body_schema["required"])
314
-
348
+
315
349
  return schema
316
-
350
+
317
351
  def _handle_api_request(self, path: str, method: str, operation: Dict[str, Any], **kwargs) -> str:
318
352
  """
319
353
  Handle an API request to the Rootly API.
320
-
354
+
321
355
  Args:
322
356
  path: The API path.
323
357
  method: The HTTP method.
324
358
  operation: The Swagger operation object.
325
359
  **kwargs: The parameters for the request.
326
-
360
+
327
361
  Returns:
328
362
  The API response as a JSON string.
329
363
  """
330
364
  logger.debug(f"Handling API request: {method} {path}")
331
365
  logger.debug(f"Request parameters: {kwargs}")
332
-
366
+
333
367
  # Extract path parameters
334
368
  path_params = re.findall(r"\{([^}]+)\}", path)
335
369
  actual_path = path
336
-
370
+
337
371
  # Replace path parameters in the URL
338
372
  for param in path_params:
339
373
  if param in kwargs:
340
374
  actual_path = actual_path.replace(f"{{{param}}}", str(kwargs.pop(param)))
341
-
375
+
342
376
  # Separate query parameters and body parameters
343
377
  query_params = {}
344
378
  body_params = {}
345
-
379
+
346
380
  if method.lower() == "get":
347
381
  # For GET requests, all remaining parameters are query parameters
348
382
  query_params = kwargs
349
-
383
+
350
384
  # Add default pagination for incident-related endpoints
351
385
  if "incidents" in path and method.lower() == "get":
352
386
  # Check if pagination parameters are already specified
353
387
  has_pagination = any(param.startswith("page[") for param in query_params.keys())
354
-
388
+
355
389
  # If no pagination parameters are specified, add default page size
356
390
  if not has_pagination:
357
391
  query_params["page[size]"] = self.default_page_size
@@ -361,13 +395,13 @@ class RootlyMCPServer(FastMCP):
361
395
  for param in operation.get("parameters", []):
362
396
  param_name = param.get("name")
363
397
  param_in = param.get("in")
364
-
398
+
365
399
  if param_in == "query" and param_name in kwargs:
366
400
  query_params[param_name] = kwargs.pop(param_name)
367
-
401
+
368
402
  # All remaining parameters go in the request body
369
403
  body_params = kwargs
370
-
404
+
371
405
  # Make the API request
372
406
  try:
373
407
  response = self.client.make_request(
@@ -379,4 +413,4 @@ class RootlyMCPServer(FastMCP):
379
413
  return response
380
414
  except Exception as e:
381
415
  logger.error(f"Error calling Rootly API: {e}")
382
- return json.dumps({"error": str(e)})
416
+ return json.dumps({"error": str(e)})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: A Model Context Protocol server for Rootly APIs using OpenAPI spec
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
@@ -29,6 +29,7 @@ A Model Context Protocol (MCP) server for Rootly API. This server dynamically ge
29
29
  ## Features
30
30
 
31
31
  - Dynamically generated MCP tools based on Rootly's OpenAPI specification
32
+ - Swagger specification is bundled with the package
32
33
  - Automatic fetching of the latest Swagger spec if not found locally
33
34
  - Authentication via Rootly API token
34
35
  - Default pagination (10 items) for incidents endpoints to prevent context window overflow
@@ -79,9 +80,10 @@ rootly-mcp-server
79
80
  ```
80
81
 
81
82
  The server will automatically:
82
- 1. Look for a local `swagger.json` file in the current and parent directories
83
- 2. If not found, download the latest Swagger spec from Rootly's servers
84
- 3. Cache the downloaded spec to `swagger.json` in the current directory for future use
83
+ 1. Use the bundled Swagger specification that comes with the package
84
+ 2. If not found in the package, look for a local `swagger.json` file in the current and parent directories
85
+ 3. If still not found, download the latest Swagger spec from Rootly's servers
86
+ 4. Cache the downloaded spec to `swagger.json` in the current directory for future use
85
87
 
86
88
  You can also specify a custom Swagger file path:
87
89
  ```bash
@@ -0,0 +1,12 @@
1
+ rootly_mcp_server/__init__.py,sha256=n-YajkwxYg0eoVvtYfYTY6slaktHTYvQbasg15HwGKo,628
2
+ rootly_mcp_server/__main__.py,sha256=vRu8UuyhWnpHiC8vtgyv8G-WpFwumj393NtoT2lUSBk,4058
3
+ rootly_mcp_server/client.py,sha256=vvaY_UaYLobeHbJwgsmFNX-2ABpYoKxMTie8DRBo_xk,3865
4
+ rootly_mcp_server/server.py,sha256=unRG4xzFHdWX2ITcugEWz1XXhJJXNOTlc4nuwavnQL8,16181
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.4.dist-info/METADATA,sha256=NoYBOwXWGIGTzgqkvFZrWL5xGjPaBrax9S2zPmD2EBE,3775
9
+ rootly_mcp_server-0.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ rootly_mcp_server-0.0.4.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
11
+ rootly_mcp_server-0.0.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
12
+ rootly_mcp_server-0.0.4.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- rootly_mcp_server/__init__.py,sha256=n-YajkwxYg0eoVvtYfYTY6slaktHTYvQbasg15HwGKo,628
2
- rootly_mcp_server/__main__.py,sha256=vRu8UuyhWnpHiC8vtgyv8G-WpFwumj393NtoT2lUSBk,4058
3
- rootly_mcp_server/client.py,sha256=vvaY_UaYLobeHbJwgsmFNX-2ABpYoKxMTie8DRBo_xk,3865
4
- rootly_mcp_server/server.py,sha256=C9VkrZEfkYy_MZwKw5gMOf__WhV7JMFE7BUqpax2nos,15330
5
- rootly_mcp_server/test_client.py,sha256=xFQ4cfUpD6qs-aLidy56B3nnV38EFvUe_eBHOqZOC9o,2191
6
- rootly_mcp_server-0.0.2.dist-info/METADATA,sha256=AXFB-cXLGYsS3E-GFeI2KQakclint98yY3z-2km6eFY,3619
7
- rootly_mcp_server-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- rootly_mcp_server-0.0.2.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
9
- rootly_mcp_server-0.0.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
10
- rootly_mcp_server-0.0.2.dist-info/RECORD,,