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>
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-gptmodel provider (LiteLLM's OpenAI-compatible endpoint athttps://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 asnpx -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 —npxruns whatever the registry serves, and that process gets your tokens. - the
-
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/modelswith 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 exportsLITELLM_KEY(from~/.config/mcp-sync/key) plusOPENCODE_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/skillsand wired in throughskills.pathsin 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/skillsso the atomic replace can never delete a skill the user wrote by hand. Seeskills/README.md. -
install.sh— one-time bootstrap (curl-pipe). Fetchessync.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=0to force a pull every shell.