rootly-mcp-server 2.3.1__tar.gz → 2.3.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.
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/CHANGELOG.md +10 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/PKG-INFO +1 -1
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/pyproject.toml +1 -1
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/server_defaults.py +12 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/spec_transform.py +37 -6
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/transport.py +82 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_transport_module.py +71 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.dockerignore +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.gitattributes +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.github/dependabot.yml +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.github/openapi-audit-remote-baseline.json +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.github/workflows/ci.yml +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.github/workflows/dependency-review.yml +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.github/workflows/openapi-audit.yml +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.github/workflows/pypi-release.yml +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.gitignore +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.semaphore/deploy-staging.yml +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.semaphore/deploy.yml +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.semaphore/semaphore.yml +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.semaphore/update-task-definition.sh +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/CONTRIBUTING.md +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/Dockerfile +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/LICENSE +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/README.md +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/examples/skills/README.md +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/gemini-extension.json +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/rootly-mcp-server-demo.gif +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/rootly_openapi.json +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/scripts/audit_openapi.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/scripts/setup-hooks.sh +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/server.json +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/__init__.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/__main__.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/audit.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/client.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/code_mode.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/data/__init__.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/exceptions.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/legacy_server.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/mcp_error.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/monitoring.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/och_client.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/pagination.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/payload_stripping.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/security.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/server.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/smart_utils.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/tools/__init__.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/tools/alerts.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/tools/incidents.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/tools/oncall.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/tools/resources.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/utils.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/validators.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/README.md +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/conftest.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/integration/local/test_basic.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/integration/local/test_smart_tools.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/integration/local/test_tool_allowlists.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/integration/remote/test_essential.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/test_client.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_alert_stripping.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_authentication.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_code_mode_module.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_exceptions.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_http_headers.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_legacy_server_module.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_main_transport.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_mcp_error_module.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_och_client.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_oncall_handoff.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_oncall_metrics.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_oncall_new_tools.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_oncall_severity_normalization.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_payload_stripping_module.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_security.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_server.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_server_defaults_module.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_smart_utils.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_tools.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_utils.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_validators.py +0 -0
- {rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/uv.lock +0 -0
|
@@ -5,6 +5,16 @@ All notable changes to the Rootly MCP Server will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.2] - Released 2026-04-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Generated Write Payload Forwarding**: Normalized generated OpenAPI write-tool payloads to unwrap top-level `body` envelopes before forwarding to the Rootly API, fixing failures such as `createWorkflow` returning `Invalid payload ({data: "Expected hash"})`
|
|
13
|
+
|
|
14
|
+
### Testing
|
|
15
|
+
|
|
16
|
+
- **Request-Path Regression Coverage**: Added transport regression tests that assert write requests forward unwrapped JSON payloads and preserve already-correct non-envelope payloads
|
|
17
|
+
|
|
8
18
|
## [2.3.1] - Released 2026-04-23
|
|
9
19
|
|
|
10
20
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rootly-mcp-server
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.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.3.
|
|
3
|
+
version = "2.3.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.12"
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/server_defaults.py
RENAMED
|
@@ -373,6 +373,7 @@ DEFAULT_WRITE_ALLOWED_PATHS = [
|
|
|
373
373
|
"/workflows",
|
|
374
374
|
"/workflows/{workflow_id}",
|
|
375
375
|
"/workflow_groups",
|
|
376
|
+
"/workflow_groups/{id}",
|
|
376
377
|
"/workflows/{workflow_id}/workflow_tasks",
|
|
377
378
|
"/workflow_tasks/{id}",
|
|
378
379
|
"/workflows/{workflow_id}/form_field_conditions",
|
|
@@ -394,6 +395,7 @@ DEFAULT_WRITE_ALLOWED_PATHS = [
|
|
|
394
395
|
"/live_call_routers",
|
|
395
396
|
"/live_call_routers/{id}",
|
|
396
397
|
# Post-incident and retrospectives - create + update
|
|
398
|
+
"/post_incident_reviews",
|
|
397
399
|
"/post_incident_reviews/{id}",
|
|
398
400
|
"/retrospective_processes",
|
|
399
401
|
"/retrospective_processes/{id}",
|
|
@@ -401,6 +403,7 @@ DEFAULT_WRITE_ALLOWED_PATHS = [
|
|
|
401
403
|
"/retrospective_process_groups/{id}",
|
|
402
404
|
"/retrospective_processes/{retrospective_process_id}/retrospective_steps",
|
|
403
405
|
"/retrospective_steps/{id}",
|
|
406
|
+
"/postmortem_templates",
|
|
404
407
|
"/postmortem_templates/{id}",
|
|
405
408
|
# Communications - create + update
|
|
406
409
|
"/communications_groups",
|
|
@@ -413,11 +416,15 @@ DEFAULT_WRITE_ALLOWED_PATHS = [
|
|
|
413
416
|
"/communications_types/{id}",
|
|
414
417
|
# Causes and catalog - create + update
|
|
415
418
|
"/causes",
|
|
419
|
+
"/causes/{id}",
|
|
416
420
|
"/catalogs",
|
|
421
|
+
"/catalogs/{id}",
|
|
417
422
|
"/catalogs/{catalog_id}/entities",
|
|
423
|
+
"/catalog_entities/{id}",
|
|
418
424
|
# Sub-statuses - create + update
|
|
419
425
|
"/sub_statuses",
|
|
420
426
|
"/sub_statuses/{id}",
|
|
427
|
+
"/incident_sub_statuses",
|
|
421
428
|
"/incident_sub_statuses/{id}",
|
|
422
429
|
# User notification preferences - create + update
|
|
423
430
|
"/users/{user_id}/notification_rules",
|
|
@@ -435,13 +442,18 @@ DEFAULT_WRITE_ALLOWED_PATHS = [
|
|
|
435
442
|
# Form and field management - create + update
|
|
436
443
|
"/custom_fields",
|
|
437
444
|
"/custom_fields/{id}",
|
|
445
|
+
"/custom_field_options",
|
|
438
446
|
"/custom_field_options/{id}",
|
|
439
447
|
"/form_sets",
|
|
440
448
|
"/form_sets/{id}",
|
|
441
449
|
"/form_field_placements/{id}",
|
|
442
450
|
"/form_field_placement_conditions/{id}",
|
|
443
451
|
"/form_set_conditions/{id}",
|
|
452
|
+
# Status pages - create + update
|
|
453
|
+
"/status-pages",
|
|
454
|
+
"/status-pages/{id}",
|
|
444
455
|
# Status page templates
|
|
456
|
+
"/status_page_templates",
|
|
445
457
|
"/status_page_templates/{id}",
|
|
446
458
|
]
|
|
447
459
|
|
|
@@ -493,22 +493,30 @@ def _filter_openapi_spec(
|
|
|
493
493
|
}
|
|
494
494
|
)
|
|
495
495
|
|
|
496
|
-
# Also clean up any remaining broken references in components
|
|
496
|
+
# Also clean up any remaining broken references in components.
|
|
497
|
+
# Schemas with broken $refs are patched in-place (broken refs replaced with generic objects)
|
|
498
|
+
# rather than removed, so that write tools like createWorkflow retain their field structure.
|
|
497
499
|
if "components" in filtered_spec and "schemas" in filtered_spec["components"]:
|
|
498
500
|
schemas = filtered_spec["components"]["schemas"]
|
|
499
|
-
# Remove or fix any schemas that reference missing components
|
|
500
501
|
schemas_to_remove = []
|
|
501
502
|
for schema_name, schema_def in schemas.items():
|
|
502
503
|
if isinstance(schema_def, dict) and _has_broken_references(schema_def):
|
|
503
|
-
|
|
504
|
+
patched = _patch_broken_refs(schema_def)
|
|
505
|
+
if _has_broken_references(patched):
|
|
506
|
+
# Still broken after patching — remove it entirely
|
|
507
|
+
schemas_to_remove.append(schema_name)
|
|
508
|
+
else:
|
|
509
|
+
schemas[schema_name] = patched
|
|
510
|
+
logger.debug(f"Patched broken references in schema: {schema_name}")
|
|
504
511
|
|
|
505
512
|
for schema_name in schemas_to_remove:
|
|
506
513
|
logger.debug(f"Removing schema with broken references: {schema_name}")
|
|
507
514
|
del schemas[schema_name]
|
|
508
515
|
|
|
509
|
-
# Clean up
|
|
510
|
-
removed_schemas = set()
|
|
516
|
+
# Clean up operation-level references to schemas that were fully removed (not just patched)
|
|
517
|
+
removed_schemas: set[str] = set()
|
|
511
518
|
if "components" in filtered_spec and "schemas" in filtered_spec["components"]:
|
|
519
|
+
existing = set(filtered_spec["components"]["schemas"].keys())
|
|
512
520
|
removed_schemas = {
|
|
513
521
|
"new_workflow",
|
|
514
522
|
"update_workflow",
|
|
@@ -520,7 +528,7 @@ def _filter_openapi_spec(
|
|
|
520
528
|
"update_workflow_task",
|
|
521
529
|
"workflow_task_response",
|
|
522
530
|
"workflow_task_list",
|
|
523
|
-
}
|
|
531
|
+
} - existing # Only replace refs to schemas that were actually removed
|
|
524
532
|
|
|
525
533
|
for path, path_item in filtered_spec.get("paths", {}).items():
|
|
526
534
|
for method, operation in path_item.items():
|
|
@@ -747,3 +755,26 @@ def _has_broken_references(schema_def: dict[str, Any]) -> bool:
|
|
|
747
755
|
return True
|
|
748
756
|
|
|
749
757
|
return False
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _patch_broken_refs(schema_def: dict[str, Any]) -> dict[str, Any]:
|
|
761
|
+
"""Recursively replace broken $refs with a generic object schema.
|
|
762
|
+
|
|
763
|
+
This preserves the parent schema's field structure (e.g. new_workflow keeps
|
|
764
|
+
name/description/etc.) while neutralising unresolvable nested references like
|
|
765
|
+
incident_trigger_params.
|
|
766
|
+
"""
|
|
767
|
+
if "$ref" in schema_def and _has_broken_references(schema_def):
|
|
768
|
+
return {"type": "object", "additionalProperties": True}
|
|
769
|
+
|
|
770
|
+
result: dict[str, Any] = {}
|
|
771
|
+
for key, value in schema_def.items():
|
|
772
|
+
if isinstance(value, dict):
|
|
773
|
+
result[key] = _patch_broken_refs(value)
|
|
774
|
+
elif isinstance(value, list):
|
|
775
|
+
result[key] = [
|
|
776
|
+
_patch_broken_refs(item) if isinstance(item, dict) else item for item in value
|
|
777
|
+
]
|
|
778
|
+
else:
|
|
779
|
+
result[key] = value
|
|
780
|
+
return result
|
|
@@ -651,6 +651,8 @@ class AuthenticatedHTTPXClient:
|
|
|
651
651
|
# Transform query parameters
|
|
652
652
|
if "params" in kwargs:
|
|
653
653
|
kwargs["params"] = self._transform_params(kwargs["params"])
|
|
654
|
+
if "json" in kwargs:
|
|
655
|
+
kwargs["json"] = self._normalize_request_json_payload(method, kwargs["json"])
|
|
654
656
|
|
|
655
657
|
# Log incoming headers for debugging (before transformation)
|
|
656
658
|
incoming_headers = kwargs.get("headers", {})
|
|
@@ -720,9 +722,89 @@ class AuthenticatedHTTPXClient:
|
|
|
720
722
|
response = self._maybe_normalize_incident_form_field_selection_response(
|
|
721
723
|
method, url, response
|
|
722
724
|
)
|
|
725
|
+
response = self._maybe_annotate_404_response(method, url, response)
|
|
723
726
|
|
|
724
727
|
return response
|
|
725
728
|
|
|
729
|
+
@staticmethod
|
|
730
|
+
def _normalize_request_json_payload(method: str, payload: Any) -> Any:
|
|
731
|
+
"""Normalize JSON payloads forwarded by generated tools.
|
|
732
|
+
|
|
733
|
+
Some generated write tools expose a top-level `body` parameter, which can
|
|
734
|
+
result in JSON payloads of shape `{"body": {...}}`. Rootly API write
|
|
735
|
+
endpoints expect the inner object as the actual request body.
|
|
736
|
+
"""
|
|
737
|
+
if method.upper() not in {"POST", "PUT", "PATCH"}:
|
|
738
|
+
return payload
|
|
739
|
+
if not isinstance(payload, dict):
|
|
740
|
+
return payload
|
|
741
|
+
body = payload.get("body")
|
|
742
|
+
if len(payload) == 1 and isinstance(body, dict):
|
|
743
|
+
return body
|
|
744
|
+
return payload
|
|
745
|
+
|
|
746
|
+
# Matches UUID v4 format: 8-4-4-4-12 lowercase hex chars
|
|
747
|
+
_UUID_RE = re.compile(
|
|
748
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
@staticmethod
|
|
752
|
+
def _path_has_id_segment(url: str) -> bool:
|
|
753
|
+
"""Return True if the URL path ends with what looks like an ID segment.
|
|
754
|
+
|
|
755
|
+
Collection paths (/v1/heartbeats, /v1/status-pages) return False.
|
|
756
|
+
Individual-resource paths (/v1/heartbeats/123) return True.
|
|
757
|
+
|
|
758
|
+
Hyphenated resource names like 'status-pages' must not be confused with
|
|
759
|
+
UUID-style IDs — the UUID check requires strict hex-only segments.
|
|
760
|
+
"""
|
|
761
|
+
path = AuthenticatedHTTPXClient._path_for_url(url)
|
|
762
|
+
last = path.rstrip("/").rsplit("/", 1)[-1]
|
|
763
|
+
return bool(last and (last.isdigit() or AuthenticatedHTTPXClient._UUID_RE.match(last)))
|
|
764
|
+
|
|
765
|
+
@staticmethod
|
|
766
|
+
def _maybe_annotate_404_response(
|
|
767
|
+
method: str, url: str, response: httpx.Response
|
|
768
|
+
) -> httpx.Response:
|
|
769
|
+
"""Append a plan-gating hint to 404 responses.
|
|
770
|
+
|
|
771
|
+
Rootly returns 404 for endpoints that are locked to a specific subscription
|
|
772
|
+
tier, even when the request is valid. The response body uses the generic
|
|
773
|
+
title "Not found or unauthorized" with no plan-specific discriminator.
|
|
774
|
+
|
|
775
|
+
Heuristic: a 404 on a collection path (no trailing ID) is almost certainly
|
|
776
|
+
plan gating regardless of method. A 404 on an ID path during a write is
|
|
777
|
+
ambiguous — the resource may simply not exist — so the hint is softened.
|
|
778
|
+
"""
|
|
779
|
+
if response.status_code != 404:
|
|
780
|
+
return response
|
|
781
|
+
has_id = AuthenticatedHTTPXClient._path_has_id_segment(url)
|
|
782
|
+
is_write = method.upper() in {"POST", "PUT", "PATCH"}
|
|
783
|
+
# Skip GET on ID paths — those are ordinary "resource not found" responses
|
|
784
|
+
if has_id and not is_write:
|
|
785
|
+
return response
|
|
786
|
+
try:
|
|
787
|
+
body = response.json()
|
|
788
|
+
if not has_id:
|
|
789
|
+
hint = (
|
|
790
|
+
"This 404 most likely means the feature is not enabled on your Rootly plan. "
|
|
791
|
+
"Contact Rootly support to enable it for your organisation."
|
|
792
|
+
)
|
|
793
|
+
else:
|
|
794
|
+
hint = (
|
|
795
|
+
"This 404 may mean the resource does not exist, or that the feature is not "
|
|
796
|
+
"enabled on your Rootly plan. Contact Rootly support if the resource exists "
|
|
797
|
+
"and the error persists."
|
|
798
|
+
)
|
|
799
|
+
if isinstance(body, dict):
|
|
800
|
+
body.setdefault("_plan_gating_hint", hint)
|
|
801
|
+
else:
|
|
802
|
+
body = {"original_response": body, "_plan_gating_hint": hint}
|
|
803
|
+
response._content = json.dumps(body).encode() # noqa: SLF001
|
|
804
|
+
except Exception: # nosec B110 - Safe fallback; annotation is best-effort
|
|
805
|
+
pass
|
|
806
|
+
return response
|
|
807
|
+
|
|
726
808
|
@staticmethod
|
|
727
809
|
def _is_alert_endpoint(url: str) -> bool:
|
|
728
810
|
"""Check if a URL is an alert endpoint (but not alert sub-resources like events)."""
|
|
@@ -523,6 +523,54 @@ class TestTransportModule:
|
|
|
523
523
|
assert error_context["upstream_path"] == "/v1/alerts"
|
|
524
524
|
assert error_context["upstream_log_level"] == "error"
|
|
525
525
|
|
|
526
|
+
@pytest.mark.asyncio
|
|
527
|
+
async def test_authenticated_client_unwraps_body_envelope_on_write_requests(self):
|
|
528
|
+
response = httpx.Response(
|
|
529
|
+
200,
|
|
530
|
+
request=httpx.Request("POST", "https://api.rootly.com/v1/workflows"),
|
|
531
|
+
content=b'{"data":{"id":"wf-1"}}',
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
with patch.object(
|
|
535
|
+
transport.AuthenticatedHTTPXClient, "_get_api_token", return_value="token"
|
|
536
|
+
):
|
|
537
|
+
client = transport.AuthenticatedHTTPXClient(hosted=False, transport="stdio")
|
|
538
|
+
client.client.request = AsyncMock(return_value=response)
|
|
539
|
+
|
|
540
|
+
await client.request(
|
|
541
|
+
"POST",
|
|
542
|
+
"/v1/workflows",
|
|
543
|
+
json={"body": {"genius_workflow": {"name": "MCP verification workflow"}}},
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
_, kwargs = client.client.request.call_args
|
|
547
|
+
assert kwargs["json"] == {"genius_workflow": {"name": "MCP verification workflow"}}
|
|
548
|
+
|
|
549
|
+
@pytest.mark.asyncio
|
|
550
|
+
async def test_authenticated_client_preserves_non_envelope_payload_on_update_requests(self):
|
|
551
|
+
response = httpx.Response(
|
|
552
|
+
200,
|
|
553
|
+
request=httpx.Request("PATCH", "https://api.rootly.com/v1/workflows/wf-1"),
|
|
554
|
+
content=b'{"data":{"id":"wf-1"}}',
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
with patch.object(
|
|
558
|
+
transport.AuthenticatedHTTPXClient, "_get_api_token", return_value="token"
|
|
559
|
+
):
|
|
560
|
+
client = transport.AuthenticatedHTTPXClient(hosted=False, transport="stdio")
|
|
561
|
+
client.client.request = AsyncMock(return_value=response)
|
|
562
|
+
|
|
563
|
+
await client.request(
|
|
564
|
+
"PATCH",
|
|
565
|
+
"/v1/workflows/wf-1",
|
|
566
|
+
json={"data": {"type": "workflows", "attributes": {"name": "Updated"}}},
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
_, kwargs = client.client.request.call_args
|
|
570
|
+
assert kwargs["json"] == {
|
|
571
|
+
"data": {"type": "workflows", "attributes": {"name": "Updated"}}
|
|
572
|
+
}
|
|
573
|
+
|
|
526
574
|
def test_sanitize_log_excerpt_redacts_tokens_and_paths(self):
|
|
527
575
|
excerpt = transport._sanitize_log_excerpt(
|
|
528
576
|
'Bearer rootly_1234567890 File "/Users/spencercheng/app.py" failed'
|
|
@@ -560,6 +608,29 @@ class TestTransportModule:
|
|
|
560
608
|
assert result["data"][0]["relationships"]["alerts"] == {"count": 2}
|
|
561
609
|
assert "included" not in result
|
|
562
610
|
|
|
611
|
+
def test_normalize_request_json_payload_unwraps_body_for_write_methods(self):
|
|
612
|
+
payload = {"body": {"genius_workflow": {"name": "workflow-name"}}}
|
|
613
|
+
|
|
614
|
+
result = transport.AuthenticatedHTTPXClient._normalize_request_json_payload("POST", payload)
|
|
615
|
+
|
|
616
|
+
assert result == {"genius_workflow": {"name": "workflow-name"}}
|
|
617
|
+
|
|
618
|
+
def test_normalize_request_json_payload_does_not_unwrap_for_get(self):
|
|
619
|
+
payload = {"body": {"query": "database timeout"}}
|
|
620
|
+
|
|
621
|
+
result = transport.AuthenticatedHTTPXClient._normalize_request_json_payload("GET", payload)
|
|
622
|
+
|
|
623
|
+
assert result == payload
|
|
624
|
+
|
|
625
|
+
def test_normalize_request_json_payload_keeps_non_envelope_dict(self):
|
|
626
|
+
payload = {"data": {"type": "incidents"}, "body": {"ignored": True}}
|
|
627
|
+
|
|
628
|
+
result = transport.AuthenticatedHTTPXClient._normalize_request_json_payload(
|
|
629
|
+
"PATCH", payload
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
assert result == payload
|
|
633
|
+
|
|
563
634
|
def test_strip_heavy_user_data_keeps_profile_essentials(self):
|
|
564
635
|
data = {
|
|
565
636
|
"data": [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/.github/openapi-audit-remote-baseline.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/payload_stripping.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/tools/incidents.py
RENAMED
|
File without changes
|
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/src/rootly_mcp_server/tools/resources.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/integration/local/test_smart_tools.py
RENAMED
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/integration/local/test_tool_allowlists.py
RENAMED
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/integration/remote/test_essential.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_oncall_severity_normalization.py
RENAMED
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_payload_stripping_module.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rootly_mcp_server-2.3.1 → rootly_mcp_server-2.3.3}/tests/unit/test_server_defaults_module.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|