rootly-mcp-server 2.2.20__tar.gz → 2.2.22__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.20 → rootly_mcp_server-2.2.22}/PKG-INFO +42 -2
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/README.md +41 -1
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/pyproject.toml +1 -1
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/__main__.py +46 -0
- rootly_mcp_server-2.2.22/src/rootly_mcp_server/audit.py +104 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/code_mode.py +4 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/server.py +101 -3
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/server_defaults.py +84 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/spec_transform.py +36 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/tools/incidents.py +147 -144
- rootly_mcp_server-2.2.22/tests/integration/local/test_tool_allowlists.py +140 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_code_mode_module.py +4 -0
- rootly_mcp_server-2.2.22/tests/unit/test_main_transport.py +135 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_server.py +141 -7
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_server_defaults_module.py +15 -1
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_tools.py +18 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/uv.lock +1 -1
- rootly_mcp_server-2.2.20/tests/unit/test_main_transport.py +0 -31
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.dockerignore +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.gitattributes +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.github/dependabot.yml +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.github/openapi-audit-remote-baseline.json +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.github/workflows/ci.yml +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.github/workflows/dependency-review.yml +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.github/workflows/openapi-audit.yml +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.github/workflows/pypi-release.yml +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.gitignore +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.semaphore/deploy-staging.yml +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.semaphore/deploy.yml +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.semaphore/semaphore.yml +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/.semaphore/update-task-definition.sh +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/CHANGELOG.md +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/CONTRIBUTING.md +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/Dockerfile +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/LICENSE +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/examples/skills/README.md +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/gemini-extension.json +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/rootly-mcp-server-demo.gif +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/rootly_openapi.json +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/scripts/audit_openapi.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/scripts/setup-hooks.sh +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/server.json +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/__init__.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/client.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/data/__init__.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/exceptions.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/legacy_server.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/mcp_error.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/monitoring.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/och_client.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/pagination.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/payload_stripping.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/security.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/smart_utils.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/tools/__init__.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/tools/alerts.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/tools/oncall.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/tools/resources.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/transport.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/utils.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/validators.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/README.md +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/conftest.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/integration/local/test_basic.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/integration/local/test_smart_tools.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/integration/remote/test_essential.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/test_client.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_alert_stripping.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_authentication.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_exceptions.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_http_headers.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_legacy_server_module.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_mcp_error_module.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_och_client.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_oncall_handoff.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_oncall_metrics.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_oncall_new_tools.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_oncall_severity_normalization.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_payload_stripping_module.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_security.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_smart_utils.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_transport_module.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_utils.py +0 -0
- {rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/tests/unit/test_validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rootly-mcp-server
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.22
|
|
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
|
|
@@ -334,7 +334,8 @@ A **Global API Key** is recommended for organization-wide queries and for action
|
|
|
334
334
|
"rootly-mcp-server"
|
|
335
335
|
],
|
|
336
336
|
"env": {
|
|
337
|
-
"ROOTLY_API_TOKEN": "<YOUR_ROOTLY_API_TOKEN>"
|
|
337
|
+
"ROOTLY_API_TOKEN": "<YOUR_ROOTLY_API_TOKEN>",
|
|
338
|
+
"ROOTLY_MCP_ENABLE_WRITE_TOOLS": "true"
|
|
338
339
|
}
|
|
339
340
|
}
|
|
340
341
|
}
|
|
@@ -349,12 +350,51 @@ Choose one transport per server process:
|
|
|
349
350
|
- **SSE** endpoint path: `/sse`
|
|
350
351
|
- **Code Mode (experimental)** endpoint path: `/mcp-codemode` in hosted dual-transport mode
|
|
351
352
|
|
|
353
|
+
Both hosted and self-hosted deployments expose all tools by default (read and write) for consistency. To restrict to read-only tools, start the server with `--no-enable-write-tools` or set `ROOTLY_MCP_ENABLE_WRITE_TOOLS=false`.
|
|
354
|
+
|
|
355
|
+
To expose only a specific subset of MCP tools on a self-hosted deployment, set `ROOTLY_MCP_ENABLED_TOOLS` (or pass `--enabled-tools`) with a comma-separated allowlist of exact tool names, for example `list_incidents,getIncident,get_server_version`.
|
|
356
|
+
|
|
357
|
+
To discover the exact tool names available under your current self-hosted configuration, run:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
ROOTLY_API_TOKEN=<YOUR_ROOTLY_API_TOKEN> \
|
|
361
|
+
uv run python -m rootly_mcp_server --list-tools
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
This prints the effective MCP tool names after applying your current settings, including `ROOTLY_MCP_ENABLE_WRITE_TOOLS` and `ROOTLY_MCP_ENABLED_TOOLS`.
|
|
365
|
+
|
|
366
|
+
Smoke-test a self-hosted allowlist:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
ROOTLY_API_TOKEN=<YOUR_ROOTLY_API_TOKEN> \
|
|
370
|
+
ROOTLY_MCP_ENABLED_TOOLS=list_incidents,getIncident,get_server_version \
|
|
371
|
+
uv run python -m rootly_mcp_server --transport streamable-http --log-level ERROR
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Then connect an MCP client to `http://127.0.0.1:8000/mcp` and verify `tools/list` returns only:
|
|
375
|
+
|
|
376
|
+
```text
|
|
377
|
+
get_server_version
|
|
378
|
+
getIncident
|
|
379
|
+
list_incidents
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
To include specific write tools for self-hosted testing, add both the write flag and the allowlist:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
ROOTLY_API_TOKEN=<YOUR_ROOTLY_API_TOKEN> \
|
|
386
|
+
ROOTLY_MCP_ENABLE_WRITE_TOOLS=true \
|
|
387
|
+
ROOTLY_MCP_ENABLED_TOOLS=createIncident,createWorkflowTask,listTeams \
|
|
388
|
+
uv run python -m rootly_mcp_server --transport streamable-http --log-level ERROR
|
|
389
|
+
```
|
|
390
|
+
|
|
352
391
|
Example Docker run (Streamable HTTP):
|
|
353
392
|
|
|
354
393
|
```bash
|
|
355
394
|
docker run -p 8000:8000 \
|
|
356
395
|
-e ROOTLY_TRANSPORT=streamable-http \
|
|
357
396
|
-e ROOTLY_API_TOKEN=<YOUR_ROOTLY_API_TOKEN> \
|
|
397
|
+
-e ROOTLY_MCP_ENABLE_WRITE_TOOLS=true \
|
|
358
398
|
rootly-mcp-server
|
|
359
399
|
```
|
|
360
400
|
|
|
@@ -290,7 +290,8 @@ A **Global API Key** is recommended for organization-wide queries and for action
|
|
|
290
290
|
"rootly-mcp-server"
|
|
291
291
|
],
|
|
292
292
|
"env": {
|
|
293
|
-
"ROOTLY_API_TOKEN": "<YOUR_ROOTLY_API_TOKEN>"
|
|
293
|
+
"ROOTLY_API_TOKEN": "<YOUR_ROOTLY_API_TOKEN>",
|
|
294
|
+
"ROOTLY_MCP_ENABLE_WRITE_TOOLS": "true"
|
|
294
295
|
}
|
|
295
296
|
}
|
|
296
297
|
}
|
|
@@ -305,12 +306,51 @@ Choose one transport per server process:
|
|
|
305
306
|
- **SSE** endpoint path: `/sse`
|
|
306
307
|
- **Code Mode (experimental)** endpoint path: `/mcp-codemode` in hosted dual-transport mode
|
|
307
308
|
|
|
309
|
+
Both hosted and self-hosted deployments expose all tools by default (read and write) for consistency. To restrict to read-only tools, start the server with `--no-enable-write-tools` or set `ROOTLY_MCP_ENABLE_WRITE_TOOLS=false`.
|
|
310
|
+
|
|
311
|
+
To expose only a specific subset of MCP tools on a self-hosted deployment, set `ROOTLY_MCP_ENABLED_TOOLS` (or pass `--enabled-tools`) with a comma-separated allowlist of exact tool names, for example `list_incidents,getIncident,get_server_version`.
|
|
312
|
+
|
|
313
|
+
To discover the exact tool names available under your current self-hosted configuration, run:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
ROOTLY_API_TOKEN=<YOUR_ROOTLY_API_TOKEN> \
|
|
317
|
+
uv run python -m rootly_mcp_server --list-tools
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
This prints the effective MCP tool names after applying your current settings, including `ROOTLY_MCP_ENABLE_WRITE_TOOLS` and `ROOTLY_MCP_ENABLED_TOOLS`.
|
|
321
|
+
|
|
322
|
+
Smoke-test a self-hosted allowlist:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
ROOTLY_API_TOKEN=<YOUR_ROOTLY_API_TOKEN> \
|
|
326
|
+
ROOTLY_MCP_ENABLED_TOOLS=list_incidents,getIncident,get_server_version \
|
|
327
|
+
uv run python -m rootly_mcp_server --transport streamable-http --log-level ERROR
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Then connect an MCP client to `http://127.0.0.1:8000/mcp` and verify `tools/list` returns only:
|
|
331
|
+
|
|
332
|
+
```text
|
|
333
|
+
get_server_version
|
|
334
|
+
getIncident
|
|
335
|
+
list_incidents
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
To include specific write tools for self-hosted testing, add both the write flag and the allowlist:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
ROOTLY_API_TOKEN=<YOUR_ROOTLY_API_TOKEN> \
|
|
342
|
+
ROOTLY_MCP_ENABLE_WRITE_TOOLS=true \
|
|
343
|
+
ROOTLY_MCP_ENABLED_TOOLS=createIncident,createWorkflowTask,listTeams \
|
|
344
|
+
uv run python -m rootly_mcp_server --transport streamable-http --log-level ERROR
|
|
345
|
+
```
|
|
346
|
+
|
|
308
347
|
Example Docker run (Streamable HTTP):
|
|
309
348
|
|
|
310
349
|
```bash
|
|
311
350
|
docker run -p 8000:8000 \
|
|
312
351
|
-e ROOTLY_TRANSPORT=streamable-http \
|
|
313
352
|
-e ROOTLY_API_TOKEN=<YOUR_ROOTLY_API_TOKEN> \
|
|
353
|
+
-e ROOTLY_MCP_ENABLE_WRITE_TOOLS=true \
|
|
314
354
|
rootly-mcp-server
|
|
315
355
|
```
|
|
316
356
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rootly-mcp-server"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.22"
|
|
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"
|
|
@@ -6,6 +6,7 @@ This module provides the main entry point for the Rootly MCP Server.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
|
+
import asyncio
|
|
9
10
|
import logging
|
|
10
11
|
import os
|
|
11
12
|
import sys
|
|
@@ -22,6 +23,7 @@ from .code_mode import (
|
|
|
22
23
|
from .exceptions import RootlyConfigurationError, RootlyMCPError
|
|
23
24
|
from .security import validate_api_token
|
|
24
25
|
from .server import create_rootly_mcp_server, get_hosted_auth_middleware
|
|
26
|
+
from .server_defaults import enabled_tools_from_env, write_tools_enabled_from_env
|
|
25
27
|
|
|
26
28
|
TransportName = Literal["stdio", "sse", "streamable-http", "both"]
|
|
27
29
|
TRANSPORT_ALIASES: dict[str, TransportName] = {
|
|
@@ -113,6 +115,23 @@ def parse_args():
|
|
|
113
115
|
action="store_true",
|
|
114
116
|
help="Expose a separate hosted Code Mode endpoint (HTTP only)",
|
|
115
117
|
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--no-enable-write-tools",
|
|
120
|
+
dest="enable_write_tools",
|
|
121
|
+
action="store_false",
|
|
122
|
+
default=True,
|
|
123
|
+
help="Disable write tools to expose read-only operations",
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--enabled-tools",
|
|
127
|
+
type=str,
|
|
128
|
+
help="Comma-separated allowlist of exact MCP tool names to expose",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--list-tools",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help="Print the exact MCP tool names exposed by the current configuration, then exit",
|
|
134
|
+
)
|
|
116
135
|
parser.add_argument(
|
|
117
136
|
"--code-mode-path",
|
|
118
137
|
type=str,
|
|
@@ -183,6 +202,8 @@ def get_server():
|
|
|
183
202
|
hosted = os.getenv("ROOTLY_HOSTED", "false").lower() in ("true", "1", "yes")
|
|
184
203
|
base_url = os.getenv("ROOTLY_BASE_URL")
|
|
185
204
|
transport = normalize_transport_or_default(os.getenv("ROOTLY_TRANSPORT", "stdio"))
|
|
205
|
+
enable_write_tools = write_tools_enabled_from_env(default=True)
|
|
206
|
+
enabled_tools = enabled_tools_from_env()
|
|
186
207
|
|
|
187
208
|
# Parse allowed paths from environment variable
|
|
188
209
|
allowed_paths = None
|
|
@@ -198,9 +219,17 @@ def get_server():
|
|
|
198
219
|
hosted=hosted,
|
|
199
220
|
base_url=base_url,
|
|
200
221
|
transport=transport,
|
|
222
|
+
enable_write_tools=enable_write_tools,
|
|
223
|
+
enabled_tools=enabled_tools,
|
|
201
224
|
)
|
|
202
225
|
|
|
203
226
|
|
|
227
|
+
async def _get_sorted_tool_names(server) -> list[str]:
|
|
228
|
+
"""Return the effective MCP tool names for the provided server."""
|
|
229
|
+
tools = await server.list_tools()
|
|
230
|
+
return sorted(tool.name for tool in tools)
|
|
231
|
+
|
|
232
|
+
|
|
204
233
|
# Create the server instance for FastMCP CLI (follows quickstart pattern).
|
|
205
234
|
# Avoid eager construction when executing `python -m rootly_mcp_server`, because
|
|
206
235
|
# CLI flags like `--hosted` and `--transport` are parsed later in `main()`.
|
|
@@ -368,11 +397,19 @@ def main():
|
|
|
368
397
|
allowed_paths = None
|
|
369
398
|
if args.allowed_paths:
|
|
370
399
|
allowed_paths = [path.strip() for path in args.allowed_paths.split(",")]
|
|
400
|
+
enabled_tools = (
|
|
401
|
+
{tool.strip() for tool in args.enabled_tools.split(",") if tool.strip()}
|
|
402
|
+
if args.enabled_tools
|
|
403
|
+
else enabled_tools_from_env()
|
|
404
|
+
)
|
|
371
405
|
|
|
372
406
|
logger.info(f"Initializing server with name: {args.name}")
|
|
373
407
|
# argparse already normalizes/validates --transport via type=normalize_transport
|
|
374
408
|
normalized_transport = args.transport
|
|
375
409
|
code_mode_enabled = args.enable_code_mode or code_mode_enabled_from_env(default=True)
|
|
410
|
+
enable_write_tools = args.enable_write_tools or write_tools_enabled_from_env(
|
|
411
|
+
default=hosted_mode
|
|
412
|
+
)
|
|
376
413
|
code_mode_path = (
|
|
377
414
|
normalize_code_mode_path(args.code_mode_path)
|
|
378
415
|
if args.code_mode_path
|
|
@@ -385,8 +422,15 @@ def main():
|
|
|
385
422
|
hosted=hosted_mode,
|
|
386
423
|
base_url=args.base_url,
|
|
387
424
|
transport=normalized_transport,
|
|
425
|
+
enable_write_tools=enable_write_tools,
|
|
426
|
+
enabled_tools=enabled_tools,
|
|
388
427
|
)
|
|
389
428
|
|
|
429
|
+
if args.list_tools:
|
|
430
|
+
for tool_name in asyncio.run(_get_sorted_tool_names(server)):
|
|
431
|
+
print(tool_name)
|
|
432
|
+
return
|
|
433
|
+
|
|
390
434
|
code_mode_server = None
|
|
391
435
|
if code_mode_enabled:
|
|
392
436
|
if not hosted_mode:
|
|
@@ -403,6 +447,8 @@ def main():
|
|
|
403
447
|
allowed_paths=allowed_paths,
|
|
404
448
|
hosted=hosted_mode,
|
|
405
449
|
base_url=args.base_url,
|
|
450
|
+
enable_write_tools=enable_write_tools,
|
|
451
|
+
enabled_tools=enabled_tools,
|
|
406
452
|
)
|
|
407
453
|
logger.info("Code Mode enabled at path: %s", code_mode_path)
|
|
408
454
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Security audit logging for Rootly MCP Server."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
# Context variables for tracking session info
|
|
10
|
+
current_session: ContextVar[str | None] = ContextVar("current_session", default=None)
|
|
11
|
+
current_user: ContextVar[str | None] = ContextVar("current_user", default=None)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuditLogger:
|
|
15
|
+
"""Structured audit logger for security-relevant events."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, logger_name: str = "rootly_mcp_server.audit"):
|
|
18
|
+
self.logger = logging.getLogger(logger_name)
|
|
19
|
+
self.logger.setLevel(logging.INFO)
|
|
20
|
+
|
|
21
|
+
# JSON formatter for structured logs
|
|
22
|
+
formatter = logging.Formatter(
|
|
23
|
+
'{"timestamp": "%(asctime)s", "level": "%(levelname)s", "audit_event": %(message)s}'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Add console handler if none exists
|
|
27
|
+
if not self.logger.handlers:
|
|
28
|
+
handler = logging.StreamHandler()
|
|
29
|
+
handler.setFormatter(formatter)
|
|
30
|
+
self.logger.addHandler(handler)
|
|
31
|
+
|
|
32
|
+
def log_permission_change(self, action: str, details: dict[str, Any]):
|
|
33
|
+
"""Log permission configuration changes."""
|
|
34
|
+
event = {
|
|
35
|
+
"event_type": "permission_change",
|
|
36
|
+
"action": action,
|
|
37
|
+
"session_id": current_session.get(),
|
|
38
|
+
"user_id": current_user.get(),
|
|
39
|
+
"timestamp": time.time(),
|
|
40
|
+
**details,
|
|
41
|
+
}
|
|
42
|
+
self.logger.info(json.dumps(event))
|
|
43
|
+
|
|
44
|
+
def log_tool_validation(
|
|
45
|
+
self, enabled_tools: set[str], valid_tools: set[str], invalid_tools: list[str]
|
|
46
|
+
):
|
|
47
|
+
"""Log tool name validation results."""
|
|
48
|
+
event = {
|
|
49
|
+
"event_type": "tool_validation",
|
|
50
|
+
"requested_tools_count": len(enabled_tools),
|
|
51
|
+
"valid_tools_count": len(valid_tools),
|
|
52
|
+
"invalid_tools_count": len(invalid_tools),
|
|
53
|
+
"invalid_tools": invalid_tools,
|
|
54
|
+
"session_id": current_session.get(),
|
|
55
|
+
"timestamp": time.time(),
|
|
56
|
+
}
|
|
57
|
+
self.logger.info(json.dumps(event))
|
|
58
|
+
|
|
59
|
+
def log_server_start(self, config: dict[str, Any]):
|
|
60
|
+
"""Log server startup with security-relevant configuration."""
|
|
61
|
+
event = {
|
|
62
|
+
"event_type": "server_start",
|
|
63
|
+
"write_tools_enabled": config.get("enable_write_tools", True),
|
|
64
|
+
"tool_count": config.get("tool_count", 0),
|
|
65
|
+
"hosted_mode": config.get("hosted", False),
|
|
66
|
+
"allowlist_enabled": bool(config.get("enabled_tools")),
|
|
67
|
+
"transport": config.get("transport", "unknown"),
|
|
68
|
+
"timestamp": time.time(),
|
|
69
|
+
}
|
|
70
|
+
self.logger.info(json.dumps(event))
|
|
71
|
+
|
|
72
|
+
def log_tool_access_attempt(
|
|
73
|
+
self, tool_name: str, method: str, success: bool, details: dict[str, Any] | None = None
|
|
74
|
+
):
|
|
75
|
+
"""Log individual tool access attempts."""
|
|
76
|
+
event = {
|
|
77
|
+
"event_type": "tool_access",
|
|
78
|
+
"tool_name": tool_name,
|
|
79
|
+
"http_method": method,
|
|
80
|
+
"success": success,
|
|
81
|
+
"session_id": current_session.get(),
|
|
82
|
+
"user_id": current_user.get(),
|
|
83
|
+
"timestamp": time.time(),
|
|
84
|
+
**(details or {}),
|
|
85
|
+
}
|
|
86
|
+
self.logger.info(json.dumps(event))
|
|
87
|
+
|
|
88
|
+
def log_configuration_error(
|
|
89
|
+
self, error_type: str, message: str, details: dict[str, Any] | None = None
|
|
90
|
+
):
|
|
91
|
+
"""Log configuration validation errors."""
|
|
92
|
+
event = {
|
|
93
|
+
"event_type": "configuration_error",
|
|
94
|
+
"error_type": error_type,
|
|
95
|
+
"message": message,
|
|
96
|
+
"session_id": current_session.get(),
|
|
97
|
+
"timestamp": time.time(),
|
|
98
|
+
**(details or {}),
|
|
99
|
+
}
|
|
100
|
+
self.logger.warning(json.dumps(event))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Global audit logger instance
|
|
104
|
+
audit = AuditLogger()
|
|
@@ -280,6 +280,8 @@ def create_rootly_codemode_server(
|
|
|
280
280
|
allowed_paths: list[str] | None = None,
|
|
281
281
|
hosted: bool = False,
|
|
282
282
|
base_url: str | None = None,
|
|
283
|
+
enable_write_tools: bool | None = None,
|
|
284
|
+
enabled_tools: set[str] | None = None,
|
|
283
285
|
) -> FastMCP:
|
|
284
286
|
"""Create a Rootly MCP server instance wrapped with Code Mode."""
|
|
285
287
|
mcp: FastMCP = create_rootly_mcp_server(
|
|
@@ -289,6 +291,8 @@ def create_rootly_codemode_server(
|
|
|
289
291
|
hosted=hosted,
|
|
290
292
|
base_url=base_url,
|
|
291
293
|
transport="streamable-http",
|
|
294
|
+
enable_write_tools=enable_write_tools,
|
|
295
|
+
enabled_tools=enabled_tools,
|
|
292
296
|
)
|
|
293
297
|
mcp.add_transform(build_code_mode_transform())
|
|
294
298
|
return mcp
|
|
@@ -17,7 +17,7 @@ import fastmcp.server.middleware as fastmcp_middleware
|
|
|
17
17
|
import mcp.types as mt
|
|
18
18
|
from fastmcp import FastMCP
|
|
19
19
|
|
|
20
|
-
from . import legacy_server, payload_stripping, server_defaults, spec_transform, transport
|
|
20
|
+
from . import audit, legacy_server, payload_stripping, server_defaults, spec_transform, transport
|
|
21
21
|
from .exceptions import RootlyAuthenticationError
|
|
22
22
|
from .mcp_error import MCPError
|
|
23
23
|
from .security import mask_sensitive_data, sanitize_error_message
|
|
@@ -66,6 +66,7 @@ strip_heavy_nested_data = payload_stripping.strip_heavy_nested_data
|
|
|
66
66
|
_generate_recommendation = server_defaults._generate_recommendation
|
|
67
67
|
DEFAULT_ALLOWED_PATHS = server_defaults.DEFAULT_ALLOWED_PATHS
|
|
68
68
|
DEFAULT_DELETE_ALLOWED_PATHS = server_defaults.DEFAULT_DELETE_ALLOWED_PATHS
|
|
69
|
+
DEFAULT_WRITE_ALLOWED_PATHS = server_defaults.DEFAULT_WRITE_ALLOWED_PATHS
|
|
69
70
|
RootlyMCPServer = legacy_server.RootlyMCPServer
|
|
70
71
|
|
|
71
72
|
|
|
@@ -375,6 +376,9 @@ def create_rootly_mcp_server(
|
|
|
375
376
|
base_url: str | None = None,
|
|
376
377
|
transport: str = "stdio",
|
|
377
378
|
delete_allowed_paths: list[str] | None = None,
|
|
379
|
+
enable_write_tools: bool | None = None,
|
|
380
|
+
write_allowed_paths: list[str] | None = None,
|
|
381
|
+
enabled_tools: set[str] | None = None,
|
|
378
382
|
) -> FastMCP:
|
|
379
383
|
"""
|
|
380
384
|
Create a Rootly MCP Server using FastMCP's OpenAPI integration.
|
|
@@ -384,10 +388,16 @@ def create_rootly_mcp_server(
|
|
|
384
388
|
name: Name of the MCP server.
|
|
385
389
|
allowed_paths: List of API paths to include. If None, includes default paths.
|
|
386
390
|
delete_allowed_paths: Path templates where DELETE operations are exposed.
|
|
387
|
-
If None,
|
|
391
|
+
If None, destructive delete tools remain disabled by default.
|
|
388
392
|
hosted: Whether the server is hosted (affects authentication).
|
|
389
393
|
base_url: Base URL for Rootly API. If None, uses ROOTLY_BASE_URL env var or default.
|
|
390
394
|
transport: Transport protocol (stdio, sse, or streamable-http).
|
|
395
|
+
enable_write_tools: Whether non-destructive write tools are exposed.
|
|
396
|
+
If None, uses ROOTLY_MCP_ENABLE_WRITE_TOOLS.
|
|
397
|
+
write_allowed_paths: Path templates where POST/PUT/PATCH operations are exposed
|
|
398
|
+
when write tools are enabled. If None, uses DEFAULT_WRITE_ALLOWED_PATHS.
|
|
399
|
+
enabled_tools: Optional allowlist of exact MCP tool names to expose.
|
|
400
|
+
If None, uses ROOTLY_MCP_ENABLED_TOOLS when set.
|
|
391
401
|
|
|
392
402
|
Returns:
|
|
393
403
|
A FastMCP server instance.
|
|
@@ -395,8 +405,14 @@ def create_rootly_mcp_server(
|
|
|
395
405
|
# Set default allowed paths if none provided
|
|
396
406
|
if allowed_paths is None:
|
|
397
407
|
allowed_paths = DEFAULT_ALLOWED_PATHS
|
|
408
|
+
if enable_write_tools is None:
|
|
409
|
+
enable_write_tools = server_defaults.write_tools_enabled_from_env(default=True)
|
|
410
|
+
if enabled_tools is None:
|
|
411
|
+
enabled_tools = server_defaults.enabled_tools_from_env()
|
|
398
412
|
if delete_allowed_paths is None:
|
|
399
|
-
delete_allowed_paths =
|
|
413
|
+
delete_allowed_paths = []
|
|
414
|
+
if write_allowed_paths is None:
|
|
415
|
+
write_allowed_paths = DEFAULT_WRITE_ALLOWED_PATHS if enable_write_tools else []
|
|
400
416
|
|
|
401
417
|
# Add /v1 prefix to paths if not present
|
|
402
418
|
allowed_paths_v1 = [
|
|
@@ -405,6 +421,9 @@ def create_rootly_mcp_server(
|
|
|
405
421
|
delete_allowed_paths_v1 = [
|
|
406
422
|
f"/v1{path}" if not path.startswith("/v1") else path for path in delete_allowed_paths
|
|
407
423
|
]
|
|
424
|
+
write_allowed_paths_v1 = [
|
|
425
|
+
f"/v1{path}" if not path.startswith("/v1") else path for path in write_allowed_paths
|
|
426
|
+
]
|
|
408
427
|
|
|
409
428
|
logger.info(f"Creating Rootly MCP Server with allowed paths: {allowed_paths_v1}")
|
|
410
429
|
|
|
@@ -417,9 +436,73 @@ def create_rootly_mcp_server(
|
|
|
417
436
|
swagger_spec,
|
|
418
437
|
allowed_paths_v1,
|
|
419
438
|
delete_allowed_paths=delete_allowed_paths_v1,
|
|
439
|
+
write_allowed_paths=write_allowed_paths_v1,
|
|
440
|
+
enable_write_tools=enable_write_tools,
|
|
441
|
+
enabled_operation_ids=enabled_tools,
|
|
420
442
|
)
|
|
443
|
+
|
|
444
|
+
# Validate enabled tools against the filtered spec (after path filtering)
|
|
445
|
+
if enabled_tools:
|
|
446
|
+
valid_tools, invalid_tools = server_defaults.validate_tool_names(
|
|
447
|
+
enabled_tools, filtered_spec.get("paths", {})
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if invalid_tools:
|
|
451
|
+
audit.audit.log_configuration_error(
|
|
452
|
+
"invalid_tool_names",
|
|
453
|
+
f"Invalid tool names in allowlist after filtering: {', '.join(invalid_tools)}",
|
|
454
|
+
{"invalid_tools": invalid_tools, "valid_tools": list(valid_tools)},
|
|
455
|
+
)
|
|
456
|
+
logger.warning(
|
|
457
|
+
"Invalid tool names in allowlist (will be ignored): %s. "
|
|
458
|
+
"Use --list-tools to see available options.",
|
|
459
|
+
", ".join(sorted(invalid_tools)),
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if not valid_tools and enabled_tools:
|
|
463
|
+
error_msg = "No valid tools found in allowlist after path filtering"
|
|
464
|
+
audit.audit.log_configuration_error(
|
|
465
|
+
"no_valid_tools", error_msg, {"requested_tools": list(enabled_tools)}
|
|
466
|
+
)
|
|
467
|
+
raise ValueError(error_msg)
|
|
468
|
+
|
|
469
|
+
# Log validation results
|
|
470
|
+
audit.audit.log_tool_validation(enabled_tools, valid_tools, invalid_tools)
|
|
471
|
+
|
|
472
|
+
# Update the filtered spec to only include valid tools
|
|
473
|
+
if valid_tools != enabled_tools:
|
|
474
|
+
filtered_spec = _filter_openapi_spec(
|
|
475
|
+
swagger_spec,
|
|
476
|
+
allowed_paths_v1,
|
|
477
|
+
delete_allowed_paths=delete_allowed_paths_v1,
|
|
478
|
+
write_allowed_paths=write_allowed_paths_v1,
|
|
479
|
+
enable_write_tools=enable_write_tools,
|
|
480
|
+
enabled_operation_ids=valid_tools,
|
|
481
|
+
)
|
|
421
482
|
logger.info(f"Filtered spec to {len(filtered_spec.get('paths', {}))} allowed paths")
|
|
422
483
|
|
|
484
|
+
# Log server configuration for audit trail
|
|
485
|
+
config_info = {
|
|
486
|
+
"enable_write_tools": enable_write_tools,
|
|
487
|
+
"tool_count": len(filtered_spec.get("paths", {})),
|
|
488
|
+
"hosted": hosted,
|
|
489
|
+
"enabled_tools": list(enabled_tools) if enabled_tools else None,
|
|
490
|
+
"transport": transport,
|
|
491
|
+
"server_name": name,
|
|
492
|
+
}
|
|
493
|
+
audit.audit.log_server_start(config_info)
|
|
494
|
+
|
|
495
|
+
# Log permission changes
|
|
496
|
+
if enable_write_tools:
|
|
497
|
+
audit.audit.log_permission_change(
|
|
498
|
+
"write_tools_enabled",
|
|
499
|
+
{
|
|
500
|
+
"reason": "explicit_configuration",
|
|
501
|
+
"write_paths_count": len(write_allowed_paths_v1),
|
|
502
|
+
"hosted_mode": hosted,
|
|
503
|
+
},
|
|
504
|
+
)
|
|
505
|
+
|
|
423
506
|
# Sanitize all parameter names in the filtered spec to be MCP-compliant
|
|
424
507
|
parameter_mapping = sanitize_parameters_in_spec(filtered_spec)
|
|
425
508
|
logger.info(
|
|
@@ -559,6 +642,7 @@ def create_rootly_mcp_server(
|
|
|
559
642
|
strip_heavy_nested_data=strip_heavy_nested_data,
|
|
560
643
|
mcp_error=MCPError,
|
|
561
644
|
generate_recommendation=_generate_recommendation,
|
|
645
|
+
enable_write_tools=enable_write_tools,
|
|
562
646
|
)
|
|
563
647
|
|
|
564
648
|
register_oncall_tools(
|
|
@@ -580,6 +664,20 @@ def create_rootly_mcp_server(
|
|
|
580
664
|
mcp_error=MCPError,
|
|
581
665
|
)
|
|
582
666
|
|
|
667
|
+
if enabled_tools is not None:
|
|
668
|
+
component_names = [
|
|
669
|
+
component.name
|
|
670
|
+
for component_key, component in mcp._local_provider._components.items() # noqa: SLF001
|
|
671
|
+
if component_key.startswith("tool:") and getattr(component, "name", None)
|
|
672
|
+
]
|
|
673
|
+
for tool_name in component_names:
|
|
674
|
+
if tool_name not in enabled_tools:
|
|
675
|
+
mcp.local_provider.remove_tool(tool_name)
|
|
676
|
+
logger.info(
|
|
677
|
+
"Applied MCP tool allowlist: %s",
|
|
678
|
+
", ".join(sorted(enabled_tools)),
|
|
679
|
+
)
|
|
680
|
+
|
|
583
681
|
# In hosted HTTP modes, configure ASGI middleware for auth token capture.
|
|
584
682
|
# Callers retrieve via get_hosted_auth_middleware() and pass to server.run(middleware=...).
|
|
585
683
|
global _hosted_auth_middleware
|
{rootly_mcp_server-2.2.20 → rootly_mcp_server-2.2.22}/src/rootly_mcp_server/server_defaults.py
RENAMED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
# Set up logger
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Environment variable constants
|
|
13
|
+
class EnvVars:
|
|
14
|
+
"""Centralized environment variable names for type safety and IDE support."""
|
|
15
|
+
|
|
16
|
+
API_TOKEN = "ROOTLY_API_TOKEN" # nosec B105 - Environment variable name, not password
|
|
17
|
+
BASE_URL = "ROOTLY_BASE_URL"
|
|
18
|
+
SERVER_NAME = "ROOTLY_SERVER_NAME"
|
|
19
|
+
HOSTED = "ROOTLY_HOSTED"
|
|
20
|
+
ENABLE_WRITE_TOOLS = "ROOTLY_MCP_ENABLE_WRITE_TOOLS"
|
|
21
|
+
ENABLED_TOOLS = "ROOTLY_MCP_ENABLED_TOOLS"
|
|
22
|
+
TRANSPORT = "ROOTLY_TRANSPORT"
|
|
23
|
+
ALLOWED_PATHS = "ROOTLY_ALLOWED_PATHS"
|
|
24
|
+
SWAGGER_PATH = "ROOTLY_SWAGGER_PATH"
|
|
25
|
+
LOG_LEVEL = "ROOTLY_LOG_LEVEL"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_csv_set(raw: str | None) -> set[str] | None:
|
|
29
|
+
"""Parse a comma-separated environment value into a normalized set."""
|
|
30
|
+
if raw is None:
|
|
31
|
+
return None
|
|
32
|
+
parsed = {item.strip() for item in raw.split(",") if item.strip()}
|
|
33
|
+
return parsed or None
|
|
34
|
+
|
|
5
35
|
|
|
6
36
|
def _generate_recommendation(solution_data: dict) -> str:
|
|
7
37
|
"""Generate a high-level recommendation based on solution analysis."""
|
|
@@ -41,8 +71,51 @@ def _generate_recommendation(solution_data: dict) -> str:
|
|
|
41
71
|
)
|
|
42
72
|
|
|
43
73
|
|
|
74
|
+
def write_tools_enabled_from_env(default: bool = False) -> bool:
|
|
75
|
+
"""Return whether non-destructive write tools should be exposed."""
|
|
76
|
+
raw = os.getenv(EnvVars.ENABLE_WRITE_TOOLS)
|
|
77
|
+
if raw is None:
|
|
78
|
+
return default
|
|
79
|
+
return raw.strip().lower() in ("1", "true", "yes", "on")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_tool_names(
|
|
83
|
+
enabled_tools: set[str], available_operations: dict
|
|
84
|
+
) -> tuple[set[str], list[str]]:
|
|
85
|
+
"""
|
|
86
|
+
Validate tool names against available OpenAPI operations.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
enabled_tools: Set of tool names to validate
|
|
90
|
+
available_operations: OpenAPI paths dict from swagger spec
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
tuple: (valid_tools, invalid_tools)
|
|
94
|
+
"""
|
|
95
|
+
if not enabled_tools:
|
|
96
|
+
return set(), []
|
|
97
|
+
|
|
98
|
+
# Extract all available operation IDs from OpenAPI spec
|
|
99
|
+
available_tool_names = set()
|
|
100
|
+
for path_data in available_operations.values():
|
|
101
|
+
for method_data in path_data.values():
|
|
102
|
+
if isinstance(method_data, dict) and (operation_id := method_data.get("operationId")):
|
|
103
|
+
available_tool_names.add(operation_id)
|
|
104
|
+
|
|
105
|
+
valid_tools = enabled_tools & available_tool_names
|
|
106
|
+
invalid_tools = list(enabled_tools - available_tool_names)
|
|
107
|
+
|
|
108
|
+
return valid_tools, invalid_tools
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def enabled_tools_from_env() -> set[str] | None:
|
|
112
|
+
"""Return an optional explicit allowlist of MCP tool names to expose."""
|
|
113
|
+
return _parse_csv_set(os.getenv(EnvVars.ENABLED_TOOLS))
|
|
114
|
+
|
|
115
|
+
|
|
44
116
|
# Default allowed API paths
|
|
45
117
|
DEFAULT_ALLOWED_PATHS = [
|
|
118
|
+
"/incidents",
|
|
46
119
|
"/incidents/{incident_id}/alerts",
|
|
47
120
|
"/alerts",
|
|
48
121
|
"/alerts/{id}",
|
|
@@ -109,6 +182,17 @@ DEFAULT_ALLOWED_PATHS = [
|
|
|
109
182
|
"/on_call_roles/{on_call_role_id}",
|
|
110
183
|
]
|
|
111
184
|
|
|
185
|
+
# Non-destructive write operations are only exposed for these path families when
|
|
186
|
+
# write tools are explicitly enabled. This keeps the default surface focused on
|
|
187
|
+
# read-only workflows and avoids exposing broader admin/config writes.
|
|
188
|
+
DEFAULT_WRITE_ALLOWED_PATHS = [
|
|
189
|
+
"/incidents/{incident_id}/action_items",
|
|
190
|
+
"/incidents/{incident_id}/form_field_selections",
|
|
191
|
+
"/incident_form_field_selections/{id}",
|
|
192
|
+
"/workflows/{workflow_id}/workflow_tasks",
|
|
193
|
+
"/workflow_tasks/{id}",
|
|
194
|
+
]
|
|
195
|
+
|
|
112
196
|
# DELETE operations are only exposed for these high-priority screenshot families.
|
|
113
197
|
# All other DELETE operations remain disabled in MCP by default.
|
|
114
198
|
DEFAULT_DELETE_ALLOWED_PATHS = [
|