diff --git a/cmd/lab-monitoring/main.go b/cmd/lab-monitoring/main.go index b68da9e..263d14a 100644 --- a/cmd/lab-monitoring/main.go +++ b/cmd/lab-monitoring/main.go @@ -88,6 +88,10 @@ func serveCommand() *cli.Command { Usage: "Session TTL for HTTP transport", Value: 30 * time.Minute, }, + &cli.BoolFlag{ + Name: "enable-silences", + Usage: "Enable the create_silence tool (write operation, disabled by default)", + }, }, Action: func(c *cli.Context) error { return runServe(c) @@ -177,7 +181,10 @@ func runServe(c *cli.Context) error { } server := mcp.NewGenericServer(logger, config) - monitoring.RegisterHandlers(server, prom, am) + opts := monitoring.HandlerOptions{ + EnableSilences: c.Bool("enable-silences"), + } + monitoring.RegisterHandlers(server, prom, am, opts) transport := c.String("transport") switch transport { diff --git a/internal/monitoring/handlers.go b/internal/monitoring/handlers.go index de67c72..419c3ac 100644 --- a/internal/monitoring/handlers.go +++ b/internal/monitoring/handlers.go @@ -18,8 +18,8 @@ func AlertSummary(am *AlertmanagerClient) string { silenced := false inhibited := false alerts, err := am.ListAlerts(context.Background(), AlertFilters{ - Active: &active, - Silenced: &silenced, + Active: &active, + Silenced: &silenced, Inhibited: &inhibited, }) if err != nil || len(alerts) == 0 { @@ -54,8 +54,15 @@ func AlertSummary(am *AlertmanagerClient) string { len(alerts), strings.Join(parts, ", ")) } +// HandlerOptions configures which handlers are registered. +type HandlerOptions struct { + // EnableSilences enables the create_silence tool, which is a write operation. + // Disabled by default as a safety measure. + EnableSilences bool +} + // RegisterHandlers registers all monitoring tool handlers on the MCP server. -func RegisterHandlers(server *mcp.Server, prom *PrometheusClient, am *AlertmanagerClient) { +func RegisterHandlers(server *mcp.Server, prom *PrometheusClient, am *AlertmanagerClient, opts HandlerOptions) { server.RegisterTool(listAlertsTool(), makeListAlertsHandler(am)) server.RegisterTool(getAlertTool(), makeGetAlertHandler(am)) server.RegisterTool(searchMetricsTool(), makeSearchMetricsHandler(prom)) @@ -63,7 +70,9 @@ func RegisterHandlers(server *mcp.Server, prom *PrometheusClient, am *Alertmanag server.RegisterTool(queryTool(), makeQueryHandler(prom)) server.RegisterTool(listTargetsTool(), makeListTargetsHandler(prom)) server.RegisterTool(listSilencesTool(), makeListSilencesHandler(am)) - server.RegisterTool(createSilenceTool(), makeCreateSilenceHandler(am)) + if opts.EnableSilences { + server.RegisterTool(createSilenceTool(), makeCreateSilenceHandler(am)) + } } // Tool definitions diff --git a/internal/monitoring/handlers_test.go b/internal/monitoring/handlers_test.go index 69435e8..371cc12 100644 --- a/internal/monitoring/handlers_test.go +++ b/internal/monitoring/handlers_test.go @@ -26,7 +26,7 @@ func setupTestServer(t *testing.T, promHandler, amHandler http.HandlerFunc) (*mc prom := NewPrometheusClient(promSrv.URL) am := NewAlertmanagerClient(amSrv.URL) - RegisterHandlers(server, prom, am) + RegisterHandlers(server, prom, am, HandlerOptions{EnableSilences: true}) cleanup := func() { promSrv.Close() @@ -304,7 +304,60 @@ func TestHandler_ToolCount(t *testing.T) { ) defer cleanup() - // Send a tools/list request + tools := listTools(t, server) + if len(tools) != 8 { + t.Errorf("expected 8 tools with silences enabled, got %d", len(tools)) + for _, tool := range tools { + t.Logf(" tool: %s", tool.Name) + } + } + + // Verify create_silence is present + found := false + for _, tool := range tools { + if tool.Name == "create_silence" { + found = true + break + } + } + if !found { + t.Error("expected create_silence tool when silences enabled") + } +} + +func TestHandler_ToolCountWithoutSilences(t *testing.T) { + promSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + amSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + defer promSrv.Close() + defer amSrv.Close() + + logger := log.New(io.Discard, "", 0) + config := mcp.DefaultMonitoringConfig() + server := mcp.NewGenericServer(logger, config) + + prom := NewPrometheusClient(promSrv.URL) + am := NewAlertmanagerClient(amSrv.URL) + RegisterHandlers(server, prom, am, HandlerOptions{EnableSilences: false}) + + tools := listTools(t, server) + if len(tools) != 7 { + t.Errorf("expected 7 tools without silences, got %d", len(tools)) + for _, tool := range tools { + t.Logf(" tool: %s", tool.Name) + } + } + + // Verify create_silence is NOT present + for _, tool := range tools { + if tool.Name == "create_silence" { + t.Error("expected create_silence tool to be absent when silences disabled") + } + } +} + +func listTools(t *testing.T, server *mcp.Server) []mcp.Tool { + t.Helper() + req := &mcp.Request{ JSONRPC: "2.0", ID: 1, @@ -328,12 +381,7 @@ func TestHandler_ToolCount(t *testing.T) { t.Fatalf("failed to unmarshal result: %v", err) } - if len(listResult.Tools) != 8 { - t.Errorf("expected 8 tools, got %d", len(listResult.Tools)) - for _, tool := range listResult.Tools { - t.Logf(" tool: %s", tool.Name) - } - } + return listResult.Tools } // callTool is a test helper that calls a tool through the MCP server. diff --git a/nix/lab-monitoring-module.nix b/nix/lab-monitoring-module.nix index 6a15dbe..b6f8d94 100644 --- a/nix/lab-monitoring-module.nix +++ b/nix/lab-monitoring-module.nix @@ -33,6 +33,12 @@ in description = "Alertmanager base URL."; }; + enableSilences = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable the create_silence tool (write operation, disabled by default)."; + }; + http = { address = lib.mkOption { type = lib.types.str; @@ -102,8 +108,9 @@ in script = let httpFlags = mkHttpFlags cfg.http; + silenceFlag = lib.optionalString cfg.enableSilences "--enable-silences"; in '' - exec ${cfg.package}/bin/lab-monitoring serve ${httpFlags} + exec ${cfg.package}/bin/lab-monitoring serve ${httpFlags} ${silenceFlag} ''; serviceConfig = {