fix(mcp): get_anomaly shows Steward's own restarts beside every anomaly, and a router's containers (#5862)

All 16 Traefik 4xx anomalies on 2026-10-15 came back with nothing
around them, though 15 were Steward's own restarts. A router has no
host, and every events source was read by host.

- Core: each break of more than 90 s in Steward's once-a-minute health
  sample is a "steward" event beside every anomaly, placed or not.
- Plugins: an optional mcp_resource_events(db, source, resource, since,
  until) for a resource with no host. Traefik's gives the container
  events of the swarm service behind a router, on any host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 15:58:28 -04:00
co-authored by Claude Opus 5.5
parent a24b9da69d
commit 91bb199d14
8 changed files with 139 additions and 10 deletions
+10 -1
View File
@@ -82,6 +82,9 @@ class StewardMCPServer(MCPServer):
# plugin name -> its `mcp_events(db, host_id, host_name, since, until)`:
# what it knows happened on a host, for get_anomaly.
self.event_sources: dict[str, object] = {}
# plugin name -> its `mcp_resource_events(db, source, resource, since,
# until)`: what it knows happened to a resource with no host.
self.resource_event_sources: dict[str, object] = {}
# ── registration ─────────────────────────────────────────────────────────
def register_tool(self, fn, *, role: UserRole, read_only: bool, plugin: str | None = None) -> None:
@@ -108,7 +111,9 @@ class StewardMCPServer(MCPServer):
"""Add a plugin's tools via its optional `register_mcp_tools(register)`,
its optional `mcp_overview(db)` as a get_overview findings source, and
its optional `mcp_events(db, host_id, host_name, since, until)` as a
source of what happened on a host (get_anomaly).
source of what happened on a host (get_anomaly), and its optional
`mcp_resource_events(db, source, resource, since, until)` for what
happened to a resource with no host.
Idempotent per plugin (hot-reload drops its previous tools first) and
tolerant: a raising hook is logged and leaves the plugin with no tools,
@@ -123,6 +128,10 @@ class StewardMCPServer(MCPServer):
events = getattr(module, "mcp_events", None)
if callable(events):
self.event_sources[name] = events
self.resource_event_sources.pop(name, None)
resource_events = getattr(module, "mcp_resource_events", None)
if callable(resource_events):
self.resource_event_sources[name] = resource_events
hook = getattr(module, "register_mcp_tools", None)
if hook is None:
return