Files
dotfiles/mcp-config/README.md
T
noconnorandClaude Opus 4.8 70b049625a mcp-config: run MCP servers client-side, not on the gateway
Every API worth exposing (Gitea, Proxmox) is user-scoped, so hosting MCP
centrally means one shared token per server: everyone acts as the same
identity and attribution is lost. LiteLLM's per-request BYOK cannot cover
all of them either -- it builds its tool->server map from a headerless
background listing, and gitea-mcp hard-exits without a token, so it never
registers. A split brain (some MCPs server-side, some client-side) is worse
than either, so all of them move client-side.

Servers are declared local, spawned via npx with pinned versions, and read
each user's OWN credentials via {env:VAR}. No token is ever written to the
config; the file stays identical for every user.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:10:11 -04:00

6.5 KiB

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 <pkg>@<version>. 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 <pkg>@<pinned-version> 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:

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:

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.<name> 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.