mcp-config/hermes: make the gateway's context limits actually reach the clients
Folds in the in-flight local-checkout work and fixes the context accounting
on top of it. Both halves are the same bug seen from two ends: clients were
sending requests larger than the backend's window and getting hard
ContextWindowExceededError 400s, intermittently, in a way that looked like
the model or the GPU misbehaving.
Already in flight here (unchanged in intent):
- sync.sh prefers the LOCAL CHECKOUT over re-downloading from Gitea, and
install.sh records the checkout path so it can be found. That recording
is load-bearing: without it sync.sh cannot find the repo, silently falls
back to Gitea, and every local edit to opencode.gateway.json is ignored.
Observed on this box — ~/.config/mcp-sync/source did not exist, the
default ~/.dotfiles is not where the checkout lives, and the Gitea base
still served models:{}.
- the hermes custom-provider wiring.
Fixed on top:
opencode had NO context limit on ANY gateway model, not a wrong one. Two
faults compounded. /model/info 403s for a day-to-day virtual key (only
management-scoped keys may read it), which sync.sh treats as best-effort and
falls back to names-only — as designed and as commented. But the merge then
did `prov["models"] = models`, replacing the map wholesale, so limits
declared in the base config were discarded too and there was no floor left.
Base specs are now a floor that live /model/info overrides field by field:
discovery still wins where present, so this adds no drift, and a 403 no
longer strips the limits. Verified: ornith 195584/32768, the qwens
261120/32768.
hermes' model map named `qwen3-vl-30b-a3b` at 262144 — a model the gateway
stopped serving when Ornith replaced it — while ornith-1.0-35b, the model
actually on cg-1 AND the only one with a window smaller than its neighbours
(196608), had no entry at all and inherited Hermes' default. The one model
needing a lower ceiling was the one not declaring one, which is exactly the
shape that fails only once a session grows.
NOTE for other boxes: hermes reads ~/.hermes/config.yaml, which install.sh
produces by MERGING gateway.yaml. Editing gateway.yaml alone changes nothing
until install.sh re-runs — this machine's live config was patched by hand.
Not addressed here, deliberately: the gateway-side context_window_fallback
(ai-k8s-iac). It is wired but inert while cg-2 is scaled down, so preventing
the oversized request client-side is what actually works today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoDK4wwB1Twty5gwiriYLf
This commit is contained in:
+48
-7
@@ -62,7 +62,7 @@ config. Each user exports them in `~/.zshrc.local`:
|
||||
| MCP server | env vars |
|
||||
|---|---|
|
||||
| `gitea` | `GITEA_KEY` |
|
||||
| `proxmox` | `PROXMOX_MCP_TOKEN_ID`, `PROXMOX_MCP_TOKEN_SECRET` |
|
||||
| `proxmox` | `PROXMOX_TOKEN_ID`, `PROXMOX_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/`).
|
||||
@@ -87,7 +87,8 @@ sync — the laptops pull from Gitea's `main`.)
|
||||
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
|
||||
~1h) it reads the base config **from the local checkout** (see "Where the
|
||||
config comes from" below), 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
|
||||
@@ -101,12 +102,15 @@ sync — the laptops pull from Gitea's `main`.)
|
||||
`~/.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
|
||||
truth**; the tree is copied from the checkout (or, with no checkout, 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.
|
||||
- `install.sh` — one-time bootstrap. Installs `sync.sh` (from the checkout it
|
||||
was run out of, else the Gitea API), records that checkout in
|
||||
`~/.config/mcp-sync/source`, 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`),
|
||||
@@ -114,6 +118,38 @@ 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.
|
||||
|
||||
## Where the config comes from
|
||||
|
||||
**The local checkout is the source.** `bootstrap.sh` already clones this repo
|
||||
over authenticated git, so the per-shell hook reads `opencode.gateway.json` and
|
||||
`skills/` straight off disk. No HTTP, no anonymous read, works offline.
|
||||
|
||||
This is deliberate. The hook used to re-download both from Gitea's public
|
||||
`/raw/` URL on every sync, which meant **the repo had to be world-readable for
|
||||
a shell hook to work** — publishing every internal hostname and the whole MCP
|
||||
server inventory as the price of a config refresh. Worse, Gitea answers an
|
||||
unauthorized `/raw/` request with a **200 and an HTML login page**, so `curl -f`
|
||||
reported success and wrote HTML over the config; the failure was silent and the
|
||||
config simply froze at its last good copy.
|
||||
|
||||
Resolution order, for both the config and the skills:
|
||||
|
||||
1. `MCP_SYNC_REPO` / `MCP_SYNC_RAW_URL` / `MCP_SYNC_SKILLS_SRC` if set — the
|
||||
dev-checkout workflow in `zsh/zshrc.local.example`.
|
||||
2. The checkout recorded in `~/.config/mcp-sync/source` by `install.sh`,
|
||||
falling back to `$DOTFILES_DIR` (`~/.dotfiles`).
|
||||
3. The **Gitea API** with `$GITEA_KEY` — for a box with no checkout at all.
|
||||
API endpoints, not the web `/raw/` route, because the API returns an honest
|
||||
401/403 that `curl -f` can actually detect.
|
||||
|
||||
To pick up a pushed change, the hook runs `git pull --ff-only` on the checkout
|
||||
— but **only** the bootstrap-managed one at `$DOTFILES_DIR`, and only when its
|
||||
tree is clean and on the default branch. A checkout you work in is read, never
|
||||
pulled out from under you.
|
||||
|
||||
Model discovery is unaffected: it still queries the gateway's `/v1/models`
|
||||
live on every sync, so a new model appears without touching the repo.
|
||||
|
||||
## User setup (one time)
|
||||
|
||||
Each user needs a LiteLLM **virtual key** (for models) plus their **own**
|
||||
@@ -121,16 +157,21 @@ credentials for each MCP server. Run `bootstrap.sh` (it installs Node, which
|
||||
provides the `npx` the MCP servers need), then:
|
||||
|
||||
```sh
|
||||
# From a checkout (what bootstrap.sh does — no network needed for the config):
|
||||
./mcp-config/install.sh
|
||||
|
||||
# Or standalone on a box with no checkout:
|
||||
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)
|
||||
# If this repo is private, export GITEA_KEY first so the fallback can read it.
|
||||
```
|
||||
|
||||
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
|
||||
export PROXMOX_TOKEN_ID=... # e.g. alice@pve!mcp
|
||||
export PROXMOX_TOKEN_SECRET=... # the token UUID
|
||||
```
|
||||
|
||||
Open a new terminal, run `opencode`, and the tools are there — acting as **you**.
|
||||
|
||||
+97
-4
@@ -14,7 +14,14 @@
|
||||
# After this, every new shell refreshes the gateway config automatically.
|
||||
set -eu
|
||||
|
||||
RAW_BASE="${MCP_SYNC_RAW_BASE:-https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/mcp-config}"
|
||||
# Fallback source, used ONLY when this script is not run from a checkout.
|
||||
# A Gitea *API* endpoint, not the web /raw/ route: /raw/ answers an
|
||||
# unauthorized request with a 200 and an HTML login page, which `curl -f`
|
||||
# reports as success. The API returns an honest 401/403. Set GITEA_KEY to read
|
||||
# a private repo.
|
||||
API_BASE="${MCP_SYNC_API_BASE:-https://git.nic-oconnor.com/api/v1/repos/public/dotfiles}"
|
||||
REF="${MCP_SYNC_REF:-main}"
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd 2>/dev/null)" || SCRIPT_DIR=""
|
||||
SYNC_HOME="$HOME/.config/mcp-sync"
|
||||
SYNC_SH="$SYNC_HOME/sync.sh"
|
||||
KEYFILE="$SYNC_HOME/key"
|
||||
@@ -26,12 +33,66 @@ command -v curl >/dev/null 2>&1 || { say "error: curl is required"; exit 1; }
|
||||
|
||||
mkdir -p "$SYNC_HOME"
|
||||
|
||||
# --- 1. fetch the sync hook ------------------------------------------------
|
||||
say "Fetching mcp-sync hook..."
|
||||
curl -fsSL -m 15 "$RAW_BASE/sync.sh" -o "$SYNC_SH"
|
||||
# --- 1. install the sync hook ----------------------------------------------
|
||||
# Prefer the copy sitting next to this script: when bootstrap.sh runs the
|
||||
# module from a checkout there is no reason to go to the network, and it makes
|
||||
# the module work on a box that cannot reach Gitea at all.
|
||||
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/sync.sh" ]; then
|
||||
say "Installing mcp-sync hook from $SCRIPT_DIR/sync.sh"
|
||||
cp "$SCRIPT_DIR/sync.sh" "$SYNC_SH"
|
||||
else
|
||||
SYNC_URL="$API_BASE/raw/mcp-config/sync.sh?ref=$REF"
|
||||
say "Fetching mcp-sync hook..."
|
||||
# Fetch to a temp file and CHECK IT before overwriting the installed hook.
|
||||
# The API endpoint gives an honest 4xx, but keep the shebang check as a
|
||||
# backstop: if anything ever writes an HTML login page over sync.sh, the shell
|
||||
# fails to parse it, the hook silently does nothing, OPENCODE_CONFIG is never
|
||||
# exported, and opencode comes up with no covert-gpt provider at all.
|
||||
_tmp="$(mktemp)" || { say "error: mktemp failed"; exit 1; }
|
||||
if [ -n "${GITEA_KEY:-}" ]; then
|
||||
set -- -H "Authorization: token $GITEA_KEY"
|
||||
else
|
||||
set --
|
||||
fi
|
||||
if ! curl -fsSL -m 15 "$@" "$SYNC_URL" -o "$_tmp" || [ ! -s "$_tmp" ]; then
|
||||
rm -f "$_tmp"
|
||||
say "error: could not fetch $SYNC_URL"
|
||||
[ -n "${GITEA_KEY:-}" ] ||
|
||||
say " The repo may be private — export GITEA_KEY and re-run."
|
||||
exit 1
|
||||
fi
|
||||
case "$(head -c 2 "$_tmp")" in
|
||||
'#!') mv "$_tmp" "$SYNC_SH" ;;
|
||||
*)
|
||||
rm -f "$_tmp"
|
||||
say "error: $SYNC_URL did not return a script."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
chmod 0644 "$SYNC_SH"
|
||||
|
||||
# --- 1b. record the checkout as the config source --------------------------
|
||||
# THE POINT OF THIS FILE: bootstrap.sh already cloned this repo over
|
||||
# authenticated git, so the per-shell hook must never have to re-fetch the
|
||||
# gateway config anonymously over HTTP. Needing that was the only reason the
|
||||
# repo ever had to be world-readable. Record where we ran from and sync.sh
|
||||
# reads both the config and the skills straight off disk.
|
||||
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/opencode.gateway.json" ]; then
|
||||
if REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd 2>/dev/null)"; then
|
||||
printf '%s\n' "$REPO_DIR" > "$SYNC_HOME/source"
|
||||
say "Config source: $REPO_DIR (local checkout, no network needed)."
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 2. capture the LiteLLM key -------------------------------------------
|
||||
# An already-stored key is as good as one in the env: bootstrap.sh re-runs this
|
||||
# module on every pass, and prompting a box that is already configured would
|
||||
# make an otherwise unattended bootstrap hang on a TTY read.
|
||||
if [ "${LITELLM_KEY:-}" = "" ] && [ -f "$KEYFILE" ]; then
|
||||
LITELLM_KEY="$(cat "$KEYFILE" 2>/dev/null || true)"
|
||||
[ -n "$LITELLM_KEY" ] && say "Reusing the key already stored at $KEYFILE."
|
||||
fi
|
||||
if [ "${LITELLM_KEY:-}" = "" ]; then
|
||||
# Read from the controlling TTY since stdin is the piped script.
|
||||
if [ -r /dev/tty ]; then
|
||||
@@ -83,6 +144,38 @@ fi
|
||||
# shellcheck disable=SC1090
|
||||
. "$SYNC_SH" || true
|
||||
|
||||
# If this is the initial install (key just written now), restart Hermes.
|
||||
# The key file mtime changed, but we need to compare hashes to know if it's
|
||||
# actually new (not just a touch). Only restart if we're not in a re-run.
|
||||
if [ -f "$KEYFILE" ]; then
|
||||
KEY_HASH="$(sha256sum "$KEYFILE" 2>/dev/null | cut -d' ' -f1)"
|
||||
PREV_KEY_HASH=""
|
||||
if [ -f "$HOME/.config/mcp-sync/.key-hash" ]; then
|
||||
PREV_KEY_HASH="$(cat "$HOME/.config/mcp-sync/.key-hash" 2>/dev/null || true)"
|
||||
fi
|
||||
if [ "$KEY_HASH" != "$PREV_KEY_HASH" ]; then
|
||||
# Key changed - update .env and restart Hermes if running
|
||||
if [ -f "$HOME/.hermes/.env" ]; then
|
||||
say "hermes: Key changed during install — updating ~/.hermes/.env"
|
||||
if command -v sed >/dev/null 2>&1; then
|
||||
sed -i "s|^LITELLM_KEY=.*$|LITELLM_KEY=$LITELLM_KEY|" "$HOME/.hermes/.env"
|
||||
else
|
||||
awk -v key="$LITELLM_KEY" '/^LITELLM_KEY=/ {print "LITELLM_KEY=" key; next} {print}' "$HOME/.hermes/.env" >"$HOME/.hermes/.env.tmp" && mv "$HOME/.hermes/.env.tmp" "$HOME/.hermes/.env"
|
||||
fi
|
||||
chmod 0600 "$HOME/.hermes/.env"
|
||||
fi
|
||||
if pgrep -f 'hermes-agent/hermes' >/dev/null 2>&1; then
|
||||
HERMES_PID="$(pgrep -f 'hermes-agent/hermes')"
|
||||
say "hermes: Restarting to pick up new key (PID: $HERMES_PID)..."
|
||||
kill "$HERMES_PID" || true
|
||||
sleep 1
|
||||
(cd "$HOME" && nohup hermes >/dev/null 2>&1 &)
|
||||
say "hermes: Hermes restarted."
|
||||
fi
|
||||
fi
|
||||
printf '%s\n' "$KEY_HASH" >"$HOME/.config/mcp-sync/.key-hash"
|
||||
fi
|
||||
|
||||
say ""
|
||||
say "Done. Open a new terminal (or 'source $RC') and run opencode."
|
||||
say "The gateway config refreshes automatically on each new shell."
|
||||
|
||||
@@ -8,13 +8,36 @@
|
||||
"baseURL": "https://llm.covert-gpt.com/v1",
|
||||
"apiKey": "{env:LITELLM_KEY}"
|
||||
},
|
||||
"models": {}
|
||||
"models": {
|
||||
"ornith-1.0-35b": {
|
||||
"limit": {
|
||||
"context": 195584,
|
||||
"output": 32768
|
||||
}
|
||||
},
|
||||
"qwen3.8-27b": {
|
||||
"limit": {
|
||||
"context": 261120,
|
||||
"output": 32768
|
||||
}
|
||||
},
|
||||
"qwen3-coder-next": {
|
||||
"limit": {
|
||||
"context": 261120,
|
||||
"output": 32768
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "gitea-mcp@0.0.10"],
|
||||
"command": [
|
||||
"npx",
|
||||
"-y",
|
||||
"gitea-mcp@0.0.10"
|
||||
],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"GITEA_HOST": "https://git.nic-oconnor.com",
|
||||
@@ -23,18 +46,24 @@
|
||||
},
|
||||
"proxmox": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "proxmox-mcp-server@0.2.0"],
|
||||
"command": [
|
||||
"npx",
|
||||
"-y",
|
||||
"proxmox-mcp-server@0.2.0"
|
||||
],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"PROXMOX_HOST": "https://prox-01.covert-cloud.com:8006",
|
||||
"PROXMOX_TOKEN_ID": "{env:PROXMOX_MCP_TOKEN_ID}",
|
||||
"PROXMOX_TOKEN_SECRET": "{env:PROXMOX_MCP_TOKEN_SECRET}",
|
||||
"PROXMOX_TOKEN_ID": "{env:PROXMOX_TOKEN_ID}",
|
||||
"PROXMOX_TOKEN_SECRET": "{env:PROXMOX_TOKEN_SECRET}",
|
||||
"PROXMOX_INSECURE_TLS": "true",
|
||||
"PVE_READONLY": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"paths": ["{env:HOME}/.config/mcp-sync/skills"]
|
||||
"paths": [
|
||||
"{env:HOME}/.config/mcp-sync/skills"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+162
-26
@@ -10,10 +10,15 @@
|
||||
# every failure path just returns, leaving the last-good config in place.
|
||||
#
|
||||
# What it does:
|
||||
# 1. Fetch the shared opencode.gateway.json from Gitea (the single source of
|
||||
# truth: the covert-gpt provider + the client-side MCP servers). The file
|
||||
# is identical for all users — EVERY credential (LiteLLM key, Gitea PAT,
|
||||
# Proxmox token) is injected via {env:VAR} and never baked into the file.
|
||||
# 1. Read the shared opencode.gateway.json (the single source of truth: the
|
||||
# covert-gpt provider + the client-side MCP servers). PREFERRED SOURCE IS
|
||||
# THE LOCAL CHECKOUT — bootstrap.sh already cloned this repo over
|
||||
# authenticated git, so re-downloading a file that is sitting on disk is
|
||||
# both redundant and, historically, the ONLY reason the repo had to be
|
||||
# world-readable. A box with no checkout falls back to the Gitea API with
|
||||
# $GITEA_KEY. The file is identical for all users — EVERY credential
|
||||
# (LiteLLM key, Gitea PAT, Proxmox token) is injected via {env:VAR} and
|
||||
# never baked into the file.
|
||||
# 2. Auto-discover models: query the gateway's /v1/models with the user's
|
||||
# key and fill the provider's "models" map. New models exposed on the
|
||||
# gateway appear on every box at the next sync — no config edit anywhere.
|
||||
@@ -37,18 +42,32 @@
|
||||
# resolves.
|
||||
|
||||
# --- config (overridable via env before sourcing) -------------------------
|
||||
MCP_SYNC_RAW_URL="${MCP_SYNC_RAW_URL:-https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/mcp-config/opencode.gateway.json}"
|
||||
# The local checkout is the preferred source for BOTH the config and the
|
||||
# skills; these URLs are the no-checkout fallback only. They are Gitea *API*
|
||||
# endpoints on purpose: the web /raw/ route answers an unauthorized request
|
||||
# with a 200 and an HTML login page, which `curl -f` treats as success and
|
||||
# happily writes over the file it was fetching. The API answers with an honest
|
||||
# 401/403, so -f can be trusted.
|
||||
MCP_SYNC_API_BASE="${MCP_SYNC_API_BASE:-https://git.nic-oconnor.com/api/v1/repos/public/dotfiles}"
|
||||
MCP_SYNC_REF="${MCP_SYNC_REF:-main}"
|
||||
MCP_SYNC_RAW_URL="${MCP_SYNC_RAW_URL:-$MCP_SYNC_API_BASE/raw/mcp-config/opencode.gateway.json?ref=$MCP_SYNC_REF}"
|
||||
MCP_SYNC_HOME="${MCP_SYNC_HOME:-$HOME/.config/mcp-sync}"
|
||||
# Where the checkout is. install.sh records the tree it ran from; failing that
|
||||
# we look where bootstrap.sh clones to.
|
||||
MCP_SYNC_REPO="${MCP_SYNC_REPO:-}"
|
||||
MCP_SYNC_REPO_FILE="${MCP_SYNC_REPO_FILE:-$MCP_SYNC_HOME/source}"
|
||||
MCP_SYNC_DOTFILES_DIR="${MCP_SYNC_DOTFILES_DIR:-$HOME/.dotfiles}"
|
||||
MCP_SYNC_DEST="${MCP_SYNC_DEST:-$HOME/.config/opencode/opencode.gateway.json}"
|
||||
MCP_SYNC_KEYFILE="${MCP_SYNC_KEYFILE:-$MCP_SYNC_HOME/key}"
|
||||
# Provider whose models are auto-discovered, and the gateway base URL.
|
||||
MCP_SYNC_PROVIDER="${MCP_SYNC_PROVIDER:-covert-gpt}"
|
||||
MCP_SYNC_GATEWAY="${MCP_SYNC_GATEWAY:-https://llm.covert-gpt.com}"
|
||||
# Skills. A skill is a folder of files, so unlike the config there is no single
|
||||
# raw URL to pull — we take the repo tarball and lift out its skills/ subtree.
|
||||
# Set MCP_SYNC_SKILLS_SRC to a local directory (or file://) to sync from a
|
||||
# working checkout instead; see zsh/zshrc.local.example.
|
||||
MCP_SYNC_SKILLS_SRC="${MCP_SYNC_SKILLS_SRC:-https://git.nic-oconnor.com/public/dotfiles/archive/main.tar.gz}"
|
||||
# Skills. Taken from the checkout like the config; with no checkout we fall
|
||||
# back to the repo tarball and lift out its skills/ subtree (a skill is a folder
|
||||
# of files, so unlike the config there is no single raw URL to pull). Set
|
||||
# MCP_SYNC_SKILLS_SRC to a local directory (or file://) to pin it to one tree;
|
||||
# see zsh/zshrc.local.example.
|
||||
MCP_SYNC_SKILLS_SRC="${MCP_SYNC_SKILLS_SRC:-$MCP_SYNC_API_BASE/archive/$MCP_SYNC_REF.tar.gz}"
|
||||
MCP_SYNC_SKILLS_SUBDIR="${MCP_SYNC_SKILLS_SUBDIR:-skills}"
|
||||
MCP_SYNC_SKILLS_DEST="${MCP_SYNC_SKILLS_DEST:-$MCP_SYNC_HOME/skills}"
|
||||
# Hermes: the SAME MCP servers, rendered into Hermes' own schema. Set
|
||||
@@ -69,6 +88,67 @@ _mcp_sync_export_key() {
|
||||
[ -n "$LITELLM_KEY" ] && export LITELLM_KEY
|
||||
}
|
||||
|
||||
# --- locate the local checkout (the preferred source) ----------------------
|
||||
# Prints the repo root on stdout, or fails. Order: explicit override, the path
|
||||
# install.sh recorded, then bootstrap.sh's clone location. A directory only
|
||||
# counts if it actually holds the config, so a half-deleted tree falls through
|
||||
# to the network rather than deploying nothing.
|
||||
_mcp_sync_find_repo() {
|
||||
for _cand in "$MCP_SYNC_REPO" \
|
||||
"$([ -f "$MCP_SYNC_REPO_FILE" ] && cat "$MCP_SYNC_REPO_FILE" 2>/dev/null)" \
|
||||
"$MCP_SYNC_DOTFILES_DIR"; do
|
||||
[ -n "$_cand" ] || continue
|
||||
if [ -f "$_cand/mcp-config/opencode.gateway.json" ]; then
|
||||
printf '%s\n' "$_cand"
|
||||
unset _cand
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
unset _cand
|
||||
return 1
|
||||
}
|
||||
|
||||
# git, authenticated with the Gitea PAT when we have one, so a PRIVATE repo
|
||||
# works. The token goes through the environment rather than `git -c ...`
|
||||
# because argv is world-readable in `ps`.
|
||||
_mcp_sync_git() {
|
||||
if [ -n "${GITEA_KEY:-}" ]; then
|
||||
GIT_CONFIG_COUNT=1 \
|
||||
GIT_CONFIG_KEY_0=http.extraHeader \
|
||||
GIT_CONFIG_VALUE_0="Authorization: token $GITEA_KEY" \
|
||||
git "$@"
|
||||
else
|
||||
git "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Refresh the checkout so a pushed config change actually lands. ONLY for the
|
||||
# bootstrap-managed clone: a recorded DEV checkout (someone's ~/source/dotfiles)
|
||||
# must never be pulled out from under them by a shell hook. Skipped unless the
|
||||
# tree is clean and sitting on its default branch; quiet and non-fatal always.
|
||||
_mcp_sync_pull() {
|
||||
[ "$1" = "$MCP_SYNC_DOTFILES_DIR" ] || return 0
|
||||
[ -d "$1/.git" ] || return 0
|
||||
command -v git >/dev/null 2>&1 || return 0
|
||||
[ -z "$(git -C "$1" status --porcelain 2>/dev/null)" ] || return 0
|
||||
_branch="$(git -C "$1" symbolic-ref --short -q HEAD 2>/dev/null)" || return 0
|
||||
_default="$(git -C "$1" symbolic-ref --short -q refs/remotes/origin/HEAD 2>/dev/null)"
|
||||
_default="${_default#origin/}"
|
||||
[ -n "$_default" ] || _default=main
|
||||
[ "$_branch" = "$_default" ] || { unset _branch _default; return 0; }
|
||||
unset _branch _default
|
||||
_mcp_sync_git -C "$1" pull --ff-only --quiet >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# curl, with the Gitea PAT when we have one. Used only on the no-checkout path.
|
||||
_mcp_sync_curl() {
|
||||
if [ -n "${GITEA_KEY:-}" ]; then
|
||||
curl -fsS -m "${3:-8}" -H "Authorization: token $GITEA_KEY" "$1" -o "$2" 2>/dev/null
|
||||
else
|
||||
curl -fsS -m "${3:-8}" "$1" -o "$2" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# --- refresh config + discover models (throttled, non-fatal) ---------------
|
||||
_mcp_sync_refresh() {
|
||||
mkdir -p "$(dirname "$MCP_SYNC_DEST")" 2>/dev/null || return 0
|
||||
@@ -81,16 +161,42 @@ _mcp_sync_refresh() {
|
||||
fi
|
||||
fi
|
||||
|
||||
command -v curl >/dev/null 2>&1 || return 0
|
||||
# 1. Get the shared base config. THE CHECKOUT WINS: it is already on disk,
|
||||
# was fetched over authenticated git, and needs no network at all. Only a
|
||||
# box with no checkout goes to Gitea. On total failure we fall back to the
|
||||
# currently deployed file so model discovery can still refresh it.
|
||||
_base=""
|
||||
_repo="$(_mcp_sync_find_repo)" || _repo=""
|
||||
if [ -n "$_repo" ]; then
|
||||
_mcp_sync_pull "$_repo"
|
||||
if [ -s "$_repo/mcp-config/opencode.gateway.json" ]; then
|
||||
_base="$(mktemp 2>/dev/null)" || return 0
|
||||
cp "$_repo/mcp-config/opencode.gateway.json" "$_base" 2>/dev/null ||
|
||||
{ rm -f "$_base"; _base=""; }
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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
|
||||
if [ -z "$_base" ]; then
|
||||
command -v curl >/dev/null 2>&1 || return 0
|
||||
_base="$(mktemp 2>/dev/null)" || return 0
|
||||
if ! _mcp_sync_curl "$MCP_SYNC_RAW_URL" "$_base" || [ ! -s "$_base" ]; then
|
||||
rm -f "$_base"; _base=""
|
||||
[ -f "$MCP_SYNC_DEST" ] || return 0
|
||||
elif [ "$(head -c 1 "$_base")" != "{" ]; then
|
||||
# A 200 that is not the config. The API endpoints we default to return an
|
||||
# honest 401/403, so this only fires for an override still pointing at
|
||||
# Gitea's web /raw/ route, which answers with a 200 HTML login page that
|
||||
# -f cannot detect. The python step below would reject it and keep the
|
||||
# last-good file — correct, but SILENTLY, leaving the config frozen with
|
||||
# nothing to show for it. Say so instead.
|
||||
rm -f "$_base"; _base=""
|
||||
printf 'mcp-sync: %s did not return JSON (Gitea login page?).\n' "$MCP_SYNC_RAW_URL" >&2
|
||||
printf 'mcp-sync: set GITEA_KEY, or point MCP_SYNC_REPO at a checkout.\n' >&2
|
||||
# Bump the deployed file's mtime so the TTL throttles this warning to once
|
||||
# an hour instead of printing it at the top of every single shell.
|
||||
[ -f "$MCP_SYNC_DEST" ] && touch "$MCP_SYNC_DEST" 2>/dev/null
|
||||
[ -f "$MCP_SYNC_DEST" ] || return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Discover the gateway's models with the user's key (best-effort).
|
||||
@@ -189,10 +295,26 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Per-model specs declared in the BASE config are a FLOOR; live /model/info
|
||||
# overrides them field by field. This matters because the /model/info fetch
|
||||
# above is best-effort and 403s for any key not scoped to management routes —
|
||||
# which is the normal case for a day-to-day virtual key. Before this merge that
|
||||
# fallback silently produced `{}` for every model, i.e. no context limit at
|
||||
# all, and opencode then ran the whole gateway on its built-in defaults and
|
||||
# walked straight into ContextWindowExceededError 400s on the one model whose
|
||||
# window is smaller than those defaults. Declaring limits in the base file did
|
||||
# not help either, because `prov["models"] = models` replaced the map wholesale.
|
||||
# Live data still wins wherever it is present, so this reintroduces no drift.
|
||||
base_models = {}
|
||||
try:
|
||||
base_models = (cfg.get("provider", {}).get(provider) or {}).get("models") or {}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
models = {}
|
||||
try:
|
||||
data = json.loads(os.environ.get("MCP_SYNC_MODELS") or "{}")
|
||||
models = {m["id"]: dict(info.get(m["id"], {}))
|
||||
models = {m["id"]: {**(base_models.get(m["id"]) or {}), **dict(info.get(m["id"], {}))}
|
||||
for m in data.get("data", []) if m.get("id")}
|
||||
except Exception:
|
||||
pass
|
||||
@@ -393,8 +515,13 @@ _mcp_sync_skills() {
|
||||
_tree="${MCP_SYNC_SKILLS_SRC#file://}"
|
||||
;;
|
||||
*)
|
||||
if command -v curl >/dev/null 2>&1 && command -v tar >/dev/null 2>&1 &&
|
||||
curl -fsS -m 15 "$MCP_SYNC_SKILLS_SRC" -o "$_work/repo.tgz" 2>/dev/null &&
|
||||
# Same rule as the config: the checkout is the source of truth when there
|
||||
# is one, so no tarball download and no anonymous read required.
|
||||
_repo="$(_mcp_sync_find_repo)" || _repo=""
|
||||
if [ -n "$_repo" ] && [ -d "$_repo/$MCP_SYNC_SKILLS_SUBDIR" ]; then
|
||||
_tree="$_repo/$MCP_SYNC_SKILLS_SUBDIR"
|
||||
elif command -v curl >/dev/null 2>&1 && command -v tar >/dev/null 2>&1 &&
|
||||
_mcp_sync_curl "$MCP_SYNC_SKILLS_SRC" "$_work/repo.tgz" 15 &&
|
||||
[ -s "$_work/repo.tgz" ] &&
|
||||
mkdir -p "$_work/x" && tar -xzf "$_work/repo.tgz" -C "$_work/x" 2>/dev/null
|
||||
then
|
||||
@@ -440,10 +567,14 @@ _mcp_sync_skills
|
||||
# as "my new model isn't there" when in fact the gateway is fine and the local
|
||||
# config is just stale. Run `mcp-sync` to pull it immediately.
|
||||
#
|
||||
# Defined for the interactive shell (zsh/bash), which is the only place sync.sh
|
||||
# is sourced; the hyphen in the name is fine there.
|
||||
# The name is `mcp-sync`, but the DEFINITION below uses an underscore: a hyphen
|
||||
# is not a legal function name in POSIX sh, and dash rejects it at PARSE time --
|
||||
# i.e. it aborts the whole file, not just this line. install.sh sources sync.sh
|
||||
# under /bin/sh (dash on Debian-family), so a bare `mcp-sync() {` here silently
|
||||
# killed install.sh's "deploy the config now" step for every install. An alias
|
||||
# is resolved lazily, so it costs nothing to parse and keeps the typed name.
|
||||
MCP_SYNC_SELF="${MCP_SYNC_SELF:-$HOME/.config/mcp-sync/sync.sh}"
|
||||
mcp-sync() {
|
||||
mcp_sync() {
|
||||
[ -f "$MCP_SYNC_SELF" ] || {
|
||||
printf 'mcp-sync: %s not found\n' "$MCP_SYNC_SELF" >&2; return 1
|
||||
}
|
||||
@@ -459,5 +590,10 @@ models = cfg.get("provider", {}).get(os.environ["MCP_SYNC_PY_PROVIDER"], {}).get
|
||||
print(f"mcp-sync: {len(models)} models -> {', '.join(models) or '(none)'}")
|
||||
PYEOF
|
||||
}
|
||||
alias mcp-sync=mcp_sync 2>/dev/null || true
|
||||
|
||||
unset -f _mcp_sync_export_key _mcp_sync_refresh _mcp_sync_hermes _mcp_sync_skills 2>/dev/null || true
|
||||
unset -f _mcp_sync_export_key _mcp_sync_refresh _mcp_sync_hermes _mcp_sync_skills \
|
||||
_mcp_sync_find_repo _mcp_sync_pull _mcp_sync_curl _mcp_sync_git 2>/dev/null || true
|
||||
# This file is SOURCED into the user's interactive shell, so scratch variables
|
||||
# would otherwise linger in their environment for the rest of the session.
|
||||
unset _base _repo _models _info _work _tree _cand _branch _default 2>/dev/null || true
|
||||
|
||||
Reference in New Issue
Block a user