# mcp-config — shared opencode config for laptops Single source of truth for what opencode sees: the **covert-gpt model provider** (LiteLLM at `llm.covert-gpt.com`) and the **MCP servers**, which run **client-side**. Users install once; their config then refreshes automatically on each new shell. ## MCP runs on the CLIENT, never on the gateway The LiteLLM gateway hosts **no MCP servers** — deliberately. Every API worth exposing (Gitea, Proxmox, UniFi, NetBird, k8s) is **user-scoped**, so hosting it centrally means one shared token per server: every user acts as the same identity and attribution is gone. LiteLLM *can* do per-request BYOK (env values written `${X-Header}` are filled from the caller's headers — verified working for proxmox on v1.87.0), but only for servers that tolerate a credential-less spawn. LiteLLM builds its tool→server mapping from a **headerless** background listing, and `gitea-mcp` hard-exits without a token, so it never registers and its tool calls 404. The gateway could therefore host only *some* servers — and a split brain (some MCPs server-side, some client-side) is worse than either. So **all** MCP servers are defined here, each reading the user's **own** credentials from their own environment. The config is identical for everyone; only the secrets differ. Credentials are referenced as `{env:VAR}` and are **never** written into the config. Each user exports them in `~/.zshrc.local`: | MCP server | env vars | |---|---| | `gitea` | `GITEA_KEY` | | `proxmox` | `PROXMOX_MCP_TOKEN_ID`, `PROXMOX_MCP_TOKEN_SECRET` | **Requires `npx`** — the MCP servers are npm packages that opencode spawns as `npx -y @`. `bootstrap.sh` installs Node for this (`node/`). Without it every server dies with "command not found", which opencode surfaces only as a dead server — easy to misread as a broken config. This repo lives in Gitea at `public/dotfiles`. (It is mirrored from the `ai-k8s-iac` repo under `mcp-config/`; edit it there or here and keep them in sync — the laptops pull from Gitea's `main`.) ## How it works - `opencode.gateway.json` — the shared base config: - the `covert-gpt` model provider (LiteLLM's OpenAI-compatible endpoint at `https://llm.covert-gpt.com/v1`, models map left empty — it is filled by discovery at sync time), and - the **MCP servers**, each `type: local`, spawned by opencode as `npx -y @` on the user's own machine. **Identical for every user**: every credential (the LiteLLM key, the Gitea PAT, the Proxmox token) is injected via `{env:VAR}` and never written into the file. Versions are pinned — `npx` runs whatever the registry serves, and that process gets your tokens. - `sync.sh` — sourced from each user's shell rc. On a new shell (throttled ~1h) it pulls the base config from Gitea, queries the gateway's `/v1/models` with the user's key to **auto-discover models** (opencode can't list models from a custom provider itself, so the map is materialized client-side; if discovery fails the previous map is carried over), deploys the repo's **skills** (see below), deploys the config to `~/.config/opencode/opencode.gateway.json`, and exports `LITELLM_KEY` (from `~/.config/mcp-sync/key`) plus `OPENCODE_CONFIG` (pointing at the deployed file — opencode does NOT auto-load extra files in its config dir, so without this export the file is ignored). Silent + non-fatal: a Gitea hiccup never breaks the shell, last-good config stays in place. - `../skills/` — the shared skill folders, deployed to `~/.config/mcp-sync/skills` and wired in through `skills.paths` in the config above. Skills are plain files, so — unlike models and MCP servers — there is nothing on the gateway to discover and **the repo is the source of truth**; the tree is fetched whole from the repo tarball. They deploy to mcp-sync's own directory rather than `~/.config/opencode/skills` so the atomic replace can never delete a skill the user wrote by hand. See `skills/README.md`. - `install.sh` — one-time bootstrap (curl-pipe). Fetches `sync.sh`, stores the user's key (0600), and adds the rc hook. opencode merges the `OPENCODE_CONFIG` file on top of the user's global `~/.config/opencode/opencode.json` (and under any project `opencode.json`), so the synced config coexists with each user's own settings and any **local-only** MCP servers (e.g. a client-side `kubernetes` pointed at their own kubeconfig). Sync never touches those. ## User setup (one time) Each user needs a LiteLLM **virtual key** (for models) plus their **own** credentials for each MCP server. Run `bootstrap.sh` (it installs Node, which provides the `npx` the MCP servers need), then: ```sh curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/mcp-config/install.sh | sh # (prompts for the sk-... key; or pass it: ... | LITELLM_KEY=sk-... sh) ``` Then export your own MCP credentials in `~/.zshrc.local`: ```sh export GITEA_KEY=... # your Gitea PAT (repo + issue scopes) export PROXMOX_MCP_TOKEN_ID=... # e.g. alice@pve!mcp export PROXMOX_MCP_TOKEN_SECRET=... # the token UUID ``` Open a new terminal, run `opencode`, and the tools are there — acting as **you**. ## Adding an MCP server (admin) Add it to `mcp.` in `opencode.gateway.json` as `type: local`, with a **pinned** package version and `{env:VAR}` refs for every secret, then push to `main`. Users pick it up within the sync TTL (or immediately on a new shell). Two hard requirements: - It must be an **npm package** (opencode spawns it with `npx`). - Its credentials must be **per-user**, via `{env:VAR}`. Never commit a token, and never introduce a shared one — that is the whole reason MCP is not on the gateway. Models are different: they live on the gateway (`charts/inference/values.yaml` → `model_list`) and are auto-discovered by the `/v1/models` query in `sync.sh`. No client change needed. ## Adding a skill (admin) Add a folder under `skills/` and push to `main` — see `skills/README.md`. Verify with `opencode debug skill`. ## Notes / caveats - **Local-server precedence**: opencode merges with later-config-wins on key conflicts. If a user names a local server the same as a gateway one, theirs wins. Use distinct names. - **Key rotation**: re-run `install.sh` (or edit `~/.config/mcp-sync/key`). - **Other clients**: this ships an opencode config. Claude Code / open-webui would need their own equivalent file + a parallel sync entry; not done yet. - **Throttle**: override with `MCP_SYNC_TTL=0` to force a pull every shell.