0.24.0 — MCP tool improvements: type filter for get_relations, health filter for get_health_summary, live HTTP probe for ping_service
get_relations now accepts an optional 'types' (comma-separated) parameter to filter relationship types — filters out the noisy exec/targets edges that previously drowned useful host/provides edges. get_health_summary now accepts an optional 'health' (comma-separated) parameter to return only entities in specific health states (e.g. 'health=down,stale') instead of the full 100+ entity list. ping_service now: - Falls back to e.attributes->>'public_host' when 'url' is not set (covers LXCs that only have public_host in the graph) - Performs a live HTTP HEAD probe against the resolved URL, returning the actual status code instead of just the scheduler's stale health state Also: fixed matrix.hubris.network DNS record (was pointing to dead VPS), pruned 6 dead graph edges, wired url attributes on 7 LXCs, added VPS HTTP monitoring check, and resolved the 18k-occurrence unmonitored signal. This session's audit is documented as document:nomos/2026-08-05-dns-monitoring-improvements-for-strong-hosted-services.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -63,18 +64,30 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
||||
nStr(args["type"]), nStr(args["state"]), nStr(args["q"]), limit), "entity_table"), nil
|
||||
}},
|
||||
|
||||
{tool: &mcp.Tool{Name: "get_relations", Description: "List inbound/outbound edges for one entity",
|
||||
InputSchema: objSchema(prop{"entity_id", "string", "Entity slug"}),
|
||||
{tool: &mcp.Tool{Name: "get_relations", Description: "List inbound/outbound edges for one entity, optionally filtered by relationship type",
|
||||
InputSchema: objSchema(
|
||||
prop{"entity_id", "string", "Entity slug"},
|
||||
prop{"types", "string", "Comma-separated relationship types to include (e.g. 'hosts,provides,depends-on'). Omit for all."},
|
||||
),
|
||||
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := argsMap(req)
|
||||
slug, _ := args["entity_id"].(string)
|
||||
return queryRows(ctx, pool, `
|
||||
typesStr, _ := args["types"].(string)
|
||||
if slug == "" {
|
||||
return textResult("entity_id is required"), nil
|
||||
}
|
||||
query := `
|
||||
SELECT r.type, src.slug AS source, tgt.slug AS target
|
||||
FROM relationships r
|
||||
JOIN entities src ON src.id = r.source_id
|
||||
JOIN entities tgt ON tgt.id = r.target_id
|
||||
WHERE (src.slug = $1 OR tgt.slug = $1) AND r.valid_to IS NULL
|
||||
ORDER BY r.type`, slug), nil
|
||||
WHERE (src.slug = $1 OR tgt.slug = $1) AND r.valid_to IS NULL`
|
||||
if typesStr != "" {
|
||||
query += ` AND r.type = ANY(string_to_array($2, ','))`
|
||||
return queryRows(ctx, pool, query, slug, typesStr), nil
|
||||
}
|
||||
query += ` ORDER BY r.type`
|
||||
return queryRows(ctx, pool, query, slug), nil
|
||||
}},
|
||||
|
||||
{tool: &mcp.Tool{Name: "get_blast_radius", Description: "Find entities affected if this entity goes down",
|
||||
@@ -90,14 +103,23 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
||||
slug, depth), nil
|
||||
}},
|
||||
|
||||
{tool: &mcp.Tool{Name: "get_health_summary", Description: "Full fleet health per entity — healthy/degraded/down/unknown",
|
||||
InputSchema: objSchema(),
|
||||
{tool: &mcp.Tool{Name: "get_health_summary", Description: "Fleet health per entity — optionally filter by health state(s)",
|
||||
InputSchema: objSchema(
|
||||
prop{"health", "string", "Comma-separated health states to include (e.g. 'down,stale'). Omit for all."},
|
||||
),
|
||||
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return queryRows(ctx, pool, `
|
||||
args := argsMap(req)
|
||||
healthStr, _ := args["health"].(string)
|
||||
query := `
|
||||
SELECT e.slug, e.type, st.health, st.last_check_at
|
||||
FROM entity_status st JOIN entities e ON e.id = st.entity_id
|
||||
WHERE e.type <> 'check'
|
||||
ORDER BY e.slug`), nil
|
||||
WHERE e.type <> 'check'`
|
||||
if healthStr != "" {
|
||||
query += ` AND st.health = ANY(string_to_array($1, ','))`
|
||||
return queryRows(ctx, pool, query, healthStr), nil
|
||||
}
|
||||
query += ` ORDER BY e.slug`
|
||||
return queryRows(ctx, pool, query), nil
|
||||
}},
|
||||
|
||||
{tool: &mcp.Tool{Name: "get_audit_trail", Description: "Query the audit log",
|
||||
@@ -726,7 +748,7 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
||||
(e.attributes->>'pve_id')::int`, statePtr), "lxc_list"), nil
|
||||
}},
|
||||
|
||||
{tool: &mcp.Tool{Name: "ping_service", Description: "Check if a service is reachable via HTTP",
|
||||
{tool: &mcp.Tool{Name: "ping_service", Description: "Check if a service is reachable via HTTP — returns scheduler health state plus a live HTTP probe",
|
||||
InputSchema: objSchema(prop{"service_slug", "string", "Service entity slug"}),
|
||||
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := argsMap(req)
|
||||
@@ -735,7 +757,13 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
||||
return textResult("service_slug is required"), nil
|
||||
}
|
||||
rows, err := pool.Query(ctx, `
|
||||
SELECT st.health, st.last_check_at, e.attributes->>'url' AS url
|
||||
SELECT st.health, st.last_check_at,
|
||||
COALESCE(
|
||||
e.attributes->>'url',
|
||||
CASE WHEN e.attributes->>'public_host' IS NOT NULL
|
||||
THEN 'https://' || e.attributes->>'public_host'
|
||||
END
|
||||
) AS url
|
||||
FROM entity_status st
|
||||
JOIN entities e ON e.id = st.entity_id
|
||||
WHERE e.slug = $1`, slug)
|
||||
@@ -749,9 +777,17 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
||||
var health, lastCheck, url string
|
||||
rows.Scan(&health, &lastCheck, &url)
|
||||
if url == "" {
|
||||
url = "(no URL in entity attributes)"
|
||||
return textResult(fmt.Sprintf("health=%s last_check=%s url=no-url (entity has no url or public_host attribute)", health, lastCheck)), nil
|
||||
}
|
||||
return textResult(fmt.Sprintf("health=%s last_check=%s url=%s", health, lastCheck, url)), nil
|
||||
// Live HTTP probe — HEAD request to check current state
|
||||
code := "n/a"
|
||||
if resp, err := http.Head(url); err == nil {
|
||||
resp.Body.Close()
|
||||
code = fmt.Sprintf("%d", resp.StatusCode)
|
||||
} else {
|
||||
code = fmt.Sprintf("err: %v", err)
|
||||
}
|
||||
return textResult(fmt.Sprintf("health=%s last_check=%s url=%s http=%s", health, lastCheck, url, code)), nil
|
||||
}},
|
||||
|
||||
{tool: &mcp.Tool{Name: "tail_log", Description: "Get recent log lines from a service via journalctl",
|
||||
|
||||
Reference in New Issue
Block a user