rootly-mcp-server 2.3.3__tar.gz → 2.3.7__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.3 → rootly_mcp_server-2.3.7}/.gitignore +0 -1
- rootly_mcp_server-2.3.7/.mcp.json +10 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/CHANGELOG.md +65 -31
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/Dockerfile +1 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/PKG-INFO +3 -3
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/pyproject.toml +3 -3
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/__main__.py +33 -6
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/server.py +40 -1
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/server_defaults.py +1 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/incidents.py +170 -8
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/resources.py +12 -4
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/transport.py +18 -9
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/utils.py +21 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_main_transport.py +60 -2
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_server.py +43 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_tools.py +437 -2
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_transport_module.py +176 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/uv.lock +130 -113
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.dockerignore +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.gitattributes +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/dependabot.yml +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/openapi-audit-remote-baseline.json +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/workflows/ci.yml +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/workflows/dependency-review.yml +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/workflows/openapi-audit.yml +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/workflows/pypi-release.yml +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.semaphore/deploy-staging.yml +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.semaphore/deploy.yml +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.semaphore/semaphore.yml +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.semaphore/update-task-definition.sh +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/CONTRIBUTING.md +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/LICENSE +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/README.md +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/examples/skills/README.md +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/gemini-extension.json +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/rootly-mcp-server-demo.gif +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/rootly_openapi.json +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/scripts/audit_openapi.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/scripts/setup-hooks.sh +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/server.json +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/__init__.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/audit.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/client.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/code_mode.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/data/__init__.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/exceptions.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/legacy_server.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/mcp_error.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/monitoring.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/och_client.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/pagination.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/payload_stripping.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/security.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/smart_utils.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/spec_transform.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/__init__.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/alerts.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/oncall.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/validators.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/README.md +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/conftest.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/integration/local/test_basic.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/integration/local/test_smart_tools.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/integration/local/test_tool_allowlists.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/integration/remote/test_essential.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/test_client.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_alert_stripping.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_authentication.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_code_mode_module.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_exceptions.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_http_headers.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_legacy_server_module.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_mcp_error_module.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_och_client.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_oncall_handoff.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_oncall_metrics.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_oncall_new_tools.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_oncall_severity_normalization.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_payload_stripping_module.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_security.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_server_defaults_module.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_smart_utils.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_utils.py +0 -0
- {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_validators.py +0 -0
|
@@ -5,59 +5,93 @@ 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.
|
|
8
|
+
## [2.3.7] - Released 2026-05-08
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
- **MCP OAuth2 Authorization Discovery**: Added hosted-only OAuth protected resource metadata and `WWW-Authenticate` discovery headers so compatible MCP clients can discover Rootly's authorization server automatically
|
|
9
13
|
|
|
10
14
|
### Fixed
|
|
11
15
|
|
|
12
|
-
- **
|
|
16
|
+
- **Hosted Streamable HTTP Session Accumulation**: Defaulted hosted streamable HTTP to stateless mode so the server no longer accumulates long-lived MCP sessions when clients do not explicitly terminate them
|
|
17
|
+
- **Full Hosted HTTP Coverage**: Applied the stateless hosted default to both dual-transport deployments and direct hosted `streamable-http` mode for consistent leak mitigation
|
|
18
|
+
|
|
19
|
+
### Configuration
|
|
20
|
+
|
|
21
|
+
- **Hosted Docker Default**: Set `FASTMCP_STATELESS_HTTP=true` in the Docker image so standard hosted deployments use the safer stateless configuration by default while still allowing explicit operator override
|
|
13
22
|
|
|
14
23
|
### Testing
|
|
15
24
|
|
|
16
|
-
- **
|
|
25
|
+
- **Hosted Transport Coverage**: Added focused tests for hosted stateless defaults and explicit `FASTMCP_STATELESS_HTTP` override behavior across both transport entry paths
|
|
17
26
|
|
|
18
|
-
## [2.3.
|
|
27
|
+
## [2.3.6] - Released 2026-05-07
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
- **Open Plugins Discovery Support**: Added a root `.mcp.json` so plugin catalogs like cursor.directory can auto-detect the hosted Rootly MCP configuration and generate one-click install flows
|
|
32
|
+
|
|
33
|
+
### Dependencies
|
|
34
|
+
|
|
35
|
+
- **Routine Dependency Refresh**: Pulled in the latest minor and patch dependency updates from Dependabot
|
|
36
|
+
|
|
37
|
+
## [2.3.5] - Released 2026-05-07
|
|
19
38
|
|
|
20
39
|
### Fixed
|
|
21
40
|
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **Test Accuracy**: Corrected alert source tool-name assertions in unit tests (`createAlertSource` / `updateAlertSource`) to prevent false confidence
|
|
41
|
+
- **Sequential Incident Reference Resolution**: Added server-side resolution for incident references like `4460`, `#4460`, and `INC-4460`, so callers no longer need to convert sequential incident numbers to UUIDs themselves
|
|
42
|
+
- **Broader Incident Tool Support**: Applied the same reference resolution behavior across `getIncident`, `updateIncident`, `find_related_incidents`, `suggest_solutions`, and the `incident://{incident_id}` resource for a more consistent incident workflow
|
|
25
43
|
|
|
26
|
-
###
|
|
44
|
+
### Testing
|
|
27
45
|
|
|
28
|
-
-
|
|
29
|
-
- Security exclusions for sensitive families remain unchanged
|
|
46
|
+
- **Incident Reference Coverage**: Added focused test coverage for UUID and sequential incident references, not-found handling, bounded lookup behavior, and the expanded incident tool/resource paths
|
|
30
47
|
|
|
31
|
-
## [2.3.
|
|
48
|
+
## [2.3.4] - Released 2026-04-30
|
|
32
49
|
|
|
33
50
|
### Features
|
|
34
51
|
|
|
35
|
-
- **
|
|
36
|
-
- **Enhanced Tool Discoverability**: Added contextual "WHEN TO USE" guidance to tool descriptions for improved AI agent decision-making
|
|
37
|
-
- **Workflow-Focused Tool Subsets**: Introduced 5 predefined tool configurations optimized for specific use cases (Incident Response, Schedule Management, Monitoring Setup, Retrospectives, Administration)
|
|
38
|
-
- **Operational Context Resources**: Added new MCP resources for real-time operational awareness:
|
|
39
|
-
- `rootly://oncall-status` - Live on-call status across teams for incident response context
|
|
40
|
-
- `rootly://workflow-guide` - Step-by-step workflow guidance for common Rootly operations
|
|
41
|
-
- **Expanded Write Operations**: Added 54 non-destructive write operations while maintaining security boundaries
|
|
42
|
-
- **API Path Coverage**: Increased DEFAULT_ALLOWED_PATHS from ~80 to 151 endpoints covering workflows, dashboards, communications, playbooks, and monitoring
|
|
52
|
+
- **Incident Timeline Events Enabled by Default**: Enabled creating incident timeline events by default, so timeline entries can now be created through MCP without extra configuration
|
|
43
53
|
|
|
44
|
-
###
|
|
54
|
+
### Dependencies
|
|
45
55
|
|
|
46
|
-
- **
|
|
47
|
-
- **Environment Configuration**: Improved environment-based tool subset configuration following MCP best practices
|
|
48
|
-
- **Error Handling**: Robust error handling in new resource implementations with proper categorization
|
|
56
|
+
- **Routine Dependency Refresh**: Pulled in the latest minor and patch dependency updates from Dependabot
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
## [2.3.3] - Released 2026-04-23
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
- **Configuration Examples**: Added 5 workflow-focused environment configurations for optimal AI performance
|
|
54
|
-
- **API Coverage**: Documented comprehensive endpoint coverage while highlighting security exclusions
|
|
60
|
+
### Fixed
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
- **Schema and Path Interpretation**: Improved how the MCP interprets API schemas and path patterns so more tools can be exposed correctly
|
|
63
|
+
- **Write Path Coverage**: Fixed write-path coverage gaps so generated tools line up more reliably with endpoints that support write actions
|
|
64
|
+
- **Write Availability Guidance**: Improved the guidance returned when a write action is not available for the current endpoint or configuration
|
|
65
|
+
- **Hyphenated Resource Matching**: Tightened path matching so hyphenated resources like `status-pages` are handled correctly
|
|
66
|
+
|
|
67
|
+
## [2.3.2] - Released 2026-04-23
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Generated Write Request Shape**: Fixed how generated MCP tools send create and update requests for API-backed endpoints
|
|
72
|
+
- **Affected Endpoints**: This affected tools tied to endpoints like `/v1/workflows`, `/v1/workflow_groups`, `/v1/schedules`, and other generated write operations that expected a specific request shape
|
|
73
|
+
- **Create Workflow Reliability**: As a result, tools like `createWorkflow` can complete successfully instead of stopping during request submission
|
|
74
|
+
|
|
75
|
+
### Testing
|
|
76
|
+
|
|
77
|
+
- **Request-Path Regression Coverage**: Added transport regression tests that assert write requests forward unwrapped JSON payloads and preserve already-correct non-envelope payloads
|
|
78
|
+
|
|
79
|
+
## [2.3.1] - Released 2026-04-23
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **Restored Create Actions**: Restored missing create actions for key configuration endpoints, including workflows, workflow groups, schedules, schedule rotations, escalation policies, escalation paths, services, teams, and environments
|
|
84
|
+
- **Create and Update Parity**: Closed the gap where users could list or update those records but could not create new ones through MCP
|
|
85
|
+
- **Test Accuracy**: Corrected alert source tool-name assertions in unit tests (`createAlertSource` / `updateAlertSource`)
|
|
86
|
+
|
|
87
|
+
## [2.3.0] - Released 2026-04-23
|
|
88
|
+
|
|
89
|
+
### Features
|
|
57
90
|
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
- **
|
|
91
|
+
- **Broader API Coverage**: Expanded the MCP to cover many more Rootly API areas, including workflows, workflow groups, workflow tasks, schedules, schedule rotations, escalation policies, escalation paths, services, teams, environments, dashboards, playbooks, retrospectives, and monitoring-related resources
|
|
92
|
+
- **More Write Actions**: Added more write actions across those areas, especially update actions, so the MCP can do more than just read data
|
|
93
|
+
- **Wider Operational Use**: Made the MCP useful for more operational and configuration work, not just incident search and on-call lookups
|
|
94
|
+
- **Workflow-Focused Tooling**: Introduced workflow-focused tool subsets and supporting resources for common MCP use cases
|
|
61
95
|
|
|
62
96
|
### Testing
|
|
63
97
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rootly-mcp-server
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.7
|
|
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
|
|
@@ -24,10 +24,10 @@ Requires-Python: >=3.12
|
|
|
24
24
|
Requires-Dist: authlib>=1.6.11
|
|
25
25
|
Requires-Dist: brotli>=1.2.0
|
|
26
26
|
Requires-Dist: cryptography>=46.0.7
|
|
27
|
-
Requires-Dist: fastmcp[code-mode]==3.2.
|
|
27
|
+
Requires-Dist: fastmcp[code-mode]==3.2.4
|
|
28
28
|
Requires-Dist: httpx==0.28.1
|
|
29
29
|
Requires-Dist: numpy>=1.24.0
|
|
30
|
-
Requires-Dist: pydantic==2.
|
|
30
|
+
Requires-Dist: pydantic==2.13.3
|
|
31
31
|
Requires-Dist: pygments>=2.20.0
|
|
32
32
|
Requires-Dist: python-dotenv>=1.2.2
|
|
33
33
|
Requires-Dist: python-multipart>=0.0.26
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rootly-mcp-server"
|
|
3
|
-
version = "2.3.
|
|
3
|
+
version = "2.3.7"
|
|
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"
|
|
@@ -36,7 +36,7 @@ classifiers = [
|
|
|
36
36
|
"License :: OSI Approved :: Apache Software License",
|
|
37
37
|
]
|
|
38
38
|
dependencies = [
|
|
39
|
-
"fastmcp[code-mode]==3.2.
|
|
39
|
+
"fastmcp[code-mode]==3.2.4", # Pin to patched version with Code Mode support
|
|
40
40
|
"authlib>=1.6.11", # Override vulnerable transitive authlib (GHSA-jj8c-mmj3-mmgv)
|
|
41
41
|
"python-dotenv>=1.2.2", # Override vulnerable transitive python-dotenv (GHSA-m8f7-34r5-grfg)
|
|
42
42
|
"python-multipart>=0.0.26", # Override vulnerable transitive python-multipart (CVE-2026-40347)
|
|
@@ -44,7 +44,7 @@ dependencies = [
|
|
|
44
44
|
"Pygments>=2.20.0", # Override vulnerable transitive Pygments
|
|
45
45
|
"requests==2.33.1", # Pin to patched version
|
|
46
46
|
"httpx==0.28.1", # Pin to specific version
|
|
47
|
-
"pydantic==2.
|
|
47
|
+
"pydantic==2.13.3", # Pin to specific version
|
|
48
48
|
"brotli>=1.2.0", # For Brotli compression support in httpx
|
|
49
49
|
"scikit-learn>=1.3.0", # For text similarity and ML utilities
|
|
50
50
|
"numpy>=1.24.0", # For numerical operations
|
|
@@ -63,6 +63,20 @@ def normalize_transport_or_default(value: str, default: TransportName = "stdio")
|
|
|
63
63
|
return default
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def streamable_http_stateless_enabled(*, hosted: bool, fastmcp_stateless_http: bool) -> bool:
|
|
67
|
+
"""Choose streamable HTTP session mode with a safe hosted default.
|
|
68
|
+
|
|
69
|
+
Hosted streamable HTTP traffic is high-churn and most clients do not send
|
|
70
|
+
DELETE to close MCP sessions. On current MCP SDK versions that leaks
|
|
71
|
+
stateful session transports until process restart. We therefore default
|
|
72
|
+
hosted deployments to stateless mode unless the operator explicitly sets
|
|
73
|
+
``FASTMCP_STATELESS_HTTP``.
|
|
74
|
+
"""
|
|
75
|
+
if "FASTMCP_STATELESS_HTTP" in os.environ:
|
|
76
|
+
return fastmcp_stateless_http
|
|
77
|
+
return hosted
|
|
78
|
+
|
|
79
|
+
|
|
66
80
|
def parse_args():
|
|
67
81
|
"""Parse command-line arguments."""
|
|
68
82
|
parser = argparse.ArgumentParser(description="Start the Rootly MCP server for API integration.")
|
|
@@ -259,7 +273,12 @@ def run_dual_http_server(
|
|
|
259
273
|
sse_path = fastmcp.settings.sse_path
|
|
260
274
|
streamable_path = fastmcp.settings.streamable_http_path
|
|
261
275
|
message_path = fastmcp.settings.message_path
|
|
262
|
-
stateless_http =
|
|
276
|
+
stateless_http = streamable_http_stateless_enabled(
|
|
277
|
+
hosted=True, fastmcp_stateless_http=fastmcp.settings.stateless_http
|
|
278
|
+
)
|
|
279
|
+
logger.info(
|
|
280
|
+
"Streamable HTTP configured in %s mode", "stateless" if stateless_http else "stateful"
|
|
281
|
+
)
|
|
263
282
|
|
|
264
283
|
sse_transport = SseServerTransport(message_path)
|
|
265
284
|
|
|
@@ -453,6 +472,11 @@ def main():
|
|
|
453
472
|
logger.info("Code Mode enabled at path: %s", code_mode_path)
|
|
454
473
|
|
|
455
474
|
logger.info(f"Running server with transport: {normalized_transport}...")
|
|
475
|
+
direct_streamable_stateless_http = streamable_http_stateless_enabled(
|
|
476
|
+
hosted=hosted_mode,
|
|
477
|
+
fastmcp_stateless_http=os.getenv("FASTMCP_STATELESS_HTTP", "").lower()
|
|
478
|
+
in ("true", "1", "yes"),
|
|
479
|
+
)
|
|
456
480
|
if normalized_transport == "both":
|
|
457
481
|
run_dual_http_server(
|
|
458
482
|
server=server,
|
|
@@ -464,13 +488,16 @@ def main():
|
|
|
464
488
|
elif normalized_transport == "stdio":
|
|
465
489
|
server.run(transport=normalized_transport)
|
|
466
490
|
else:
|
|
467
|
-
|
|
468
|
-
transport
|
|
469
|
-
middleware
|
|
491
|
+
run_kwargs = {
|
|
492
|
+
"transport": normalized_transport,
|
|
493
|
+
"middleware": get_hosted_auth_middleware(),
|
|
470
494
|
# Override FastMCP's default of 0s to allow active SSE connections
|
|
471
495
|
# to finish gracefully during deployments (avoids 502s).
|
|
472
|
-
uvicorn_config
|
|
473
|
-
|
|
496
|
+
"uvicorn_config": {"timeout_graceful_shutdown": 30},
|
|
497
|
+
}
|
|
498
|
+
if normalized_transport == "streamable-http":
|
|
499
|
+
run_kwargs["stateless_http"] = direct_streamable_stateless_http
|
|
500
|
+
server.run(**run_kwargs)
|
|
474
501
|
|
|
475
502
|
except FileNotFoundError as e:
|
|
476
503
|
logger.error(f"File not found: {e}")
|
|
@@ -25,7 +25,12 @@ from .tools.alerts import register_alert_tools
|
|
|
25
25
|
from .tools.incidents import register_incident_tools
|
|
26
26
|
from .tools.oncall import register_oncall_tools
|
|
27
27
|
from .tools.resources import register_resource_handlers
|
|
28
|
-
from .utils import
|
|
28
|
+
from .utils import (
|
|
29
|
+
OAUTH_PROTECTED_RESOURCE_PATH,
|
|
30
|
+
is_mcp_server_url_static,
|
|
31
|
+
resolve_mcp_server_url,
|
|
32
|
+
sanitize_parameters_in_spec,
|
|
33
|
+
)
|
|
29
34
|
|
|
30
35
|
# Set up logger
|
|
31
36
|
logger = logging.getLogger(__name__)
|
|
@@ -541,6 +546,40 @@ def create_rootly_mcp_server(
|
|
|
541
546
|
|
|
542
547
|
return PlainTextResponse("OK")
|
|
543
548
|
|
|
549
|
+
# OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
550
|
+
# MCP clients fetch this to discover which authorization server to use.
|
|
551
|
+
if hosted:
|
|
552
|
+
from starlette.responses import JSONResponse
|
|
553
|
+
|
|
554
|
+
async def _oauth_protected_resource_handler(request):
|
|
555
|
+
mcp_server_url = resolve_mcp_server_url(request)
|
|
556
|
+
|
|
557
|
+
cache = "max-age=3600" if is_mcp_server_url_static() else "no-store"
|
|
558
|
+
return JSONResponse(
|
|
559
|
+
{
|
|
560
|
+
"resource": mcp_server_url,
|
|
561
|
+
"authorization_servers": [base_url],
|
|
562
|
+
"scopes_supported": [
|
|
563
|
+
"openid",
|
|
564
|
+
"profile",
|
|
565
|
+
"email",
|
|
566
|
+
"all",
|
|
567
|
+
],
|
|
568
|
+
"bearer_methods_supported": ["header"],
|
|
569
|
+
},
|
|
570
|
+
headers={"Cache-Control": cache},
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# RFC 9728 §5: clients may request the path-suffixed variant first
|
|
574
|
+
# (e.g. /.well-known/oauth-protected-resource/mcp for a resource at /mcp).
|
|
575
|
+
@mcp.custom_route(OAUTH_PROTECTED_RESOURCE_PATH + "/{path:path}", methods=["GET"])
|
|
576
|
+
async def oauth_protected_resource_suffixed(request):
|
|
577
|
+
return await _oauth_protected_resource_handler(request)
|
|
578
|
+
|
|
579
|
+
@mcp.custom_route(OAUTH_PROTECTED_RESOURCE_PATH, methods=["GET"])
|
|
580
|
+
async def oauth_protected_resource(request):
|
|
581
|
+
return await _oauth_protected_resource_handler(request)
|
|
582
|
+
|
|
544
583
|
# Add some custom tools for enhanced functionality
|
|
545
584
|
|
|
546
585
|
@mcp.tool()
|
{rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/server_defaults.py
RENAMED
|
@@ -349,6 +349,7 @@ DEFAULT_WRITE_ALLOWED_PATHS = [
|
|
|
349
349
|
# Incident mutations
|
|
350
350
|
"/alert_events/{id}",
|
|
351
351
|
"/incidents/{incident_id}/action_items",
|
|
352
|
+
"/incidents/{incident_id}/events",
|
|
352
353
|
"/incidents/{incident_id}/form_field_selections",
|
|
353
354
|
"/incident_form_field_selections/{id}",
|
|
354
355
|
# On-call schedules - create + update
|
{rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/incidents.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from collections.abc import Awaitable, Callable
|
|
6
7
|
from typing import Annotated, Any, Literal, cast
|
|
7
8
|
|
|
@@ -22,6 +23,12 @@ INCIDENT_LIST_FIELDS = (
|
|
|
22
23
|
"id,sequential_id,title,summary,status,severity,created_at,updated_at,url,"
|
|
23
24
|
"started_at,resolved_at,retrospective_progress_status"
|
|
24
25
|
)
|
|
26
|
+
INCIDENT_REFERENCE_FIELDS = "id,sequential_id"
|
|
27
|
+
INCIDENT_UUID_RE = re.compile(
|
|
28
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
|
|
29
|
+
re.IGNORECASE,
|
|
30
|
+
)
|
|
31
|
+
INCIDENT_SEQUENTIAL_REF_RE = re.compile(r"^(?:#|INC-)?(\d+)$")
|
|
25
32
|
|
|
26
33
|
|
|
27
34
|
def _split_csv_values(value: str) -> list[str]:
|
|
@@ -76,6 +83,110 @@ def _summarize_incident_record(incident: dict[str, Any]) -> dict[str, Any]:
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
|
|
86
|
+
def _extract_sequential_id(incident: dict[str, Any]) -> int | None:
|
|
87
|
+
"""Extract a numeric sequential incident ID from a Rootly incident record."""
|
|
88
|
+
attrs = incident.get("attributes", {})
|
|
89
|
+
sequential_id = attrs.get("sequential_id")
|
|
90
|
+
if sequential_id is None:
|
|
91
|
+
return None
|
|
92
|
+
try:
|
|
93
|
+
return int(sequential_id)
|
|
94
|
+
except (TypeError, ValueError):
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _normalize_incident_reference(reference: str) -> tuple[str, str | int]:
|
|
99
|
+
"""Classify and normalize an incident reference."""
|
|
100
|
+
normalized = _normalize_optional_text(reference)
|
|
101
|
+
if normalized is None:
|
|
102
|
+
raise ValueError("Incident reference is required")
|
|
103
|
+
if INCIDENT_UUID_RE.match(normalized):
|
|
104
|
+
return ("uuid", normalized)
|
|
105
|
+
sequential_match = INCIDENT_SEQUENTIAL_REF_RE.match(normalized)
|
|
106
|
+
if sequential_match:
|
|
107
|
+
return ("sequential", int(sequential_match.group(1)))
|
|
108
|
+
return ("direct", normalized)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def _resolve_incident_reference_to_uuid(
|
|
112
|
+
incident_reference: str,
|
|
113
|
+
make_authenticated_request: MakeAuthenticatedRequest,
|
|
114
|
+
) -> str:
|
|
115
|
+
"""Resolve supported incident references to the Rootly incident UUID."""
|
|
116
|
+
reference_kind, normalized_reference = _normalize_incident_reference(incident_reference)
|
|
117
|
+
if reference_kind in {"uuid", "direct"}:
|
|
118
|
+
return cast(str, normalized_reference)
|
|
119
|
+
|
|
120
|
+
target_sequential_id = cast(int, normalized_reference)
|
|
121
|
+
page_cache: dict[int, tuple[list[dict[str, Any]], dict[str, Any]]] = {}
|
|
122
|
+
|
|
123
|
+
async def _fetch_page(page_number: int) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
124
|
+
cached = page_cache.get(page_number)
|
|
125
|
+
if cached is not None:
|
|
126
|
+
return cached
|
|
127
|
+
|
|
128
|
+
response = await make_authenticated_request(
|
|
129
|
+
"GET",
|
|
130
|
+
"/v1/incidents",
|
|
131
|
+
params={
|
|
132
|
+
"page[size]": 100,
|
|
133
|
+
"page[number]": page_number,
|
|
134
|
+
"fields[incidents]": INCIDENT_REFERENCE_FIELDS,
|
|
135
|
+
"include": "",
|
|
136
|
+
"sort": "-created_at",
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
response_data = response.json()
|
|
141
|
+
incidents = cast(list[dict[str, Any]], response_data.get("data", []))
|
|
142
|
+
meta = cast(dict[str, Any], response_data.get("meta", {}))
|
|
143
|
+
page_cache[page_number] = (incidents, meta)
|
|
144
|
+
return incidents, meta
|
|
145
|
+
|
|
146
|
+
incidents, meta = await _fetch_page(1)
|
|
147
|
+
total_pages = int(meta.get("total_pages") or 1)
|
|
148
|
+
|
|
149
|
+
left = 1
|
|
150
|
+
right = total_pages
|
|
151
|
+
|
|
152
|
+
while left <= right:
|
|
153
|
+
page_number = (left + right) // 2
|
|
154
|
+
if page_number == 1:
|
|
155
|
+
page_incidents, _ = incidents, meta
|
|
156
|
+
else:
|
|
157
|
+
page_incidents, _ = await _fetch_page(page_number)
|
|
158
|
+
|
|
159
|
+
sequential_ids = [
|
|
160
|
+
sequential_id
|
|
161
|
+
for sequential_id in (_extract_sequential_id(incident) for incident in page_incidents)
|
|
162
|
+
if sequential_id is not None
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
if not sequential_ids:
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
page_max = max(sequential_ids)
|
|
169
|
+
page_min = min(sequential_ids)
|
|
170
|
+
|
|
171
|
+
if target_sequential_id > page_max:
|
|
172
|
+
right = page_number - 1
|
|
173
|
+
continue
|
|
174
|
+
if target_sequential_id < page_min:
|
|
175
|
+
left = page_number + 1
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
for incident in page_incidents:
|
|
179
|
+
if _extract_sequential_id(incident) == target_sequential_id:
|
|
180
|
+
incident_uuid = incident.get("id")
|
|
181
|
+
if isinstance(incident_uuid, str) and incident_uuid:
|
|
182
|
+
return incident_uuid
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
raise LookupError(f"Incident reference not found: INC-{target_sequential_id}")
|
|
186
|
+
|
|
187
|
+
raise LookupError(f"Incident reference not found: INC-{target_sequential_id}")
|
|
188
|
+
|
|
189
|
+
|
|
79
190
|
def register_incident_tools(
|
|
80
191
|
mcp: Any,
|
|
81
192
|
make_authenticated_request: MakeAuthenticatedRequest,
|
|
@@ -624,11 +735,21 @@ def register_incident_tools(
|
|
|
624
735
|
|
|
625
736
|
@mcp.tool(name="getIncident")
|
|
626
737
|
async def get_incident(
|
|
627
|
-
incident_id: Annotated[
|
|
738
|
+
incident_id: Annotated[
|
|
739
|
+
str,
|
|
740
|
+
Field(
|
|
741
|
+
description="Incident reference to retrieve: UUID, bare number like 4460, #4460, or INC-4460"
|
|
742
|
+
),
|
|
743
|
+
],
|
|
628
744
|
) -> JsonDict:
|
|
629
745
|
"""Retrieve a single incident with PIR-related fields for direct verification."""
|
|
630
746
|
try:
|
|
631
|
-
|
|
747
|
+
resolved_incident_id = await _resolve_incident_reference_to_uuid(
|
|
748
|
+
incident_id, make_authenticated_request
|
|
749
|
+
)
|
|
750
|
+
response = await make_authenticated_request(
|
|
751
|
+
"GET", f"/v1/incidents/{resolved_incident_id}"
|
|
752
|
+
)
|
|
632
753
|
response.raise_for_status()
|
|
633
754
|
|
|
634
755
|
response_data = response.json()
|
|
@@ -636,6 +757,22 @@ def register_incident_tools(
|
|
|
636
757
|
stripped = strip_heavy_nested_data({"data": [response_data["data"]]})
|
|
637
758
|
response_data["data"] = stripped["data"][0]
|
|
638
759
|
return cast(JsonDict, response_data)
|
|
760
|
+
except ValueError as e:
|
|
761
|
+
return cast(
|
|
762
|
+
JsonDict,
|
|
763
|
+
mcp_error.tool_error(
|
|
764
|
+
f"Failed to retrieve incident: {e}",
|
|
765
|
+
"validation_error",
|
|
766
|
+
),
|
|
767
|
+
)
|
|
768
|
+
except LookupError as e:
|
|
769
|
+
return cast(
|
|
770
|
+
JsonDict,
|
|
771
|
+
mcp_error.tool_error(
|
|
772
|
+
f"Failed to retrieve incident: {e}",
|
|
773
|
+
"not_found",
|
|
774
|
+
),
|
|
775
|
+
)
|
|
639
776
|
except Exception as e:
|
|
640
777
|
error_type, error_message = mcp_error.categorize_error(e)
|
|
641
778
|
return cast(
|
|
@@ -744,7 +881,12 @@ def register_incident_tools(
|
|
|
744
881
|
|
|
745
882
|
@mcp.tool(name="updateIncident")
|
|
746
883
|
async def update_incident(
|
|
747
|
-
incident_id: Annotated[
|
|
884
|
+
incident_id: Annotated[
|
|
885
|
+
str,
|
|
886
|
+
Field(
|
|
887
|
+
description="Incident reference to update: UUID, bare number like 4460, #4460, or INC-4460"
|
|
888
|
+
),
|
|
889
|
+
],
|
|
748
890
|
retrospective_progress_status: Annotated[
|
|
749
891
|
str | None,
|
|
750
892
|
Field(
|
|
@@ -791,8 +933,11 @@ def register_incident_tools(
|
|
|
791
933
|
}
|
|
792
934
|
|
|
793
935
|
try:
|
|
936
|
+
resolved_incident_id = await _resolve_incident_reference_to_uuid(
|
|
937
|
+
incident_id, make_authenticated_request
|
|
938
|
+
)
|
|
794
939
|
response = await make_authenticated_request(
|
|
795
|
-
"PUT", f"/v1/incidents/{
|
|
940
|
+
"PUT", f"/v1/incidents/{resolved_incident_id}", json=payload
|
|
796
941
|
)
|
|
797
942
|
response.raise_for_status()
|
|
798
943
|
|
|
@@ -842,11 +987,15 @@ def register_incident_tools(
|
|
|
842
987
|
"""
|
|
843
988
|
try:
|
|
844
989
|
target_incident: dict[str, Any] = {}
|
|
990
|
+
resolved_incident_id = ""
|
|
845
991
|
|
|
846
992
|
if incident_id:
|
|
847
993
|
# Get the target incident details by ID
|
|
994
|
+
resolved_incident_id = await _resolve_incident_reference_to_uuid(
|
|
995
|
+
incident_id, make_authenticated_request
|
|
996
|
+
)
|
|
848
997
|
target_response = await make_authenticated_request(
|
|
849
|
-
"GET", f"/v1/incidents/{
|
|
998
|
+
"GET", f"/v1/incidents/{resolved_incident_id}"
|
|
850
999
|
)
|
|
851
1000
|
target_response.raise_for_status()
|
|
852
1001
|
target_incident_data = strip_heavy_nested_data(
|
|
@@ -898,7 +1047,9 @@ def register_incident_tools(
|
|
|
898
1047
|
# Filter out the target incident itself if it exists
|
|
899
1048
|
if incident_id:
|
|
900
1049
|
historical_incidents = [
|
|
901
|
-
inc
|
|
1050
|
+
inc
|
|
1051
|
+
for inc in historical_incidents
|
|
1052
|
+
if str(inc.get("id")) != str(resolved_incident_id)
|
|
902
1053
|
]
|
|
903
1054
|
|
|
904
1055
|
if not historical_incidents:
|
|
@@ -907,6 +1058,7 @@ def register_incident_tools(
|
|
|
907
1058
|
"message": "No historical incidents found for comparison",
|
|
908
1059
|
"target_incident": {
|
|
909
1060
|
"id": incident_id or "synthetic",
|
|
1061
|
+
"resolved_incident_id": resolved_incident_id or None,
|
|
910
1062
|
"title": target_incident.get("attributes", {}).get(
|
|
911
1063
|
"title", incident_description
|
|
912
1064
|
),
|
|
@@ -941,6 +1093,7 @@ def register_incident_tools(
|
|
|
941
1093
|
return {
|
|
942
1094
|
"target_incident": {
|
|
943
1095
|
"id": incident_id or "synthetic",
|
|
1096
|
+
"resolved_incident_id": resolved_incident_id or None,
|
|
944
1097
|
"title": target_incident.get("attributes", {}).get(
|
|
945
1098
|
"title", incident_description
|
|
946
1099
|
),
|
|
@@ -988,10 +1141,16 @@ def register_incident_tools(
|
|
|
988
1141
|
"""
|
|
989
1142
|
try:
|
|
990
1143
|
target_incident: dict[str, Any] = {}
|
|
1144
|
+
resolved_incident_id = ""
|
|
991
1145
|
|
|
992
1146
|
if incident_id:
|
|
993
1147
|
# Get incident details by ID
|
|
994
|
-
|
|
1148
|
+
resolved_incident_id = await _resolve_incident_reference_to_uuid(
|
|
1149
|
+
incident_id, make_authenticated_request
|
|
1150
|
+
)
|
|
1151
|
+
response = await make_authenticated_request(
|
|
1152
|
+
"GET", f"/v1/incidents/{resolved_incident_id}"
|
|
1153
|
+
)
|
|
995
1154
|
response.raise_for_status()
|
|
996
1155
|
incident_data = strip_heavy_nested_data({"data": [response.json().get("data", {})]})
|
|
997
1156
|
target_incident = incident_data.get("data", [{}])[0]
|
|
@@ -1039,7 +1198,9 @@ def register_incident_tools(
|
|
|
1039
1198
|
# Filter out target incident if it exists
|
|
1040
1199
|
if incident_id:
|
|
1041
1200
|
historical_incidents = [
|
|
1042
|
-
inc
|
|
1201
|
+
inc
|
|
1202
|
+
for inc in historical_incidents
|
|
1203
|
+
if str(inc.get("id")) != str(resolved_incident_id)
|
|
1043
1204
|
]
|
|
1044
1205
|
|
|
1045
1206
|
if not historical_incidents:
|
|
@@ -1073,6 +1234,7 @@ def register_incident_tools(
|
|
|
1073
1234
|
return {
|
|
1074
1235
|
"target_incident": {
|
|
1075
1236
|
"id": incident_id or "synthetic",
|
|
1237
|
+
"resolved_incident_id": resolved_incident_id or None,
|
|
1076
1238
|
"title": target_incident.get("attributes", {}).get("title", incident_title),
|
|
1077
1239
|
"description": target_incident.get("attributes", {}).get(
|
|
1078
1240
|
"summary", incident_description
|