Files
totalmcp/INSTALL-UNRAID.md
jason 7e73e0de71
Build and Push Docker Image / build (push) Successful in 7s
update docs
2026-05-09 23:31:14 -05:00

353 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<UUID>`). 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://<your-unifi-controller>` | `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 510 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 <CLAUDE_CODE_TOKEN>" 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 <CLAUDE_CODE_TOKEN>"
```
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 <GID>` 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:<your-token>` |
| 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.