feat: default list_alerts to active alerts only

Change list_alerts (MCP tool) and alerts (CLI command) to show only
active (non-silenced, non-inhibited) alerts by default. Add state=all
option and --all CLI flag to show all alerts when needed.

- MCP: list_alerts with no state param now returns active alerts only
- MCP: list_alerts with state=all returns all alerts (previous default)
- CLI: alerts command defaults to active, --all shows everything
- Add tests for new default behavior and state=all option
- Update README with new CLI examples
- Bump version to 0.3.0
- Clarify version bumping rules in CLAUDE.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 19:59:37 +01:00
parent 9dfe61e170
commit 9b16a5fe86
10 changed files with 158 additions and 55 deletions

View File

@@ -302,18 +302,19 @@ All three tools should pass with no issues before merging a feature branch.
**IMPORTANT**: When running `nix build`, `nix run`, or similar commands, new files must be tracked by git first. Nix flakes only see git-tracked files. If you create new files, run `git add <file>` before attempting nix operations. **IMPORTANT**: When running `nix build`, `nix run`, or similar commands, new files must be tracked by git first. Nix flakes only see git-tracked files. If you create new files, run `git add <file>` before attempting nix operations.
### Version Bumping ### Version Bumping
Version bumps should be done once per feature branch, not per commit. Rules: Version bumps should be done once per feature branch, not per commit. **Only bump versions for packages that were actually changed** — different packages can have different version numbers.
Rules for determining bump type:
- **Patch bump** (0.1.0 → 0.1.1): Changes to Go code within `internal/` that affect a program - **Patch bump** (0.1.0 → 0.1.1): Changes to Go code within `internal/` that affect a program
- **Minor bump** (0.1.0 → 0.2.0): Changes to Go code outside `internal/` (e.g., `cmd/`) - **Minor bump** (0.1.0 → 0.2.0): Changes to Go code outside `internal/` (e.g., `cmd/`)
- **Major bump** (0.1.0 → 1.0.0): Breaking changes to CLI usage or MCP protocol - **Major bump** (0.1.0 → 1.0.0): Breaking changes to CLI usage or MCP protocol
Version is defined in multiple places that must stay in sync: Each package's version is defined in multiple places that must stay in sync *for that package*:
- `cmd/nixpkgs-search/main.go` - **lab-monitoring**: `cmd/lab-monitoring/main.go` + `internal/mcp/server.go` (`DefaultMonitoringConfig`)
- `cmd/nixos-options/main.go` - **nixpkgs-search**: `cmd/nixpkgs-search/main.go` + `internal/mcp/server.go` (`DefaultNixOSConfig`, `DefaultNixpkgsPackagesConfig`)
- `cmd/hm-options/main.go` - **nixos-options**: `cmd/nixos-options/main.go` + `internal/mcp/server.go` (`DefaultNixOSConfig`)
- `cmd/lab-monitoring/main.go` - **hm-options**: `cmd/hm-options/main.go` + `internal/mcp/server.go` (`DefaultHomeManagerConfig`)
- `internal/mcp/server.go` (in `DefaultNixOSConfig`, `DefaultHomeManagerConfig`, `DefaultNixpkgsPackagesConfig`, `DefaultMonitoringConfig`) - **nix/package.nix**: Shared across all packages (bump to highest version when any package changes)
- `nix/package.nix`
### User Preferences ### User Preferences
- User prefers PostgreSQL over SQLite (has homelab infrastructure) - User prefers PostgreSQL over SQLite (has homelab infrastructure)

View File

@@ -244,9 +244,10 @@ hm-options get programs.git.enable
**Lab Monitoring CLI:** **Lab Monitoring CLI:**
```bash ```bash
# List alerts # List alerts (defaults to active only)
lab-monitoring alerts lab-monitoring alerts
lab-monitoring alerts --state active lab-monitoring alerts --all # Include silenced/inhibited alerts
lab-monitoring alerts --state all # Same as --all
lab-monitoring alerts --severity critical lab-monitoring alerts --severity critical
# Execute PromQL queries # Execute PromQL queries
@@ -341,7 +342,7 @@ hm-options -d "sqlite://my.db" index hm-unstable
| Tool | Description | | Tool | Description |
|------|-------------| |------|-------------|
| `list_alerts` | List alerts with optional filters (state, severity, receiver) | | `list_alerts` | List alerts with optional filters (state, severity, receiver). Defaults to active alerts only; use state=all to include silenced/inhibited |
| `get_alert` | Get full details for a specific alert by fingerprint | | `get_alert` | Get full details for a specific alert by fingerprint |
| `search_metrics` | Search metric names with substring filter, enriched with metadata | | `search_metrics` | Search metric names with substring filter, enriched with metadata |
| `get_metric_metadata` | Get type, help text, and unit for a specific metric | | `get_metric_metadata` | Get type, help text, and unit for a specific metric |

View File

@@ -20,7 +20,7 @@ import (
const ( const (
defaultDatabase = "sqlite://hm-options.db" defaultDatabase = "sqlite://hm-options.db"
version = "0.2.1" version = "0.3.0"
) )
func main() { func main() {

View File

@@ -15,7 +15,7 @@ import (
"git.t-juice.club/torjus/labmcp/internal/monitoring" "git.t-juice.club/torjus/labmcp/internal/monitoring"
) )
const version = "0.2.0" const version = "0.3.0"
func main() { func main() {
app := &cli.App{ app := &cli.App{
@@ -113,12 +113,16 @@ func alertsCommand() *cli.Command {
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "state", Name: "state",
Usage: "Filter by state: active, suppressed, unprocessed", Usage: "Filter by state: active (default), suppressed, unprocessed, all",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "severity", Name: "severity",
Usage: "Filter by severity label", Usage: "Filter by severity label",
}, },
&cli.BoolFlag{
Name: "all",
Usage: "Show all alerts including silenced and inhibited (shorthand for --state all)",
},
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return runAlerts(c) return runAlerts(c)
@@ -226,9 +230,16 @@ func runAlerts(c *cli.Context) error {
am := monitoring.NewAlertmanagerClient(c.String("alertmanager-url")) am := monitoring.NewAlertmanagerClient(c.String("alertmanager-url"))
filters := monitoring.AlertFilters{} filters := monitoring.AlertFilters{}
if state := c.String("state"); state != "" {
// Determine state filter: --all flag takes precedence, then --state, then default to active
state := c.String("state")
if c.Bool("all") {
state = "all"
}
switch state { switch state {
case "active": case "active", "":
// Default to active alerts only (non-silenced, non-inhibited)
active := true active := true
filters.Active = &active filters.Active = &active
silenced := false silenced := false
@@ -241,8 +252,10 @@ func runAlerts(c *cli.Context) error {
case "unprocessed": case "unprocessed":
unprocessed := true unprocessed := true
filters.Unprocessed = &unprocessed filters.Unprocessed = &unprocessed
case "all":
// No filters - return everything
} }
}
if severity := c.String("severity"); severity != "" { if severity := c.String("severity"); severity != "" {
filters.Filter = append(filters.Filter, fmt.Sprintf(`severity="%s"`, severity)) filters.Filter = append(filters.Filter, fmt.Sprintf(`severity="%s"`, severity))
} }

View File

@@ -19,7 +19,7 @@ import (
const ( const (
defaultDatabase = "sqlite://nixos-options.db" defaultDatabase = "sqlite://nixos-options.db"
version = "0.2.1" version = "0.3.0"
) )
func main() { func main() {

View File

@@ -20,7 +20,7 @@ import (
const ( const (
defaultDatabase = "sqlite://nixpkgs-search.db" defaultDatabase = "sqlite://nixpkgs-search.db"
version = "0.2.1" version = "0.3.0"
) )
func main() { func main() {

View File

@@ -45,7 +45,7 @@ type ServerConfig struct {
func DefaultNixOSConfig() ServerConfig { func DefaultNixOSConfig() ServerConfig {
return ServerConfig{ return ServerConfig{
Name: "nixos-options", Name: "nixos-options",
Version: "0.2.1", Version: "0.3.0",
DefaultChannel: "nixos-stable", DefaultChannel: "nixos-stable",
SourceName: "nixpkgs", SourceName: "nixpkgs",
Mode: ModeOptions, Mode: ModeOptions,
@@ -65,7 +65,7 @@ This ensures option documentation matches the nixpkgs version the project actual
func DefaultNixpkgsPackagesConfig() ServerConfig { func DefaultNixpkgsPackagesConfig() ServerConfig {
return ServerConfig{ return ServerConfig{
Name: "nixpkgs-packages", Name: "nixpkgs-packages",
Version: "0.2.1", Version: "0.3.0",
DefaultChannel: "nixos-stable", DefaultChannel: "nixos-stable",
SourceName: "nixpkgs", SourceName: "nixpkgs",
Mode: ModePackages, Mode: ModePackages,
@@ -83,7 +83,7 @@ This ensures package information matches the nixpkgs version the project actuall
func DefaultMonitoringConfig() ServerConfig { func DefaultMonitoringConfig() ServerConfig {
return ServerConfig{ return ServerConfig{
Name: "lab-monitoring", Name: "lab-monitoring",
Version: "0.2.0", Version: "0.3.0",
Mode: ModeCustom, Mode: ModeCustom,
Instructions: `Lab Monitoring MCP Server - Query Prometheus metrics and Alertmanager alerts. Instructions: `Lab Monitoring MCP Server - Query Prometheus metrics and Alertmanager alerts.
@@ -102,7 +102,7 @@ All queries are executed against live Prometheus, Alertmanager, and Loki HTTP AP
func DefaultHomeManagerConfig() ServerConfig { func DefaultHomeManagerConfig() ServerConfig {
return ServerConfig{ return ServerConfig{
Name: "hm-options", Name: "hm-options",
Version: "0.2.1", Version: "0.3.0",
DefaultChannel: "hm-stable", DefaultChannel: "hm-stable",
SourceName: "home-manager", SourceName: "home-manager",
Mode: ModeOptions, Mode: ModeOptions,

View File

@@ -91,8 +91,8 @@ func listAlertsTool() mcp.Tool {
Properties: map[string]mcp.Property{ Properties: map[string]mcp.Property{
"state": { "state": {
Type: "string", Type: "string",
Description: "Filter by alert state: 'active', 'suppressed', or 'unprocessed'", Description: "Filter by alert state: 'active', 'suppressed', 'unprocessed', or 'all' (default: active)",
Enum: []string{"active", "suppressed", "unprocessed"}, Enum: []string{"active", "suppressed", "unprocessed", "all"},
}, },
"severity": { "severity": {
Type: "string", Type: "string",
@@ -236,9 +236,10 @@ func makeListAlertsHandler(am *AlertmanagerClient) mcp.ToolHandler {
return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) { return func(ctx context.Context, args map[string]interface{}) (mcp.CallToolResult, error) {
filters := AlertFilters{} filters := AlertFilters{}
if state, ok := args["state"].(string); ok && state != "" { state, _ := args["state"].(string)
switch state { switch state {
case "active": case "active", "":
// Default to active alerts only (non-silenced, non-inhibited)
active := true active := true
filters.Active = &active filters.Active = &active
silenced := false silenced := false
@@ -251,7 +252,8 @@ func makeListAlertsHandler(am *AlertmanagerClient) mcp.ToolHandler {
case "unprocessed": case "unprocessed":
unprocessed := true unprocessed := true
filters.Unprocessed = &unprocessed filters.Unprocessed = &unprocessed
} case "all":
// No filters - return everything
} }
if severity, ok := args["severity"].(string); ok && severity != "" { if severity, ok := args["severity"].(string); ok && severity != "" {

View File

@@ -81,6 +81,92 @@ func TestHandler_ListAlerts(t *testing.T) {
} }
} }
func TestHandler_ListAlertsDefaultsToActive(t *testing.T) {
// Test that list_alerts with no state param defaults to active filters
server, cleanup := setupTestServer(t,
nil,
func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
// Default should apply active filters
if q.Get("active") != "true" {
t.Errorf("expected default active=true, got %s", q.Get("active"))
}
if q.Get("silenced") != "false" {
t.Errorf("expected default silenced=false, got %s", q.Get("silenced"))
}
if q.Get("inhibited") != "false" {
t.Errorf("expected default inhibited=false, got %s", q.Get("inhibited"))
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[]`))
},
)
defer cleanup()
result := callTool(t, server, "list_alerts", map[string]interface{}{})
if result.IsError {
t.Fatalf("unexpected error: %s", result.Content[0].Text)
}
}
func TestHandler_ListAlertsStateAll(t *testing.T) {
// Test that list_alerts with state=all applies no filters
server, cleanup := setupTestServer(t,
nil,
func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
// state=all should not set any filter params
if q.Get("active") != "" {
t.Errorf("expected no active param for state=all, got %s", q.Get("active"))
}
if q.Get("silenced") != "" {
t.Errorf("expected no silenced param for state=all, got %s", q.Get("silenced"))
}
if q.Get("inhibited") != "" {
t.Errorf("expected no inhibited param for state=all, got %s", q.Get("inhibited"))
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[
{
"annotations": {},
"endsAt": "2024-01-01T01:00:00Z",
"fingerprint": "fp1",
"receivers": [{"name": "default"}],
"startsAt": "2024-01-01T00:00:00Z",
"status": {"inhibitedBy": [], "silencedBy": [], "state": "active"},
"updatedAt": "2024-01-01T00:00:00Z",
"generatorURL": "",
"labels": {"alertname": "ActiveAlert", "severity": "critical"}
},
{
"annotations": {},
"endsAt": "2024-01-01T01:00:00Z",
"fingerprint": "fp2",
"receivers": [{"name": "default"}],
"startsAt": "2024-01-01T00:00:00Z",
"status": {"inhibitedBy": [], "silencedBy": ["s1"], "state": "suppressed"},
"updatedAt": "2024-01-01T00:00:00Z",
"generatorURL": "",
"labels": {"alertname": "SilencedAlert", "severity": "warning"}
}
]`))
},
)
defer cleanup()
result := callTool(t, server, "list_alerts", map[string]interface{}{
"state": "all",
})
if result.IsError {
t.Fatalf("unexpected error: %s", result.Content[0].Text)
}
if !strings.Contains(result.Content[0].Text, "2 alert") {
t.Errorf("expected output to contain '2 alert', got: %s", result.Content[0].Text)
}
}
func TestHandler_GetAlert(t *testing.T) { func TestHandler_GetAlert(t *testing.T) {
server, cleanup := setupTestServer(t, server, cleanup := setupTestServer(t,
nil, nil,

View File

@@ -7,7 +7,7 @@
buildGoModule { buildGoModule {
inherit pname src; inherit pname src;
version = "0.2.1"; version = "0.3.0";
vendorHash = "sha256-D0KIxQC9ctIAaHBFTvkhBE06uOZwDUcIw8471Ug2doY="; vendorHash = "sha256-D0KIxQC9ctIAaHBFTvkhBE06uOZwDUcIw8471Ug2doY=";