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.
- rootly_mcp_server-2.2.16/.github/openapi-audit-remote-baseline.json +6 -0
- rootly_mcp_server-2.2.16/.github/workflows/openapi-audit.yml +80 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/CHANGELOG.md +25 -1
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/PKG-INFO +4 -4
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/README.md +2 -2
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/pyproject.toml +2 -2
- rootly_mcp_server-2.2.16/scripts/audit_openapi.py +176 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/code_mode.py +3 -1
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/server_defaults.py +3 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/spec_transform.py +142 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/incidents.py +458 -1
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_server.py +65 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_server_defaults_module.py +2 -0
- rootly_mcp_server-2.2.16/tests/unit/test_tools.py +711 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/uv.lock +5 -5
- rootly_mcp_server-2.2.14/tests/unit/test_tools.py +0 -286
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.dockerignore +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.gitattributes +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.github/dependabot.yml +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.github/workflows/ci.yml +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.github/workflows/dependency-review.yml +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.github/workflows/pypi-release.yml +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.gitignore +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.semaphore/deploy-staging.yml +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.semaphore/deploy.yml +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.semaphore/semaphore.yml +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/.semaphore/update-task-definition.sh +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/CONTRIBUTING.md +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/Dockerfile +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/LICENSE +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/examples/skills/README.md +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/gemini-extension.json +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/rootly-mcp-server-demo.gif +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/rootly_openapi.json +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/scripts/setup-hooks.sh +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/server.json +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/__init__.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/__main__.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/client.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/data/__init__.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/exceptions.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/legacy_server.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/mcp_error.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/monitoring.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/och_client.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/pagination.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/payload_stripping.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/security.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/server.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/smart_utils.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/__init__.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/alerts.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/oncall.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/tools/resources.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/transport.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/utils.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/validators.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/README.md +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/conftest.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/integration/local/test_basic.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/integration/local/test_smart_tools.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/integration/remote/test_essential.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/test_client.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_alert_stripping.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_authentication.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_code_mode_module.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_exceptions.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_http_headers.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_legacy_server_module.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_main_transport.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_mcp_error_module.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_och_client.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_oncall_handoff.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_oncall_metrics.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_oncall_new_tools.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_oncall_severity_normalization.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_payload_stripping_module.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_security.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_smart_utils.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_transport_module.py +0 -0
- {rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/tests/unit/test_utils.py +0 -0
- {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
|
-
## [
|
|
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.
|
|
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.
|
|
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
|
|
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 **
|
|
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
|
|
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 **
|
|
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.
|
|
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.
|
|
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.
|
|
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: "
|
{rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/server_defaults.py
RENAMED
|
@@ -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}",
|
{rootly_mcp_server-2.2.14 → rootly_mcp_server-2.2.16}/src/rootly_mcp_server/spec_transform.py
RENAMED
|
@@ -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:
|