mcp-config: add covert-gpt provider, auto-discover models
Two changes to the synced opencode config: - Model provider. opencode can't list models from a custom OpenAI-compatible provider itself, so sync.sh queries the gateway's /v1/models with the user's key and materializes the map client-side. New gateway models land on every box at the next sync with no config edit. If discovery fails the previous map is carried over, so a gateway hiccup never empties the list. - Aggregate MCP endpoint. One entry at /mcp/ (trailing slash required — the bare path 307-redirects and MCP clients don't follow it) exposes every server the key can access, replacing the per-server gitea entry. sync.sh also exports OPENCODE_CONFIG: opencode does not auto-load extra files from its config dir, so without it the synced file was ignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+32
-15
@@ -10,18 +10,33 @@ sync — the laptops pull from Gitea's `main`.)
|
|||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
- `opencode.gateway.json` — the shared server list. **Identical for every
|
- `opencode.gateway.json` — the shared base config: the `covert-gpt` model
|
||||||
user**: the per-user key is injected via `{env:LITELLM_KEY}`, never written
|
provider (LiteLLM's OpenAI-compatible endpoint at
|
||||||
into the file. Add a gateway server here and everyone picks it up next shell.
|
`https://llm.covert-gpt.com/v1`, models map left empty — it is filled by
|
||||||
- `sync.sh` — sourced from each user's shell rc. On a new shell it pulls
|
discovery at sync time) and a single MCP entry for the gateway's
|
||||||
`opencode.gateway.json` (throttled ~1h), drops it where opencode merges it,
|
**aggregate** endpoint `https://llm.covert-gpt.com/mcp/` (trailing slash
|
||||||
and exports `LITELLM_KEY` from `~/.config/mcp-sync/key`. Silent + non-fatal:
|
required; the bare path 307-redirects, which MCP clients don't follow).
|
||||||
a Gitea hiccup never breaks the shell, last-good config stays in place.
|
That one endpoint exposes every MCP server the user's key can access, so
|
||||||
|
new gateway servers need no client config change at all. **Identical for
|
||||||
|
every user**: the per-user key is injected via `{env:LITELLM_KEY}`, never
|
||||||
|
written into the file.
|
||||||
|
- `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 result 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.
|
||||||
- `install.sh` — one-time bootstrap (curl-pipe). Fetches `sync.sh`, stores the
|
- `install.sh` — one-time bootstrap (curl-pipe). Fetches `sync.sh`, stores the
|
||||||
user's key (0600), and adds the rc hook.
|
user's key (0600), and adds the rc hook.
|
||||||
|
|
||||||
opencode **merges** config files, so the synced `opencode.gateway.json`
|
opencode merges the `OPENCODE_CONFIG` file on top of the user's global
|
||||||
coexists with each user's own `~/.config/opencode/opencode.json` and any
|
`~/.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
|
**local-only** MCP servers (e.g. a client-side `kubernetes` pointed at their
|
||||||
own kubeconfig). Sync never touches those.
|
own kubeconfig). Sync never touches those.
|
||||||
|
|
||||||
@@ -37,13 +52,15 @@ curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/mcp-confi
|
|||||||
|
|
||||||
Open a new terminal, run `opencode`, and the gateway tools are there.
|
Open a new terminal, run `opencode`, and the gateway tools are there.
|
||||||
|
|
||||||
## Adding a gateway server (admin)
|
## Adding a gateway server or model (admin)
|
||||||
|
|
||||||
1. Register it on the gateway (`charts/inference/values.yaml` →
|
Register it on the gateway (`charts/inference/values.yaml` →
|
||||||
`proxy_config.mcp_servers`), deploy, smoke-test.
|
`proxy_config.mcp_servers` for MCP servers, `model_list` for models),
|
||||||
2. Add a matching entry to `opencode.gateway.json` here and push to `main`.
|
deploy, smoke-test. That's it — clients auto-discover both: MCP servers
|
||||||
3. Users get it automatically within the sync TTL (or immediately on a new
|
through the aggregate `/mcp/` endpoint, models via the `/v1/models` query
|
||||||
shell). No per-user action.
|
in `sync.sh`. Users pick them up within the sync TTL (or immediately on a
|
||||||
|
new shell). `opencode.gateway.json` here only needs editing for structural
|
||||||
|
changes (a new provider, endpoint URLs).
|
||||||
|
|
||||||
For **per-user / per-target** tools (cluster access, etc.) do NOT add them
|
For **per-user / per-target** tools (cluster access, etc.) do NOT add them
|
||||||
here — those run client-side with the user's own credentials. See
|
here — those run client-side with the user's own credentials. See
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"covert-gpt": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "covert-gpt (LiteLLM)",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "https://llm.covert-gpt.com/v1",
|
||||||
|
"apiKey": "{env:LITELLM_KEY}"
|
||||||
|
},
|
||||||
|
"models": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"gitea": {
|
"covert-gpt": {
|
||||||
"type": "remote",
|
"type": "remote",
|
||||||
"url": "https://llm.covert-gpt.com/mcp/gitea",
|
"url": "https://llm.covert-gpt.com/mcp/",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"headers": {
|
"headers": {
|
||||||
"Authorization": "Bearer {env:LITELLM_KEY}"
|
"Authorization": "Bearer {env:LITELLM_KEY}"
|
||||||
|
|||||||
+113
-44
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# mcp-sync — refresh the shared LiteLLM-gateway MCP config on the laptop and
|
# mcp-sync — refresh the shared LiteLLM-gateway opencode config on the laptop,
|
||||||
# export the user's LiteLLM key into the environment.
|
# auto-discover the gateway's models, and export the user's LiteLLM key.
|
||||||
#
|
#
|
||||||
# Sourced from the user's shell rc (installed by install.sh) so it runs at the
|
# Sourced from the user's shell rc (installed by install.sh) so it runs at the
|
||||||
# start of every interactive shell ("session start"). It is intentionally:
|
# start of every interactive shell ("session start"). It is intentionally:
|
||||||
@@ -10,14 +10,21 @@
|
|||||||
#
|
#
|
||||||
# What it does:
|
# What it does:
|
||||||
# 1. Fetch the shared opencode.gateway.json from Gitea (the single source of
|
# 1. Fetch the shared opencode.gateway.json from Gitea (the single source of
|
||||||
# truth for which gateway MCP servers exist). The file is identical for
|
# truth: the covert-gpt provider + the aggregate MCP endpoint). The file
|
||||||
# all users — the per-user key is injected via {env:LITELLM_KEY}, never
|
# is identical for all users — the per-user key is injected via
|
||||||
# baked into the file.
|
# {env:LITELLM_KEY}, never baked into the file.
|
||||||
# 2. Drop it at ~/.config/opencode/opencode.gateway.json. opencode MERGES
|
# 2. Auto-discover models: query the gateway's /v1/models with the user's
|
||||||
# config files, so this coexists with the user's own opencode.json and
|
# key and fill the provider's "models" map. New models exposed on the
|
||||||
# any local-only MCP servers (e.g. a client-side kubernetes pointed at
|
# gateway appear on every box at the next sync — no config edit anywhere.
|
||||||
# their kubeconfig) — those are never touched.
|
# (opencode cannot list models from a custom provider itself; the map
|
||||||
# 3. Export LITELLM_KEY from ~/.config/mcp-sync/key so {env:LITELLM_KEY}
|
# must be materialized client-side.) MCP servers need no discovery step:
|
||||||
|
# the single /mcp/ endpoint aggregates every server the key can access.
|
||||||
|
# 3. Drop the result at ~/.config/opencode/opencode.gateway.json and export
|
||||||
|
# OPENCODE_CONFIG pointing at it. opencode does NOT auto-load extra files
|
||||||
|
# in its config dir — it only merges known locations, and OPENCODE_CONFIG
|
||||||
|
# is merged between the global opencode.json and any project config. The
|
||||||
|
# user's own opencode.json and local-only MCP servers are never touched.
|
||||||
|
# 4. Export LITELLM_KEY from ~/.config/mcp-sync/key so {env:LITELLM_KEY}
|
||||||
# resolves.
|
# resolves.
|
||||||
|
|
||||||
# --- config (overridable via env before sourcing) -------------------------
|
# --- config (overridable via env before sourcing) -------------------------
|
||||||
@@ -25,40 +32,14 @@ MCP_SYNC_RAW_URL="${MCP_SYNC_RAW_URL:-https://git.nic-oconnor.com/public/dotfile
|
|||||||
MCP_SYNC_HOME="${MCP_SYNC_HOME:-$HOME/.config/mcp-sync}"
|
MCP_SYNC_HOME="${MCP_SYNC_HOME:-$HOME/.config/mcp-sync}"
|
||||||
MCP_SYNC_DEST="${MCP_SYNC_DEST:-$HOME/.config/opencode/opencode.gateway.json}"
|
MCP_SYNC_DEST="${MCP_SYNC_DEST:-$HOME/.config/opencode/opencode.gateway.json}"
|
||||||
MCP_SYNC_KEYFILE="${MCP_SYNC_KEYFILE:-$MCP_SYNC_HOME/key}"
|
MCP_SYNC_KEYFILE="${MCP_SYNC_KEYFILE:-$MCP_SYNC_HOME/key}"
|
||||||
# Throttle: only re-pull if the cached copy is older than this many seconds
|
# Provider whose models are auto-discovered, and the gateway base URL.
|
||||||
# (default 1h). Keeps every new shell from hammering Gitea.
|
MCP_SYNC_PROVIDER="${MCP_SYNC_PROVIDER:-covert-gpt}"
|
||||||
|
MCP_SYNC_GATEWAY="${MCP_SYNC_GATEWAY:-https://llm.covert-gpt.com}"
|
||||||
|
# Throttle: only re-sync if the deployed copy is older than this many seconds
|
||||||
|
# (default 1h). Keeps every new shell from hammering Gitea/the gateway.
|
||||||
MCP_SYNC_TTL="${MCP_SYNC_TTL:-3600}"
|
MCP_SYNC_TTL="${MCP_SYNC_TTL:-3600}"
|
||||||
|
|
||||||
# --- 1+2. refresh the gateway config (throttled, non-fatal) ---------------
|
# --- export the per-user key (first: model discovery needs it) -------------
|
||||||
_mcp_sync_fetch() {
|
|
||||||
mkdir -p "$(dirname "$MCP_SYNC_DEST")" || return 0
|
|
||||||
|
|
||||||
# Skip if we pulled recently (mtime within TTL). `find -mmin` is portable
|
|
||||||
# enough; fall back to always-fetch if find is unavailable.
|
|
||||||
if [ -f "$MCP_SYNC_DEST" ] && command -v find >/dev/null 2>&1; then
|
|
||||||
if find "$MCP_SYNC_DEST" -mmin -"$(( MCP_SYNC_TTL / 60 ))" 2>/dev/null | grep -q .; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
command -v curl >/dev/null 2>&1 || return 0
|
|
||||||
tmp="$(mktemp 2>/dev/null)" || return 0
|
|
||||||
# -f: fail on HTTP error; -s: silent; -m: hard timeout so a hung network
|
|
||||||
# can't stall the shell.
|
|
||||||
if curl -fsS -m 8 "$MCP_SYNC_RAW_URL" -o "$tmp" 2>/dev/null; then
|
|
||||||
# Only replace if it's non-empty and valid JSON (don't clobber good config
|
|
||||||
# with an error page).
|
|
||||||
if [ -s "$tmp" ] && { ! command -v python3 >/dev/null 2>&1 || python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$tmp" 2>/dev/null; }; then
|
|
||||||
mv "$tmp" "$MCP_SYNC_DEST" 2>/dev/null || rm -f "$tmp"
|
|
||||||
else
|
|
||||||
rm -f "$tmp"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
rm -f "$tmp"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. export the per-user key -------------------------------------------
|
|
||||||
_mcp_sync_export_key() {
|
_mcp_sync_export_key() {
|
||||||
[ -f "$MCP_SYNC_KEYFILE" ] || return 0
|
[ -f "$MCP_SYNC_KEYFILE" ] || return 0
|
||||||
# Strip whitespace/newline; export so {env:LITELLM_KEY} resolves in opencode.
|
# Strip whitespace/newline; export so {env:LITELLM_KEY} resolves in opencode.
|
||||||
@@ -66,6 +47,94 @@ _mcp_sync_export_key() {
|
|||||||
[ -n "$LITELLM_KEY" ] && export LITELLM_KEY
|
[ -n "$LITELLM_KEY" ] && export LITELLM_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
_mcp_sync_fetch
|
# --- refresh config + discover models (throttled, non-fatal) ---------------
|
||||||
|
_mcp_sync_refresh() {
|
||||||
|
mkdir -p "$(dirname "$MCP_SYNC_DEST")" 2>/dev/null || return 0
|
||||||
|
|
||||||
|
# Skip if we synced recently (mtime within TTL). `find -mmin` is portable
|
||||||
|
# enough; fall back to always-sync if find is unavailable.
|
||||||
|
if [ -f "$MCP_SYNC_DEST" ] && command -v find >/dev/null 2>&1; then
|
||||||
|
if find "$MCP_SYNC_DEST" -mmin -"$(( MCP_SYNC_TTL / 60 ))" 2>/dev/null | grep -q .; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v curl >/dev/null 2>&1 || return 0
|
||||||
|
|
||||||
|
# 1. Fetch the shared base config. On failure fall back to the currently
|
||||||
|
# deployed file so model discovery can still refresh it.
|
||||||
|
_base="$(mktemp 2>/dev/null)" || return 0
|
||||||
|
# -f: fail on HTTP error; -s: silent; -m: hard timeout so a hung network
|
||||||
|
# can't stall the shell.
|
||||||
|
if ! curl -fsS -m 8 "$MCP_SYNC_RAW_URL" -o "$_base" 2>/dev/null || [ ! -s "$_base" ]; then
|
||||||
|
rm -f "$_base"; _base=""
|
||||||
|
[ -f "$MCP_SYNC_DEST" ] || return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Discover the gateway's models with the user's key (best-effort).
|
||||||
|
_models=""
|
||||||
|
if [ -n "${LITELLM_KEY:-}" ]; then
|
||||||
|
_models="$(curl -fsS -m 8 -H "Authorization: Bearer $LITELLM_KEY" \
|
||||||
|
"$MCP_SYNC_GATEWAY/v1/models" 2>/dev/null)" || _models=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Merge and deploy. python3 validates the base (don't clobber good config
|
||||||
|
# with an error page), injects the discovered models, and — when discovery
|
||||||
|
# failed — carries over the previous deployed models map so a gateway
|
||||||
|
# hiccup never empties the model list. Without python3, deploy the raw
|
||||||
|
# base as-is (no discovery).
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
MCP_SYNC_BASE="$_base" MCP_SYNC_MODELS="$_models" \
|
||||||
|
MCP_SYNC_PY_DEST="$MCP_SYNC_DEST" MCP_SYNC_PY_PROVIDER="$MCP_SYNC_PROVIDER" \
|
||||||
|
python3 - <<'PYEOF' 2>/dev/null || true
|
||||||
|
import json, os, tempfile
|
||||||
|
|
||||||
|
dest = os.environ["MCP_SYNC_PY_DEST"]
|
||||||
|
provider = os.environ["MCP_SYNC_PY_PROVIDER"]
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
with open(path) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
base_path = os.environ.get("MCP_SYNC_BASE") or dest
|
||||||
|
try:
|
||||||
|
cfg = load(base_path)
|
||||||
|
except Exception:
|
||||||
|
raise SystemExit(0) # invalid base: keep last-good deployed config
|
||||||
|
|
||||||
|
models = {}
|
||||||
|
try:
|
||||||
|
data = json.loads(os.environ.get("MCP_SYNC_MODELS") or "{}")
|
||||||
|
models = {m["id"]: {} for m in data.get("data", []) if m.get("id")}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not models:
|
||||||
|
try: # discovery failed: carry over the previous deployed map
|
||||||
|
models = load(dest)["provider"][provider]["models"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
prov = cfg.get("provider", {}).get(provider)
|
||||||
|
if prov is not None and models:
|
||||||
|
prov["models"] = models
|
||||||
|
|
||||||
|
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(dest))
|
||||||
|
with os.fdopen(fd, "w") as f:
|
||||||
|
json.dump(cfg, f, indent=2)
|
||||||
|
os.replace(tmp, dest)
|
||||||
|
PYEOF
|
||||||
|
elif [ -n "$_base" ]; then
|
||||||
|
mv "$_base" "$MCP_SYNC_DEST" 2>/dev/null || true
|
||||||
|
_base=""
|
||||||
|
fi
|
||||||
|
[ -n "$_base" ] && rm -f "$_base"
|
||||||
|
}
|
||||||
|
|
||||||
_mcp_sync_export_key
|
_mcp_sync_export_key
|
||||||
unset -f _mcp_sync_fetch _mcp_sync_export_key 2>/dev/null || true
|
_mcp_sync_refresh
|
||||||
|
|
||||||
|
# opencode only merges config from known locations; point OPENCODE_CONFIG at
|
||||||
|
# the synced file so it is layered on top of the user's global opencode.json.
|
||||||
|
[ -f "$MCP_SYNC_DEST" ] && export OPENCODE_CONFIG="$MCP_SYNC_DEST"
|
||||||
|
|
||||||
|
unset -f _mcp_sync_export_key _mcp_sync_refresh 2>/dev/null || true
|
||||||
|
|||||||
Reference in New Issue
Block a user