# Unraid Install Guide — totalmcp Step-by-step walkthrough for deploying the **totalmcp** gateway on Unraid using the **Docker Add Container** GUI. Targets Unraid 6.12+ / 7.0+. After installing, totalmcp is reachable at: - **LAN:** `http://10.2.0.35:8811` - **Health:** `http://10.2.0.35:8811/health` (no auth) - **MCP endpoint:** `http://10.2.0.35:8811/mcp` (Bearer token required) - **Public proxy** (optional, after NPM entry): `https://mcp.alwisp.com` once the existing `gitea-mcp` proxy is repointed --- ## 0. Prerequisites ### Gitea Container Registry login The image lives in your private Gitea registry, so the Unraid host needs credentials. 1. Open the Unraid web UI → **Console** (top right) or SSH into the box. 2. Run: ```bash docker login git.alwisp.com ``` 3. Enter your Gitea username and a personal access token (with `read:packages` scope). Credentials are saved to `/root/.docker/config.json`. ### Confirm `br0` and pick the IP - `br0` should already exist (every other ALPHA container uses it). - Reserve **`10.2.0.35`** for totalmcp (next free above codedump @ `.34`). - If `.35` is taken when you go to assign it, pick the next free IP and update `PLAN.md` + `SERVICES.md` to match. ### Pre-create the appdata directory ```bash mkdir -p /mnt/user/appdata/totalmcp/data mkdir -p /mnt/user/appdata/totalmcp/logs chown -R 99:100 /mnt/user/appdata/totalmcp ``` (99:100 = `nobody:users`, the standard Unraid container ownership.) ### Gather your tokens Have these in a scratch file before starting — easier than tab-switching during the GUI install. | Token | Where to get it | |---|---| | **`AGENT_TOKENS`** | Generate fresh: `openssl rand -hex 32` (run 3× — one per agent: claude-code, antigravity, codex). Save them — you'll paste the same values into agent configs after install. | | **`GITEA_TOKEN`** | `git.alwisp.com` → your avatar → **Settings** → **Applications** → **Generate New Token**. Scopes: `repo`, `issue`, `read:user`. Copy the token immediately (Gitea won't show it again). | | **`UNRAID_API_KEY`** | On the Unraid host: `unraid-api start`, then `unraid-api key` to print the key. (Requires the unraid-api plugin from Apps.) | | **`OPENCLAW_HOST`** | Already known: `http://10.2.0.26:18789` (NOVA). No token — LAN-only. | | **`UNIFI_API_KEY`** | UniFi Access UI → **Settings** → **Security** → **API Tokens** → **Create**. Scopes: read all, plus the writes you want. | | **`UNIFI_SITE_ID`** | UniFi Access UI URL contains it (`/locations/`). Optional. | | **`CODEX_DB_PATH`** | Container path is `/app/codex/db.sqlite` (after the volume mount). Host file: `/mnt/user/appdata/codex/db.sqlite`. | | **`RACKMAPPER_TOKEN`** | RackMapper UI → API tokens. Optional if RackMapper has no auth. | | **`STREAMVAULT_*`** | Skip — service not deployed yet. | --- ## 1. Add Container — top-level fields **Docker tab → Add Container** | Field | Value | |---|---| | **Name** | `totalmcp` | | **Overview** | `Unified MCP gateway for ALPHA — exposes every backend service to Claude Code, Codex, Antigravity` | | **Repository** | `git.alwisp.com/jason/totalmcp:latest` | | **Docker Hub URL** | (leave blank — private registry) | | **Icon URL** | (optional — set later) | | **WebUI** | `http://[IP]:[PORT:8811]/health` | | **Extra Parameters** | `--pids-limit=2048 --user 0:0` (the `--user 0:0` is needed for the `docker` plugin — see §3) | | **Post Arguments** | (leave blank) | | **Network Type** | `Custom: br0` | | **Use IPv4 only** | ☑ checked | | **Fixed IP address (optional)** | `10.2.0.35` | | **Privileged** | ☐ unchecked | | **Console shell command** | `Shell` | | **Auto update / Restart Policy** | `unless-stopped` | > **Why `br0` instead of `bridge`?** br0 gives the gateway its own LAN IP so agents on workstations connect directly without going through Unraid's port-mapped 10.2.0.2:8811. Matches the rest of the ALPHA service catalog. --- ## 2. Port mappings Click **Add another Path, Port, Variable, Label or Device** → **Port**. | Container Port | Host Port | Connection Type | Description | |---|---|---|---| | `8811` | `8811` | `TCP` | MCP gateway (HTTP + SSE) | > With `br0` and a fixed IP, the host-port column is mostly cosmetic — the container has its own IP. Listed for completeness. --- ## 3. Path mappings Click **Add Path** for each row. ### Required | Container Path | Host Path | Access Mode | Description | |---|---|---|---| | `/app/data` | `/mnt/user/appdata/totalmcp/data` | `Read/Write` | SQLite event log + plugin runtime data | | `/app/logs` | `/mnt/user/appdata/totalmcp/logs` | `Read/Write` | Structured JSON logs (Phase 5) | ### Required if `docker` plugin is enabled (Phase 2) | Container Path | Host Path | Access Mode | Description | |---|---|---|---| | `/var/run/docker.sock` | `/var/run/docker.sock` | `Read/Write` | Docker socket — needed for start/stop/restart | > Read-only is enough for `docker_list_containers`/`get_logs`/`get_stats`, but `start`/`stop`/`restart` require RW. > ⚠️ **Permission gotcha:** the container runs as the unprivileged `mcp` user, but Unraid's `/var/run/docker.sock` is owned by `root:root` (mode 660). The `mcp` user has no access by default — `docker_list_containers` will return `EACCES`. The simplest fix is to add `--user 0:0` to the **Extra Parameters** field at the top of the Add Container form (run the container as root). For a LAN-only gateway behind bearer auth on a single-tenant box, this tradeoff is acceptable. See troubleshooting §7 for stricter alternatives. ### Required if `codex-mrp` plugin is enabled (Phase 3) | Container Path | Host Path | Access Mode | Description | |---|---|---|---| | `/app/codex/db.sqlite` | `/mnt/user/appdata/codex/db.sqlite` | `Read/Write` | CODEX SQLite database | > Plugin reads in WAL mode so concurrent reads with CODEX itself are safe. Writes (`codex_create_work_order`) bypass CODEX validation — use the CODEX UI for routine work-order creation. --- ## 4. Environment variables Click **Add Variable** for each row. Group by phase — only add variables for plugins you're enabling now. ### Core (always required) | Variable | Value | Description | |---|---|---| | `NODE_ENV` | `production` | | | `PORT` | `8811` | Must match the port mapping above | | `LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` | | `GATEWAY_VERSION` | `0.1.0` | | | `PLUGINS_DIR` | `./dist/plugins` | (default — only override for advanced setups) | | `ENABLED_PLUGINS` | `gitea,unraid,docker,openclaw,unifi,codex-mrp,streamvault,rackmapper` | Comma-separated. Trim to only the plugins you've configured. | | `AGENT_TOKENS` | `claude-code:TOKEN1,antigravity:TOKEN2,codex:TOKEN3` | Generate with `openssl rand -hex 32`. **Save the same tokens in your agent configs.** | ### Phase 1 — Gitea + Unraid | Variable | Value | Required for plugin | |---|---|---| | `GITEA_HOST` | `https://git.alwisp.com` | `gitea` | | `GITEA_TOKEN` | *(Gitea PAT with repo + issue + admin scopes)* | `gitea` | | `UNRAID_HOST` | `http://10.2.0.2` | `unraid` | | `UNRAID_API_KEY` | *(generated via `unraid-api start` on the host)* | `unraid` | ### Phase 2 — Docker + OpenClaw | Variable | Value | Required for plugin | |---|---|---| | `OPENCLAW_HOST` | `http://10.2.0.26:18789` | `openclaw` (NOVA) | > The `docker` plugin needs no env vars — it uses the mounted socket. ### Phase 3 — Service Connectors | Variable | Value | Required for plugin | |---|---|---| | `UNIFI_HOST` | `https://` | `unifi` | | `UNIFI_API_KEY` | *(UniFi Access → Settings → Security → API tokens)* | `unifi` | | `UNIFI_SITE_ID` | *(your site UUID, optional)* | `unifi` | | `CODEX_DB_PATH` | `/app/codex/db.sqlite` | `codex-mrp` (must match the path mapping above) | | `STREAMVAULT_HOST` | `http://streamvault:3100` | `streamvault` (skip until StreamVault is deployed) | | `STREAMVAULT_TOKEN` | *(if StreamVault requires auth)* | `streamvault` (optional) | | `RACKMAPPER_HOST` | `http://10.2.0.23` | `rackmapper` | | `RACKMAPPER_TOKEN` | *(if RackMapper requires auth)* | `rackmapper` (optional) | ### Phase 6 — Deferred | Variable | Value | Required for plugin | |---|---|---| | `CHRONICLE_HOST` | *(after deploy)* | `chronicle` (deferred) | | `CHRONICLE_TOKEN` | *(after deploy)* | `chronicle` (deferred) | | `OBSIDIAN_REST_HOST` | `http://10.2.0.2:27123` | `obsidian` (deferred) | | `OBSIDIAN_API_KEY` | *(Obsidian Local REST API plugin)* | `obsidian` (deferred) | ### Phase 7+ (defer until Phase 7 ships) The full set is documented in [.env.example](.env.example) — `NPM_*`, `UISP_*`, `TRANSMISSION_*`, `SYNCTHING_*`, `PLEX_*`, `NYAA_*`, `HA_*`, `INVOICENINJA_*`, `FABDASH_*`, `CPAS_*`, `WFH_*`, `BREEDR_*`, `CODEDUMP_*`, `UITRACKER_*`, `STEPVIEW_*`, `QRKNIT_*`, `MEMER_*`, `ALWISP_WEB_*`. Add only when those plugin phases land. --- ## 5. Apply & verify 1. Click **Apply** at the bottom of the Add Container form. 2. Unraid pulls the image from `git.alwisp.com/jason/totalmcp:latest` and starts the container. 3. Watch the **Docker tab** — totalmcp should show **started** within 5–10 seconds. ### Liveness check From the Unraid console (or any LAN host): ```bash curl http://10.2.0.35:8811/health ``` Expected: ```json { "status": "ok", "version": "0.1.0", "plugins": 8, "enabled": ["gitea","unraid","docker","openclaw","unifi","codex-mrp","streamvault","rackmapper"] } ``` ### Plugin diagnostics ```bash curl -H "Authorization: Bearer " http://10.2.0.35:8811/plugins ``` You'll see a per-plugin breakdown with tool counts. If a plugin is missing, check the container logs: ```bash docker logs totalmcp | grep -E "plugin_(loaded|connect_failed|invalid)" ``` ### Connect Claude Code ```bash claude mcp add --scope user --transport http totalmcp http://10.2.0.35:8811/mcp \ -H "Authorization: Bearer " ``` Then run `/mcp` inside Claude Code — the totalmcp tools should appear in the catalog. ### Per-plugin smoke tests Run these from Claude Code in order of risk (lowest first). Each row = the safest first call to confirm a plugin actually works end-to-end. | Plugin | First call | Expected on success | Likely failure mode | |---|---|---|---| | **gitea** | `gitea_list_repos` | `{ repos: [...] }` including the `totalmcp` repo | 401 → `GITEA_TOKEN` missing/wrong scopes | | **unraid** | `unraid_host_summary` | `{ host, os, uptime, cpu, memory, array }` | GraphQL field shape mismatch — adjust queries in `src/plugins/unraid/index.ts` to match your unraid-api version | | **docker** | `docker_list_containers` | ~35 containers from your Unraid stack | `EACCES` on `/var/run/docker.sock` — confirm `--user 0:0` is in Extra Parameters | | **openclaw** | `openclaw_list_models` | List of models on NOVA | Connection timeout → confirm NOVA at `10.2.0.26:18789` is reachable from `10.2.0.35` | | **unifi** | `unifi_list_sites` | List of UniFi Access locations | 404 on `/api/v1/developer/locations` — UniFi Access REST paths vary by version; adjust the plugin's endpoint paths | | **codex-mrp** | `codex_list_work_orders` | `{ workOrders: [...] }` | `no such table: work_orders` — **expected.** Run `sqlite3 /mnt/user/appdata/codex/db.sqlite ".schema"`, then update the SQL in `src/plugins/codex-mrp/index.ts` to match real CODEX table names | | **streamvault** | (skip) | n/a | Service not deployed — `onLoad` will warn at startup, tool calls return connection error. Disable by removing from `ENABLED_PLUGINS` until ready. | | **rackmapper** | `rackmapper_list_racks` | `{ racks: [...] }` | 404 — RackMapper API path may differ; adjust | ### What to expect on first run - **gitea**, **openclaw**, **docker** are highest-confidence. Built against well-documented APIs that haven't drifted. - **unraid**, **unifi** are medium-confidence. APIs evolve between versions; field shapes may need adjustment. - **codex-mrp** is low-confidence. Placeholder SQL by design — needs the real CODEX schema before it works. - **streamvault** will be in failure state until the service exists. Disable for now. - **rackmapper** depends on whether RackMapper has a JSON API; adjust if not. Capture each plugin's failure (if any) and we'll fix the schemas/paths in a follow-up pass. --- ## 6. Updates ### Manual (one-time) 1. Docker tab → **totalmcp** → click the icon → **Force Update**. 2. Unraid pulls `git.alwisp.com/jason/totalmcp:latest` and restarts the container in place. 3. Re-verify via `curl /health`. ### Automated (Phase 4 — Gitea Actions CI) Once the CI pipeline lands, every push to `main` rebuilds the image. To pick up updates without manual intervention: - Set Unraid's update polling to a short interval (Settings → Docker → "Check for updates every"), or - Hit the Docker tab → **Check for Updates** button when ready. --- ## 7. Troubleshooting ### `/health` returns 500 or container crash-loops Check the logs: ```bash docker logs totalmcp ``` Common causes: - **`Invalid environment configuration`** — `AGENT_TOKENS` malformed or `PORT` not a number. Fix the Variable in the Unraid GUI and the container will auto-restart. - **`Cannot find module ...`** — image was built without one of the new deps (e.g., `dockerode` or `better-sqlite3`). Force-update to pull the latest image. ### `docker` plugin: `docker_connect_failed` or `EACCES` on socket Most common cause: the container is running as the unprivileged `mcp` user but `/var/run/docker.sock` is owned by `root:root` on the host. **Quickest fix:** add `--user 0:0` to **Extra Parameters** on the container (run as root inside the container). **Stricter alternatives** if you don't want root in the container: 1. Find the docker GID on the host: `stat -c '%g' /var/run/docker.sock` (commonly `281` on Unraid). 2. Either: - Override the container's group with `--group-add ` in Extra Parameters, **or** - Rebuild the image with that GID baked in (modify the Dockerfile's `addgroup` step). 3. Or run a [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) in front and point `dockerode` at the proxy URL via a `socketPath` override. For a LAN-only gateway behind bearer auth on a single-tenant Unraid box, `--user 0:0` is the pragmatic choice. Also confirm the path mapping is `/var/run/docker.sock → /var/run/docker.sock` with **Read/Write** access mode. ### `codex-mrp` plugin: `codex_mrp_connect_failed` - Check that `/mnt/user/appdata/codex/db.sqlite` exists on the host. - Confirm the path mapping is `/app/codex/db.sqlite` (file path, not directory). - Confirm `CODEX_DB_PATH=/app/codex/db.sqlite` matches. ### `unraid` plugin: `unraid_connect_failed` - Run `unraid-api status` on the host to confirm the API plugin is running. - Confirm `UNRAID_API_KEY` matches the key from `unraid-api key`. ### Authentication 401s from Claude Code - Token mismatch — the `AGENT_TOKENS` value in Unraid must contain the same token Claude Code is sending. - Format reminder: `agentName:token,agentName2:token2` (no spaces, comma-separated). ### Port collision on 8811 If something else on `10.2.0.35` (or your chosen IP) already binds 8811, change `PORT` and the port mapping to `8812` (or whichever) and update agent configs accordingly. The PLAN.md default is 8811. --- ## 8. Reference: minimal viable install If you just want the gateway running with **only the Gitea plugin** to start: | Setting | Value | |---|---| | Repository | `git.alwisp.com/jason/totalmcp:latest` | | Network / IP | `br0` / `10.2.0.35` | | Port | `8811:8811 TCP` | | Path | `/app/data → /mnt/user/appdata/totalmcp/data (RW)` | | Variable `NODE_ENV` | `production` | | Variable `PORT` | `8811` | | Variable `AGENT_TOKENS` | `claude-code:` | | Variable `ENABLED_PLUGINS` | `gitea` | | Variable `GITEA_HOST` | `https://git.alwisp.com` | | Variable `GITEA_TOKEN` | *(your PAT)* | Apply, verify with `curl /health`, then expand by adding more variables and updating `ENABLED_PLUGINS` as each phase comes online. > **Note:** even the minimal install does **not** need `--user 0:0` since the `docker` plugin isn't enabled. Add `--user 0:0` only when you turn `docker` on.