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.
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/CHANGELOG.md +10 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/PKG-INFO +2 -32
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/README.md +1 -31
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/pyproject.toml +1 -1
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/server.py +0 -8
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/__init__.py +0 -2
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/incidents.py +225 -139
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_tools.py +65 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/uv.lock +1 -1
- rootly_mcp_server-2.2.18/src/rootly_mcp_server/data/chatgpt_incident_workbench.html +0 -576
- rootly_mcp_server-2.2.18/src/rootly_mcp_server/tools/chatgpt_app.py +0 -350
- rootly_mcp_server-2.2.18/tests/unit/test_chatgpt_app_module.py +0 -229
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.dockerignore +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.gitattributes +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/dependabot.yml +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/openapi-audit-remote-baseline.json +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/workflows/ci.yml +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/workflows/dependency-review.yml +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/workflows/openapi-audit.yml +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.github/workflows/pypi-release.yml +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.gitignore +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.semaphore/deploy-staging.yml +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.semaphore/deploy.yml +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.semaphore/semaphore.yml +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/.semaphore/update-task-definition.sh +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/CONTRIBUTING.md +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/Dockerfile +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/LICENSE +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/examples/skills/README.md +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/gemini-extension.json +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/rootly-mcp-server-demo.gif +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/rootly_openapi.json +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/scripts/audit_openapi.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/scripts/setup-hooks.sh +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/server.json +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/__init__.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/__main__.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/client.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/code_mode.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/data/__init__.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/exceptions.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/legacy_server.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/mcp_error.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/monitoring.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/och_client.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/pagination.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/payload_stripping.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/security.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/server_defaults.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/smart_utils.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/spec_transform.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/alerts.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/oncall.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/resources.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/transport.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/utils.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/validators.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/README.md +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/conftest.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/integration/local/test_basic.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/integration/local/test_smart_tools.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/integration/remote/test_essential.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/test_client.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_alert_stripping.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_authentication.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_code_mode_module.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_exceptions.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_http_headers.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_legacy_server_module.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_main_transport.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_mcp_error_module.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_och_client.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_oncall_handoff.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_oncall_metrics.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_oncall_new_tools.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_oncall_severity_normalization.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_payload_stripping_module.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_security.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_server.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_server_defaults_module.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_smart_utils.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_transport_module.py +0 -0
- {rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/tests/unit/test_utils.py +0 -0
- {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.
|
|
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.
|
|
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,
|
{rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/__init__.py
RENAMED
|
@@ -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",
|
{rootly_mcp_server-2.2.18 → rootly_mcp_server-2.2.19}/src/rootly_mcp_server/tools/incidents.py
RENAMED
|
@@ -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
|
|
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
|
|
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()
|