rootly-mcp-server 2.0.4__py3-none-any.whl → 2.0.6__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.
@@ -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
- allowed_paths=allowed_paths,
176
+ custom_allowed_paths=allowed_paths,
177
177
  hosted=hosted_mode,
178
178
  base_url=args.base_url,
179
179
  )
@@ -18,10 +18,11 @@ def load_rootly_openapi_spec() -> dict:
18
18
  Load Rootly OpenAPI spec with smart fallback logic.
19
19
 
20
20
  Loading priority:
21
- 1. Check current directory for rootly_openapi.json
22
- 2. Check parent directories for rootly_openapi.json
23
- 3. Check for swagger.json files
24
- 4. Only as last resort, fetch from URL and cache locally
21
+ 1. Check for Claude-compatible swagger_claude_fixed.json
22
+ 2. Check current directory for rootly_openapi.json
23
+ 3. Check parent directories for rootly_openapi.json
24
+ 4. Check for swagger.json files
25
+ 5. Only as last resort, fetch from URL and cache locally
25
26
 
26
27
  Returns:
27
28
  dict: The OpenAPI specification
@@ -31,7 +32,19 @@ def load_rootly_openapi_spec() -> dict:
31
32
  """
32
33
  current_dir = Path.cwd()
33
34
 
34
- # Check for rootly_openapi.json in current directory and parents
35
+ # Priority 1: Check for Claude-compatible swagger file first
36
+ for check_dir in [current_dir] + list(current_dir.parents):
37
+ spec_file = check_dir / "swagger_claude_fixed.json"
38
+ if spec_file.is_file():
39
+ logger.info(f"Found Claude-compatible OpenAPI spec at {spec_file}")
40
+ try:
41
+ with open(spec_file, "r") as f:
42
+ return json.load(f)
43
+ except Exception as e:
44
+ logger.warning(f"Failed to load {spec_file}: {e}")
45
+ continue
46
+
47
+ # Priority 2: Check for rootly_openapi.json in current directory and parents
35
48
  for check_dir in [current_dir] + list(current_dir.parents):
36
49
  spec_file = check_dir / "rootly_openapi.json"
37
50
  if spec_file.is_file():
@@ -43,7 +56,7 @@ def load_rootly_openapi_spec() -> dict:
43
56
  logger.warning(f"Failed to load {spec_file}: {e}")
44
57
  continue
45
58
 
46
- # Check for swagger.json in current directory and parents
59
+ # Priority 3: Check for swagger.json in current directory and parents
47
60
  for check_dir in [current_dir] + list(current_dir.parents):
48
61
  spec_file = check_dir / "swagger.json"
49
62
  if spec_file.is_file():
@@ -2,8 +2,7 @@
2
2
  """
3
3
  Rootly FastMCP Server (RouteMap Version)
4
4
 
5
- Alternative implementation using FastMCP's RouteMap system for filtering
6
- instead of pre-filtering the OpenAPI spec.
5
+ Working implementation using FastMCP's RouteMap system with proper response handling.
7
6
  """
8
7
 
9
8
  import httpx
@@ -11,7 +10,6 @@ from fastmcp import FastMCP
11
10
  from fastmcp.server.openapi import RouteMap, MCPType
12
11
  import os
13
12
  import logging
14
- from pathlib import Path
15
13
  from typing import Optional, List
16
14
 
17
15
  # Import the shared OpenAPI loader
@@ -36,8 +34,79 @@ def create_rootly_mcp_server(
36
34
  raise ValueError("ROOTLY_API_TOKEN environment variable is required")
37
35
 
38
36
  logger.info("Creating authenticated HTTP client...")
39
- # Create authenticated HTTP client
40
- client = httpx.AsyncClient(
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
110
  base_url=base_url or "https://api.rootly.com",
42
111
  headers={
43
112
  "Authorization": f"Bearer {ROOTLY_API_TOKEN}",
@@ -73,73 +142,125 @@ def create_rootly_mcp_server(
73
142
  fix_array_types(openapi_spec)
74
143
  logger.info("✅ Fixed OpenAPI spec compatibility issues")
75
144
 
76
- logger.info("Creating FastMCP server with pre-filtered OpenAPI spec...")
145
+ logger.info("Creating FastMCP server with RouteMap filtering...")
77
146
 
78
- # Define the specific endpoints we want to include
79
- if custom_allowed_paths:
80
- allowed_paths = set(custom_allowed_paths)
81
- else:
82
- allowed_paths = {
83
- # Core incident management
84
- "/v1/incidents",
85
- "/v1/incidents/{incident_id}/alerts",
86
- "/v1/incidents/{incident_id}/action_items",
87
-
88
- # Alert management
89
- "/v1/alerts",
90
- "/v1/alerts/{id}",
91
-
92
- # Configuration entities
93
- "/v1/severities",
94
- "/v1/severities/{id}",
95
- "/v1/incident_types",
96
- "/v1/incident_types/{id}",
97
- "/v1/functionalities",
98
- "/v1/functionalities/{id}",
99
-
100
- # Organization
101
- "/v1/teams",
102
- "/v1/teams/{id}",
103
- "/v1/users",
104
- "/v1/users/me",
105
- "/v1/users/{id}",
106
-
107
- # Infrastructure
108
- "/v1/services",
109
- "/v1/services/{id}",
110
- "/v1/environments",
111
- "/v1/environments/{id}",
112
-
113
- # Action items
114
- "/v1/action_items",
115
- "/v1/action_items/{id}",
116
-
117
- # Workflows
118
- "/v1/workflows",
119
- "/v1/workflows/{id}",
120
-
121
- # Status pages
122
- "/v1/status-pages",
123
- "/v1/status-pages/{id}"
124
- }
125
-
126
- # Filter the OpenAPI spec to only include allowed paths
127
- original_paths = openapi_spec.get("paths", {})
128
- filtered_paths = {path: spec for path, spec in original_paths.items() if path in allowed_paths}
129
-
130
- logger.info(f"📊 Filtered OpenAPI spec from {len(original_paths)} paths to {len(filtered_paths)} paths")
131
- logger.info(f"🔍 Allowed paths: {sorted(allowed_paths)}")
132
- logger.info(f" Filtered paths: {sorted(filtered_paths.keys())}")
147
+ # Define custom route maps for filtering specific endpoints
148
+ route_maps = [
149
+ # Core incident management - list endpoints
150
+ RouteMap(
151
+ pattern=r"^/v1/incidents$",
152
+ mcp_type=MCPType.TOOL,
153
+ mcp_tags={"incidents", "core", "list"}
154
+ ),
155
+ # Incident detail endpoints
156
+ RouteMap(
157
+ pattern=r"^/v1/incidents/\{.*\}$",
158
+ mcp_type=MCPType.TOOL,
159
+ mcp_tags={"incidents", "detail"}
160
+ ),
161
+ # Incident relationships
162
+ RouteMap(
163
+ pattern=r"^/v1/incidents/\{.*\}/.*$",
164
+ mcp_type=MCPType.TOOL,
165
+ mcp_tags={"incidents", "relationships"}
166
+ ),
167
+
168
+ # Alert management
169
+ RouteMap(
170
+ pattern=r"^/v1/alerts$",
171
+ mcp_type=MCPType.TOOL,
172
+ mcp_tags={"alerts", "core", "list"}
173
+ ),
174
+ RouteMap(
175
+ pattern=r"^/v1/alerts/\{.*\}$",
176
+ mcp_type=MCPType.TOOL,
177
+ mcp_tags={"alerts", "detail"}
178
+ ),
179
+
180
+ # Users - both list and detail
181
+ RouteMap(
182
+ pattern=r"^/v1/users$",
183
+ mcp_type=MCPType.TOOL,
184
+ mcp_tags={"users", "list"}
185
+ ),
186
+ RouteMap(
187
+ pattern=r"^/v1/users/me$",
188
+ mcp_type=MCPType.TOOL,
189
+ mcp_tags={"users", "current"}
190
+ ),
191
+ RouteMap(
192
+ pattern=r"^/v1/users/\{.*\}$",
193
+ mcp_type=MCPType.TOOL,
194
+ mcp_tags={"users", "detail"}
195
+ ),
196
+
197
+ # Teams
198
+ RouteMap(
199
+ pattern=r"^/v1/teams$",
200
+ mcp_type=MCPType.TOOL,
201
+ mcp_tags={"teams", "list"}
202
+ ),
203
+ RouteMap(
204
+ pattern=r"^/v1/teams/\{.*\}$",
205
+ mcp_type=MCPType.TOOL,
206
+ mcp_tags={"teams", "detail"}
207
+ ),
208
+
209
+ # Services
210
+ RouteMap(
211
+ pattern=r"^/v1/services$",
212
+ mcp_type=MCPType.TOOL,
213
+ mcp_tags={"services", "list"}
214
+ ),
215
+ RouteMap(
216
+ pattern=r"^/v1/services/\{.*\}$",
217
+ mcp_type=MCPType.TOOL,
218
+ mcp_tags={"services", "detail"}
219
+ ),
220
+
221
+ # Configuration entities - list patterns
222
+ RouteMap(
223
+ pattern=r"^/v1/(severities|incident_types|environments)$",
224
+ mcp_type=MCPType.TOOL,
225
+ mcp_tags={"configuration", "list"}
226
+ ),
227
+ # Configuration entities - detail patterns
228
+ RouteMap(
229
+ pattern=r"^/v1/(severities|incident_types|environments)/\{.*\}$",
230
+ mcp_type=MCPType.TOOL,
231
+ mcp_tags={"configuration", "detail"}
232
+ ),
233
+
234
+ # Exclude everything else
235
+ RouteMap(
236
+ pattern=r".*",
237
+ mcp_type=MCPType.EXCLUDE
238
+ )
239
+ ]
133
240
 
134
- openapi_spec["paths"] = filtered_paths
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'}"
135
254
 
136
- # Create MCP server without route maps for now to test basic functionality
255
+ # Create MCP server with custom route maps and response handling
137
256
  mcp = FastMCP.from_openapi(
138
257
  openapi_spec=openapi_spec,
139
258
  client=client,
140
259
  name=name,
141
260
  timeout=30.0,
142
- tags={"rootly", "incident-management", "evaluation"}
261
+ tags={"rootly", "incident-management", "evaluation"},
262
+ route_maps=route_maps,
263
+ mcp_component_fn=ensure_mcp_response
143
264
  )
144
265
 
145
266
  logger.info(f"✅ Created MCP server with RouteMap filtering successfully")
@@ -148,8 +269,6 @@ def create_rootly_mcp_server(
148
269
  return mcp
149
270
 
150
271
 
151
-
152
-
153
272
  def main():
154
273
  """Main entry point."""
155
274
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.0.4
3
+ Version: 2.0.6
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
@@ -0,0 +1,13 @@
1
+ rootly_mcp_server/__init__.py,sha256=H3Dd4Od-PEff8VdbMgnblS6cCEBCb9XqYdI_-wmtQqk,628
2
+ rootly_mcp_server/__main__.py,sha256=fQLaI6b91ujqhgK9hiFN9xdGfFQC52a8S9Vk310bJXQ,6461
3
+ rootly_mcp_server/client.py,sha256=05TsHVJ3WtLH0k4R19Yzwwx4xcmjCKH6hS3uKcTMwRA,4678
4
+ rootly_mcp_server/rootly_openapi_loader.py,sha256=oVD4jjWBPbuaTshIs_oxEgXQ21i0erONFy5P4yxr7c8,3782
5
+ rootly_mcp_server/routemap_server.py,sha256=LJGMkDJBpuujVdqsnO7aLVTjHIsJ4ojB3iYwrkPIxiw,10370
6
+ rootly_mcp_server/server.py,sha256=2paE39caYRVuQG4yWEEOqZewbvpFREmf-J01lPQ5elw,19756
7
+ rootly_mcp_server/test_client.py,sha256=8p1aJHrEt_Tj2NuJzTnTHw-ZeW816P99fJi5bhPidyc,5119
8
+ rootly_mcp_server/data/__init__.py,sha256=fO8a0bQnRVEoRMHKvhFzj10bhoaw7VsI51czc2MsUm4,143
9
+ rootly_mcp_server-2.0.6.dist-info/METADATA,sha256=LNhgXwDvA7ee1rM-37ofYE41bE6-Xbx4SGCbMqcbRwE,6141
10
+ rootly_mcp_server-2.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ rootly_mcp_server-2.0.6.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
12
+ rootly_mcp_server-2.0.6.dist-info/licenses/LICENSE,sha256=c9w9ZZGl14r54tsP40oaq5adTVX_HMNHozPIH2ymzmw,11341
13
+ rootly_mcp_server-2.0.6.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- rootly_mcp_server/__init__.py,sha256=H3Dd4Od-PEff8VdbMgnblS6cCEBCb9XqYdI_-wmtQqk,628
2
- rootly_mcp_server/__main__.py,sha256=MLfP2v4-D3znGMhv6iew6A9oDrv0eSkdv19p30E22mo,6454
3
- rootly_mcp_server/client.py,sha256=05TsHVJ3WtLH0k4R19Yzwwx4xcmjCKH6hS3uKcTMwRA,4678
4
- rootly_mcp_server/rootly_openapi_loader.py,sha256=GebIDHzEMV6XCuPox3SoBOG42lyaAb3RFlhWoCdIkeE,3159
5
- rootly_mcp_server/routemap_server.py,sha256=DDcuY68_B4uqg8aOuDmUp0zhrlpIxQtw0h5265ZYULk,5631
6
- rootly_mcp_server/server.py,sha256=2paE39caYRVuQG4yWEEOqZewbvpFREmf-J01lPQ5elw,19756
7
- rootly_mcp_server/test_client.py,sha256=8p1aJHrEt_Tj2NuJzTnTHw-ZeW816P99fJi5bhPidyc,5119
8
- rootly_mcp_server/data/__init__.py,sha256=fO8a0bQnRVEoRMHKvhFzj10bhoaw7VsI51czc2MsUm4,143
9
- rootly_mcp_server-2.0.4.dist-info/METADATA,sha256=PrhSnP_eZ7xQLW6X-xQV5IScALGY7GaE1cBkcdlSZHw,6141
10
- rootly_mcp_server-2.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- rootly_mcp_server-2.0.4.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
12
- rootly_mcp_server-2.0.4.dist-info/licenses/LICENSE,sha256=c9w9ZZGl14r54tsP40oaq5adTVX_HMNHozPIH2ymzmw,11341
13
- rootly_mcp_server-2.0.4.dist-info/RECORD,,