Files
dotfiles/mcp-config/README.md
T
noconnorandClaude Opus 4.8 cd09df07f6 mcp-config: add mcp-sync to force a refresh, ignoring the TTL
The sync runs only at shell start and is throttled to MCP_SYNC_TTL (1h), so a
model added on the gateway can take up to an hour AND a new terminal before
opencode sees it. There was no way to say "fetch it now", so the recurring
symptom was "I added a model and it isn't showing up" -- with a healthy
gateway and a correctly-scoped key.

`mcp-sync` re-sources the hook with the throttle disabled and prints the
resulting model list, so it also answers "what does the client actually have?"
in one step.

README gains a section splitting the two failure modes: a model the gateway
returns but opencode lacks is a stale client; a model the gateway does not
return is a LiteLLM model_list / key-scoping problem.

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

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

"I added a model on the gateway and opencode doesn't see it"

Almost always a stale local config, not a gateway or key problem. The sync runs only at shell start and is throttled to MCP_SYNC_TTL (1h), so a new model can take up to an hour and a new terminal to appear.

Pull it immediately:

mcp-sync          # force a refresh now, ignoring the TTL; prints the model list

If the model still isn't listed, it isn't reaching your key — check the gateway, not the client:

curl -fsS -H "Authorization: Bearer $LITELLM_KEY" \
  https://llm.covert-gpt.com/v1/models | jq -r '.data[].id'

Whatever that prints is exactly what sync.sh will deploy. A model present here but missing in opencode is a sync problem; a model missing here is a LiteLLM model_list or key-scoping problem.

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: mcp-sync forces one refresh; MCP_SYNC_TTL=0 in the environment makes every new shell pull.
  • python3 is required for model discovery. Without it sync.sh deploys the base config verbatim, so the provider comes up with an EMPTY model list. It warns when this happens; bootstrap.sh installs python3.