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.
Files changed (84) hide show
  1. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.gitignore +0 -1
  2. rootly_mcp_server-2.3.7/.mcp.json +10 -0
  3. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/CHANGELOG.md +65 -31
  4. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/Dockerfile +1 -0
  5. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/PKG-INFO +3 -3
  6. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/pyproject.toml +3 -3
  7. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/__main__.py +33 -6
  8. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/server.py +40 -1
  9. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/server_defaults.py +1 -0
  10. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/incidents.py +170 -8
  11. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/resources.py +12 -4
  12. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/transport.py +18 -9
  13. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/utils.py +21 -0
  14. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_main_transport.py +60 -2
  15. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_server.py +43 -0
  16. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_tools.py +437 -2
  17. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_transport_module.py +176 -0
  18. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/uv.lock +130 -113
  19. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.dockerignore +0 -0
  20. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.gitattributes +0 -0
  21. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/dependabot.yml +0 -0
  22. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/openapi-audit-remote-baseline.json +0 -0
  23. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/workflows/ci.yml +0 -0
  24. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/workflows/dependency-review.yml +0 -0
  25. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/workflows/openapi-audit.yml +0 -0
  26. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.github/workflows/pypi-release.yml +0 -0
  27. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.semaphore/deploy-staging.yml +0 -0
  28. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.semaphore/deploy.yml +0 -0
  29. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.semaphore/semaphore.yml +0 -0
  30. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/.semaphore/update-task-definition.sh +0 -0
  31. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/CONTRIBUTING.md +0 -0
  32. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/LICENSE +0 -0
  33. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/README.md +0 -0
  34. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/examples/skills/README.md +0 -0
  35. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/gemini-extension.json +0 -0
  36. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/rootly-mcp-server-demo.gif +0 -0
  37. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/rootly_openapi.json +0 -0
  38. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/scripts/audit_openapi.py +0 -0
  39. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/scripts/setup-hooks.sh +0 -0
  40. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/server.json +0 -0
  41. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/__init__.py +0 -0
  42. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/audit.py +0 -0
  43. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/client.py +0 -0
  44. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/code_mode.py +0 -0
  45. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/data/__init__.py +0 -0
  46. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/exceptions.py +0 -0
  47. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/legacy_server.py +0 -0
  48. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/mcp_error.py +0 -0
  49. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/monitoring.py +0 -0
  50. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/och_client.py +0 -0
  51. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/pagination.py +0 -0
  52. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/payload_stripping.py +0 -0
  53. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/security.py +0 -0
  54. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/smart_utils.py +0 -0
  55. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/spec_transform.py +0 -0
  56. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/__init__.py +0 -0
  57. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/alerts.py +0 -0
  58. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/tools/oncall.py +0 -0
  59. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/src/rootly_mcp_server/validators.py +0 -0
  60. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/README.md +0 -0
  61. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/conftest.py +0 -0
  62. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/integration/local/test_basic.py +0 -0
  63. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/integration/local/test_smart_tools.py +0 -0
  64. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/integration/local/test_tool_allowlists.py +0 -0
  65. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/integration/remote/test_essential.py +0 -0
  66. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/test_client.py +0 -0
  67. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_alert_stripping.py +0 -0
  68. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_authentication.py +0 -0
  69. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_code_mode_module.py +0 -0
  70. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_exceptions.py +0 -0
  71. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_http_headers.py +0 -0
  72. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_legacy_server_module.py +0 -0
  73. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_mcp_error_module.py +0 -0
  74. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_och_client.py +0 -0
  75. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_oncall_handoff.py +0 -0
  76. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_oncall_metrics.py +0 -0
  77. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_oncall_new_tools.py +0 -0
  78. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_oncall_severity_normalization.py +0 -0
  79. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_payload_stripping_module.py +0 -0
  80. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_security.py +0 -0
  81. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_server_defaults_module.py +0 -0
  82. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_smart_utils.py +0 -0
  83. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_utils.py +0 -0
  84. {rootly_mcp_server-2.3.3 → rootly_mcp_server-2.3.7}/tests/unit/test_validators.py +0 -0
@@ -199,4 +199,3 @@ test_output/
199
199
  !CONTRIBUTING.md
200
200
  !CHANGELOG.md
201
201
  .beads/
202
- .mcp.json
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "rootly": {
4
+ "url": "https://mcp.rootly.com/mcp",
5
+ "headers": {
6
+ "Authorization": "Bearer ${ROOTLY_API_TOKEN}"
7
+ }
8
+ }
9
+ }
10
+ }
@@ -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.2] - Released 2026-04-23
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
- - **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"})`
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
- - **Request-Path Regression Coverage**: Added transport regression tests that assert write requests forward unwrapped JSON payloads and preserve already-correct non-envelope payloads
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.1] - Released 2026-04-23
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
- - **Create Operations Restored**: Restored missing create operations for workflows, schedules, and configuration resources by expanding `DEFAULT_WRITE_ALLOWED_PATHS` to include collection/base paths (not only `/{id}` update paths)
23
- - **Write Surface Completeness**: Re-enabled create + update parity across key families including workflows, workflow groups, schedules, escalation policies, services, teams, environments, dashboards, playbooks, monitoring, retrospectives, communications, causes, and catalogs
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
- ### Notes
44
+ ### Testing
27
45
 
28
- - `createWorkflow` remains the correct tool name (singular), not `createWorkflows`
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.0] - Released 2026-04-23
48
+ ## [2.3.4] - Released 2026-04-30
32
49
 
33
50
  ### Features
34
51
 
35
- - **Comprehensive API Coverage Expansion**: Expanded MCP tool surface from 124 to 150+ tools by updating to latest Rootly API specification with 95+ operations
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
- ### Enhanced
54
+ ### Dependencies
45
55
 
46
- - **Tool Descriptions**: Enhanced incident and on-call tool descriptions with specific usage context and best practices
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
- ### Documentation
58
+ ## [2.3.3] - Released 2026-04-23
51
59
 
52
- - **README Updates**: Updated tool counts, added workflow subset configurations, and enhanced setup guidance
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
- ### Security
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
- - **Maintained Boundaries**: Preserved security exclusions for sensitive operations (API keys, user management, webhooks, financial data)
59
- - **Curated Approach**: Continued read-first philosophy with carefully selected write operations
60
- - **Input Validation**: Maintained existing security controls and validation patterns
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
 
@@ -28,6 +28,7 @@ EXPOSE 8000
28
28
  ENV PYTHONUNBUFFERED=1
29
29
  ENV FASTMCP_HOST=0.0.0.0
30
30
  ENV FASTMCP_PORT=8000
31
+ ENV FASTMCP_STATELESS_HTTP=true
31
32
  ENV ROOTLY_TRANSPORT=both
32
33
  ENV ROOTLY_LOG_LEVEL=INFO
33
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.3.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.0
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.12.5
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"
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.0", # Pin to patched version with Code Mode support
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.12.5", # Pin to specific version
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 = fastmcp.settings.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
- server.run(
468
- transport=normalized_transport,
469
- middleware=get_hosted_auth_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={"timeout_graceful_shutdown": 30},
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 sanitize_parameters_in_spec
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()
@@ -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
@@ -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[str, Field(description="Incident ID to retrieve")],
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
- response = await make_authenticated_request("GET", f"/v1/incidents/{incident_id}")
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[str, Field(description="Incident ID to update")],
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/{incident_id}", json=payload
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/{incident_id}"
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 for inc in historical_incidents if str(inc.get("id")) != str(incident_id)
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
- response = await make_authenticated_request("GET", f"/v1/incidents/{incident_id}")
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 for inc in historical_incidents if str(inc.get("id")) != str(incident_id)
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