diff --git a/server/internal/mcp/tools_fleet.go b/server/internal/mcp/tools_fleet.go index dc83d0b..e513bb3 100644 --- a/server/internal/mcp/tools_fleet.go +++ b/server/internal/mcp/tools_fleet.go @@ -20,14 +20,14 @@ type serverSummary struct { } // models.Server has no Online bool: it stores Status as one of "pending", -// "online" or "offline" (see internal/services/servers.go). Online here +// "active" or "offline" (see internal/services/servers.go). Online here // mirrors that string the same way the REST layer treats it. func summariseServer(s models.Server) serverSummary { return serverSummary{ ID: s.ServerID, Hostname: s.Hostname, OS: s.OSInfo, - Online: s.Status == "online", + Online: s.Status == "active", Tags: s.Tags, } } diff --git a/server/internal/mcp/tools_test.go b/server/internal/mcp/tools_test.go index c18f32d..b22a97a 100644 --- a/server/internal/mcp/tools_test.go +++ b/server/internal/mcp/tools_test.go @@ -58,3 +58,21 @@ func TestServerSummaryStaysSmall(t *testing.T) { t.Errorf("30 servers serialise to %d bytes, want at most 8000", len(out)) } } + +// The real status vocabulary is "pending" / "active" / "offline" (see +// internal/services/servers.go) — "online" is never assigned anywhere. A +// server carrying the live status ("active") must project as Online: true, +// or list_servers/get_server misreport the entire fleet as down. +func TestSummariseServerReportsActiveAsOnline(t *testing.T) { + active := summariseServer(models.Server{ServerID: "srv-active", Status: "active"}) + if !active.Online { + t.Errorf("server with status %q should be online, got Online=false", "active") + } + + for _, status := range []string{"pending", "offline"} { + s := summariseServer(models.Server{ServerID: "srv-" + status, Status: status}) + if s.Online { + t.Errorf("server with status %q should not be online, got Online=true", status) + } + } +} diff --git a/server/internal/mcp/tools_work.go b/server/internal/mcp/tools_work.go index 575a26d..0507181 100644 --- a/server/internal/mcp/tools_work.go +++ b/server/internal/mcp/tools_work.go @@ -133,7 +133,8 @@ type auditEventSummary struct { type listAuditResult struct { Events []auditEventSummary `json:"events"` - Total int `json:"shown"` + Total int64 `json:"total"` + Shown int `json:"shown"` } // ---- secrets ---- @@ -433,14 +434,22 @@ func init() { Name: "search_fleet", Scope: "vulns:read", Description: "Search every server's installed packages by name across the whole fleet — " + - "answers questions like \"which hosts still run OpenSSL 1.1\". Pass version_below to " + - "further narrow to versions that sort earlier than the given string.", + "answers questions like \"which hosts still run OpenSSL 1.1\". version_below is not " + + "currently supported: filtering package versions correctly requires knowing each " + + "distribution's own version-ordering scheme (dpkg/rpm/apk), which this tool cannot " + + "determine, so it refuses rather than guess with a lexicographic comparison. Every " + + "matching install is returned; compare versions yourself.", Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) { name := stringArg(args, "name") if name == "" { return nil, fmt.Errorf("name is required") } - versionBelow := stringArg(args, "version_below") + if stringArg(args, "version_below") != "" { + return nil, fmt.Errorf("version_below is not supported: correct version ordering " + + "depends on each host's distribution (dpkg/rpm/apk each order differently), " + + "which this tool cannot resolve here — omit version_below and every matching " + + "install is returned instead") + } hits, err := services.SearchPackages(c.InstanceID, name) if err != nil { @@ -453,9 +462,6 @@ func init() { if len(out) == limit { break } - if versionBelow != "" && h.Version >= versionBelow { - continue - } // A hit's server must be resolved through the token's own // scope: SearchPackages runs unscoped across the instance, // so a server outside the token's tag restriction is @@ -477,7 +483,7 @@ func init() { "Filter by event_type prefix (e.g. \"workflow\", \"key\", \"server\").", Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) { limit := int64(pageLimit(args)) - events, _, err := services.ListAuditEvents(c.InstanceID, services.AuditFilter{ + events, total, err := services.ListAuditEvents(c.InstanceID, services.AuditFilter{ Category: stringArg(args, "event_type"), Limit: limit, }) @@ -488,7 +494,7 @@ func init() { for _, e := range events { out = append(out, auditEventSummary{At: e.CreatedAt, Type: e.EventType, Actor: e.Actor, Detail: e.Details}) } - return listAuditResult{Events: out, Total: len(out)}, nil + return listAuditResult{Events: out, Total: total, Shown: len(out)}, nil }, })