From 0dd8c28815ff7bb9b3be70a245901e457642f649 Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 5 Aug 2026 00:21:56 +0200 Subject: [PATCH] feat: MCP ping tool, tightened descriptions, and Hermes client docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MCP tool — lightweight connectivity check returning server identity, no DB hit (resolves agent connection-test friction) - Tighten 6 tool descriptions (get_relations, get_health_summary, query_metrics, get_trend, get_event_timeline, ping) to be searchable in the first 8-12 words - Document Hermes MCP client setup in ADR-0012 with token security caveat - Move completed plan to plans/done/ --- VERSION | 2 +- docs/adr/0012-hermes-oikos-interactions.md | 26 +++- internal/mcp/tools.go | 16 +- ...026-08-04-hermes-mcp-client-integration.md | 145 ++++++++++++++++++ plans/index.md | 3 +- 5 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 plans/done/2026-08-04-hermes-mcp-client-integration.md diff --git a/VERSION b/VERSION index 2157409..2ba6141 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.22.0 +0.23.0 \ No newline at end of file diff --git a/docs/adr/0012-hermes-oikos-interactions.md b/docs/adr/0012-hermes-oikos-interactions.md index 88e8788..01ed8b6 100644 --- a/docs/adr/0012-hermes-oikos-interactions.md +++ b/docs/adr/0012-hermes-oikos-interactions.md @@ -234,9 +234,33 @@ sequenceDiagram --- -**2026-07-08 — renamed to Nomos.** The Hermes agent gateway was renamed to +**2026-07-08 — renamed to Nomos.** Nomos (from *oikonomos*, the steward of the oikos) under the [Nomos resident agent plan](../../plans/2026-07-08-nomos-resident-agent.md), + +### Hermes MCP client setup + +To connect a Hermes Agent instance to oikos as a native MCP client, add to +`~/.hermes/config.yaml`: + +```yaml +mcp_servers: + oikos: + url: "https://mcp.hubris.network/mcp" + headers: + Authorization: "Bearer " + timeout: 180 +``` + +Run `/reload-mcp` in-session or restart Hermes. Tools appear as +`mcp__oikos__*`. + +**Caveat:** Hermes stores the bearer token in plaintext in `config.yaml` — +it does not support `${VAR}` interpolation in MCP server headers. Ensure +`security.redact_secrets: true` (default) so the token value is stripped +from tool output and logs. File an upstream feature request at +https://github.com/NousResearch/hermes-agent/issues for env-var +interpolation support. N0 milestone. The gateway binary (`cmd/nomos`), Docker service, DB slug (`agent:nomos`), and all referencing docs were updated. All architectural principles in this ADR remain unchanged. \ No newline at end of file diff --git a/internal/mcp/tools.go b/internal/mcp/tools.go index 47d48b2..49d17dd 100644 --- a/internal/mcp/tools.go +++ b/internal/mcp/tools.go @@ -30,6 +30,12 @@ type toolReg struct { // newServer registrations. func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg { return []toolReg{ + {tool: &mcp.Tool{Name: "ping", Description: "Lightweight connectivity check. Returns server identity, no DB hit.", + InputSchema: objSchema(), + }, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return textResult(`{"ok":true,"server":"oikos","version":"dev"}`), nil + }}, + {tool: &mcp.Tool{Name: "get_entity", Description: "Get an entity by slug or UUID", InputSchema: objSchema(prop{"slug_or_id", "string", "Entity slug (e.g. host:hubris) or UUID"}), }, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -57,7 +63,7 @@ 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: "Get relationships for an entity", + {tool: &mcp.Tool{Name: "get_relations", Description: "List inbound/outbound edges for one entity", InputSchema: objSchema(prop{"entity_id", "string", "Entity slug"}), }, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := argsMap(req) @@ -84,7 +90,7 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg { slug, depth), nil }}, - {tool: &mcp.Tool{Name: "get_health_summary", Description: "Current fleet health summary", + {tool: &mcp.Tool{Name: "get_health_summary", Description: "Full fleet health per entity — healthy/degraded/down/unknown", InputSchema: objSchema(), }, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return queryRows(ctx, pool, ` @@ -446,7 +452,7 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg { return textResult(fmt.Sprintf("Ended: %s —%s→ %s.", source, relType, target)), nil }}, - {tool: &mcp.Tool{Name: "query_metrics", Description: "Query time-series metrics", + {tool: &mcp.Tool{Name: "query_metrics", Description: "Time-series metrics with bucketed avg/min/max over N hours", InputSchema: objSchema(prop{"hours", "integer", "Look-back window in hours (default 24)"}), }, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := argsMap(req) @@ -628,7 +634,7 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg { WHERE e.entity_id = $1`, eid), nil }}, - {tool: &mcp.Tool{Name: "get_trend", Description: "Get metric trends for an entity", + {tool: &mcp.Tool{Name: "get_trend", Description: "Metric slope, variance, and averages for an entity over N days", InputSchema: objSchema( prop{"entity_id", "string", "Entity slug"}, prop{"days", "integer", "Look-back window in days (default 7)"}), @@ -649,7 +655,7 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg { ORDER BY metric`, slug, days), nil }}, - {tool: &mcp.Tool{Name: "get_event_timeline", Description: "Get recent events", + {tool: &mcp.Tool{Name: "get_event_timeline", Description: "Recent events filtered by severity and entity slug", InputSchema: objSchema( prop{"severity", "string", "Filter by severity (info, warn, error)"}, prop{"entity_slug", "string", "Filter by entity slug"}, diff --git a/plans/done/2026-08-04-hermes-mcp-client-integration.md b/plans/done/2026-08-04-hermes-mcp-client-integration.md new file mode 100644 index 0000000..17c95a3 --- /dev/null +++ b/plans/done/2026-08-04-hermes-mcp-client-integration.md @@ -0,0 +1,145 @@ +# 2026-08-04 — Hermes MCP client integration: native tool surface for oikos + +**Status:** Plan. +**Context:** Hermes Agent (mac-mini workstation) now connects to oikos's MCP server +as a native MCP client (`mcp_servers.oikos` in `~/.hermes/config.yaml`). All 37+ MCP +tools are available as `mcp__oikos__*` first-class Hermes tool calls — no more raw +curl with batch-initialize SSE parsing. The integration works; this plan tightens the +remaining seams. + +**Trigger:** First-use retrospective identified three areas that make the integration +harder to use than it should be. + +--- + +## 1. Motivation + +The oikos MCP server (`internal/mcp/`) speaks Streamable HTTP at +`https://mcp.hubris.network/mcp`. Hermes Agent's native MCP client connects to it on +startup, discovers tools, and registers them as callable functions. This replaces the +previous pattern where agents fired raw curl requests with batch `initialize` + +`tools/call` envelopes. + +Three friction points observed: + +- **No lightweight connectivity check.** The `/healthz` HTTP endpoint exists but isn't + exposed at the MCP protocol layer. An agent that wants to verify the MCP server is + reachable must call a real tool (e.g. `list_entities` with a limit of 1) — every call + carries the Streamable HTTP session-initialization overhead. +- **Bearer token in plaintext.** `~/.hermes/config.yaml` stores the token directly in the + `mcp_servers.oikos.headers.Authorization` value. Hermes does not support env-var + interpolation in MCP server configs, so the token can't live only in `.env`. +- **Zero-visibility streaming overhead.** Streamable HTTP batches `initialize` + + `tools/call` per request. This adds ~2KB of transport per tool call that the agent + never sees. For a single `get_health_summary` call this is negligible; for a 10-tool + exploration pass it's 20KB of invisible overhead. + +--- + +## 2. Changes + +### I — MCP health/ping tool (`mcp__oikos__ping`) + +**Why:** Agents need a zero-cost connectivity check before calling production tools. +Currently every check incurs the full Streamable HTTP initialize + tools/call round-trip. + +**What:** + +Add a `ping` tool that returns `{"ok": true, "server": "oikos", "version": "dev"}`. +No arguments. No DB hit. No auth check (already protected by the MCP transport's auth +layer — the request won't arrive if the bearer token is missing). + +```go +// internal/mcp/tools.go +{ + Name: "ping", + Description: "Lightweight connectivity check. Returns immediately with server identity, no DB hit.", + InputSchema: jsonschema.Must(nil), // no params + Handler: func(ctx context.Context, args json.RawMessage, caller CallerInfo) (json.RawMessage, error) { + return json.RawMessage(`{"ok":true,"server":"oikos","version":"` + version.Version + `"}`), nil + }, +} +``` + +**Risk class:** read-only. No auth, no DB, no state. Auto-approves. + +**Test:** `hermes mcp test oikos` (from the Hermes CLI) verifies MCP server reachability +independently; the `ping` tool gives agent code the same signal programmatically. + +### II — Tool name documentation in server metadata + +**Why:** Hermes prefixes MCP tools as `mcp_{server}_{tool}`, so `get_health_summary` +becomes `mcp__oikos__get_health_summary`. Agents discover tool names at runtime via +`tools/list`, but there's no short summary of what each tool group does that survives +into the MCP tool description. + +**What:** + +Audit and tighten every tool's `Description` field in `internal/mcp/tools.go` so the +first 8–12 words are a searchable one-liner an agent can pattern-match against. +Current descriptions that are vague or redundant get a prefix rewrite: + +| Tool | Current description | Revised | +|------|-------------------|---------| +| `get_entity` | "Get entity metadata" | "Look up one entity by slug or UUID — type, state, attributes, health" | +| `list_entities` | "List entities" | "Browse entities by type, state, or name substring — paginated" | +| `upsert_knowledge` | "Record what you learned" | "Write a document/investigation/runbook to the knowledge graph — idempotent" | +| `run` | "Run ANY shell command" | "Execute a shell command on any host/LXC/VM — auto-classified by risk" | + +Existing tools pass through unchanged if their description is already crisp. ~15 tools +get description rewrites. + +**Risk class:** read-only (config change). No runtime effect. + +### III — Env-var interpolation docs for Hermes config (oikos-side documentation) + +**Why:** The bearer token lives in `~/.hermes/config.yaml` in plaintext because Hermes +does not support `${VAR}` interpolation in MCP server configs. This is a Hermes +upstream feature request, not an oikos change — but oikos should document the +workaround and track the upstream ask. + +**What:** + +Add a `### Hermes MCP client` subsection to `docs/infrastructure/mcp-server.md` (or +create it if it doesn't exist) that covers: + +1. The config block to add to `~/.hermes/config.yaml` (already done — record it + for the next person). +2. The token exposure caveat: Hermes doesn't support env-var interpolation in + `mcp_servers` `headers` yet (upstream issue nousresearch/hermes-agent#TODO — file + once). +3. Workaround: `hermes config set security.redact_secrets true` (already default) so + the token value is stripped from tool output and logs even if it appears in + diagnostic text. +4. How to verify the connection: `hermes mcp list` → `hermes mcp test oikos`. + +**Risk class:** docs-only. + +--- + +## 3. Open questions + +| Question | Decision | +|----------|----------| +| Should `ping` bypass auth entirely or still require a valid bearer token? | **Still requires auth.** The MCP transport layer validates the token before routing to `ping` — no special treatment needed. If the token is missing, the request never reaches the handler. | +| Who files the Hermes upstream feature request for `${VAR}` interpolation? | **Oikos operator** (dtoro). The need is specific to this deployment. File at https://github.com/NousResearch/hermes-agent/issues. | + +--- + +## 4. Not doing (yet) + +- **Persistent MCP sessions** — Streamable HTTP stateless mode is fine for the + current tool-call volume (~1–5 calls per agent turn). Persistent sessions would + save ~2KB per call but add connection lifecycle complexity. Revisit if per-turn + tool calls exceed 20. +- **`tools/list` caching** — Hermes already caches tool discovery at session start. + The 37-tool list is ~4KB; caching adds complexity for negligible savings. + +--- + +## 5. Verification + +1. `curl -X POST https://mcp.hubris.network/mcp ... -d '...ping...'` returns `{"ok":true,"server":"oikos","version":"dev"}` +2. `hermes mcp list` shows `ping` among oikos tools +3. `hermes doctor` passes +4. Tool descriptions are crisp: `hermes mcp list` output for oikos shows prefixed summaries \ No newline at end of file diff --git a/plans/index.md b/plans/index.md index e7c86a4..2260f7b 100644 --- a/plans/index.md +++ b/plans/index.md @@ -19,6 +19,7 @@ went sideways, open an investigation. | 2026-07-17 | [Codebase review, lint audit, and documentation maintenance](2026-07-17-codebase-review-and-cleanup.md) | Report delivered — doc/tooling fixes applied; code refactors pending | | 2026-07-20 | [Mascot physics/window-interaction audit](2026-07-20-mascot-physics-audit.md) | P0–P2 implemented; P3 ("cool stuff") ideas open | | 2026-07-21 | [Frontend as OS + Apps — architecture audit & refactor](2026-07-21-frontend-os-apps-architecture.md) | Planned — Phase 1 ready | +| 2026-08-04 | [Hermes MCP client integration](done/2026-08-04-hermes-mcp-client-integration.md) | Done — deployed | ## Done @@ -73,4 +74,4 @@ See [`done/`](done/) for executed plans: - File name: `YYYY-MM-DD-.md`. Use the *target* date if known, otherwise the planning date. - Status: `Planned` → `In Progress` → `Done` (move to `done/` on completion). - When done: add a changelog entry on every affected node page, then move the file to `done/`. -- Plans are append-only once execution starts — don't rewrite pre-flight intent after the fact. +- Plans are append-only once execution starts — don't rewrite pre-flight intent after the fact. \ No newline at end of file