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.
- rootly_mcp_server/__init__.py +1 -1
- rootly_mcp_server/__main__.py +6 -6
- rootly_mcp_server/client.py +2 -2
- rootly_mcp_server/routemap_server.py +57 -142
- rootly_mcp_server/server.py +318 -184
- rootly_mcp_server/test_client.py +11 -9
- rootly_mcp_server/utils.py +105 -0
- {rootly_mcp_server-2.0.6.dist-info → rootly_mcp_server-2.0.9.dist-info}/METADATA +28 -26
- rootly_mcp_server-2.0.9.dist-info/RECORD +13 -0
- rootly_mcp_server/rootly_openapi_loader.py +0 -109
- rootly_mcp_server-2.0.6.dist-info/RECORD +0 -13
- {rootly_mcp_server-2.0.6.dist-info → rootly_mcp_server-2.0.9.dist-info}/WHEEL +0 -0
- {rootly_mcp_server-2.0.6.dist-info → rootly_mcp_server-2.0.9.dist-info}/entry_points.txt +0 -0
- {rootly_mcp_server-2.0.6.dist-info → rootly_mcp_server-2.0.9.dist-info}/licenses/LICENSE +0 -0
rootly_mcp_server/__init__.py
CHANGED
rootly_mcp_server/__main__.py
CHANGED
|
@@ -10,9 +10,7 @@ import logging
|
|
|
10
10
|
import os
|
|
11
11
|
import sys
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
allowed_paths=allowed_paths,
|
|
177
177
|
hosted=hosted_mode,
|
|
178
178
|
base_url=args.base_url,
|
|
179
179
|
)
|
rootly_mcp_server/client.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
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
|
|
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
|
-
#
|
|
106
|
+
# Configuration entities
|
|
181
107
|
RouteMap(
|
|
182
|
-
pattern=r"^/v1/
|
|
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/
|
|
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/
|
|
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
|
-
#
|
|
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/
|
|
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
|
-
#
|
|
130
|
+
# Infrastructure
|
|
131
|
+
RouteMap(
|
|
132
|
+
pattern=r"^/v1/services(\{id\})?$",
|
|
133
|
+
mcp_type=MCPType.TOOL
|
|
134
|
+
),
|
|
210
135
|
RouteMap(
|
|
211
|
-
pattern=r"^/v1/
|
|
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/
|
|
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
|
-
#
|
|
146
|
+
# Workflows
|
|
222
147
|
RouteMap(
|
|
223
|
-
pattern=r"^/v1/(
|
|
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/(
|
|
230
|
-
mcp_type=MCPType.TOOL
|
|
231
|
-
|
|
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
|
-
#
|
|
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(
|
|
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:
|