From 2a08cdaf2ea008e3de907f725c8debdeb39d475f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Wed, 4 Feb 2026 23:16:52 +0100 Subject: [PATCH] feat: include active alert count in MCP server instructions Add InstructionsFunc callback to ServerConfig, called during each initialize handshake to generate dynamic instructions. The lab-monitoring server uses this to query Alertmanager and include a count of active non-silenced alerts, so the LLM can proactively inform the user. Co-Authored-By: Claude Opus 4.5 --- cmd/lab-monitoring/main.go | 7 +++++- internal/mcp/server.go | 12 ++++++++- internal/monitoring/handlers.go | 44 +++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/cmd/lab-monitoring/main.go b/cmd/lab-monitoring/main.go index 410782a..b68da9e 100644 --- a/cmd/lab-monitoring/main.go +++ b/cmd/lab-monitoring/main.go @@ -168,10 +168,15 @@ func runServe(c *cli.Context) error { logger := log.New(os.Stderr, "[mcp] ", log.LstdFlags) config := mcp.DefaultMonitoringConfig() - server := mcp.NewGenericServer(logger, config) prom := monitoring.NewPrometheusClient(c.String("prometheus-url")) am := monitoring.NewAlertmanagerClient(c.String("alertmanager-url")) + + config.InstructionsFunc = func() string { + return monitoring.AlertSummary(am) + } + + server := mcp.NewGenericServer(logger, config) monitoring.RegisterHandlers(server, prom, am) transport := c.String("transport") diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 0a97f75..2c6c055 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -30,6 +30,9 @@ type ServerConfig struct { Version string // Instructions are the server instructions sent to clients. Instructions string + // InstructionsFunc, if set, is called during initialization to generate + // dynamic instructions. Its return value is appended to Instructions. + InstructionsFunc func() string // DefaultChannel is the default channel to use when no revision is specified. DefaultChannel string // SourceName is the name of the source repository (e.g., "nixpkgs", "home-manager"). @@ -244,6 +247,13 @@ func (s *Server) handleInitialize(req *Request) *Response { s.logger.Printf("Client: %s %s, protocol: %s", params.ClientInfo.Name, params.ClientInfo.Version, params.ProtocolVersion) + instructions := s.config.Instructions + if s.config.InstructionsFunc != nil { + if extra := s.config.InstructionsFunc(); extra != "" { + instructions += "\n\n" + extra + } + } + result := InitializeResult{ ProtocolVersion: ProtocolVersion, Capabilities: Capabilities{ @@ -255,7 +265,7 @@ func (s *Server) handleInitialize(req *Request) *Response { Name: s.config.Name, Version: s.config.Version, }, - Instructions: s.config.Instructions, + Instructions: instructions, } return &Response{ diff --git a/internal/monitoring/handlers.go b/internal/monitoring/handlers.go index 79e6237..de67c72 100644 --- a/internal/monitoring/handlers.go +++ b/internal/monitoring/handlers.go @@ -10,6 +10,50 @@ import ( "git.t-juice.club/torjus/labmcp/internal/mcp" ) +// AlertSummary queries Alertmanager for active (non-silenced) alerts and returns +// a short summary string. Returns empty string if there are no alerts or if +// Alertmanager is unreachable. +func AlertSummary(am *AlertmanagerClient) string { + active := true + silenced := false + inhibited := false + alerts, err := am.ListAlerts(context.Background(), AlertFilters{ + Active: &active, + Silenced: &silenced, + Inhibited: &inhibited, + }) + if err != nil || len(alerts) == 0 { + return "" + } + + // Count by severity + severities := make(map[string]int) + for _, a := range alerts { + sev := a.Labels["severity"] + if sev == "" { + sev = "unknown" + } + severities[sev]++ + } + + var parts []string + // Show critical first if present + if n, ok := severities["critical"]; ok { + parts = append(parts, fmt.Sprintf("%d critical", n)) + delete(severities, "critical") + } + if n, ok := severities["warning"]; ok { + parts = append(parts, fmt.Sprintf("%d warning", n)) + delete(severities, "warning") + } + for sev, n := range severities { + parts = append(parts, fmt.Sprintf("%d %s", n, sev)) + } + + return fmt.Sprintf("ALERT STATUS: %d active alert(s) firing (%s). Use list_alerts for details. Let the user know about this.", + len(alerts), strings.Join(parts, ", ")) +} + // RegisterHandlers registers all monitoring tool handlers on the MCP server. func RegisterHandlers(server *mcp.Server, prom *PrometheusClient, am *AlertmanagerClient) { server.RegisterTool(listAlertsTool(), makeListAlertsHandler(am))