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"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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
|
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",
|
{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"}),
|
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) {
|
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
args := argsMap(req)
|
args := argsMap(req)
|
||||||
slug, _ := args["entity_id"].(string)
|
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
|
SELECT r.type, src.slug AS source, tgt.slug AS target
|
||||||
FROM relationships r
|
FROM relationships r
|
||||||
JOIN entities src ON src.id = r.source_id
|
JOIN entities src ON src.id = r.source_id
|
||||||
JOIN entities tgt ON tgt.id = r.target_id
|
JOIN entities tgt ON tgt.id = r.target_id
|
||||||
WHERE (src.slug = $1 OR tgt.slug = $1) AND r.valid_to IS NULL
|
WHERE (src.slug = $1 OR tgt.slug = $1) AND r.valid_to IS NULL`
|
||||||
ORDER BY r.type`, slug), nil
|
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",
|
{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
|
slug, depth), nil
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{tool: &mcp.Tool{Name: "get_health_summary", Description: "Full fleet health per entity — healthy/degraded/down/unknown",
|
{tool: &mcp.Tool{Name: "get_health_summary", Description: "Fleet health per entity — optionally filter by health state(s)",
|
||||||
InputSchema: objSchema(),
|
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) {
|
}, 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
|
SELECT e.slug, e.type, st.health, st.last_check_at
|
||||||
FROM entity_status st JOIN entities e ON e.id = st.entity_id
|
FROM entity_status st JOIN entities e ON e.id = st.entity_id
|
||||||
WHERE e.type <> 'check'
|
WHERE e.type <> 'check'`
|
||||||
ORDER BY e.slug`), nil
|
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",
|
{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
|
(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"}),
|
InputSchema: objSchema(prop{"service_slug", "string", "Service entity slug"}),
|
||||||
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
args := argsMap(req)
|
args := argsMap(req)
|
||||||
@@ -735,7 +757,13 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
|||||||
return textResult("service_slug is required"), nil
|
return textResult("service_slug is required"), nil
|
||||||
}
|
}
|
||||||
rows, err := pool.Query(ctx, `
|
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
|
FROM entity_status st
|
||||||
JOIN entities e ON e.id = st.entity_id
|
JOIN entities e ON e.id = st.entity_id
|
||||||
WHERE e.slug = $1`, slug)
|
WHERE e.slug = $1`, slug)
|
||||||
@@ -749,9 +777,17 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
|||||||
var health, lastCheck, url string
|
var health, lastCheck, url string
|
||||||
rows.Scan(&health, &lastCheck, &url)
|
rows.Scan(&health, &lastCheck, &url)
|
||||||
if 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",
|
{tool: &mcp.Tool{Name: "tail_log", Description: "Get recent log lines from a service via journalctl",
|
||||||
|
|||||||
Reference in New Issue
Block a user