Eco MCP battery: node-local privileged surface for warded agents to inspect on-disk server state #42

Open
opened 2026-07-03 19:40:19 +00:00 by coilyco-ops · 1 comment
Member

Goal

Let warded directors and engineers inspect the state of the local Eco game server on disk. Rather than bind-mount the live EcoServer/ tree into every warded container (heavy, and leaks Discord/Chronicler/API tokens sitting in Configs/), expose the state over a network MCP surface the agents fetch. This is the inside-out counterpart to eco-app's existing outside-in MCP.

Two views, this is the second

  • Outside-in (exists) - eco-app's current MCP: hits Eco's public /info endpoint, redacts player names, serves anyone. Live runtime, network-visible.
  • Inside-out (this issue) - a node-local MCP reading what only a process on kai-server can see: raw Configs/, Storage/ saves + Backup/ (×40), the Chronicler SQLite history, per-subsystem Logs/, unredacted admin data, mod install state, and the loopback-only admin endpoints. Privileged, at-rest + admin, warded-agents-only.

Home

Fold into coilyco-gaming/eco-app as a second, privileged MCP surface (distinct deployment from the public one). Decided.

Where it runs - node-local k3s pattern

The Eco server is a native systemd service (eco-server.service) at /home/kai/Steam/steamapps/common/EcoServer/, NOT in k3s. But k3s runs on the same host, so a pod pinned to kai-server:

  • hostPath-mounts EcoServer/{Configs,Storage,Logs,Mods} read-only, and
  • reaches the server's 127.0.0.1:3001 / :3002 admin endpoints over the hostAliases loopback pin.

MCP is exposed over the tailnet sidecar (reuse eco-app's deploy idiom), ACL'd to the ward/kai-server tailnet, not the public ingress. This is a node-agent MCP: the same DaemonSet-or-node-pinned + hostPath shape as node-exporter / promtail / CSI node plugins.

Tool inventory (provisional)

Save + world (Storage/): eco_save_status (Game.eco/Game.db size + age since last autosave), eco_backup_list (Backup/ count, cadence, newest/oldest), eco_world_meta (size, seed, cluster centers, world-days).

Config (Configs/, redacted, enum-not-path): eco_config_get (named .eco/.diff.json), eco_config_diff (diff vs *.original.json - what's tuned off default), eco_mod_configs (MightyMooseCore, DiscordLink, StrangeWorlds, NidToolbox - secrets stripped).

Chronicler / events (SQLite, read RO): eco_events_recent (joins, laws, elections, deaths), eco_player_activity (from EcoReplay.db / Chronicler, redaction-gated).

Logs (Logs/<subsystem>/): eco_log_tail, eco_log_grep (named subsystem enum: MightyMooseCore, NidToolbox, web, Kestrel, migrations).

Live runtime (loopback :3001 admin/exporter): eco_live_status (players, world time, next meteor, uptime - unredacted), eco_service_health (systemd active-state + last restart).

Mods (Mods/): eco_mods_installed (modules + versions, drift vs push-mods manifest).

Security boundary

  • Caller - warded directors/engineers only; tailnet ACL, not public.
  • Read-only - every mount RO, every tool a read. Mutation (restart, mod push, world reset) stays with operator scripts + systemd, never a tool.
  • Redaction - three levels: public (names hashed), operator (names shown, secrets stripped), raw (operator + secrets - default DENY). Proposed default: operator for the trusted warded callers; raw off by default. Secrets (Discord bot token, /eco/server-api-token, any token in Configs) never returned above raw.
  • No arbitrary path read - config/log tools take an enum of known names, not a filesystem path, so a tool can't be walked into ../../.ssh.

Proposed defaults for the open questions

  • Redaction default = operator (names shown, secrets always stripped). Open to holding at public.
  • Scope = at-rest first (disk + logs + Chronicler - the state only this surface can serve). Live-runtime tools reuse eco-app's existing /info client as a thin add-on, not a reimplementation.
  • Chronicler read = read the SQLite file directly RO (don't link the mod's interop).
  • Generalize later - build the Eco battery concrete first, then extract the reusable node-local spine (node-pin scaffold + hostPath + tailnet sidecar + redaction levels + enum-not-path file access) as a general node-local MCP template, with Eco as instance #1. Don't abstract before one real instance exists.

Build phases

  1. Scaffold the MCP (Python, mcp SDK) with eco_save_status + eco_backup_list + eco_config_get against a local checkout of the state dirs - prove tools + redaction on real files.
  2. Node-local deploy: k3s manifest in coilyco-bridge/deploy/services/, hostPath RO mounts, node pin, loopback pin, tailnet sidecar, ward ACL.
  3. Add Chronicler + logs + live-runtime tool groups.
  4. Warded-agent smoke: a director/engineer session calls the tailnet MCP and inspects state end-to-end.
  5. If generalizing: extract the node-local spine into a template, leave Eco as instance #1.

Context

  • Live server root: /home/kai/Steam/steamapps/common/EcoServer/ (the eco-server-setup skill's /home/ubuntu/games/eco/ paths are stale legacy).
  • On-disk state confirmed: Storage/Game.db (256MB world SQLite), Game.eco save, EcoReplay.db, Backup/ ×40, Configs/*.eco + *.diff.json + *.original.json, Logs/{MightyMooseCore,NidToolbox,web,Microsoft.AspNetCore.*,migrations}/.
  • Server listens 0.0.0.0:3001 (Kestrel/ASP.NET, 200 on / and /api) + :3002.
  • eco-app already owns the outside-in public MCP (replaced the deprecated standalone coilysiren/eco-mcp-app).

Filed from a spec drafting session on kai-server.

## Goal Let warded **directors and engineers** inspect the state of the local Eco game server on disk. Rather than bind-mount the live `EcoServer/` tree into every warded container (heavy, and leaks Discord/Chronicler/API tokens sitting in `Configs/`), expose the state over a **network MCP surface** the agents fetch. This is the **inside-out** counterpart to eco-app's existing outside-in MCP. ## Two views, this is the second - **Outside-in (exists)** - eco-app's current MCP: hits Eco's public `/info` endpoint, redacts player names, serves anyone. Live runtime, network-visible. - **Inside-out (this issue)** - a node-local MCP reading what only a process on kai-server can see: raw `Configs/`, `Storage/` saves + `Backup/` (×40), the Chronicler SQLite history, per-subsystem `Logs/`, unredacted admin data, mod install state, and the loopback-only admin endpoints. Privileged, at-rest + admin, warded-agents-only. ## Home Fold into **`coilyco-gaming/eco-app`** as a **second, privileged MCP surface** (distinct deployment from the public one). Decided. ## Where it runs - node-local k3s pattern The Eco server is a native systemd service (`eco-server.service`) at `/home/kai/Steam/steamapps/common/EcoServer/`, NOT in k3s. But k3s runs on the same host, so a pod **pinned to kai-server**: - `hostPath`-mounts `EcoServer/{Configs,Storage,Logs,Mods}` **read-only**, and - reaches the server's `127.0.0.1:3001` / `:3002` admin endpoints over the `hostAliases` loopback pin. MCP is exposed over the tailnet sidecar (reuse eco-app's deploy idiom), ACL'd to the ward/kai-server tailnet, not the public ingress. This is a **node-agent MCP**: the same DaemonSet-or-node-pinned + hostPath shape as node-exporter / promtail / CSI node plugins. ## Tool inventory (provisional) **Save + world** (`Storage/`): `eco_save_status` (Game.eco/Game.db size + age since last autosave), `eco_backup_list` (Backup/ count, cadence, newest/oldest), `eco_world_meta` (size, seed, cluster centers, world-days). **Config** (`Configs/`, redacted, enum-not-path): `eco_config_get` (named .eco/.diff.json), `eco_config_diff` (diff vs *.original.json - what's tuned off default), `eco_mod_configs` (MightyMooseCore, DiscordLink, StrangeWorlds, NidToolbox - secrets stripped). **Chronicler / events** (SQLite, read RO): `eco_events_recent` (joins, laws, elections, deaths), `eco_player_activity` (from EcoReplay.db / Chronicler, redaction-gated). **Logs** (`Logs/<subsystem>/`): `eco_log_tail`, `eco_log_grep` (named subsystem enum: MightyMooseCore, NidToolbox, web, Kestrel, migrations). **Live runtime** (loopback `:3001` admin/exporter): `eco_live_status` (players, world time, next meteor, uptime - unredacted), `eco_service_health` (systemd active-state + last restart). **Mods** (`Mods/`): `eco_mods_installed` (modules + versions, drift vs push-mods manifest). ## Security boundary - **Caller** - warded directors/engineers only; tailnet ACL, not public. - **Read-only** - every mount RO, every tool a read. Mutation (restart, mod push, world reset) stays with operator scripts + systemd, never a tool. - **Redaction** - three levels: `public` (names hashed), `operator` (names shown, secrets stripped), `raw` (operator + secrets - default DENY). **Proposed default: `operator`** for the trusted warded callers; `raw` off by default. Secrets (Discord bot token, `/eco/server-api-token`, any token in Configs) never returned above `raw`. - **No arbitrary path read** - config/log tools take an enum of known names, not a filesystem path, so a tool can't be walked into `../../.ssh`. ## Proposed defaults for the open questions - **Redaction default** = `operator` (names shown, secrets always stripped). Open to holding at `public`. - **Scope** = **at-rest first** (disk + logs + Chronicler - the state only this surface can serve). Live-runtime tools reuse eco-app's existing `/info` client as a thin add-on, not a reimplementation. - **Chronicler read** = read the SQLite file directly RO (don't link the mod's interop). - **Generalize later** - build the Eco battery concrete first, then extract the reusable node-local spine (node-pin scaffold + hostPath + tailnet sidecar + redaction levels + enum-not-path file access) as a general node-local MCP template, with Eco as instance #1. Don't abstract before one real instance exists. ## Build phases 1. Scaffold the MCP (Python, mcp SDK) with `eco_save_status` + `eco_backup_list` + `eco_config_get` against a local checkout of the state dirs - prove tools + redaction on real files. 2. Node-local deploy: k3s manifest in `coilyco-bridge/deploy/services/`, hostPath RO mounts, node pin, loopback pin, tailnet sidecar, ward ACL. 3. Add Chronicler + logs + live-runtime tool groups. 4. Warded-agent smoke: a director/engineer session calls the tailnet MCP and inspects state end-to-end. 5. If generalizing: extract the node-local spine into a template, leave Eco as instance #1. ## Context - Live server root: `/home/kai/Steam/steamapps/common/EcoServer/` (the eco-server-setup skill's `/home/ubuntu/games/eco/` paths are stale legacy). - On-disk state confirmed: `Storage/Game.db` (256MB world SQLite), `Game.eco` save, `EcoReplay.db`, `Backup/` ×40, `Configs/*.eco` + `*.diff.json` + `*.original.json`, `Logs/{MightyMooseCore,NidToolbox,web,Microsoft.AspNetCore.*,migrations}/`. - Server listens `0.0.0.0:3001` (Kestrel/ASP.NET, 200 on `/` and `/api`) + `:3002`. - eco-app already owns the outside-in public MCP (replaced the deprecated standalone `coilysiren/eco-mcp-app`). _Filed from a spec drafting session on kai-server._
Owner

Two refinements (director session, 2026-07-05):

1. REST is out of scope — MCP-only. The surface stays a network MCP; no parallel REST API. (It still consumes the loopback admin REST endpoints, but only re-exposes them as MCP tools.)

2. Access model: add an /admin mount + the Authelia OAuth overlay for the claude.ai-hosted-connector path, complementing the tailnet-agent path this spec already has. Two callers, one surface:

  • Warded agents / local Claude Code (on the tailnet) — reach the privileged MCP directly; the tailnet already authenticates them. This is the spec's original tailnet ACL, warded-agents-only model.
  • Kai via claude.ai (off-tailnet, hosted connector) — needs an OAuth 2.1 gate. Reuse the auth overlay from coilyco-bridge/deploy#27 (Traefik ForwardAuth -> Authelia, node-stats-mcp is instance #1) and its fleet generalization deploy#30. claude.ai is the OAuth client; Authelia issues the token; no Anthropic identity to gate on; a single-user custom connector needs no DCR.

Ergonomics: the auth is not eco-app code — it is an ingress-layer overlay (deploy repo). eco-app only adds the /admin Starlette mount, feature-flagged (ECO_ADMIN_ENABLED) so only the privileged node-local deployment exposes it. eco-mcp.coilysiren.me already serves eco-app's MCP through Traefik + external-dns + LE, so there is no ingress to invent, only a gate to add in front of one path.

Threat model unchanged: the overlay authenticates who, it does not relax this spec's redaction levels — secrets stay default-DENY even for an authenticated caller. On-disk data access (hostPath RO, node-pin) is orthogonal to the auth gate and stays as specified.

**Two refinements (director session, 2026-07-05):** **1. REST is out of scope — MCP-only.** The surface stays a network MCP; no parallel REST API. (It still *consumes* the loopback admin REST endpoints, but only re-exposes them as MCP tools.) **2. Access model: add an `/admin` mount + the Authelia OAuth overlay for the claude.ai-hosted-connector path**, complementing the tailnet-agent path this spec already has. Two callers, one surface: - **Warded agents / local Claude Code** (on the tailnet) — reach the privileged MCP directly; the tailnet already authenticates them. This is the spec's original `tailnet ACL, warded-agents-only` model. - **Kai via claude.ai** (off-tailnet, hosted connector) — needs an OAuth 2.1 gate. Reuse the **auth overlay** from [coilyco-bridge/deploy#27](https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/27) (Traefik ForwardAuth -> Authelia, node-stats-mcp is instance #1) and its fleet generalization [deploy#30](https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/30). claude.ai is the OAuth *client*; Authelia issues the token; no Anthropic identity to gate on; a single-user custom connector needs no DCR. **Ergonomics:** the auth is **not eco-app code** — it is an ingress-layer overlay (deploy repo). eco-app only adds the `/admin` Starlette mount, feature-flagged (`ECO_ADMIN_ENABLED`) so only the privileged node-local deployment exposes it. `eco-mcp.coilysiren.me` already serves eco-app's MCP through Traefik + external-dns + LE, so there is no ingress to invent, only a gate to add in front of one path. **Threat model unchanged:** the overlay authenticates *who*, it does not relax this spec's redaction levels — secrets stay default-DENY even for an authenticated caller. On-disk data access (hostPath RO, node-pin) is orthogonal to the auth gate and stays as specified.
coilyco-ops added
P3
and removed
P2
labels 2026-07-10 09:00:49 +00:00
Sign in to join this conversation.
No description provided.