rootly-mcp-server 2.0.6__py3-none-any.whl → 2.0.9__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.
@@ -13,7 +13,7 @@ Features:
13
13
  from .server import RootlyMCPServer
14
14
  from .client import RootlyClient
15
15
 
16
- __version__ = "2.0.0"
16
+ __version__ = "2.0.1"
17
17
  __all__ = [
18
18
  'RootlyMCPServer',
19
19
  'RootlyClient',
@@ -10,9 +10,7 @@ import logging
10
10
  import os
11
11
  import sys
12
12
  from pathlib import Path
13
- from typing import Optional, List
14
-
15
- from .routemap_server import create_rootly_mcp_server
13
+ from .server import create_rootly_mcp_server
16
14
 
17
15
 
18
16
  def parse_args():
@@ -137,7 +135,7 @@ def get_server():
137
135
  return create_rootly_mcp_server(
138
136
  swagger_path=swagger_path,
139
137
  name=server_name,
140
- custom_allowed_paths=allowed_paths,
138
+ allowed_paths=allowed_paths,
141
139
  hosted=hosted,
142
140
  base_url=base_url,
143
141
  )
@@ -161,7 +159,9 @@ def main():
161
159
  logger.warning("--host argument is deprecated, use --hosted instead")
162
160
  hosted_mode = True
163
161
 
164
- check_api_token()
162
+ # Only check API token if not in hosted mode
163
+ if not hosted_mode:
164
+ check_api_token()
165
165
 
166
166
  try:
167
167
  # Parse allowed paths from command line argument
@@ -173,7 +173,7 @@ def main():
173
173
  server = create_rootly_mcp_server(
174
174
  swagger_path=args.swagger_path,
175
175
  name=args.name,
176
- custom_allowed_paths=allowed_paths,
176
+ allowed_paths=allowed_paths,
177
177
  hosted=hosted_mode,
178
178
  base_url=args.base_url,
179
179
  )
@@ -6,7 +6,7 @@ import os
6
6
  import json
7
7
  import logging
8
8
  import requests
9
- from typing import Optional, Dict, Any, Union
9
+ from typing import Optional, Dict, Any
10
10
 
11
11
  # Set up logger
12
12
  logger = logging.getLogger(__name__)
@@ -123,7 +123,7 @@ class RootlyClient:
123
123
  try:
124
124
  error_response["status_code"] = e.response.status_code
125
125
  error_response["response_text"] = e.response.text
126
- except:
126
+ except Exception:
127
127
  pass
128
128
 
129
129
  return json.dumps(error_response, indent=2)
@@ -2,7 +2,8 @@
2
2
  """
3
3
  Rootly FastMCP Server (RouteMap Version)
4
4
 
5
- Working implementation using FastMCP's RouteMap system with proper response handling.
5
+ Alternative implementation using FastMCP's RouteMap system for filtering
6
+ instead of pre-filtering the OpenAPI spec.
6
7
  """
7
8
 
8
9
  import httpx
@@ -10,10 +11,13 @@ from fastmcp import FastMCP
10
11
  from fastmcp.server.openapi import RouteMap, MCPType
11
12
  import os
12
13
  import logging
14
+ import sys
15
+ from pathlib import Path
13
16
  from typing import Optional, List
14
17
 
15
18
  # Import the shared OpenAPI loader
16
- from .rootly_openapi_loader import load_rootly_openapi_spec
19
+ sys.path.append(str(Path(__file__).parent.parent.parent))
20
+ from rootly_openapi_loader import load_rootly_openapi_spec
17
21
 
18
22
  # Configure logging
19
23
  logging.basicConfig(level=logging.INFO)
@@ -22,7 +26,7 @@ logger = logging.getLogger(__name__)
22
26
  def create_rootly_mcp_server(
23
27
  swagger_path: Optional[str] = None,
24
28
  name: str = "Rootly API Server (RouteMap Filtered)",
25
- custom_allowed_paths: Optional[List[str]] = None,
29
+ allowed_paths: Optional[List[str]] = None,
26
30
  hosted: bool = False,
27
31
  base_url: Optional[str] = None,
28
32
  ):
@@ -34,79 +38,8 @@ def create_rootly_mcp_server(
34
38
  raise ValueError("ROOTLY_API_TOKEN environment variable is required")
35
39
 
36
40
  logger.info("Creating authenticated HTTP client...")
37
- # Create a custom HTTP client wrapper that ensures string responses
38
- class StringifyingClient:
39
- def __init__(self, base_url: str, headers: dict, timeout: float):
40
- self._client = httpx.AsyncClient(
41
- base_url=base_url,
42
- headers=headers,
43
- timeout=timeout
44
- )
45
-
46
- async def __aenter__(self):
47
- await self._client.__aenter__()
48
- return self
49
-
50
- async def __aexit__(self, exc_type, exc_val, exc_tb):
51
- await self._client.__aexit__(exc_type, exc_val, exc_tb)
52
-
53
- async def request(self, method: str, url: str, **kwargs):
54
- """Override request to return responses that FastMCP can handle."""
55
- response = await self._client.request(method, url, **kwargs)
56
-
57
- # Create a response that returns the raw text instead of structured JSON
58
- class TextResponse:
59
- def __init__(self, original_response):
60
- self.status_code = original_response.status_code
61
- self.headers = original_response.headers
62
- self.url = original_response.url
63
- self.request = original_response.request
64
- self._original = original_response
65
-
66
- # Pre-compute the text response
67
- if original_response.status_code == 200:
68
- try:
69
- import json
70
- data = original_response.json()
71
- self._text_response = json.dumps(data, indent=2)
72
- except Exception:
73
- self._text_response = original_response.text or "No content"
74
- else:
75
- self._text_response = f"Error: HTTP {original_response.status_code} - {original_response.text}"
76
-
77
- def json(self):
78
- """Return the original JSON data for structured_content."""
79
- try:
80
- # Return the original JSON data so FastMCP can handle structured_content
81
- return self._original.json()
82
- except Exception:
83
- # If JSON parsing fails, return a wrapper structure
84
- return {"result": self._text_response}
85
-
86
- @property
87
- def text(self):
88
- """Return the formatted JSON as text."""
89
- return self._text_response
90
-
91
- @property
92
- def content(self):
93
- return self._text_response.encode('utf-8')
94
-
95
- def raise_for_status(self):
96
- """Delegate to original response."""
97
- return self._original.raise_for_status()
98
-
99
- def __getattr__(self, name):
100
- """Delegate any missing attributes to original response."""
101
- return getattr(self._original, name)
102
-
103
- return TextResponse(response)
104
-
105
- def __getattr__(self, name):
106
- return getattr(self._client, name)
107
-
108
- # Create authenticated HTTP client with string conversion
109
- client = StringifyingClient(
41
+ # Create authenticated HTTP client
42
+ client = httpx.AsyncClient(
110
43
  base_url=base_url or "https://api.rootly.com",
111
44
  headers={
112
45
  "Authorization": f"Bearer {ROOTLY_API_TOKEN}",
@@ -146,89 +79,84 @@ def create_rootly_mcp_server(
146
79
 
147
80
  # Define custom route maps for filtering specific endpoints
148
81
  route_maps = [
149
- # Core incident management - list endpoints
82
+ # Core incident management
150
83
  RouteMap(
151
84
  pattern=r"^/v1/incidents$",
152
- mcp_type=MCPType.TOOL,
153
- mcp_tags={"incidents", "core", "list"}
85
+ mcp_type=MCPType.TOOL
154
86
  ),
155
- # Incident detail endpoints
156
87
  RouteMap(
157
- pattern=r"^/v1/incidents/\{.*\}$",
158
- mcp_type=MCPType.TOOL,
159
- mcp_tags={"incidents", "detail"}
88
+ pattern=r"^/v1/incidents/\{incident_id\}/alerts$",
89
+ mcp_type=MCPType.TOOL
160
90
  ),
161
- # Incident relationships
162
91
  RouteMap(
163
- pattern=r"^/v1/incidents/\{.*\}/.*$",
164
- mcp_type=MCPType.TOOL,
165
- mcp_tags={"incidents", "relationships"}
92
+ pattern=r"^/v1/incidents/\{incident_id\}/action_items$",
93
+ mcp_type=MCPType.TOOL
166
94
  ),
167
95
 
168
96
  # Alert management
169
97
  RouteMap(
170
98
  pattern=r"^/v1/alerts$",
171
- mcp_type=MCPType.TOOL,
172
- mcp_tags={"alerts", "core", "list"}
99
+ mcp_type=MCPType.TOOL
173
100
  ),
174
101
  RouteMap(
175
- pattern=r"^/v1/alerts/\{.*\}$",
176
- mcp_type=MCPType.TOOL,
177
- mcp_tags={"alerts", "detail"}
102
+ pattern=r"^/v1/alerts/\{id\}$",
103
+ mcp_type=MCPType.TOOL
178
104
  ),
179
105
 
180
- # Users - both list and detail
106
+ # Configuration entities
181
107
  RouteMap(
182
- pattern=r"^/v1/users$",
183
- mcp_type=MCPType.TOOL,
184
- mcp_tags={"users", "list"}
108
+ pattern=r"^/v1/severities(\{id\})?$",
109
+ mcp_type=MCPType.TOOL
185
110
  ),
186
111
  RouteMap(
187
- pattern=r"^/v1/users/me$",
188
- mcp_type=MCPType.TOOL,
189
- mcp_tags={"users", "current"}
112
+ pattern=r"^/v1/incident_types(\{id\})?$",
113
+ mcp_type=MCPType.TOOL
190
114
  ),
191
115
  RouteMap(
192
- pattern=r"^/v1/users/\{.*\}$",
193
- mcp_type=MCPType.TOOL,
194
- mcp_tags={"users", "detail"}
116
+ pattern=r"^/v1/functionalities(\{id\})?$",
117
+ mcp_type=MCPType.TOOL
195
118
  ),
196
119
 
197
- # Teams
120
+ # Organization
198
121
  RouteMap(
199
- pattern=r"^/v1/teams$",
200
- mcp_type=MCPType.TOOL,
201
- mcp_tags={"teams", "list"}
122
+ pattern=r"^/v1/teams(\{id\})?$",
123
+ mcp_type=MCPType.TOOL
202
124
  ),
203
125
  RouteMap(
204
- pattern=r"^/v1/teams/\{.*\}$",
205
- mcp_type=MCPType.TOOL,
206
- mcp_tags={"teams", "detail"}
126
+ pattern=r"^/v1/users(\{id\}|/me)?$",
127
+ mcp_type=MCPType.TOOL
207
128
  ),
208
129
 
209
- # Services
130
+ # Infrastructure
131
+ RouteMap(
132
+ pattern=r"^/v1/services(\{id\})?$",
133
+ mcp_type=MCPType.TOOL
134
+ ),
210
135
  RouteMap(
211
- pattern=r"^/v1/services$",
212
- mcp_type=MCPType.TOOL,
213
- mcp_tags={"services", "list"}
136
+ pattern=r"^/v1/environments(\{id\})?$",
137
+ mcp_type=MCPType.TOOL
214
138
  ),
139
+
140
+ # Action items
215
141
  RouteMap(
216
- pattern=r"^/v1/services/\{.*\}$",
217
- mcp_type=MCPType.TOOL,
218
- mcp_tags={"services", "detail"}
142
+ pattern=r"^/v1/incident_action_items(\{id\})?$",
143
+ mcp_type=MCPType.TOOL
219
144
  ),
220
145
 
221
- # Configuration entities - list patterns
146
+ # Workflows
222
147
  RouteMap(
223
- pattern=r"^/v1/(severities|incident_types|environments)$",
224
- mcp_type=MCPType.TOOL,
225
- mcp_tags={"configuration", "list"}
148
+ pattern=r"^/v1/workflows(\{id\})?$",
149
+ mcp_type=MCPType.TOOL
226
150
  ),
227
- # Configuration entities - detail patterns
228
151
  RouteMap(
229
- pattern=r"^/v1/(severities|incident_types|environments)/\{.*\}$",
230
- mcp_type=MCPType.TOOL,
231
- mcp_tags={"configuration", "detail"}
152
+ pattern=r"^/v1/workflow_runs(\{id\})?$",
153
+ mcp_type=MCPType.TOOL
154
+ ),
155
+
156
+ # Status pages
157
+ RouteMap(
158
+ pattern=r"^/v1/status_pages(\{id\})?$",
159
+ mcp_type=MCPType.TOOL
232
160
  ),
233
161
 
234
162
  # Exclude everything else
@@ -238,37 +166,24 @@ def create_rootly_mcp_server(
238
166
  )
239
167
  ]
240
168
 
241
- # Custom response handler to ensure proper MCP output format
242
- def ensure_mcp_response(route, component):
243
- """Ensure all responses work with FastMCP's structured content system."""
244
- # Set output schema to handle structured JSON data
245
- component.output_schema = {
246
- "type": "object",
247
- "description": "Rootly API response data",
248
- "additionalProperties": True
249
- }
250
-
251
- # Add description
252
- if hasattr(component, 'description'):
253
- component.description = f"🔧 {component.description or 'Rootly API endpoint'}"
254
-
255
- # Create MCP server with custom route maps and response handling
169
+ # Create MCP server with custom route maps
256
170
  mcp = FastMCP.from_openapi(
257
171
  openapi_spec=openapi_spec,
258
172
  client=client,
259
173
  name=name,
260
174
  timeout=30.0,
261
175
  tags={"rootly", "incident-management", "evaluation"},
262
- route_maps=route_maps,
263
- mcp_component_fn=ensure_mcp_response
176
+ route_maps=route_maps
264
177
  )
265
178
 
266
- logger.info(f"✅ Created MCP server with RouteMap filtering successfully")
179
+ logger.info("✅ Created MCP server with RouteMap filtering successfully")
267
180
  logger.info("🚀 Selected Rootly API endpoints are now available as MCP tools")
268
181
 
269
182
  return mcp
270
183
 
271
184
 
185
+
186
+
272
187
  def main():
273
188
  """Main entry point."""
274
189
  try: