Skip to content

Commit 48a5979

Browse files
Add --disable-write flag to control write operations (#385)
* Add --disable-write flag to control write operations * Address PR feedback: Add alerting tools, tests, and documentation - Add create_alert_rule, update_alert_rule, and delete_alert_rule to --disable-write flag - Add comprehensive Read-Only Mode section to README documenting all disabled write tools - Add tests to verify --disable-write flag behavior works correctly * Add annotation write tools to --disable-write flag - Include create_annotation, create_graphite_annotation, update_annotation, and patch_annotation - Update README to document annotation tools in Read-Only Mode section - Update tests to verify annotation write tools are properly disabled/enabled
1 parent 903f1ff commit 48a5979

File tree

10 files changed

+202
-27
lines changed

10 files changed

+202
-27
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ The `mcp-grafana` binary supports various command-line flags for configuration:
240240
- `--disable-datasource`: Disable datasource tools
241241
- `--disable-incident`: Disable incident tools
242242
- `--disable-prometheus`: Disable prometheus tools
243+
- `--disable-write`: Disable write tools (create/update operations)
243244
- `--disable-loki`: Disable loki tools
244245
- `--disable-alerting`: Disable alerting tools
245246
- `--disable-dashboard`: Disable dashboard tools
@@ -250,6 +251,44 @@ The `mcp-grafana` binary supports various command-line flags for configuration:
250251
- `--disable-pyroscope`: Disable pyroscope tools
251252
- `--disable-navigation`: Disable navigation tools
252253

254+
### Read-Only Mode
255+
256+
The `--disable-write` flag provides a way to run the MCP server in read-only mode, preventing any write operations to your Grafana instance. This is useful for scenarios where you want to provide safe, read-only access such as:
257+
258+
- Using service accounts with limited read-only permissions
259+
- Providing AI assistants with observability data without modification capabilities
260+
- Running in production environments where write access should be restricted
261+
- Testing and development scenarios where you want to prevent accidental modifications
262+
263+
When `--disable-write` is enabled, the following write operations are disabled:
264+
265+
**Dashboard Tools:**
266+
- `update_dashboard`
267+
268+
**Folder Tools:**
269+
- `create_folder`
270+
271+
**Incident Tools:**
272+
- `create_incident`
273+
- `add_activity_to_incident`
274+
275+
**Alerting Tools:**
276+
- `create_alert_rule`
277+
- `update_alert_rule`
278+
- `delete_alert_rule`
279+
280+
**Annotation Tools:**
281+
- `create_annotation`
282+
- `create_graphite_annotation`
283+
- `update_annotation`
284+
- `patch_annotation`
285+
286+
**Sift Tools:**
287+
- `find_error_pattern_logs` (creates investigations)
288+
- `find_slow_requests` (creates investigations)
289+
290+
All read operations remain available, allowing you to query dashboards, run PromQL/LogQL queries, list resources, and retrieve data.
291+
253292
**Client TLS Configuration (for Grafana connections):**
254293
- `--tls-cert-file`: Path to TLS certificate file for client authentication
255294
- `--tls-key-file`: Path to TLS private key file for client authentication

cmd/mcp-grafana/main.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ type disabledTools struct {
4141
search, datasource, incident,
4242
prometheus, loki, alerting,
4343
dashboard, folder, oncall, asserts, sift, admin,
44-
pyroscope, navigation, proxied, annotations bool
44+
pyroscope, navigation, proxied, annotations, write bool
4545
}
4646

4747
// Configuration for the Grafana client.
@@ -73,6 +73,7 @@ func (dt *disabledTools) addFlags() {
7373
flag.BoolVar(&dt.pyroscope, "disable-pyroscope", false, "Disable pyroscope tools")
7474
flag.BoolVar(&dt.navigation, "disable-navigation", false, "Disable navigation tools")
7575
flag.BoolVar(&dt.proxied, "disable-proxied", false, "Disable proxied tools (tools from external MCP servers)")
76+
flag.BoolVar(&dt.write, "disable-write", false, "Disable write tools (create/update operations)")
7677
flag.BoolVar(&dt.annotations, "disable-annotations", false, "Disable annotation tools")
7778
}
7879

@@ -88,21 +89,22 @@ func (gc *grafanaConfig) addFlags() {
8889

8990
func (dt *disabledTools) addTools(s *server.MCPServer) {
9091
enabledTools := strings.Split(dt.enabledTools, ",")
92+
enableWriteTools := !dt.write
9193
maybeAddTools(s, tools.AddSearchTools, enabledTools, dt.search, "search")
9294
maybeAddTools(s, tools.AddDatasourceTools, enabledTools, dt.datasource, "datasource")
93-
maybeAddTools(s, tools.AddIncidentTools, enabledTools, dt.incident, "incident")
95+
maybeAddTools(s, func(mcp *server.MCPServer) { tools.AddIncidentTools(mcp, enableWriteTools) }, enabledTools, dt.incident, "incident")
9496
maybeAddTools(s, tools.AddPrometheusTools, enabledTools, dt.prometheus, "prometheus")
9597
maybeAddTools(s, tools.AddLokiTools, enabledTools, dt.loki, "loki")
96-
maybeAddTools(s, tools.AddAlertingTools, enabledTools, dt.alerting, "alerting")
97-
maybeAddTools(s, tools.AddDashboardTools, enabledTools, dt.dashboard, "dashboard")
98-
maybeAddTools(s, tools.AddFolderTools, enabledTools, dt.folder, "folder")
98+
maybeAddTools(s, func(mcp *server.MCPServer) { tools.AddAlertingTools(mcp, enableWriteTools) }, enabledTools, dt.alerting, "alerting")
99+
maybeAddTools(s, func(mcp *server.MCPServer) { tools.AddDashboardTools(mcp, enableWriteTools) }, enabledTools, dt.dashboard, "dashboard")
100+
maybeAddTools(s, func(mcp *server.MCPServer) { tools.AddFolderTools(mcp, enableWriteTools) }, enabledTools, dt.folder, "folder")
99101
maybeAddTools(s, tools.AddOnCallTools, enabledTools, dt.oncall, "oncall")
100102
maybeAddTools(s, tools.AddAssertsTools, enabledTools, dt.asserts, "asserts")
101-
maybeAddTools(s, tools.AddSiftTools, enabledTools, dt.sift, "sift")
103+
maybeAddTools(s, func(mcp *server.MCPServer) { tools.AddSiftTools(mcp, enableWriteTools) }, enabledTools, dt.sift, "sift")
102104
maybeAddTools(s, tools.AddAdminTools, enabledTools, dt.admin, "admin")
103105
maybeAddTools(s, tools.AddPyroscopeTools, enabledTools, dt.pyroscope, "pyroscope")
104106
maybeAddTools(s, tools.AddNavigationTools, enabledTools, dt.navigation, "navigation")
105-
maybeAddTools(s, tools.AddAnnotationTools, enabledTools, dt.annotations, "annotations")
107+
maybeAddTools(s, func(mcp *server.MCPServer) { tools.AddAnnotationTools(mcp, enableWriteTools) }, enabledTools, dt.annotations, "annotations")
106108
}
107109

108110
func newServer(transport string, dt disabledTools) (*server.MCPServer, *mcpgrafana.ToolManager) {

examples/tls_example.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func runServerWithTLS() {
157157
// Add some basic tools
158158
tools.AddSearchTools(s)
159159
tools.AddDatasourceTools(s)
160-
tools.AddDashboardTools(s)
160+
tools.AddDashboardTools(s, false) // Read-only mode (no write tools)
161161

162162
// Create stdio server with TLS-enabled context function
163163
srv := server.NewStdioServer(s)

tests/disable_write_test.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import pytest
2+
import os
3+
from mcp.client.stdio import stdio_client
4+
from mcp import ClientSession, StdioServerParameters
5+
6+
pytestmark = pytest.mark.anyio
7+
8+
9+
@pytest.fixture
10+
def grafana_env():
11+
env = {"GRAFANA_URL": os.environ.get("GRAFANA_URL", "http://localhost:3000")}
12+
# Check for the new service account token environment variable first
13+
if key := os.environ.get("GRAFANA_SERVICE_ACCOUNT_TOKEN"):
14+
env["GRAFANA_SERVICE_ACCOUNT_TOKEN"] = key
15+
elif key := os.environ.get("GRAFANA_API_KEY"):
16+
env["GRAFANA_API_KEY"] = key
17+
return env
18+
19+
20+
async def test_disable_write_flag_disables_write_tools(grafana_env):
21+
"""Test that --disable-write flag disables write tools."""
22+
params = StdioServerParameters(
23+
command=os.environ.get("MCP_GRAFANA_PATH", "../dist/mcp-grafana"),
24+
args=["--disable-write"],
25+
env=grafana_env,
26+
)
27+
async with stdio_client(params) as (read, write):
28+
async with ClientSession(read, write) as session:
29+
await session.initialize()
30+
31+
# List all available tools
32+
tools_result = await session.list_tools()
33+
tool_names = [tool.name for tool in tools_result.tools]
34+
35+
# Verify write tools are NOT present
36+
write_tools = [
37+
"update_dashboard",
38+
"create_folder",
39+
"create_incident",
40+
"add_activity_to_incident",
41+
"create_alert_rule",
42+
"update_alert_rule",
43+
"delete_alert_rule",
44+
"create_annotation",
45+
"create_graphite_annotation",
46+
"update_annotation",
47+
"patch_annotation",
48+
"find_error_pattern_logs",
49+
"find_slow_requests",
50+
]
51+
52+
for tool in write_tools:
53+
assert tool not in tool_names, f"Write tool '{tool}' should not be available with --disable-write flag"
54+
55+
# Verify read tools ARE still present
56+
read_tools = [
57+
"get_dashboard_by_uid",
58+
"list_alert_rules",
59+
"get_alert_rule_by_uid",
60+
"list_contact_points",
61+
"list_incidents",
62+
"get_incident",
63+
"get_sift_investigation",
64+
"get_annotations",
65+
"get_annotation_tags",
66+
]
67+
68+
for tool in read_tools:
69+
assert tool in tool_names, f"Read tool '{tool}' should still be available with --disable-write flag"
70+
71+
72+
async def test_without_disable_write_flag_enables_write_tools(grafana_env):
73+
"""Test that without --disable-write flag, write tools are enabled."""
74+
params = StdioServerParameters(
75+
command=os.environ.get("MCP_GRAFANA_PATH", "../dist/mcp-grafana"),
76+
args=[], # No --disable-write flag
77+
env=grafana_env,
78+
)
79+
async with stdio_client(params) as (read, write):
80+
async with ClientSession(read, write) as session:
81+
await session.initialize()
82+
83+
# List all available tools
84+
tools_result = await session.list_tools()
85+
tool_names = [tool.name for tool in tools_result.tools]
86+
87+
# Verify write tools ARE present
88+
write_tools = [
89+
"update_dashboard",
90+
"create_folder",
91+
"create_incident",
92+
"add_activity_to_incident",
93+
"create_alert_rule",
94+
"update_alert_rule",
95+
"delete_alert_rule",
96+
"create_annotation",
97+
"create_graphite_annotation",
98+
"update_annotation",
99+
"patch_annotation",
100+
"find_error_pattern_logs",
101+
"find_slow_requests",
102+
]
103+
104+
for tool in write_tools:
105+
assert tool in tool_names, f"Write tool '{tool}' should be available without --disable-write flag"
106+
107+
# Verify read tools are also present
108+
read_tools = [
109+
"get_dashboard_by_uid",
110+
"list_alert_rules",
111+
"get_alert_rule_by_uid",
112+
"list_contact_points",
113+
"list_incidents",
114+
"get_incident",
115+
"get_sift_investigation",
116+
"get_annotations",
117+
"get_annotation_tags",
118+
]
119+
120+
for tool in read_tools:
121+
assert tool in tool_names, f"Read tool '{tool}' should be available without --disable-write flag"
122+

tools/alerting.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -638,11 +638,13 @@ var DeleteAlertRule = mcpgrafana.MustTool(
638638
mcp.WithTitleAnnotation("Delete alert rule"),
639639
)
640640

641-
func AddAlertingTools(mcp *server.MCPServer) {
641+
func AddAlertingTools(mcp *server.MCPServer, enableWriteTools bool) {
642642
ListAlertRules.Register(mcp)
643643
GetAlertRuleByUID.Register(mcp)
644-
CreateAlertRule.Register(mcp)
645-
UpdateAlertRule.Register(mcp)
646-
DeleteAlertRule.Register(mcp)
644+
if enableWriteTools {
645+
CreateAlertRule.Register(mcp)
646+
UpdateAlertRule.Register(mcp)
647+
DeleteAlertRule.Register(mcp)
648+
}
647649
ListContactPoints.Register(mcp)
648650
}

tools/annotations.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,13 @@ var GetAnnotationTagsTool = mcpgrafana.MustTool(
262262
mcp.WithReadOnlyHintAnnotation(true),
263263
)
264264

265-
func AddAnnotationTools(mcp *server.MCPServer) {
265+
func AddAnnotationTools(mcp *server.MCPServer, enableWriteTools bool) {
266266
GetAnnotationsTool.Register(mcp)
267-
CreateAnnotationTool.Register(mcp)
268-
CreateGraphiteAnnotationTool.Register(mcp)
269-
UpdateAnnotationTool.Register(mcp)
270-
PatchAnnotationTool.Register(mcp)
267+
if enableWriteTools {
268+
CreateAnnotationTool.Register(mcp)
269+
CreateGraphiteAnnotationTool.Register(mcp)
270+
UpdateAnnotationTool.Register(mcp)
271+
PatchAnnotationTool.Register(mcp)
272+
}
271273
GetAnnotationTagsTool.Register(mcp)
272274
}

tools/dashboard.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -644,9 +644,11 @@ func extractVariableSummary(variable map[string]interface{}) VariableSummary {
644644
}
645645
}
646646

647-
func AddDashboardTools(mcp *server.MCPServer) {
647+
func AddDashboardTools(mcp *server.MCPServer, enableWriteTools bool) {
648648
GetDashboardByUID.Register(mcp)
649-
UpdateDashboard.Register(mcp)
649+
if enableWriteTools {
650+
UpdateDashboard.Register(mcp)
651+
}
650652
GetDashboardPanelQueries.Register(mcp)
651653
GetDashboardProperty.Register(mcp)
652654
GetDashboardSummary.Register(mcp)

tools/folder.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ var CreateFolder = mcpgrafana.MustTool(
4747
mcp.WithReadOnlyHintAnnotation(false),
4848
)
4949

50-
func AddFolderTools(mcp *server.MCPServer) {
51-
CreateFolder.Register(mcp)
50+
func AddFolderTools(mcp *server.MCPServer, enableWriteTools bool) {
51+
if enableWriteTools {
52+
CreateFolder.Register(mcp)
53+
}
5254
}

tools/incident.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,12 @@ var AddActivityToIncident = mcpgrafana.MustTool(
120120
mcp.WithTitleAnnotation("Add activity to incident"),
121121
)
122122

123-
func AddIncidentTools(mcp *server.MCPServer) {
123+
func AddIncidentTools(mcp *server.MCPServer, enableWriteTools bool) {
124124
ListIncidents.Register(mcp)
125-
CreateIncident.Register(mcp)
126-
AddActivityToIncident.Register(mcp)
125+
if enableWriteTools {
126+
CreateIncident.Register(mcp)
127+
AddActivityToIncident.Register(mcp)
128+
}
127129
GetIncident.Register(mcp)
128130
}
129131

tools/sift.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,12 +417,14 @@ var FindSlowRequests = mcpgrafana.MustTool(
417417
)
418418

419419
// AddSiftTools registers all Sift tools with the MCP server
420-
func AddSiftTools(mcp *server.MCPServer) {
420+
func AddSiftTools(mcp *server.MCPServer, enableWriteTools bool) {
421421
GetSiftInvestigation.Register(mcp)
422422
GetSiftAnalysis.Register(mcp)
423423
ListSiftInvestigations.Register(mcp)
424-
FindErrorPatternLogs.Register(mcp)
425-
FindSlowRequests.Register(mcp)
424+
if enableWriteTools {
425+
FindErrorPatternLogs.Register(mcp)
426+
FindSlowRequests.Register(mcp)
427+
}
426428
}
427429

428430
// makeRequest is a helper method to make HTTP requests and handle common response patterns

0 commit comments

Comments
 (0)