rootly-mcp-server 2.0.8__tar.gz → 2.0.9__tar.gz

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.
Files changed (24) hide show
  1. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/PKG-INFO +2 -1
  2. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/pyproject.toml +2 -1
  3. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/src/rootly_mcp_server/server.py +68 -13
  4. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/.github/workflows/pypi-release.yml +0 -0
  5. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/.gitignore +0 -0
  6. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/.semaphore/deploy.yml +0 -0
  7. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/.semaphore/semaphore.yml +0 -0
  8. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/.semaphore/update-task-definition.sh +0 -0
  9. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/Dockerfile +0 -0
  10. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/LICENSE +0 -0
  11. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/README.md +0 -0
  12. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/rootly-mcp-server-demo.gif +0 -0
  13. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/rootly_fastmcp_server.py +0 -0
  14. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/rootly_fastmcp_server_routemap.py +0 -0
  15. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/rootly_openapi.json +0 -0
  16. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/rootly_openapi_loader.py +0 -0
  17. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/src/rootly_mcp_server/__init__.py +0 -0
  18. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/src/rootly_mcp_server/__main__.py +0 -0
  19. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/src/rootly_mcp_server/client.py +0 -0
  20. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/src/rootly_mcp_server/data/__init__.py +0 -0
  21. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/src/rootly_mcp_server/routemap_server.py +0 -0
  22. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/src/rootly_mcp_server/test_client.py +0 -0
  23. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/src/rootly_mcp_server/utils.py +0 -0
  24. {rootly_mcp_server-2.0.8 → rootly_mcp_server-2.0.9}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.0.8
3
+ Version: 2.0.9
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.0.8"
3
+ version = "2.0.9"
4
4
  description = "A Model Context Protocol server for Rootly APIs using OpenAPI spec"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -20,6 +20,7 @@ dependencies = [
20
20
  "requests>=2.28.0", # For API calls
21
21
  "httpx>=0.24.0", # For async HTTP client
22
22
  "pydantic>=2.0.0", # For data validation
23
+ "brotli>=1.0.0", # For Brotli compression support in httpx
23
24
  ]
24
25
 
25
26
  [project.urls]
@@ -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.base_url = base_url
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 = {"Content-Type": "application/vnd.api+json", "Accept": "application/vnd.api+json"}
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,43 @@ def create_rootly_mcp_server(
225
243
  return PlainTextResponse("OK")
226
244
 
227
245
  # Add some custom tools for enhanced functionality
246
+ @mcp.tool()
247
+ async def debug_incidents() -> dict:
248
+ """Debug tool to inspect incidents endpoint response."""
249
+ try:
250
+ response = await make_authenticated_request("GET", "/v1/incidents", params={"page[size]": 1})
251
+ response.raise_for_status()
252
+
253
+ return {
254
+ "status_code": response.status_code,
255
+ "headers": dict(response.headers),
256
+ "content_length": len(response.content) if response.content else 0,
257
+ "content_preview": response.content[:500].decode('utf-8', errors='ignore') if response.content else "No content",
258
+ "text_preview": response.text[:500] if hasattr(response, 'text') else "No text",
259
+ "encoding": response.encoding,
260
+ "content_type": response.headers.get('content-type', 'unknown')
261
+ }
262
+ except Exception as e:
263
+ return {"error": str(e), "error_type": type(e).__name__}
264
+
265
+ @mcp.tool()
266
+ async def debug_headers() -> dict:
267
+ """Debug tool to inspect request/response headers for troubleshooting."""
268
+ try:
269
+ response = await make_authenticated_request("GET", "/v1/teams", params={"page[size]": 1})
270
+ response.raise_for_status()
271
+
272
+ return {
273
+ "request_headers": dict(response.request.headers) if response.request else {},
274
+ "response_headers": dict(response.headers),
275
+ "status_code": response.status_code,
276
+ "content_type": response.headers.get('content-type', 'unknown'),
277
+ "encoding": response.encoding,
278
+ "content_preview": str(response.content[:200]) if response.content else "No content"
279
+ }
280
+ except Exception as e:
281
+ return {"error": str(e), "error_type": type(e).__name__}
282
+
228
283
  @mcp.tool()
229
284
  def list_endpoints() -> list:
230
285
  """List all available Rootly API endpoints with their descriptions."""
@@ -262,8 +317,8 @@ def create_rootly_mcp_server(
262
317
  except Exception:
263
318
  pass # Fallback to default client behavior
264
319
 
265
- # Make the request using the underlying httpx client
266
- return await http_client.client.request(method, url, **kwargs)
320
+ # Use our custom client with proper error handling instead of bypassing it
321
+ return await http_client.request(method, url, **kwargs)
267
322
 
268
323
  @mcp.tool()
269
324
  async def search_incidents(
@@ -377,7 +432,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
377
432
  logger.info(f"Using provided Swagger path: {swagger_path}")
378
433
  if not os.path.isfile(swagger_path):
379
434
  raise FileNotFoundError(f"Swagger file not found at {swagger_path}")
380
- with open(swagger_path, "r") as f:
435
+ with open(swagger_path, "r", encoding="utf-8") as f:
381
436
  return json.load(f)
382
437
  else:
383
438
  # First, check in the package data directory
@@ -385,7 +440,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
385
440
  package_data_path = Path(__file__).parent / "data" / "swagger.json"
386
441
  if package_data_path.is_file():
387
442
  logger.info(f"Found Swagger file in package data: {package_data_path}")
388
- with open(package_data_path, "r") as f:
443
+ with open(package_data_path, "r", encoding="utf-8") as f:
389
444
  return json.load(f)
390
445
  except Exception as e:
391
446
  logger.debug(f"Could not load Swagger file from package data: {e}")
@@ -398,7 +453,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
398
453
  swagger_path = current_dir / "swagger.json"
399
454
  if swagger_path.is_file():
400
455
  logger.info(f"Found Swagger file at {swagger_path}")
401
- with open(swagger_path, "r") as f:
456
+ with open(swagger_path, "r", encoding="utf-8") as f:
402
457
  return json.load(f)
403
458
 
404
459
  # Check parent directories
@@ -406,7 +461,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
406
461
  swagger_path = parent / "swagger.json"
407
462
  if swagger_path.is_file():
408
463
  logger.info(f"Found Swagger file at {swagger_path}")
409
- with open(swagger_path, "r") as f:
464
+ with open(swagger_path, "r", encoding="utf-8") as f:
410
465
  return json.load(f)
411
466
 
412
467
  # If the file wasn't found, fetch it from the URL and save it
@@ -417,7 +472,7 @@ def _load_swagger_spec(swagger_path: Optional[str] = None) -> Dict[str, Any]:
417
472
  swagger_path = current_dir / "swagger.json"
418
473
  logger.info(f"Saving Swagger file to {swagger_path}")
419
474
  try:
420
- with open(swagger_path, "w") as f:
475
+ with open(swagger_path, "w", encoding="utf-8") as f:
421
476
  json.dump(swagger_spec, f)
422
477
  logger.info(f"Saved Swagger file to {swagger_path}")
423
478
  except Exception as e: