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 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 23:16:52 +01:00
parent 1755364bba
commit 2a08cdaf2e
3 changed files with 61 additions and 2 deletions

View File

@@ -168,10 +168,15 @@ func runServe(c *cli.Context) error {
logger := log.New(os.Stderr, "[mcp] ", log.LstdFlags) logger := log.New(os.Stderr, "[mcp] ", log.LstdFlags)
config := mcp.DefaultMonitoringConfig() config := mcp.DefaultMonitoringConfig()
server := mcp.NewGenericServer(logger, config)
prom := monitoring.NewPrometheusClient(c.String("prometheus-url")) prom := monitoring.NewPrometheusClient(c.String("prometheus-url"))
am := monitoring.NewAlertmanagerClient(c.String("alertmanager-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) monitoring.RegisterHandlers(server, prom, am)
transport := c.String("transport") transport := c.String("transport")

View File

@@ -30,6 +30,9 @@ type ServerConfig struct {
Version string Version string
// Instructions are the server instructions sent to clients. // Instructions are the server instructions sent to clients.
Instructions string 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 is the default channel to use when no revision is specified.
DefaultChannel string DefaultChannel string
// SourceName is the name of the source repository (e.g., "nixpkgs", "home-manager"). // 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", s.logger.Printf("Client: %s %s, protocol: %s",
params.ClientInfo.Name, params.ClientInfo.Version, params.ProtocolVersion) 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{ result := InitializeResult{
ProtocolVersion: ProtocolVersion, ProtocolVersion: ProtocolVersion,
Capabilities: Capabilities{ Capabilities: Capabilities{
@@ -255,7 +265,7 @@ func (s *Server) handleInitialize(req *Request) *Response {
Name: s.config.Name, Name: s.config.Name,
Version: s.config.Version, Version: s.config.Version,
}, },
Instructions: s.config.Instructions, Instructions: instructions,
} }
return &Response{ return &Response{

View File

@@ -10,6 +10,50 @@ import (
"git.t-juice.club/torjus/labmcp/internal/mcp" "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. // 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) {
server.RegisterTool(listAlertsTool(), makeListAlertsHandler(am)) server.RegisterTool(listAlertsTool(), makeListAlertsHandler(am))