rootly-mcp-server 2.2.18__tar.gz → 2.2.19__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.2.18 → rootly_mcp_server-2.2.19}/CHANGELOG.md +10 -0
  2. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/PKG-INFO +2 -32
  3. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/README.md +1 -31
  4. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/pyproject.toml +1 -1
  5. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/server.py +0 -8
  6. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/__init__.py +0 -2
  7. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/incidents.py +225 -139
  8. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_tools.py +65 -0
  9. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/uv.lock +1 -1
  10. rootly_mcp_server-2.2.18/src/rootly_mcp_server/data/chatgpt_incident_workbench.html +0 -576
  11. rootly_mcp_server-2.2.18/src/rootly_mcp_server/tools/chatgpt_app.py +0 -350
  12. rootly_mcp_server-2.2.18/tests/unit/test_chatgpt_app_module.py +0 -229
  13. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.dockerignore +0 -0
  14. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.gitattributes +0 -0
  15. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/dependabot.yml +0 -0
  16. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/openapi-audit-remote-baseline.json +0 -0
  17. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/workflows/ci.yml +0 -0
  18. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/workflows/dependency-review.yml +0 -0
  19. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/workflows/openapi-audit.yml +0 -0
  20. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/workflows/pypi-release.yml +0 -0
  21. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.gitignore +0 -0
  22. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.semaphore/deploy-staging.yml +0 -0
  23. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.semaphore/deploy.yml +0 -0
  24. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.semaphore/semaphore.yml +0 -0
  25. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.semaphore/update-task-definition.sh +0 -0
  26. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/CONTRIBUTING.md +0 -0
  27. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/Dockerfile +0 -0
  28. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/LICENSE +0 -0
  29. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/examples/skills/README.md +0 -0
  30. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/gemini-extension.json +0 -0
  31. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/rootly-mcp-server-demo.gif +0 -0
  32. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/rootly_openapi.json +0 -0
  33. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/scripts/audit_openapi.py +0 -0
  34. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/scripts/setup-hooks.sh +0 -0
  35. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/server.json +0 -0
  36. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/__init__.py +0 -0
  37. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/__main__.py +0 -0
  38. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/client.py +0 -0
  39. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/code_mode.py +0 -0
  40. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/data/__init__.py +0 -0
  41. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/exceptions.py +0 -0
  42. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/legacy_server.py +0 -0
  43. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/mcp_error.py +0 -0
  44. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/monitoring.py +0 -0
  45. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/och_client.py +0 -0
  46. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/pagination.py +0 -0
  47. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/payload_stripping.py +0 -0
  48. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/security.py +0 -0
  49. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/server_defaults.py +0 -0
  50. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/smart_utils.py +0 -0
  51. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/spec_transform.py +0 -0
  52. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/alerts.py +0 -0
  53. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/oncall.py +0 -0
  54. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/resources.py +0 -0
  55. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/transport.py +0 -0
  56. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/utils.py +0 -0
  57. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/validators.py +0 -0
  58. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/README.md +0 -0
  59. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/conftest.py +0 -0
  60. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/integration/local/test_basic.py +0 -0
  61. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/integration/local/test_smart_tools.py +0 -0
  62. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/integration/remote/test_essential.py +0 -0
  63. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/test_client.py +0 -0
  64. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_alert_stripping.py +0 -0
  65. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_authentication.py +0 -0
  66. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_code_mode_module.py +0 -0
  67. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_exceptions.py +0 -0
  68. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_http_headers.py +0 -0
  69. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_legacy_server_module.py +0 -0
  70. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_main_transport.py +0 -0
  71. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_mcp_error_module.py +0 -0
  72. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_och_client.py +0 -0
  73. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_oncall_handoff.py +0 -0
  74. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_oncall_metrics.py +0 -0
  75. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_oncall_new_tools.py +0 -0
  76. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_oncall_severity_normalization.py +0 -0
  77. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_payload_stripping_module.py +0 -0
  78. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_security.py +0 -0
  79. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_server.py +0 -0
  80. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_server_defaults_module.py +0 -0
  81. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_smart_utils.py +0 -0
  82. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_transport_module.py +0 -0
  83. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_utils.py +0 -0
  84. {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_validators.py +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.2.19] - Released 2026-04-17
9
+
10
+ ### Features
11
+
12
+ - **Scoped Incident Creation Tool**: Added a custom `createIncident` tool so agents can create incidents directly from MCP without exposing the full raw `/incidents` OpenAPI surface
13
+
14
+ ### Documentation
15
+
16
+ - **Custom Tool List Updated**: Added `createIncident` to the README custom tool section with its scoped workflow-oriented behavior
17
+
8
18
  ## [2.2.18] - Released 2026-04-15
9
19
 
10
20
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.2.18
3
+ Version: 2.2.19
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
@@ -276,36 +276,6 @@ Add to `claude_desktop_config.json`:
276
276
 
277
277
  </details>
278
278
 
279
- ## ChatGPT App Starter
280
-
281
- The server now includes a built-in ChatGPT app surface for incident exploration.
282
-
283
- - **Model-visible app tool:** `open_rootly_incident_workbench`
284
- - **App-only detail tool:** `get_rootly_incident_detail`
285
- - **UI resource:** `ui://rootly/incident-workbench.html`
286
-
287
- This gives ChatGPT a visual Rootly incident workbench without needing a separate backend.
288
-
289
- ### What it does
290
-
291
- - Applies structured incident filters such as team, time range, severity, and status
292
- - Collects a bounded set of incident summaries for the widget
293
- - Lets the widget open a detailed incident pane without exposing the detail tool to the model
294
-
295
- ### Try it in ChatGPT Developer Mode
296
-
297
- Once the MCP server is connected in ChatGPT Developer Mode, try prompts like:
298
-
299
- - `Open the Rootly incident workbench for Infrastructure incidents from the last 7 days`
300
- - `Show me resolved critical incidents for Infrastructure from April 1 through April 13`
301
- - `Open the Rootly incident workbench for database timeout incidents`
302
-
303
- ### Production auth note
304
-
305
- For internal testing and developer-mode setups, the existing bearer-token flow is enough.
306
-
307
- For a public ChatGPT app that exposes customer-specific data or write actions, plan to add OAuth 2.1 support on the MCP side before submission. The current Rootly MCP server forwards bearer tokens, but public Apps SDK submissions expect OAuth 2.1-compatible MCP authorization metadata and token verification.
308
-
309
279
  ## Rootly CLI
310
280
 
311
281
  Standalone CLI for incidents, alerts, services, and on-call operations.
@@ -445,6 +415,7 @@ The default server configuration exposes **110 tools**.
445
415
  - `check_oncall_health_risk`
446
416
  - `check_responder_availability`
447
417
  - `collect_incidents`
418
+ - `createIncident` - create a new incident with a scoped set of fields for agent workflows
448
419
  - `create_override_recommendation`
449
420
  - `find_related_incidents`
450
421
  - `getIncident` - retrieve a single incident for direct verification, including PIR-related fields
@@ -457,7 +428,6 @@ The default server configuration exposes **110 tools**.
457
428
  - `list_endpoints`
458
429
  - `list_incidents`
459
430
  - `list_shifts`
460
- - `open_rootly_incident_workbench`
461
431
  - `search_incidents`
462
432
  - `suggest_solutions`
463
433
  - `updateIncident` - scoped incident update tool for `summary` and `retrospective_progress_status`
@@ -234,36 +234,6 @@ Add to `claude_desktop_config.json`:
234
234
 
235
235
  </details>
236
236
 
237
- ## ChatGPT App Starter
238
-
239
- The server now includes a built-in ChatGPT app surface for incident exploration.
240
-
241
- - **Model-visible app tool:** `open_rootly_incident_workbench`
242
- - **App-only detail tool:** `get_rootly_incident_detail`
243
- - **UI resource:** `ui://rootly/incident-workbench.html`
244
-
245
- This gives ChatGPT a visual Rootly incident workbench without needing a separate backend.
246
-
247
- ### What it does
248
-
249
- - Applies structured incident filters such as team, time range, severity, and status
250
- - Collects a bounded set of incident summaries for the widget
251
- - Lets the widget open a detailed incident pane without exposing the detail tool to the model
252
-
253
- ### Try it in ChatGPT Developer Mode
254
-
255
- Once the MCP server is connected in ChatGPT Developer Mode, try prompts like:
256
-
257
- - `Open the Rootly incident workbench for Infrastructure incidents from the last 7 days`
258
- - `Show me resolved critical incidents for Infrastructure from April 1 through April 13`
259
- - `Open the Rootly incident workbench for database timeout incidents`
260
-
261
- ### Production auth note
262
-
263
- For internal testing and developer-mode setups, the existing bearer-token flow is enough.
264
-
265
- For a public ChatGPT app that exposes customer-specific data or write actions, plan to add OAuth 2.1 support on the MCP side before submission. The current Rootly MCP server forwards bearer tokens, but public Apps SDK submissions expect OAuth 2.1-compatible MCP authorization metadata and token verification.
266
-
267
237
  ## Rootly CLI
268
238
 
269
239
  Standalone CLI for incidents, alerts, services, and on-call operations.
@@ -403,6 +373,7 @@ The default server configuration exposes **110 tools**.
403
373
  - `check_oncall_health_risk`
404
374
  - `check_responder_availability`
405
375
  - `collect_incidents`
376
+ - `createIncident` - create a new incident with a scoped set of fields for agent workflows
406
377
  - `create_override_recommendation`
407
378
  - `find_related_incidents`
408
379
  - `getIncident` - retrieve a single incident for direct verification, including PIR-related fields
@@ -415,7 +386,6 @@ The default server configuration exposes **110 tools**.
415
386
  - `list_endpoints`
416
387
  - `list_incidents`
417
388
  - `list_shifts`
418
- - `open_rootly_incident_workbench`
419
389
  - `search_incidents`
420
390
  - `suggest_solutions`
421
391
  - `updateIncident` - scoped incident update tool for `summary` and `retrospective_progress_status`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.2.18"
3
+ version = "2.2.19"
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"
@@ -22,7 +22,6 @@ from .exceptions import RootlyAuthenticationError
22
22
  from .mcp_error import MCPError
23
23
  from .security import mask_sensitive_data, sanitize_error_message
24
24
  from .tools.alerts import register_alert_tools
25
- from .tools.chatgpt_app import register_chatgpt_app_tools
26
25
  from .tools.incidents import register_incident_tools
27
26
  from .tools.oncall import register_oncall_tools
28
27
  from .tools.resources import register_resource_handlers
@@ -576,13 +575,6 @@ def create_rootly_mcp_server(
576
575
  mcp_error=MCPError,
577
576
  )
578
577
 
579
- register_chatgpt_app_tools(
580
- mcp=mcp,
581
- make_authenticated_request=make_authenticated_request,
582
- strip_heavy_nested_data=strip_heavy_nested_data,
583
- mcp_error=MCPError,
584
- )
585
-
586
578
  register_alert_tools(
587
579
  mcp=mcp,
588
580
  make_authenticated_request=make_authenticated_request,
@@ -1,14 +1,12 @@
1
1
  """Tool registration modules for Rootly MCP server."""
2
2
 
3
3
  from .alerts import register_alert_tools
4
- from .chatgpt_app import register_chatgpt_app_tools
5
4
  from .incidents import register_incident_tools
6
5
  from .oncall import register_oncall_tools
7
6
  from .resources import register_resource_handlers
8
7
 
9
8
  __all__ = [
10
9
  "register_alert_tools",
11
- "register_chatgpt_app_tools",
12
10
  "register_incident_tools",
13
11
  "register_oncall_tools",
14
12
  "register_resource_handlers",
@@ -29,6 +29,14 @@ def _split_csv_values(value: str) -> list[str]:
29
29
  return [part.strip() for part in value.split(",") if part.strip()]
30
30
 
31
31
 
32
+ def _normalize_optional_text(value: str | None) -> str | None:
33
+ """Normalize optional text inputs by trimming whitespace and empty values."""
34
+ if value is None:
35
+ return None
36
+ normalized = value.strip()
37
+ return normalized or None
38
+
39
+
32
40
  def _extract_incident_severity(severity_value: Any) -> str | None:
33
41
  """Normalize severity values from Rootly API responses into a compact string."""
34
42
  if severity_value is None:
@@ -68,141 +76,6 @@ def _summarize_incident_record(incident: dict[str, Any]) -> dict[str, Any]:
68
76
  }
69
77
 
70
78
 
71
- async def resolve_team_names_to_ids(
72
- *,
73
- teams: str,
74
- make_authenticated_request: MakeAuthenticatedRequest,
75
- ) -> tuple[str, dict[str, str]]:
76
- """Resolve comma-separated team names/slugs to Rootly team IDs."""
77
- requested_teams = _split_csv_values(teams)
78
- if not requested_teams:
79
- return "", {}
80
-
81
- resolved_team_ids: list[str] = []
82
- resolved_team_lookup: dict[str, str] = {}
83
- unresolved_teams: list[str] = []
84
-
85
- for team in requested_teams:
86
- matched_id = None
87
-
88
- for filter_key, expected_value in (
89
- ("filter[slug]", team),
90
- ("filter[name]", team),
91
- ):
92
- response = await make_authenticated_request(
93
- "GET",
94
- "/v1/teams",
95
- params={
96
- "page[size]": 100,
97
- "page[number]": 1,
98
- filter_key: team,
99
- },
100
- )
101
- response.raise_for_status()
102
-
103
- for candidate in response.json().get("data", []):
104
- attrs = candidate.get("attributes", {})
105
- candidate_value = (
106
- attrs.get("slug")
107
- if filter_key == "filter[slug]"
108
- else attrs.get("name")
109
- )
110
- if (
111
- isinstance(candidate_value, str)
112
- and candidate_value.lower() == expected_value.lower()
113
- ):
114
- matched_id = str(candidate.get("id"))
115
- break
116
-
117
- if matched_id:
118
- break
119
-
120
- if matched_id:
121
- resolved_team_ids.append(matched_id)
122
- resolved_team_lookup[team] = matched_id
123
- else:
124
- unresolved_teams.append(team)
125
-
126
- if unresolved_teams:
127
- raise ValueError(
128
- "Could not resolve team names/slugs to team IDs: " + ", ".join(unresolved_teams)
129
- )
130
-
131
- return ",".join(dict.fromkeys(resolved_team_ids)), resolved_team_lookup
132
-
133
-
134
- async def prepare_incident_query_context(
135
- *,
136
- make_authenticated_request: MakeAuthenticatedRequest,
137
- query: str,
138
- teams: str,
139
- team_ids: str,
140
- service_ids: str,
141
- severity: str,
142
- status: str,
143
- started_after: str,
144
- started_before: str,
145
- custom_field_selected_option_ids: str,
146
- sort: Literal["created_at", "-created_at", "updated_at", "-updated_at"],
147
- ) -> tuple[dict[str, Any], dict[str, Any]]:
148
- """Build shared incident query params and filter metadata for list/collect tools."""
149
- resolved_team_lookup: dict[str, str] = {}
150
- resolved_team_ids = team_ids
151
-
152
- if teams:
153
- resolved_teams_value, resolved_team_lookup = await resolve_team_names_to_ids(
154
- teams=teams,
155
- make_authenticated_request=make_authenticated_request,
156
- )
157
- if resolved_team_ids and resolved_teams_value:
158
- combined_team_ids = _split_csv_values(resolved_team_ids) + _split_csv_values(
159
- resolved_teams_value
160
- )
161
- resolved_team_ids = ",".join(dict.fromkeys(combined_team_ids))
162
- elif resolved_teams_value:
163
- resolved_team_ids = resolved_teams_value
164
-
165
- params: dict[str, Any] = {
166
- "fields[incidents]": INCIDENT_LIST_FIELDS,
167
- "include": "",
168
- "sort": sort,
169
- }
170
-
171
- if query:
172
- params["filter[search]"] = query
173
- if resolved_team_ids:
174
- params["filter[team_ids]"] = resolved_team_ids
175
- if service_ids:
176
- params["filter[service_ids]"] = service_ids
177
- if severity:
178
- params["filter[severity]"] = severity
179
- if status:
180
- params["filter[status]"] = status
181
- if started_after:
182
- params["filter[started_at][gte]"] = started_after
183
- if started_before:
184
- params["filter[started_at][lte]"] = started_before
185
- if custom_field_selected_option_ids:
186
- params["filter[custom_field_selected_option_ids]"] = custom_field_selected_option_ids
187
-
188
- filters = {
189
- "query": query,
190
- "teams": teams,
191
- "team_ids": team_ids,
192
- "resolved_team_ids": resolved_team_ids,
193
- "resolved_team_lookup": resolved_team_lookup,
194
- "service_ids": service_ids,
195
- "severity": severity,
196
- "status": status,
197
- "started_after": started_after,
198
- "started_before": started_before,
199
- "custom_field_selected_option_ids": custom_field_selected_option_ids,
200
- "sort": sort,
201
- }
202
-
203
- return params, filters
204
-
205
-
206
79
  def register_incident_tools(
207
80
  mcp: Any,
208
81
  make_authenticated_request: MakeAuthenticatedRequest,
@@ -216,6 +89,127 @@ def register_incident_tools(
216
89
  similarity_analyzer = TextSimilarityAnalyzer()
217
90
  solution_extractor = SolutionExtractor()
218
91
 
92
+ async def _resolve_team_names_to_ids(teams: str) -> tuple[str, dict[str, str]]:
93
+ """Resolve comma-separated team names/slugs to Rootly team IDs."""
94
+ requested_teams = _split_csv_values(teams)
95
+ if not requested_teams:
96
+ return "", {}
97
+
98
+ resolved_team_ids: list[str] = []
99
+ resolved_team_lookup: dict[str, str] = {}
100
+ unresolved_teams: list[str] = []
101
+
102
+ for team in requested_teams:
103
+ matched_id = None
104
+
105
+ for filter_key, expected_value in (
106
+ ("filter[slug]", team),
107
+ ("filter[name]", team),
108
+ ):
109
+ response = await make_authenticated_request(
110
+ "GET",
111
+ "/v1/teams",
112
+ params={
113
+ "page[size]": 100,
114
+ "page[number]": 1,
115
+ filter_key: team,
116
+ },
117
+ )
118
+ response.raise_for_status()
119
+
120
+ for candidate in response.json().get("data", []):
121
+ attrs = candidate.get("attributes", {})
122
+ candidate_value = attrs.get("slug") if filter_key == "filter[slug]" else attrs.get(
123
+ "name"
124
+ )
125
+ if isinstance(candidate_value, str) and candidate_value.lower() == expected_value.lower():
126
+ matched_id = str(candidate.get("id"))
127
+ break
128
+
129
+ if matched_id:
130
+ break
131
+
132
+ if matched_id:
133
+ resolved_team_ids.append(matched_id)
134
+ resolved_team_lookup[team] = matched_id
135
+ else:
136
+ unresolved_teams.append(team)
137
+
138
+ if unresolved_teams:
139
+ raise ValueError(
140
+ "Could not resolve team names/slugs to team IDs: "
141
+ + ", ".join(unresolved_teams)
142
+ )
143
+
144
+ return ",".join(dict.fromkeys(resolved_team_ids)), resolved_team_lookup
145
+
146
+ async def _prepare_incident_query_context(
147
+ *,
148
+ query: str,
149
+ teams: str,
150
+ team_ids: str,
151
+ service_ids: str,
152
+ severity: str,
153
+ status: str,
154
+ started_after: str,
155
+ started_before: str,
156
+ custom_field_selected_option_ids: str,
157
+ sort: Literal["created_at", "-created_at", "updated_at", "-updated_at"],
158
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
159
+ """Build shared incident query params and filter metadata for list/collect tools."""
160
+ resolved_team_lookup: dict[str, str] = {}
161
+ resolved_team_ids = team_ids
162
+
163
+ if teams:
164
+ resolved_teams_value, resolved_team_lookup = await _resolve_team_names_to_ids(teams)
165
+ if resolved_team_ids and resolved_teams_value:
166
+ combined_team_ids = _split_csv_values(resolved_team_ids) + _split_csv_values(
167
+ resolved_teams_value
168
+ )
169
+ resolved_team_ids = ",".join(dict.fromkeys(combined_team_ids))
170
+ elif resolved_teams_value:
171
+ resolved_team_ids = resolved_teams_value
172
+
173
+ params: dict[str, Any] = {
174
+ "fields[incidents]": INCIDENT_LIST_FIELDS,
175
+ "include": "",
176
+ "sort": sort,
177
+ }
178
+
179
+ if query:
180
+ params["filter[search]"] = query
181
+ if resolved_team_ids:
182
+ params["filter[team_ids]"] = resolved_team_ids
183
+ if service_ids:
184
+ params["filter[service_ids]"] = service_ids
185
+ if severity:
186
+ params["filter[severity]"] = severity
187
+ if status:
188
+ params["filter[status]"] = status
189
+ if started_after:
190
+ params["filter[started_at][gte]"] = started_after
191
+ if started_before:
192
+ params["filter[started_at][lte]"] = started_before
193
+ if custom_field_selected_option_ids:
194
+ params["filter[custom_field_selected_option_ids]"] = custom_field_selected_option_ids
195
+
196
+ filters = {
197
+ "query": query,
198
+ "teams": teams,
199
+ "team_ids": team_ids,
200
+ "resolved_team_ids": resolved_team_ids,
201
+ "resolved_team_lookup": resolved_team_lookup,
202
+ "service_ids": service_ids,
203
+ "severity": severity,
204
+ "status": status,
205
+ "started_after": started_after,
206
+ "started_before": started_before,
207
+ "custom_field_selected_option_ids": custom_field_selected_option_ids,
208
+ "sort": sort,
209
+ }
210
+
211
+ return params, filters
212
+
219
213
  @mcp.tool()
220
214
  async def list_incidents(
221
215
  query: Annotated[
@@ -294,8 +288,7 @@ def register_incident_tools(
294
288
  Prefer search_incidents only for lightweight free-text lookups.
295
289
  """
296
290
  try:
297
- params, filters = await prepare_incident_query_context(
298
- make_authenticated_request=make_authenticated_request,
291
+ params, filters = await _prepare_incident_query_context(
299
292
  query=query,
300
293
  teams=teams,
301
294
  team_ids=team_ids,
@@ -429,8 +422,7 @@ def register_incident_tools(
429
422
  tool call, while keeping payload size under control.
430
423
  """
431
424
  try:
432
- params, filters = await prepare_incident_query_context(
433
- make_authenticated_request=make_authenticated_request,
425
+ params, filters = await _prepare_incident_query_context(
434
426
  query=query,
435
427
  teams=teams,
436
428
  team_ids=team_ids,
@@ -657,6 +649,100 @@ def register_incident_tools(
657
649
  ),
658
650
  )
659
651
 
652
+ @mcp.tool(name="createIncident")
653
+ async def create_incident(
654
+ title: Annotated[
655
+ str | None,
656
+ Field(description="Incident title. If omitted, Rootly may autogenerate one."),
657
+ ] = None,
658
+ summary: Annotated[
659
+ str | None,
660
+ Field(description="Incident summary or short description."),
661
+ ] = None,
662
+ severity_id: Annotated[
663
+ str | None,
664
+ Field(description="Optional severity ID to attach to the incident."),
665
+ ] = None,
666
+ service_ids: Annotated[
667
+ str | None,
668
+ Field(description="Comma-separated service IDs to attach to the incident."),
669
+ ] = None,
670
+ team_ids: Annotated[
671
+ str | None,
672
+ Field(description="Comma-separated team IDs to attach to the incident."),
673
+ ] = None,
674
+ environment_ids: Annotated[
675
+ str | None,
676
+ Field(description="Comma-separated environment IDs to attach to the incident."),
677
+ ] = None,
678
+ incident_type_ids: Annotated[
679
+ str | None,
680
+ Field(description="Comma-separated incident type IDs to attach to the incident."),
681
+ ] = None,
682
+ ) -> JsonDict:
683
+ """Create an incident with a scoped set of fields for agent-driven workflows."""
684
+ normalized_title = _normalize_optional_text(title)
685
+ normalized_summary = _normalize_optional_text(summary)
686
+
687
+ if normalized_title is None and normalized_summary is None:
688
+ return cast(
689
+ JsonDict,
690
+ mcp_error.tool_error(
691
+ "Must provide at least one of title or summary",
692
+ "validation_error",
693
+ ),
694
+ )
695
+
696
+ attributes: dict[str, Any] = {}
697
+
698
+ if normalized_title is not None:
699
+ attributes["title"] = normalized_title
700
+ if normalized_summary is not None:
701
+ attributes["summary"] = normalized_summary
702
+
703
+ normalized_severity_id = _normalize_optional_text(severity_id)
704
+ if normalized_severity_id is not None:
705
+ attributes["severity_id"] = normalized_severity_id
706
+
707
+ csv_attribute_map = (
708
+ ("service_ids", service_ids),
709
+ ("group_ids", team_ids),
710
+ ("environment_ids", environment_ids),
711
+ ("incident_type_ids", incident_type_ids),
712
+ )
713
+ for attribute_name, raw_value in csv_attribute_map:
714
+ if raw_value is None:
715
+ continue
716
+ values = _split_csv_values(raw_value)
717
+ if values:
718
+ attributes[attribute_name] = values
719
+
720
+ payload = {
721
+ "data": {
722
+ "type": "incidents",
723
+ "attributes": attributes,
724
+ }
725
+ }
726
+
727
+ try:
728
+ response = await make_authenticated_request("POST", "/v1/incidents", json=payload)
729
+ response.raise_for_status()
730
+
731
+ response_data = response.json()
732
+ if isinstance(response_data.get("data"), dict):
733
+ stripped = strip_heavy_nested_data({"data": [response_data["data"]]})
734
+ response_data["data"] = stripped["data"][0]
735
+ return cast(JsonDict, response_data)
736
+ except Exception as e:
737
+ error_type, error_message = mcp_error.categorize_error(e)
738
+ return cast(
739
+ JsonDict,
740
+ mcp_error.tool_error(
741
+ f"Failed to create incident: {error_message}",
742
+ error_type,
743
+ ),
744
+ )
745
+
660
746
  @mcp.tool(name="updateIncident")
661
747
  async def update_incident(
662
748
  incident_id: Annotated[str, Field(description="Incident ID to update")],
@@ -143,9 +143,74 @@ class TestScopedIncidentUpdateTool:
143
143
  async def test_update_incident_tool_is_registered_with_customer_facing_name(self):
144
144
  tools, _ = self._register_tools()
145
145
 
146
+ assert "createIncident" in tools
146
147
  assert "updateIncident" in tools
147
148
  assert "getIncident" in tools
148
149
 
150
+ @pytest.mark.asyncio
151
+ async def test_create_incident_sends_only_allowed_fields(self):
152
+ tools, request = self._register_tools()
153
+ response = Mock()
154
+ response.raise_for_status.return_value = None
155
+ response.json.return_value = {
156
+ "data": {
157
+ "id": "inc-123",
158
+ "type": "incidents",
159
+ "attributes": {
160
+ "title": "Database latency spike",
161
+ "summary": "Primary API requests timing out",
162
+ "severity_id": "sev-1",
163
+ "service_ids": ["svc-1", "svc-2"],
164
+ "group_ids": ["team-1", "team-2"],
165
+ "environment_ids": ["env-1"],
166
+ "incident_type_ids": ["type-1"],
167
+ },
168
+ }
169
+ }
170
+ request.return_value = response
171
+
172
+ result = await tools["createIncident"](
173
+ title=" Database latency spike ",
174
+ summary=" Primary API requests timing out ",
175
+ severity_id=" sev-1 ",
176
+ service_ids="svc-1, svc-2",
177
+ team_ids="team-1, team-2",
178
+ environment_ids="env-1",
179
+ incident_type_ids="type-1",
180
+ )
181
+
182
+ request.assert_awaited_once_with(
183
+ "POST",
184
+ "/v1/incidents",
185
+ json={
186
+ "data": {
187
+ "type": "incidents",
188
+ "attributes": {
189
+ "title": "Database latency spike",
190
+ "summary": "Primary API requests timing out",
191
+ "severity_id": "sev-1",
192
+ "service_ids": ["svc-1", "svc-2"],
193
+ "group_ids": ["team-1", "team-2"],
194
+ "environment_ids": ["env-1"],
195
+ "incident_type_ids": ["type-1"],
196
+ },
197
+ }
198
+ },
199
+ )
200
+ assert result["data"]["id"] == "inc-123"
201
+ assert result["data"]["attributes"]["title"] == "Database latency spike"
202
+
203
+ @pytest.mark.asyncio
204
+ async def test_create_incident_requires_title_or_summary(self):
205
+ tools, request = self._register_tools()
206
+
207
+ result = await tools["createIncident"](title=" ", summary=None)
208
+
209
+ request.assert_not_called()
210
+ assert result["error"] is True
211
+ assert result["error_type"] == "validation_error"
212
+ assert "Must provide at least one of title or summary" in result["message"]
213
+
149
214
  @pytest.mark.asyncio
150
215
  async def test_get_incident_fetches_single_incident(self):
151
216
  tools, request = self._register_tools()
@@ -1702,7 +1702,7 @@ wheels = [
1702
1702
 
1703
1703
  [[package]]
1704
1704
  name = "rootly-mcp-server"
1705
- version = "2.2.16"
1705
+ version = "2.2.19"
1706
1706
  source = { editable = "." }
1707
1707
  dependencies = [
1708
1708
  { name = "authlib" },