rootly-mcp-server 2.0.8__py3-none-any.whl → 2.0.10__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/server.py +47 -20
- {rootly_mcp_server-2.0.8.dist-info → rootly_mcp_server-2.0.10.dist-info}/METADATA +2 -1
- {rootly_mcp_server-2.0.8.dist-info → rootly_mcp_server-2.0.10.dist-info}/RECORD +6 -6
- {rootly_mcp_server-2.0.8.dist-info → rootly_mcp_server-2.0.10.dist-info}/WHEEL +0 -0
- {rootly_mcp_server-2.0.8.dist-info → rootly_mcp_server-2.0.10.dist-info}/entry_points.txt +0 -0
- {rootly_mcp_server-2.0.8.dist-info → rootly_mcp_server-2.0.10.dist-info}/licenses/LICENSE +0 -0
rootly_mcp_server/server.py
CHANGED
|
@@ -69,7 +69,7 @@ class AuthenticatedHTTPXClient:
|
|
|
69
69
|
"""An HTTPX client wrapper that handles Rootly API authentication and parameter transformation."""
|
|
70
70
|
|
|
71
71
|
def __init__(self, base_url: str = "https://api.rootly.com", hosted: bool = False, parameter_mapping: Optional[Dict[str, str]] = None):
|
|
72
|
-
self.
|
|
72
|
+
self._base_url = base_url
|
|
73
73
|
self.hosted = hosted
|
|
74
74
|
self._api_token = None
|
|
75
75
|
self.parameter_mapping = parameter_mapping or {}
|
|
@@ -77,15 +77,22 @@ class AuthenticatedHTTPXClient:
|
|
|
77
77
|
if not self.hosted:
|
|
78
78
|
self._api_token = self._get_api_token()
|
|
79
79
|
|
|
80
|
-
# Create the HTTPX client
|
|
81
|
-
headers = {
|
|
80
|
+
# Create the HTTPX client
|
|
81
|
+
headers = {
|
|
82
|
+
"Content-Type": "application/vnd.api+json",
|
|
83
|
+
"Accept": "application/vnd.api+json"
|
|
84
|
+
# Let httpx handle Accept-Encoding automatically with all supported formats
|
|
85
|
+
}
|
|
82
86
|
if self._api_token:
|
|
83
87
|
headers["Authorization"] = f"Bearer {self._api_token}"
|
|
84
88
|
|
|
85
89
|
self.client = httpx.AsyncClient(
|
|
86
90
|
base_url=base_url,
|
|
87
91
|
headers=headers,
|
|
88
|
-
timeout=30.0
|
|
92
|
+
timeout=30.0,
|
|
93
|
+
follow_redirects=True,
|
|
94
|
+
# Ensure proper handling of compressed responses
|
|
95
|
+
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
|
|
89
96
|
)
|
|
90
97
|
|
|
91
98
|
def _get_api_token(self) -> Optional[str]:
|
|
@@ -116,7 +123,7 @@ class AuthenticatedHTTPXClient:
|
|
|
116
123
|
if 'params' in kwargs:
|
|
117
124
|
kwargs['params'] = self._transform_params(kwargs['params'])
|
|
118
125
|
|
|
119
|
-
# Call the underlying client's request method
|
|
126
|
+
# Call the underlying client's request method and let it handle everything
|
|
120
127
|
return await self.client.request(method, url, **kwargs)
|
|
121
128
|
|
|
122
129
|
async def get(self, url: str, **kwargs):
|
|
@@ -146,8 +153,19 @@ class AuthenticatedHTTPXClient:
|
|
|
146
153
|
pass
|
|
147
154
|
|
|
148
155
|
def __getattr__(self, name):
|
|
149
|
-
# Delegate all other attributes to the underlying client
|
|
156
|
+
# Delegate all other attributes to the underlying client, except for request methods
|
|
157
|
+
if name in ['request', 'get', 'post', 'put', 'patch', 'delete']:
|
|
158
|
+
# Use our overridden methods instead
|
|
159
|
+
return getattr(self, name)
|
|
150
160
|
return getattr(self.client, name)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def base_url(self):
|
|
164
|
+
return self._base_url
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def headers(self):
|
|
168
|
+
return self.client.headers
|
|
151
169
|
|
|
152
170
|
|
|
153
171
|
def create_rootly_mcp_server(
|
|
@@ -225,6 +243,7 @@ def create_rootly_mcp_server(
|
|
|
225
243
|
return PlainTextResponse("OK")
|
|
226
244
|
|
|
227
245
|
# Add some custom tools for enhanced functionality
|
|
246
|
+
|
|
228
247
|
@mcp.tool()
|
|
229
248
|
def list_endpoints() -> list:
|
|
230
249
|
"""List all available Rootly API endpoints with their descriptions."""
|
|
@@ -262,8 +281,8 @@ def create_rootly_mcp_server(
|
|
|
262
281
|
except Exception:
|
|
263
282
|
pass # Fallback to default client behavior
|
|
264
283
|
|
|
265
|
-
#
|
|
266
|
-
return await http_client.
|
|
284
|
+
# Use our custom client with proper error handling instead of bypassing it
|
|
285
|
+
return await http_client.request(method, url, **kwargs)
|
|
267
286
|
|
|
268
287
|
@mcp.tool()
|
|
269
288
|
async def search_incidents(
|
|
@@ -281,7 +300,7 @@ def create_rootly_mcp_server(
|
|
|
281
300
|
# Single page mode
|
|
282
301
|
if page_number > 0:
|
|
283
302
|
params = {
|
|
284
|
-
"page[size]": min(page_size,
|
|
303
|
+
"page[size]": min(page_size, 5), # Keep responses very small to avoid errors
|
|
285
304
|
"page[number]": page_number,
|
|
286
305
|
"include": "",
|
|
287
306
|
}
|
|
@@ -298,10 +317,11 @@ def create_rootly_mcp_server(
|
|
|
298
317
|
# Multi-page mode (page_number = 0)
|
|
299
318
|
all_incidents = []
|
|
300
319
|
current_page = 1
|
|
301
|
-
effective_page_size = min(page_size,
|
|
320
|
+
effective_page_size = min(page_size, 5) # Keep responses very small to avoid errors
|
|
321
|
+
max_pages = 10 # Safety limit to prevent infinite loops
|
|
302
322
|
|
|
303
323
|
try:
|
|
304
|
-
while len(all_incidents) < max_results:
|
|
324
|
+
while len(all_incidents) < max_results and current_page <= max_pages:
|
|
305
325
|
params = {
|
|
306
326
|
"page[size]": effective_page_size,
|
|
307
327
|
"page[number]": current_page,
|
|
@@ -318,16 +338,23 @@ def create_rootly_mcp_server(
|
|
|
318
338
|
if "data" in response_data:
|
|
319
339
|
incidents = response_data["data"]
|
|
320
340
|
if not incidents:
|
|
341
|
+
# No more incidents available
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
# Check if we got fewer incidents than requested (last page)
|
|
345
|
+
if len(incidents) < effective_page_size:
|
|
346
|
+
all_incidents.extend(incidents)
|
|
321
347
|
break
|
|
322
348
|
|
|
323
349
|
all_incidents.extend(incidents)
|
|
324
350
|
|
|
325
|
-
# Check if
|
|
351
|
+
# Check metadata if available
|
|
326
352
|
meta = response_data.get("meta", {})
|
|
327
353
|
current_page_meta = meta.get("current_page", current_page)
|
|
328
|
-
total_pages = meta.get("total_pages"
|
|
329
|
-
|
|
330
|
-
|
|
354
|
+
total_pages = meta.get("total_pages")
|
|
355
|
+
|
|
356
|
+
# If we have reliable metadata, use it
|
|
357
|
+
if total_pages and current_page_meta >= total_pages:
|
|
331
358
|
break
|
|
332
359
|
|
|
333
360
|
current_page += 1
|
|
@@ -377,7 +404,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
|
|
|
377
404
|
logger.info(f"Using provided Swagger path: {swagger_path}")
|
|
378
405
|
if not os.path.isfile(swagger_path):
|
|
379
406
|
raise FileNotFoundError(f"Swagger file not found at {swagger_path}")
|
|
380
|
-
with open(swagger_path, "r") as f:
|
|
407
|
+
with open(swagger_path, "r", encoding="utf-8") as f:
|
|
381
408
|
return json.load(f)
|
|
382
409
|
else:
|
|
383
410
|
# First, check in the package data directory
|
|
@@ -385,7 +412,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
|
|
|
385
412
|
package_data_path = Path(__file__).parent / "data" / "swagger.json"
|
|
386
413
|
if package_data_path.is_file():
|
|
387
414
|
logger.info(f"Found Swagger file in package data: {package_data_path}")
|
|
388
|
-
with open(package_data_path, "r") as f:
|
|
415
|
+
with open(package_data_path, "r", encoding="utf-8") as f:
|
|
389
416
|
return json.load(f)
|
|
390
417
|
except Exception as e:
|
|
391
418
|
logger.debug(f"Could not load Swagger file from package data: {e}")
|
|
@@ -398,7 +425,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
|
|
|
398
425
|
swagger_path = current_dir / "swagger.json"
|
|
399
426
|
if swagger_path.is_file():
|
|
400
427
|
logger.info(f"Found Swagger file at {swagger_path}")
|
|
401
|
-
with open(swagger_path, "r") as f:
|
|
428
|
+
with open(swagger_path, "r", encoding="utf-8") as f:
|
|
402
429
|
return json.load(f)
|
|
403
430
|
|
|
404
431
|
# Check parent directories
|
|
@@ -406,7 +433,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
|
|
|
406
433
|
swagger_path = parent / "swagger.json"
|
|
407
434
|
if swagger_path.is_file():
|
|
408
435
|
logger.info(f"Found Swagger file at {swagger_path}")
|
|
409
|
-
with open(swagger_path, "r") as f:
|
|
436
|
+
with open(swagger_path, "r", encoding="utf-8") as f:
|
|
410
437
|
return json.load(f)
|
|
411
438
|
|
|
412
439
|
# If the file wasn't found, fetch it from the URL and save it
|
|
@@ -417,7 +444,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
|
|
|
417
444
|
swagger_path = current_dir / "swagger.json"
|
|
418
445
|
logger.info(f"Saving Swagger file to {swagger_path}")
|
|
419
446
|
try:
|
|
420
|
-
with open(swagger_path, "w") as f:
|
|
447
|
+
with open(swagger_path, "w", encoding="utf-8") as f:
|
|
421
448
|
json.dump(swagger_spec, f)
|
|
422
449
|
logger.info(f"Saved Swagger file to {swagger_path}")
|
|
423
450
|
except Exception as e:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rootly-mcp-server
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.10
|
|
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
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Topic :: Software Development :: Build Tools
|
|
16
16
|
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: brotli>=1.0.0
|
|
17
18
|
Requires-Dist: fastmcp>=2.9.0
|
|
18
19
|
Requires-Dist: httpx>=0.24.0
|
|
19
20
|
Requires-Dist: pydantic>=2.0.0
|
|
@@ -2,12 +2,12 @@ rootly_mcp_server/__init__.py,sha256=6pLh19IFyqE-Cve9zergkD-X_yApEkInREKmRa73T6s
|
|
|
2
2
|
rootly_mcp_server/__main__.py,sha256=_F4p65_VjnN84RtmEdESVLLH0tO5tL9qBfb2Xdvbj2E,6480
|
|
3
3
|
rootly_mcp_server/client.py,sha256=diIBINJP_z4nnQIAC1b70vQSiHaNojEfUDARC2nrKHU,4681
|
|
4
4
|
rootly_mcp_server/routemap_server.py,sha256=0LfK2EzwkFQF9SpHNvGcca5ZaxkBC80gIdDojE0aUcs,6100
|
|
5
|
-
rootly_mcp_server/server.py,sha256=
|
|
5
|
+
rootly_mcp_server/server.py,sha256=O31ByNNfKjBmmYY1y_ooPKL12b-PXaM5eNrekXzpURE,24346
|
|
6
6
|
rootly_mcp_server/test_client.py,sha256=Ytd5ZP7vImm12CT97k3p9tlkY_JNcXHSzcGGnHCBqv0,5275
|
|
7
7
|
rootly_mcp_server/utils.py,sha256=NyxdcDiFGlV2a8eBO4lKgZg0D7Gxr6xUIB0YyJGgpPA,4165
|
|
8
8
|
rootly_mcp_server/data/__init__.py,sha256=fO8a0bQnRVEoRMHKvhFzj10bhoaw7VsI51czc2MsUm4,143
|
|
9
|
-
rootly_mcp_server-2.0.
|
|
10
|
-
rootly_mcp_server-2.0.
|
|
11
|
-
rootly_mcp_server-2.0.
|
|
12
|
-
rootly_mcp_server-2.0.
|
|
13
|
-
rootly_mcp_server-2.0.
|
|
9
|
+
rootly_mcp_server-2.0.10.dist-info/METADATA,sha256=SyvYvw80MbIJVhxXvAG_ai1F9FV9N3qPx9Ls4bj3mbg,6189
|
|
10
|
+
rootly_mcp_server-2.0.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
rootly_mcp_server-2.0.10.dist-info/entry_points.txt,sha256=NE33b8VgigVPGBkboyo6pvN1Vz35HZtLybxMO4Q03PI,70
|
|
12
|
+
rootly_mcp_server-2.0.10.dist-info/licenses/LICENSE,sha256=c9w9ZZGl14r54tsP40oaq5adTVX_HMNHozPIH2ymzmw,11341
|
|
13
|
+
rootly_mcp_server-2.0.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|