feat: MCP ping tool, tightened descriptions, and Hermes client docs
- 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/
This commit is contained in:
@@ -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 (from *oikonomos*, the steward of the oikos) under the
|
||||||
[Nomos resident agent plan](../../plans/2026-07-08-nomos-resident-agent.md),
|
[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 <OIKOS_MCP_BEARER_TOKEN>"
|
||||||
|
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
|
N0 milestone. The gateway binary (`cmd/nomos`), Docker service, DB slug
|
||||||
(`agent:nomos`), and all referencing docs were updated. All architectural
|
(`agent:nomos`), and all referencing docs were updated. All architectural
|
||||||
principles in this ADR remain unchanged.
|
principles in this ADR remain unchanged.
|
||||||
@@ -30,6 +30,12 @@ type toolReg struct {
|
|||||||
// newServer registrations.
|
// newServer registrations.
|
||||||
func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
||||||
return []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",
|
{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"}),
|
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) {
|
}, 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
|
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"}),
|
InputSchema: objSchema(prop{"entity_id", "string", "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)
|
||||||
@@ -84,7 +90,7 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
|||||||
slug, depth), nil
|
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(),
|
InputSchema: objSchema(),
|
||||||
}, 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, `
|
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
|
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)"}),
|
InputSchema: objSchema(prop{"hours", "integer", "Look-back window in hours (default 24)"}),
|
||||||
}, 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)
|
||||||
@@ -628,7 +634,7 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
|||||||
WHERE e.entity_id = $1`, eid), nil
|
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(
|
InputSchema: objSchema(
|
||||||
prop{"entity_id", "string", "Entity slug"},
|
prop{"entity_id", "string", "Entity slug"},
|
||||||
prop{"days", "integer", "Look-back window in days (default 7)"}),
|
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
|
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(
|
InputSchema: objSchema(
|
||||||
prop{"severity", "string", "Filter by severity (info, warn, error)"},
|
prop{"severity", "string", "Filter by severity (info, warn, error)"},
|
||||||
prop{"entity_slug", "string", "Filter by entity slug"},
|
prop{"entity_slug", "string", "Filter by entity slug"},
|
||||||
|
|||||||
145
plans/done/2026-08-04-hermes-mcp-client-integration.md
Normal file
145
plans/done/2026-08-04-hermes-mcp-client-integration.md
Normal file
@@ -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
|
||||||
@@ -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-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-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-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
|
## Done
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user