rootly-mcp-server 2.2.14__tar.gz → 2.2.16__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 (82) hide show
  1. rootly_mcp_server-2.2.16/.github/openapi-audit-remote-baseline.json +6 -0
  2. rootly_mcp_server-2.2.16/.github/workflows/openapi-audit.yml +80 -0
  3. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/CHANGELOG.md +25 -1
  4. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/PKG-INFO +4 -4
  5. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/README.md +2 -2
  6. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/pyproject.toml +2 -2
  7. rootly_mcp_server-2.2.16/scripts/audit_openapi.py +176 -0
  8. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/code_mode.py +3 -1
  9. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/server_defaults.py +3 -0
  10. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/spec_transform.py +142 -0
  11. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/incidents.py +458 -1
  12. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_server.py +65 -0
  13. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_server_defaults_module.py +2 -0
  14. rootly_mcp_server-2.2.16/tests/unit/test_tools.py +711 -0
  15. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/uv.lock +5 -5
  16. rootly_mcp_server-2.2.14/tests/unit/test_tools.py +0 -286
  17. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.dockerignore +0 -0
  18. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.gitattributes +0 -0
  19. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.github/dependabot.yml +0 -0
  20. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.github/workflows/ci.yml +0 -0
  21. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.github/workflows/dependency-review.yml +0 -0
  22. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.github/workflows/pypi-release.yml +0 -0
  23. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.gitignore +0 -0
  24. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.semaphore/deploy-staging.yml +0 -0
  25. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.semaphore/deploy.yml +0 -0
  26. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.semaphore/semaphore.yml +0 -0
  27. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.semaphore/update-task-definition.sh +0 -0
  28. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/CONTRIBUTING.md +0 -0
  29. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/Dockerfile +0 -0
  30. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/LICENSE +0 -0
  31. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/examples/skills/README.md +0 -0
  32. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/gemini-extension.json +0 -0
  33. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/rootly-mcp-server-demo.gif +0 -0
  34. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/rootly_openapi.json +0 -0
  35. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/scripts/setup-hooks.sh +0 -0
  36. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/server.json +0 -0
  37. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/__init__.py +0 -0
  38. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/__main__.py +0 -0
  39. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/client.py +0 -0
  40. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/data/__init__.py +0 -0
  41. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/exceptions.py +0 -0
  42. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/legacy_server.py +0 -0
  43. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/mcp_error.py +0 -0
  44. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/monitoring.py +0 -0
  45. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/och_client.py +0 -0
  46. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/pagination.py +0 -0
  47. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/payload_stripping.py +0 -0
  48. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/security.py +0 -0
  49. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/server.py +0 -0
  50. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/smart_utils.py +0 -0
  51. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/__init__.py +0 -0
  52. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/alerts.py +0 -0
  53. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/oncall.py +0 -0
  54. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/resources.py +0 -0
  55. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/transport.py +0 -0
  56. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/utils.py +0 -0
  57. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/validators.py +0 -0
  58. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/README.md +0 -0
  59. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/conftest.py +0 -0
  60. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/integration/local/test_basic.py +0 -0
  61. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/integration/local/test_smart_tools.py +0 -0
  62. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/integration/remote/test_essential.py +0 -0
  63. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/test_client.py +0 -0
  64. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_alert_stripping.py +0 -0
  65. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_authentication.py +0 -0
  66. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_code_mode_module.py +0 -0
  67. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_exceptions.py +0 -0
  68. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_http_headers.py +0 -0
  69. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_legacy_server_module.py +0 -0
  70. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_main_transport.py +0 -0
  71. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_mcp_error_module.py +0 -0
  72. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_och_client.py +0 -0
  73. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_oncall_handoff.py +0 -0
  74. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_oncall_metrics.py +0 -0
  75. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_oncall_new_tools.py +0 -0
  76. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_oncall_severity_normalization.py +0 -0
  77. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_payload_stripping_module.py +0 -0
  78. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_security.py +0 -0
  79. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_smart_utils.py +0 -0
  80. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_transport_module.py +0 -0
  81. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_utils.py +0 -0
  82. {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_validators.py +0 -0
@@ -0,0 +1,6 @@
1
+ {
2
+ "missing_array_items": [
3
+ "components.schemas.new_escalation_policy_path.properties.data.properties.attributes.properties.rules.items.anyOf[0].properties.urgency_ids",
4
+ "components.schemas.update_escalation_policy_path.properties.data.properties.attributes.properties.rules.items.anyOf[0].properties.urgency_ids"
5
+ ]
6
+ }
@@ -0,0 +1,80 @@
1
+ name: OpenAPI Audit
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main, staging]
6
+ paths:
7
+ - ".github/workflows/openapi-audit.yml"
8
+ - ".github/openapi-audit-remote-baseline.json"
9
+ - "scripts/audit_openapi.py"
10
+ - "src/rootly_mcp_server/data/swagger.json"
11
+ - "src/rootly_mcp_server/spec_transform.py"
12
+ - "tests/unit/test_server.py"
13
+ push:
14
+ branches: [main, staging]
15
+ paths:
16
+ - ".github/workflows/openapi-audit.yml"
17
+ - ".github/openapi-audit-remote-baseline.json"
18
+ - "scripts/audit_openapi.py"
19
+ - "src/rootly_mcp_server/data/swagger.json"
20
+ - "src/rootly_mcp_server/spec_transform.py"
21
+ - "tests/unit/test_server.py"
22
+ workflow_dispatch:
23
+ schedule:
24
+ - cron: "23 12 * * *"
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ concurrency:
30
+ group: openapi-audit-${{ github.ref }}
31
+ cancel-in-progress: true
32
+
33
+ jobs:
34
+ bundled-audit:
35
+ name: Bundled Swagger Audit
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - name: Checkout code
39
+ uses: actions/checkout@v6
40
+
41
+ - name: Set up Python
42
+ uses: actions/setup-python@v6
43
+ with:
44
+ python-version: "3.12"
45
+
46
+ - name: Install uv
47
+ uses: astral-sh/setup-uv@7eb50e6e20e1e2009087999ba91242e80253875f # v7
48
+
49
+ - name: Install dependencies
50
+ run: uv sync --dev
51
+
52
+ - name: Audit bundled swagger
53
+ run: uv run python scripts/audit_openapi.py --filtered-defaults --instantiate-server
54
+
55
+ remote-audit:
56
+ name: Remote Swagger Audit
57
+ runs-on: ubuntu-latest
58
+ if: ${{ github.event_name != 'pull_request' }}
59
+ steps:
60
+ - name: Checkout code
61
+ uses: actions/checkout@v6
62
+
63
+ - name: Set up Python
64
+ uses: actions/setup-python@v6
65
+ with:
66
+ python-version: "3.12"
67
+
68
+ - name: Install uv
69
+ uses: astral-sh/setup-uv@7eb50e6e20e1e2009087999ba91242e80253875f # v7
70
+
71
+ - name: Install dependencies
72
+ run: uv sync --dev
73
+
74
+ - name: Audit remote swagger
75
+ run: >
76
+ uv run python scripts/audit_openapi.py
77
+ --remote
78
+ --baseline-json .github/openapi-audit-remote-baseline.json
79
+ --filtered-defaults
80
+ --instantiate-server
@@ -5,7 +5,31 @@ 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
- ## [Unreleased]
8
+ ## [2.2.16] - Released 2026-04-13
9
+
10
+ ### Enhanced
11
+
12
+ - **Improved parameter naming in `list_incidents`**: Renamed `start_time`/`end_time` to `started_after`/`started_before` for clarity
13
+ - **Enhanced team resolution logic**: Better handling of team name variations and edge cases
14
+ - **Better parameter descriptions**: More accurate and unambiguous field descriptions
15
+
16
+ ### Fixes
17
+
18
+ - Fixed confusing parameter semantics where `end_time` actually filtered `started_at` field
19
+ - Improved input validation for time-based filtering parameters
20
+
21
+ ## [2.2.15] - Released 2026-04-10
22
+
23
+ ### Highlights
24
+ - Fixed escalation path tool schemas for strict MCP clients and added OpenAPI audit coverage to catch spec regressions earlier
25
+
26
+ ### Fixes
27
+ - Ensured array schemas always include `items` so `createEscalationPath` and `updateEscalationPath` validate correctly
28
+ - Patched the bundled swagger definitions for escalation path urgency rules
29
+
30
+ ### Docs / Dependencies
31
+ - Added local and scheduled remote OpenAPI audit checks
32
+ - Upgraded `requests` to `2.33.1`
9
33
 
10
34
  ## [2.2.14] - Released 2026-04-02
11
35
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rootly-mcp-server
3
- Version: 2.2.14
3
+ Version: 2.2.16
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
@@ -29,7 +29,7 @@ Requires-Dist: httpx==0.28.1
29
29
  Requires-Dist: numpy>=1.24.0
30
30
  Requires-Dist: pydantic==2.12.5
31
31
  Requires-Dist: pygments>=2.20.0
32
- Requires-Dist: requests==2.33.0
32
+ Requires-Dist: requests==2.33.1
33
33
  Requires-Dist: scikit-learn>=1.3.0
34
34
  Provides-Extra: dev
35
35
  Requires-Dist: bandit>=1.7.0; extra == 'dev'
@@ -312,7 +312,7 @@ Choose the token type based on the access you need:
312
312
  - **Team API Key**: Access limited to entities owned by that team.
313
313
  - **Personal API Key**: Access matches the user who created it.
314
314
 
315
- A **Global API Key** is recommended for tools like `get_oncall_handoff_summary`, `get_oncall_shift_metrics`, and org-wide incident search.
315
+ A **Global API Key** is recommended for organization-wide queries and for actions that modify data, especially when workflows may span multiple teams, schedules, or incidents.
316
316
 
317
317
  ### With uv
318
318
 
@@ -405,7 +405,7 @@ docker run -p 8000:8000 \
405
405
 
406
406
  ## Supported Tools
407
407
 
408
- The default server configuration exposes **101 tools**.
408
+ The default server configuration exposes **105 tools**.
409
409
 
410
410
  ### Custom Agentic Tools
411
411
 
@@ -270,7 +270,7 @@ Choose the token type based on the access you need:
270
270
  - **Team API Key**: Access limited to entities owned by that team.
271
271
  - **Personal API Key**: Access matches the user who created it.
272
272
 
273
- A **Global API Key** is recommended for tools like `get_oncall_handoff_summary`, `get_oncall_shift_metrics`, and org-wide incident search.
273
+ A **Global API Key** is recommended for organization-wide queries and for actions that modify data, especially when workflows may span multiple teams, schedules, or incidents.
274
274
 
275
275
  ### With uv
276
276
 
@@ -363,7 +363,7 @@ docker run -p 8000:8000 \
363
363
 
364
364
  ## Supported Tools
365
365
 
366
- The default server configuration exposes **101 tools**.
366
+ The default server configuration exposes **105 tools**.
367
367
 
368
368
  ### Custom Agentic Tools
369
369
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rootly-mcp-server"
3
- version = "2.2.14"
3
+ version = "2.2.16"
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"
@@ -40,7 +40,7 @@ dependencies = [
40
40
  "authlib>=1.6.9", # Override vulnerable transitive authlib from fastmcp
41
41
  "cryptography>=46.0.6", # Override vulnerable transitive cryptography
42
42
  "Pygments>=2.20.0", # Override vulnerable transitive Pygments
43
- "requests==2.33.0", # Pin to patched version
43
+ "requests==2.33.1", # Pin to patched version
44
44
  "httpx==0.28.1", # Pin to specific version
45
45
  "pydantic==2.12.5", # Pin to specific version
46
46
  "brotli>=1.2.0", # For Brotli compression support in httpx
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+ """Audit bundled or remote OpenAPI documents for MCP tool generation issues."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ REPO_ROOT = Path(__file__).resolve().parents[1]
14
+ SRC_ROOT = REPO_ROOT / "src"
15
+ if str(SRC_ROOT) not in sys.path:
16
+ sys.path.insert(0, str(SRC_ROOT))
17
+
18
+ from rootly_mcp_server.server import create_rootly_mcp_server # noqa: E402
19
+ from rootly_mcp_server.server_defaults import ( # noqa: E402
20
+ DEFAULT_ALLOWED_PATHS,
21
+ DEFAULT_DELETE_ALLOWED_PATHS,
22
+ )
23
+ from rootly_mcp_server.spec_transform import ( # noqa: E402
24
+ SWAGGER_URL,
25
+ _fetch_swagger_from_url,
26
+ _filter_openapi_spec,
27
+ audit_openapi_spec,
28
+ has_openapi_audit_findings,
29
+ )
30
+
31
+
32
+ def _prefixed_paths(paths: list[str]) -> list[str]:
33
+ return [f"/v1{path}" if not path.startswith("/v1") else path for path in paths]
34
+
35
+
36
+ def _load_source(source_path: Path | None, source_url: str | None) -> tuple[dict[str, Any], str]:
37
+ if source_url:
38
+ return _fetch_swagger_from_url(source_url), source_url
39
+
40
+ path = source_path or (REPO_ROOT / "src" / "rootly_mcp_server" / "data" / "swagger.json")
41
+ with path.open(encoding="utf-8") as f:
42
+ return json.load(f), str(path)
43
+
44
+
45
+ def _print_findings(label: str, findings: dict[str, list[Any]]) -> None:
46
+ print(f"\n[{label}]")
47
+ if not has_openapi_audit_findings(findings):
48
+ print("ok")
49
+ return
50
+
51
+ for category, items in findings.items():
52
+ print(f"{category}: {len(items)}")
53
+ for item in items[:10]:
54
+ print(f" - {item}")
55
+
56
+
57
+ def _apply_baseline(
58
+ findings: dict[str, list[Any]], baseline: dict[str, list[Any]]
59
+ ) -> tuple[dict[str, list[Any]], dict[str, int]]:
60
+ filtered: dict[str, list[Any]] = {}
61
+ allowed_counts: dict[str, int] = {}
62
+
63
+ for category, items in findings.items():
64
+ allowed = {
65
+ json.dumps(entry, sort_keys=True) if isinstance(entry, dict) else json.dumps(entry)
66
+ for entry in baseline.get(category, [])
67
+ }
68
+
69
+ remaining: list[Any] = []
70
+ allowed_count = 0
71
+ for item in items:
72
+ token = json.dumps(item, sort_keys=True) if isinstance(item, dict) else json.dumps(item)
73
+ if token in allowed:
74
+ allowed_count += 1
75
+ continue
76
+ remaining.append(item)
77
+
78
+ filtered[category] = remaining
79
+ allowed_counts[category] = allowed_count
80
+
81
+ return filtered, allowed_counts
82
+
83
+
84
+ def _instantiate_server(spec: dict[str, Any]) -> None:
85
+ with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as tmp:
86
+ json.dump(spec, tmp)
87
+ temp_path = Path(tmp.name)
88
+
89
+ try:
90
+ create_rootly_mcp_server(swagger_path=str(temp_path), hosted=False)
91
+ finally:
92
+ temp_path.unlink(missing_ok=True)
93
+
94
+
95
+ def main() -> int:
96
+ parser = argparse.ArgumentParser(description=__doc__)
97
+ parser.add_argument(
98
+ "--path",
99
+ type=Path,
100
+ help="Audit a local OpenAPI JSON file. Defaults to the bundled swagger.json.",
101
+ )
102
+ parser.add_argument(
103
+ "--url",
104
+ help=f"Audit a remote OpenAPI JSON URL. Defaults to {SWAGGER_URL!r} when --remote is used.",
105
+ )
106
+ parser.add_argument(
107
+ "--remote",
108
+ action="store_true",
109
+ help="Audit the default remote Rootly swagger URL.",
110
+ )
111
+ parser.add_argument(
112
+ "--filtered-defaults",
113
+ action="store_true",
114
+ help="Also audit the spec after applying the default MCP path filters.",
115
+ )
116
+ parser.add_argument(
117
+ "--instantiate-server",
118
+ action="store_true",
119
+ help="Instantiate FastMCP from the audited spec to catch schema validation failures.",
120
+ )
121
+ parser.add_argument(
122
+ "--baseline-json",
123
+ type=Path,
124
+ help="Path to a JSON file containing known findings to ignore.",
125
+ )
126
+ args = parser.parse_args()
127
+
128
+ source_url = args.url or (SWAGGER_URL if args.remote else None)
129
+ if source_url and args.path:
130
+ parser.error("use either --path or --url/--remote, not both")
131
+
132
+ spec, source_label = _load_source(args.path, source_url)
133
+ raw_findings = audit_openapi_spec(spec)
134
+ baseline: dict[str, list[Any]] = {}
135
+ if args.baseline_json:
136
+ with args.baseline_json.open(encoding="utf-8") as f:
137
+ loaded = json.load(f)
138
+ baseline = loaded if isinstance(loaded, dict) else {}
139
+ raw_findings, allowed_counts = _apply_baseline(raw_findings, baseline)
140
+ else:
141
+ allowed_counts = {}
142
+
143
+ _print_findings(source_label, raw_findings)
144
+ if any(allowed_counts.values()):
145
+ print("\n[baseline]")
146
+ for category, count in allowed_counts.items():
147
+ if count:
148
+ print(f"{category}: allowed {count}")
149
+
150
+ findings_exist = has_openapi_audit_findings(raw_findings)
151
+
152
+ if args.filtered_defaults:
153
+ filtered_spec = _filter_openapi_spec(
154
+ spec,
155
+ _prefixed_paths(DEFAULT_ALLOWED_PATHS),
156
+ delete_allowed_paths=_prefixed_paths(DEFAULT_DELETE_ALLOWED_PATHS),
157
+ )
158
+ filtered_findings = audit_openapi_spec(filtered_spec)
159
+ _print_findings("filtered-defaults", filtered_findings)
160
+ findings_exist = findings_exist or has_openapi_audit_findings(filtered_findings)
161
+ else:
162
+ filtered_spec = spec
163
+
164
+ if args.instantiate_server:
165
+ try:
166
+ _instantiate_server(spec if not args.filtered_defaults else filtered_spec)
167
+ print("\n[server-instantiation]\nok")
168
+ except Exception as exc:
169
+ print(f"\n[server-instantiation]\nfailed: {exc}")
170
+ return 1
171
+
172
+ return 1 if findings_exist else 0
173
+
174
+
175
+ if __name__ == "__main__":
176
+ raise SystemExit(main())
@@ -264,7 +264,9 @@ def build_code_mode_transform() -> CodeMode:
264
264
  "the schema. For paginated data tools, pass pagination arguments like page_size, "
265
265
  "page_number, and max_results exactly as documented by that tool instead of inventing "
266
266
  "alternatives like per_page. Prefer Rootly's higher-level custom tools when they fit "
267
- "the task, then fall back to lower-level API tools as needed. Do not use client "
267
+ "the task, then fall back to lower-level API tools as needed. Use list_incidents for "
268
+ "structured incident queries with filters like teams, team_ids, start_time, end_time, "
269
+ "severity, or status; use search_incidents for lightweight free-text lookups. Do not use client "
268
270
  "prefixes like mcp__rootly-codemode__tool_search or rootly:getCurrentUser inside "
269
271
  "execute; call tool_search or the raw Rootly tool name directly. Avoid imports "
270
272
  "such as json or asyncio inside the sandbox and return native Python values instead. Example: "
@@ -61,6 +61,9 @@ DEFAULT_ALLOWED_PATHS = [
61
61
  "/incident_action_items",
62
62
  "/incident_action_items/{incident_action_item_id}",
63
63
  "/incidents/{incident_id}/action_items",
64
+ # Incident form field selections (used for incident custom field values)
65
+ "/incidents/{incident_id}/form_field_selections",
66
+ "/incident_form_field_selections/{id}",
64
67
  # Workflows
65
68
  "/workflows",
66
69
  "/workflows/{workflow_id}",
@@ -6,12 +6,15 @@ import json
6
6
  import logging
7
7
  import os
8
8
  import re
9
+ from collections import Counter
9
10
  from copy import deepcopy
10
11
  from pathlib import Path
11
12
  from typing import Any, cast
12
13
 
13
14
  import requests
14
15
 
16
+ from .utils import sanitize_parameter_name
17
+
15
18
  logger = logging.getLogger(__name__)
16
19
 
17
20
  # Default Swagger URL
@@ -527,9 +530,148 @@ def _filter_openapi_spec(
527
530
  f"Cleaned broken reference in {method.upper()} {path} response: {ref_path}"
528
531
  )
529
532
 
533
+ _ensure_array_items(filtered_spec)
534
+
530
535
  return filtered_spec
531
536
 
532
537
 
538
+ def _ensure_array_items(spec: Any) -> None:
539
+ """Ensure all array schemas define an items schema to satisfy tool validation."""
540
+ if isinstance(spec, dict):
541
+ if spec.get("type") == "array" and "items" not in spec:
542
+ # OpenAPI allows arrays without items, but MCP tool schema validation does not.
543
+ spec["items"] = {}
544
+ for value in spec.values():
545
+ _ensure_array_items(value)
546
+ return
547
+ if isinstance(spec, list):
548
+ for item in spec:
549
+ _ensure_array_items(item)
550
+
551
+
552
+ def _walk_openapi_tree(node: Any, visitor: Any, path: str = "") -> None:
553
+ """Walk an OpenAPI document and invoke a visitor with each node and its path."""
554
+ visitor(node, path)
555
+ if isinstance(node, dict):
556
+ for key, value in node.items():
557
+ child_path = f"{path}.{key}" if path else key
558
+ _walk_openapi_tree(value, visitor, child_path)
559
+ return
560
+ if isinstance(node, list):
561
+ for index, value in enumerate(node):
562
+ _walk_openapi_tree(value, visitor, f"{path}[{index}]")
563
+
564
+
565
+ def collect_missing_array_items(spec: dict[str, Any]) -> list[str]:
566
+ """Collect array schema paths that do not define an items schema."""
567
+ missing: list[str] = []
568
+
569
+ def visitor(node: Any, path: str) -> None:
570
+ if isinstance(node, dict) and node.get("type") == "array" and "items" not in node:
571
+ missing.append(path or "<root>")
572
+
573
+ _walk_openapi_tree(spec, visitor)
574
+ return missing
575
+
576
+
577
+ def collect_broken_internal_refs(spec: dict[str, Any]) -> list[dict[str, str]]:
578
+ """Collect broken internal $ref pointers in an OpenAPI document."""
579
+ broken: list[dict[str, str]] = []
580
+
581
+ def visitor(node: Any, path: str) -> None:
582
+ if not isinstance(node, dict):
583
+ return
584
+ ref = node.get("$ref")
585
+ if not isinstance(ref, str) or not ref.startswith("#/"):
586
+ return
587
+
588
+ current: Any = spec
589
+ for part in ref[2:].split("/"):
590
+ if not isinstance(current, dict) or part not in current:
591
+ broken.append({"path": path or "<root>", "ref": ref})
592
+ return
593
+ current = current[part]
594
+
595
+ _walk_openapi_tree(spec, visitor)
596
+ return broken
597
+
598
+
599
+ def collect_duplicate_operation_ids(spec: dict[str, Any]) -> list[dict[str, Any]]:
600
+ """Collect duplicate OpenAPI operationIds."""
601
+ counts: Counter[str] = Counter()
602
+ for _path, path_item in spec.get("paths", {}).items():
603
+ if not isinstance(path_item, dict):
604
+ continue
605
+ for method, operation in path_item.items():
606
+ if method.lower() not in {"get", "post", "put", "patch", "delete"}:
607
+ continue
608
+ if not isinstance(operation, dict):
609
+ continue
610
+ operation_id = operation.get("operationId")
611
+ if isinstance(operation_id, str):
612
+ counts[operation_id] += 1
613
+
614
+ return [
615
+ {"operationId": operation_id, "count": count}
616
+ for operation_id, count in sorted(counts.items())
617
+ if count > 1
618
+ ]
619
+
620
+
621
+ def collect_sanitized_parameter_collisions(spec: dict[str, Any]) -> list[dict[str, str]]:
622
+ """Collect cases where distinct parameter names sanitize to the same MCP key."""
623
+ collisions: list[dict[str, str]] = []
624
+ for path, path_item in spec.get("paths", {}).items():
625
+ if not isinstance(path_item, dict):
626
+ continue
627
+ path_level_params = path_item.get("parameters", [])
628
+ for method, operation in path_item.items():
629
+ if method.lower() not in {"get", "post", "put", "patch", "delete"}:
630
+ continue
631
+ if not isinstance(operation, dict):
632
+ continue
633
+
634
+ seen: dict[str, str] = {}
635
+ parameters = list(path_level_params) + list(operation.get("parameters", []))
636
+ for param in parameters:
637
+ if not isinstance(param, dict):
638
+ continue
639
+ name = param.get("name")
640
+ if not isinstance(name, str):
641
+ continue
642
+ sanitized = sanitize_parameter_name(name)
643
+ previous = seen.get(sanitized)
644
+ if previous and previous != name:
645
+ collisions.append(
646
+ {
647
+ "path": path,
648
+ "method": method.upper(),
649
+ "sanitized": sanitized,
650
+ "first": previous,
651
+ "second": name,
652
+ }
653
+ )
654
+ else:
655
+ seen[sanitized] = name
656
+
657
+ return collisions
658
+
659
+
660
+ def audit_openapi_spec(spec: dict[str, Any]) -> dict[str, list[Any]]:
661
+ """Audit an OpenAPI document for schema issues that break MCP tool generation."""
662
+ return {
663
+ "missing_array_items": collect_missing_array_items(spec),
664
+ "broken_internal_refs": collect_broken_internal_refs(spec),
665
+ "duplicate_operation_ids": collect_duplicate_operation_ids(spec),
666
+ "sanitized_parameter_collisions": collect_sanitized_parameter_collisions(spec),
667
+ }
668
+
669
+
670
+ def has_openapi_audit_findings(findings: dict[str, list[Any]]) -> bool:
671
+ """Return True when any audit category contains findings."""
672
+ return any(findings.values())
673
+
674
+
533
675
  def _has_broken_references(schema_def: dict[str, Any]) -> bool:
534
676
  """Check if a schema definition has broken references."""
535
677
  if "$ref" in schema_def: