rootly-mcp-server 2.2.1__tar.gz → 2.2.3__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 (55) hide show
  1. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/PKG-INFO +1 -1
  2. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/pyproject.toml +1 -1
  3. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/__init__.py +6 -1
  4. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/server.py +42 -5
  5. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_http_headers.py +41 -0
  6. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.beads/issues.jsonl +0 -0
  7. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.gitattributes +0 -0
  8. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.github/dependabot.yml +0 -0
  9. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.github/workflows/ci.yml +0 -0
  10. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.github/workflows/lint.yml +0 -0
  11. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.github/workflows/pypi-release.yml +0 -0
  12. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.github/workflows/test.yml +0 -0
  13. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.gitignore +0 -0
  14. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.semaphore/deploy.yml +0 -0
  15. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.semaphore/semaphore.yml +0 -0
  16. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/.semaphore/update-task-definition.sh +0 -0
  17. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/CONTRIBUTING.md +0 -0
  18. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/Dockerfile +0 -0
  19. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/LICENSE +0 -0
  20. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/README.md +0 -0
  21. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/examples/skills/README.md +0 -0
  22. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/rootly-mcp-server-demo.gif +0 -0
  23. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/rootly_openapi.json +0 -0
  24. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/scripts/setup-hooks.sh +0 -0
  25. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/__main__.py +0 -0
  26. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/client.py +0 -0
  27. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/data/__init__.py +0 -0
  28. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/exceptions.py +0 -0
  29. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/monitoring.py +0 -0
  30. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/och_client.py +0 -0
  31. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/pagination.py +0 -0
  32. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/security.py +0 -0
  33. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/smart_utils.py +0 -0
  34. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/texttest.json +0 -0
  35. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/utils.py +0 -0
  36. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/src/rootly_mcp_server/validators.py +0 -0
  37. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/README.md +0 -0
  38. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/conftest.py +0 -0
  39. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/integration/local/test_basic.py +0 -0
  40. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/integration/local/test_smart_tools.py +0 -0
  41. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/integration/remote/test_essential.py +0 -0
  42. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/test_client.py +0 -0
  43. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_authentication.py +0 -0
  44. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_exceptions.py +0 -0
  45. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_och_client.py +0 -0
  46. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_oncall_handoff.py +0 -0
  47. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_oncall_metrics.py +0 -0
  48. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_oncall_new_tools.py +0 -0
  49. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_security.py +0 -0
  50. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_server.py +0 -0
  51. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_smart_utils.py +0 -0
  52. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_tools.py +0 -0
  53. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_utils.py +0 -0
  54. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/tests/unit/test_validators.py +0 -0
  55. {rootly_mcp_server-2.2.1 → rootly_mcp_server-2.2.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.2.1
3
+ Version: 2.2.3
4
4
  Summary: Secure Model Context Protocol server for Rootly APIs with AI SRE capabilities, comprehensive error handling, and input validation
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.2.1"
3
+ version = "2.2.3"
4
4
  description = "Secure Model Context Protocol server for Rootly APIs with AI SRE capabilities, comprehensive error handling, and input validation"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -14,10 +14,15 @@ Features:
14
14
  - Input validation and sensitive data masking
15
15
  """
16
16
 
17
+ from importlib.metadata import PackageNotFoundError, version
18
+
17
19
  from .client import RootlyClient
18
20
  from .server import RootlyMCPServer
19
21
 
20
- __version__ = "2.1.0"
22
+ try:
23
+ __version__ = version("rootly-mcp-server")
24
+ except PackageNotFoundError:
25
+ __version__ = "dev"
21
26
  __all__ = [
22
27
  "RootlyMCPServer",
23
28
  "RootlyClient",
@@ -386,18 +386,41 @@ class AuthenticatedHTTPXClient:
386
386
  if "params" in kwargs:
387
387
  kwargs["params"] = self._transform_params(kwargs["params"])
388
388
 
389
+ # Log incoming headers for debugging (before transformation)
390
+ incoming_headers = kwargs.get("headers", {})
391
+ if incoming_headers:
392
+ logger.debug(f"Incoming headers for {method} {url}: {list(incoming_headers.keys())}")
393
+
389
394
  # ALWAYS ensure Content-Type and Accept headers are set correctly for Rootly API
390
395
  # This is critical because:
391
- # 1. FastMCP may pass headers from the MCP client request (e.g., Content-Type: application/json)
392
- # 2. Per-request headers override httpx client defaults
393
- # 3. We must force JSON-API content type regardless of what's passed in
396
+ # 1. FastMCP's get_http_headers() returns LOWERCASE header keys (e.g., "content-type")
397
+ # 2. We must remove any existing content-type/accept and set the correct JSON-API values
398
+ # 3. Handle both lowercase and mixed-case variants to be safe
394
399
  headers = dict(kwargs.get("headers") or {})
400
+ # Remove any existing content-type and accept headers (case-insensitive)
401
+ headers_to_remove = [k for k in headers if k.lower() in ("content-type", "accept")]
402
+ for key in headers_to_remove:
403
+ logger.debug(f"Removing header '{key}' with value '{headers[key]}'")
404
+ del headers[key]
405
+ # Set the correct JSON-API headers
395
406
  headers["Content-Type"] = "application/vnd.api+json"
396
407
  headers["Accept"] = "application/vnd.api+json"
397
408
  kwargs["headers"] = headers
398
409
 
399
- # Call the underlying client's request method and let it handle everything
400
- return await self.client.request(method, url, **kwargs)
410
+ # Log outgoing request
411
+ logger.debug(f"Request: {method} {url}")
412
+
413
+ response = await self.client.request(method, url, **kwargs)
414
+ logger.debug(f"Response: {method} {url} -> {response.status_code}")
415
+
416
+ # Log error responses (4xx/5xx)
417
+ if response.is_error:
418
+ logger.error(
419
+ f"HTTP {response.status_code} error for {method} {url}: "
420
+ f"{response.text[:500] if response.text else 'No response body'}"
421
+ )
422
+
423
+ return response
401
424
 
402
425
  async def get(self, url: str, **kwargs):
403
426
  """Proxy to request with GET method."""
@@ -543,6 +566,20 @@ def create_rootly_mcp_server(
543
566
 
544
567
  return endpoints
545
568
 
569
+ @mcp.tool()
570
+ def get_server_version() -> dict:
571
+ """Get the Rootly MCP server version.
572
+
573
+ Returns the current version of the deployed MCP server.
574
+ Useful for checking if the server has been updated.
575
+ """
576
+ from rootly_mcp_server import __version__
577
+
578
+ return {
579
+ "version": __version__,
580
+ "package": "rootly-mcp-server",
581
+ }
582
+
546
583
  async def make_authenticated_request(method: str, url: str, **kwargs):
547
584
  """Make an authenticated request, extracting token from MCP headers in hosted mode."""
548
585
  # In hosted mode, get token from MCP request headers
@@ -275,3 +275,44 @@ class TestFastMCPIntegrationScenario:
275
275
  call_kwargs = mock_httpx_client.request.call_args[1]
276
276
  assert call_kwargs["headers"]["Content-Type"] == "application/vnd.api+json"
277
277
  assert call_kwargs["headers"]["Accept"] == "application/vnd.api+json"
278
+
279
+ @pytest.mark.asyncio
280
+ async def test_lowercase_headers_removed_not_duplicated(self, mock_httpx_client):
281
+ """Test that lowercase headers from FastMCP are removed, not duplicated.
282
+
283
+ FastMCP's get_http_headers() returns lowercase keys like 'content-type'.
284
+ If we only SET 'Content-Type' without removing 'content-type', the dict
285
+ would have BOTH keys, causing the 415 error on the hosted server.
286
+
287
+ This is the ROOT CAUSE of the 415 error that only happens on hosted MCP:
288
+ - Local (stdio): No HTTP request context, get_http_headers() returns {}
289
+ - Hosted (SSE): HTTP request context exists, get_http_headers() returns lowercase headers
290
+ """
291
+ from rootly_mcp_server.server import AuthenticatedHTTPXClient
292
+
293
+ with patch.object(AuthenticatedHTTPXClient, "_get_api_token", return_value="test-token"):
294
+ client = AuthenticatedHTTPXClient()
295
+ client.client = mock_httpx_client
296
+
297
+ # FastMCP returns lowercase header keys from get_http_headers()
298
+ fastmcp_headers = {
299
+ "content-type": "application/json", # lowercase from FastMCP!
300
+ "accept": "application/json", # lowercase from FastMCP!
301
+ "authorization": "Bearer token",
302
+ }
303
+
304
+ await client.request("GET", "/v1/teams", headers=fastmcp_headers)
305
+
306
+ call_kwargs = mock_httpx_client.request.call_args[1]
307
+ headers = call_kwargs["headers"]
308
+
309
+ # Should have correct Content-Type (mixed case)
310
+ assert headers["Content-Type"] == "application/vnd.api+json"
311
+ assert headers["Accept"] == "application/vnd.api+json"
312
+
313
+ # Should NOT have lowercase duplicates - this is the key assertion!
314
+ assert "content-type" not in headers, "lowercase content-type should be removed"
315
+ assert "accept" not in headers, "lowercase accept should be removed"
316
+
317
+ # Other headers should be preserved
318
+ assert headers["authorization"] == "Bearer token"