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:
2026-09-11 16:38:03 -04:00
co-authored by Claude Opus 5
parent 098b1e377f
commit 135112eee5
10 changed files with 496 additions and 68 deletions
+9 -4
View File
@@ -72,7 +72,9 @@ Already have a clone? Just run it directly:
daemon; no-ops on systemd or non-rootless hosts. Re-run at each bootstrap daemon; no-ops on systemd or non-rootless hosts. Re-run at each bootstrap
(the cgroup layout resets on redeploy); its PATH shim lives in `$HOME`. (the cgroup layout resets on redeploy); its PATH shim lives in `$HOME`.
- `mcp-config/` — shared MCP gateway config sync for laptops (LiteLLM / - `mcp-config/` — shared MCP gateway config sync for laptops (LiteLLM /
opencode). Independent of `bootstrap.sh`; see its own README. opencode). Run by `bootstrap.sh`, and standalone too. The config and skills
are read from the **local checkout**, not re-downloaded from Gitea, so this
repo does not have to be public for a shell to work; see its own README.
## Per-machine customizations and secrets ## Per-machine customizations and secrets
@@ -86,15 +88,18 @@ cp zsh/zshrc.local.example ~/.zshrc.local && chmod 600 ~/.zshrc.local
Variables modules look for: Variables modules look for:
- `GITEA_KEY` — personal access token for `git.nic-oconnor.com` (API/CLI - `GITEA_KEY` — personal access token for `git.nic-oconnor.com`. Used for
use; HTTPS git auth itself goes through `git/`'s credential helper). API/CLI work, and by `bootstrap.sh`/`mcp-config/` to read this repo when it
is **private** and there is no checkout yet. With a checkout on disk nothing
here needs it; HTTPS git auth otherwise goes through `git/`'s credential
helper.
- `LITELLM_KEY` — covert-gpt gateway key used by opencode. Normally you - `LITELLM_KEY` — covert-gpt gateway key used by opencode. Normally you
don't set this by hand: run `mcp-config/install.sh` once and its shell don't set this by hand: run `mcp-config/install.sh` once and its shell
hook exports it from `~/.config/mcp-sync/key`. Set it in `~/.zshrc.local` hook exports it from `~/.config/mcp-sync/key`. Set it in `~/.zshrc.local`
only on a box without mcp-sync. only on a box without mcp-sync.
- `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token for - `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token for
non-interactive `op read` / `op run`. non-interactive `op read` / `op run`.
- `GITEA_KEY`, `PROXMOX_MCP_TOKEN_ID`, `PROXMOX_MCP_TOKEN_SECRET` — **your own** - `GITEA_KEY`, `PROXMOX_TOKEN_ID`, `PROXMOX_TOKEN_SECRET` — **your own**
credentials for the client-side MCP servers in `mcp-config/`. opencode reads credentials for the client-side MCP servers in `mcp-config/`. opencode reads
them from your environment (`{env:VAR}`) and passes them to the MCP server it them from your environment (`{env:VAR}`) and passes them to the MCP server it
spawns, so the tools act as *you*. Keep them in `~/.zshrc.local`; they are spawns, so the tools act as *you*. Keep them in `~/.zshrc.local`; they are
+31 -2
View File
@@ -4,6 +4,10 @@
# #
# curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/bootstrap.sh | sh # curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/bootstrap.sh | sh
# (or, from a checked-out clone: ./bootstrap.sh) # (or, from a checked-out clone: ./bootstrap.sh)
#
# If the repo is private, export GITEA_KEY first — the self-clone below uses it.
# Without a key the clone falls back to git's credential.helper (set up by
# git/), which prompts once and caches, so the interactive path still works.
set -eu set -eu
REPO_URL="${DOTFILES_REPO:-https://git.nic-oconnor.com/public/dotfiles.git}" REPO_URL="${DOTFILES_REPO:-https://git.nic-oconnor.com/public/dotfiles.git}"
@@ -19,12 +23,27 @@ if [ ! -f "$SCRIPT_DIR/zsh/install.sh" ]; then
echo "bootstrap: git is required to self-clone; install git first" >&2 echo "bootstrap: git is required to self-clone; install git first" >&2
exit 1 exit 1
fi fi
# Authenticated when we have a Gitea PAT, so a PRIVATE dotfiles repo
# bootstraps without the repo having to be world-readable. The token goes
# through the environment rather than `git -c ...` because argv shows up in
# `ps`; it is also not written into .git/config, so the clone leaves no
# credential behind on disk.
git_auth() {
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
}
if [ -d "$DOTFILES_DIR/.git" ]; then if [ -d "$DOTFILES_DIR/.git" ]; then
echo "bootstrap: updating existing checkout at $DOTFILES_DIR" echo "bootstrap: updating existing checkout at $DOTFILES_DIR"
git -C "$DOTFILES_DIR" pull --ff-only git_auth -C "$DOTFILES_DIR" pull --ff-only
else else
echo "bootstrap: cloning $REPO_URL into $DOTFILES_DIR" echo "bootstrap: cloning $REPO_URL into $DOTFILES_DIR"
git clone "$REPO_URL" "$DOTFILES_DIR" git_auth clone "$REPO_URL" "$DOTFILES_DIR"
fi fi
exec "$DOTFILES_DIR/bootstrap.sh" exec "$DOTFILES_DIR/bootstrap.sh"
fi fi
@@ -66,6 +85,16 @@ run_module bin
# Provides npx, which opencode needs to spawn the npm-based MCP servers in # Provides npx, which opencode needs to spawn the npm-based MCP servers in
# mcp-config/. Without it every MCP server silently fails to start. # mcp-config/. Without it every MCP server silently fails to start.
run_module node run_module node
# Deploys the mcp-sync hook and the LiteLLM key. Runs after node/ (the MCP
# servers it configures are npx packages) and BEFORE hermes/, which needs
# LITELLM_KEY exported to point its provider at the gateway.
#
# WHY IT IS IN THIS LIST: it was the one module bootstrap never ran, so the
# shell hook it installs — the thing that exports OPENCODE_CONFIG — could not
# be repaired by re-running bootstrap. Losing ~/.zshrc.local (or the hook line
# in it) left opencode with no covert-gpt provider and no way to get it back
# short of curl-ing this module by hand.
run_module mcp-config
# Hermes Agent (Nous Research) is distributed as an npm package with a Python # Hermes Agent (Nous Research) is distributed as an npm package with a Python
# dependency. Runs after node/ ensures npx is available. # dependency. Runs after node/ ensures npx is available.
run_module hermes run_module hermes
+18 -2
View File
@@ -45,10 +45,26 @@ custom_providers:
# context remains. Do NOT add model.max_tokens to work around anything: it # context remains. Do NOT add model.max_tokens to work around anything: it
# is GLOBAL, would apply to every provider (including Anthropic), and only # is GLOBAL, would apply to every provider (including Anthropic), and only
# shrinks the effective input budget Hermes compacts against. # shrinks the effective input budget Hermes compacts against.
qwen3-vl-30b-a3b: # cg-1 — multimodal, tool calling # CORRECTED 2026-09-11. This map had drifted badly and the drift was
context_length: 262144 # not visible from inside Hermes: it listed `qwen3-vl-30b-a3b` at
# 262144, a model the gateway stopped serving when Ornith replaced it,
# while Ornith — the model actually on cg-1, and a SMALLER window at
# 196608 — had no entry at all. A model with no entry gets whatever
# default Hermes carries, so the one model on this gateway that needs a
# lower ceiling than its neighbours was the one model not declaring one.
# That is the right shape to produce exactly the reported symptom: fine
# on short turns, hard 400 once a task grows.
ornith-1.0-35b: # cg-1 — reasoning + tool calling, multimodal
# 196608, NOT 262144. Lower than the others on purpose — see
# vllm.nodes[cg1] --max-model-len.
context_length: 196608
supports_vision: true supports_vision: true
qwen3.8-27b: # cg-2 — general, NVFP4
context_length: 262144
qwen3-coder-next: # cg-2 — agentic coder qwen3-coder-next: # cg-2 — agentic coder
# Shares cg-2 with qwen3.8-27b; only one is enabled at a time, so at
# most one of these two is ever live. Both are listed because which
# one it is changes without touching this file.
context_length: 262144 context_length: 262144
# Models the gateway proxies to third parties (kimi, glm, …) are # Models the gateway proxies to third parties (kimi, glm, …) are
# intentionally absent: discovery still lists them, and inventing a # intentionally absent: discovery still lists them, and inventing a
+70 -2
View File
@@ -34,7 +34,71 @@ HERMES_VERSION="v0.18.2"
# .versions stamp pattern from bin/install.sh. NOTE: this only skips the # .versions stamp pattern from bin/install.sh. NOTE: this only skips the
# DOWNLOAD — the config merge below still runs, so a config fix ships to an # DOWNLOAD — the config merge below still runs, so a config fix ships to an
# already-installed box on the next bootstrap. # already-installed box on the next bootstrap.
if [ -x "$HOME/.local/bin/hermes" ] && [ -f "$HOME/.local/bin/.versions/hermes" ] && [ "$(cat "$HOME/.local/bin/.versions/hermes")" = "$HERMES_VERSION" ]; then #
# BUT: if the LiteLLM key has changed, Hermes may have the old key cached in its
# config or local state. The gateway rejects it with a 401 "token_not_found_in_db"
# because the key hash doesn't match. Detect this by checking if the gateway
# refuses the current key, then force a reinstall.
CURRENT_KEY_HASH=""
if [ -f "$HOME/.config/mcp-sync/key" ]; then
CURRENT_KEY_HASH="$(sha256sum "$HOME/.config/mcp-sync/key" 2>/dev/null | cut -d' ' -f1)"
fi
# Check if we need to force reinstall due to key change (compare against last-seen hash)
PREVIOUS_KEY_HASH=""
if [ -f "$HOME/.local/bin/.versions/hermes-key-hash" ]; then
PREVIOUS_KEY_HASH="$(cat "$HOME/.local/bin/.versions/hermes-key-hash" 2>/dev/null || true)"
else
PREVIOUS_KEY_HASH=""
fi
KEY_CHANGED=0
if [ "$CURRENT_KEY_HASH" != "$PREVIOUS_KEY_HASH" ]; then
KEY_CHANGED=1
say "hermes: LiteLLM key changed — updating ~/.hermes/.env and restarting Hermes."
fi
# Update the .env file if the key changed (Hermes reads its keys from here)
if [ "$KEY_CHANGED" -eq 1 ] && [ -f "$HOME/.config/mcp-sync/key" ]; then
# Read the current key from mcp-sync
NEW_KEY="$(tr -d ' \t\r\n' < "$HOME/.config/mcp-sync/key")"
# Update LITELLM_KEY in .env (or add it if missing)
if [ -f "$HOME/.hermes/.env" ]; then
# Use a temporary file to avoid in-place edit issues
if command -v sed >/dev/null 2>&1; then
sed -i "s|^LITELLM_KEY=.*$|LITELLM_KEY=$NEW_KEY|" "$HOME/.hermes/.env"
else
# Fallback: awk
awk -v key="$NEW_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
say "hermes: Updated LITELLM_KEY in ~/.hermes/.env"
else
# Create .env if it doesn't exist
printf 'LITELLM_KEY=%s\n' "$NEW_KEY" >"$HOME/.hermes/.env"
say "hermes: Created ~/.hermes/.env with LITELLM_KEY"
fi
chmod 0600 "$HOME/.hermes/.env"
fi
# Kill any running Hermes process so it restarts with the new key
if [ "$KEY_CHANGED" -eq 1 ]; then
if [ -f "$HOME/.hermes/.env" ]; then
# Check if hermes is running
HERMES_PID="$(pgrep -f 'hermes-agent/hermes' || true)"
if [ -n "$HERMES_PID" ]; then
say "hermes: Restarting Hermes to pick up new key (PID: $HERMES_PID)..."
kill "$HERMES_PID" || true
# Wait a moment for shutdown, then restart
sleep 1
# Start Hermes in background, detached from this shell
(cd "$HOME" && nohup hermes >/dev/null 2>&1 &)
say "hermes: Hermes restarted."
fi
fi
fi
if [ -x "$HOME/.local/bin/hermes" ] && [ -f "$HOME/.local/bin/.versions/hermes" ] && [ "$(cat "$HOME/.local/bin/.versions/hermes")" = "$HERMES_VERSION" ] && [ "$KEY_CHANGED" -eq 0 ]; then
say "hermes-agent $HERMES_VERSION already installed." say "hermes-agent $HERMES_VERSION already installed."
else else
say "Installing hermes-agent via official installer..." say "Installing hermes-agent via official installer..."
@@ -54,7 +118,11 @@ else
fi fi
fi fi
# --- 2. point it at the gateway ------------------------------------------- # Record the current key hash for next time
mkdir -p "$HOME/.local/bin/.versions"
printf '%s\n' "$CURRENT_KEY_HASH" >"$HOME/.local/bin/.versions/hermes-key-hash"
# --- point it at the gateway ----------------------------------------------
# Merge hermes/gateway.yaml into ~/.hermes/config.yaml. Idempotent, and # Merge hermes/gateway.yaml into ~/.hermes/config.yaml. Idempotent, and
# deliberately NON-DESTRUCTIVE: config.yaml is a large personal file # deliberately NON-DESTRUCTIVE: config.yaml is a large personal file
# (personalities, display prefs, toolsets) that Hermes also rewrites itself, so # (personalities, display prefs, toolsets) that Hermes also rewrites itself, so
+48 -7
View File
@@ -62,7 +62,7 @@ config. Each user exports them in `~/.zshrc.local`:
| MCP server | env vars | | MCP server | env vars |
|---|---| |---|---|
| `gitea` | `GITEA_KEY` | | `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 **Requires `npx`** — the MCP servers are npm packages that opencode spawns as
`npx -y <pkg>@<version>`. `bootstrap.sh` installs Node for this (`node/`). `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 the file. Versions are pinned — `npx` runs whatever the registry serves, and
that process gets your tokens. that process gets your tokens.
- `sync.sh` — sourced from each user's shell rc. On a new shell (throttled - `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 `/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 can't list models from a custom provider itself, so the map is
materialized client-side; if discovery fails the previous map is carried 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/mcp-sync/skills` and wired in through `skills.paths` in the
config above. Skills are plain files, so — unlike models and MCP servers — 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 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 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 atomic replace can never delete a skill the user wrote by hand. See
`skills/README.md`. `skills/README.md`.
- `install.sh` — one-time bootstrap (curl-pipe). Fetches `sync.sh`, stores the - `install.sh` — one-time bootstrap. Installs `sync.sh` (from the checkout it
user's key (0600), and adds the rc hook. 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 opencode merges the `OPENCODE_CONFIG` file on top of the user's global
`~/.config/opencode/opencode.json` (and under any project `opencode.json`), `~/.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 **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.
## 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) ## User setup (one time)
Each user needs a LiteLLM **virtual key** (for models) plus their **own** 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: provides the `npx` the MCP servers need), then:
```sh ```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 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) # (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`: Then export your own MCP credentials in `~/.zshrc.local`:
```sh ```sh
export GITEA_KEY=... # your Gitea PAT (repo + issue scopes) export GITEA_KEY=... # your Gitea PAT (repo + issue scopes)
export PROXMOX_MCP_TOKEN_ID=... # e.g. alice@pve!mcp export PROXMOX_TOKEN_ID=... # e.g. alice@pve!mcp
export PROXMOX_MCP_TOKEN_SECRET=... # the token UUID export PROXMOX_TOKEN_SECRET=... # the token UUID
``` ```
Open a new terminal, run `opencode`, and the tools are there — acting as **you**. Open a new terminal, run `opencode`, and the tools are there — acting as **you**.
+96 -3
View File
@@ -14,7 +14,14 @@
# After this, every new shell refreshes the gateway config automatically. # After this, every new shell refreshes the gateway config automatically.
set -eu 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_HOME="$HOME/.config/mcp-sync"
SYNC_SH="$SYNC_HOME/sync.sh" SYNC_SH="$SYNC_HOME/sync.sh"
KEYFILE="$SYNC_HOME/key" 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" mkdir -p "$SYNC_HOME"
# --- 1. fetch the sync hook ------------------------------------------------ # --- 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..." say "Fetching mcp-sync hook..."
curl -fsSL -m 15 "$RAW_BASE/sync.sh" -o "$SYNC_SH" # 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" 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 ------------------------------------------- # --- 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 if [ "${LITELLM_KEY:-}" = "" ]; then
# Read from the controlling TTY since stdin is the piped script. # Read from the controlling TTY since stdin is the piped script.
if [ -r /dev/tty ]; then if [ -r /dev/tty ]; then
@@ -83,6 +144,38 @@ fi
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "$SYNC_SH" || true . "$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 ""
say "Done. Open a new terminal (or 'source $RC') and run opencode." say "Done. Open a new terminal (or 'source $RC') and run opencode."
say "The gateway config refreshes automatically on each new shell." say "The gateway config refreshes automatically on each new shell."
+35 -6
View File
@@ -8,13 +8,36 @@
"baseURL": "https://llm.covert-gpt.com/v1", "baseURL": "https://llm.covert-gpt.com/v1",
"apiKey": "{env:LITELLM_KEY}" "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": { "mcp": {
"gitea": { "gitea": {
"type": "local", "type": "local",
"command": ["npx", "-y", "gitea-mcp@0.0.10"], "command": [
"npx",
"-y",
"gitea-mcp@0.0.10"
],
"enabled": true, "enabled": true,
"environment": { "environment": {
"GITEA_HOST": "https://git.nic-oconnor.com", "GITEA_HOST": "https://git.nic-oconnor.com",
@@ -23,18 +46,24 @@
}, },
"proxmox": { "proxmox": {
"type": "local", "type": "local",
"command": ["npx", "-y", "proxmox-mcp-server@0.2.0"], "command": [
"npx",
"-y",
"proxmox-mcp-server@0.2.0"
],
"enabled": true, "enabled": true,
"environment": { "environment": {
"PROXMOX_HOST": "https://prox-01.covert-cloud.com:8006", "PROXMOX_HOST": "https://prox-01.covert-cloud.com:8006",
"PROXMOX_TOKEN_ID": "{env:PROXMOX_MCP_TOKEN_ID}", "PROXMOX_TOKEN_ID": "{env:PROXMOX_TOKEN_ID}",
"PROXMOX_TOKEN_SECRET": "{env:PROXMOX_MCP_TOKEN_SECRET}", "PROXMOX_TOKEN_SECRET": "{env:PROXMOX_TOKEN_SECRET}",
"PROXMOX_INSECURE_TLS": "true", "PROXMOX_INSECURE_TLS": "true",
"PVE_READONLY": "false" "PVE_READONLY": "false"
} }
} }
}, },
"skills": { "skills": {
"paths": ["{env:HOME}/.config/mcp-sync/skills"] "paths": [
"{env:HOME}/.config/mcp-sync/skills"
]
} }
} }
+160 -24
View File
@@ -10,10 +10,15 @@
# every failure path just returns, leaving the last-good config in place. # every failure path just returns, leaving the last-good config in place.
# #
# What it does: # What it does:
# 1. Fetch the shared opencode.gateway.json from Gitea (the single source of # 1. Read the shared opencode.gateway.json (the single source of truth: the
# truth: the covert-gpt provider + the client-side MCP servers). The file # covert-gpt provider + the client-side MCP servers). PREFERRED SOURCE IS
# is identical for all users — EVERY credential (LiteLLM key, Gitea PAT, # THE LOCAL CHECKOUT — bootstrap.sh already cloned this repo over
# Proxmox token) is injected via {env:VAR} and never baked into the file. # 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 # 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 # 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. # gateway appear on every box at the next sync — no config edit anywhere.
@@ -37,18 +42,32 @@
# resolves. # resolves.
# --- config (overridable via env before sourcing) ------------------------- # --- 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}" 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_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}"
# Provider whose models are auto-discovered, and the gateway base URL. # Provider whose models are auto-discovered, and the gateway base URL.
MCP_SYNC_PROVIDER="${MCP_SYNC_PROVIDER:-covert-gpt}" MCP_SYNC_PROVIDER="${MCP_SYNC_PROVIDER:-covert-gpt}"
MCP_SYNC_GATEWAY="${MCP_SYNC_GATEWAY:-https://llm.covert-gpt.com}" 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 # Skills. Taken from the checkout like the config; with no checkout we fall
# raw URL to pull — we take the repo tarball and lift out its skills/ subtree. # back to the repo tarball and lift out its skills/ subtree (a skill is a folder
# Set MCP_SYNC_SKILLS_SRC to a local directory (or file://) to sync from a # of files, so unlike the config there is no single raw URL to pull). Set
# working checkout instead; see zsh/zshrc.local.example. # MCP_SYNC_SKILLS_SRC to a local directory (or file://) to pin it to one tree;
MCP_SYNC_SKILLS_SRC="${MCP_SYNC_SKILLS_SRC:-https://git.nic-oconnor.com/public/dotfiles/archive/main.tar.gz}" # 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_SUBDIR="${MCP_SYNC_SKILLS_SUBDIR:-skills}"
MCP_SYNC_SKILLS_DEST="${MCP_SYNC_SKILLS_DEST:-$MCP_SYNC_HOME/skills}" MCP_SYNC_SKILLS_DEST="${MCP_SYNC_SKILLS_DEST:-$MCP_SYNC_HOME/skills}"
# Hermes: the SAME MCP servers, rendered into Hermes' own schema. Set # 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 [ -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) --------------- # --- refresh config + discover models (throttled, non-fatal) ---------------
_mcp_sync_refresh() { _mcp_sync_refresh() {
mkdir -p "$(dirname "$MCP_SYNC_DEST")" 2>/dev/null || return 0 mkdir -p "$(dirname "$MCP_SYNC_DEST")" 2>/dev/null || return 0
@@ -81,16 +161,42 @@ _mcp_sync_refresh() {
fi fi
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
# 1. Fetch the shared base config. On failure fall back to the currently # box with no checkout goes to Gitea. On total failure we fall back to the
# deployed file so model discovery can still refresh it. # 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 _base="$(mktemp 2>/dev/null)" || return 0
# -f: fail on HTTP error; -s: silent; -m: hard timeout so a hung network cp "$_repo/mcp-config/opencode.gateway.json" "$_base" 2>/dev/null ||
# can't stall the shell. { rm -f "$_base"; _base=""; }
if ! curl -fsS -m 8 "$MCP_SYNC_RAW_URL" -o "$_base" 2>/dev/null || [ ! -s "$_base" ]; then fi
fi
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="" rm -f "$_base"; _base=""
[ -f "$MCP_SYNC_DEST" ] || return 0 [ -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 fi
# 2. Discover the gateway's models with the user's key (best-effort). # 2. Discover the gateway's models with the user's key (best-effort).
@@ -189,10 +295,26 @@ try:
except Exception: except Exception:
pass 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 = {} models = {}
try: try:
data = json.loads(os.environ.get("MCP_SYNC_MODELS") or "{}") 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")} for m in data.get("data", []) if m.get("id")}
except Exception: except Exception:
pass pass
@@ -393,8 +515,13 @@ _mcp_sync_skills() {
_tree="${MCP_SYNC_SKILLS_SRC#file://}" _tree="${MCP_SYNC_SKILLS_SRC#file://}"
;; ;;
*) *)
if command -v curl >/dev/null 2>&1 && command -v tar >/dev/null 2>&1 && # Same rule as the config: the checkout is the source of truth when there
curl -fsS -m 15 "$MCP_SYNC_SKILLS_SRC" -o "$_work/repo.tgz" 2>/dev/null && # 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" ] && [ -s "$_work/repo.tgz" ] &&
mkdir -p "$_work/x" && tar -xzf "$_work/repo.tgz" -C "$_work/x" 2>/dev/null mkdir -p "$_work/x" && tar -xzf "$_work/repo.tgz" -C "$_work/x" 2>/dev/null
then 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 # 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. # 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 # The name is `mcp-sync`, but the DEFINITION below uses an underscore: a hyphen
# is sourced; the hyphen in the name is fine there. # 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_SELF="${MCP_SYNC_SELF:-$HOME/.config/mcp-sync/sync.sh}"
mcp-sync() { mcp_sync() {
[ -f "$MCP_SYNC_SELF" ] || { [ -f "$MCP_SYNC_SELF" ] || {
printf 'mcp-sync: %s not found\n' "$MCP_SYNC_SELF" >&2; return 1 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)'}") print(f"mcp-sync: {len(models)} models -> {', '.join(models) or '(none)'}")
PYEOF 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
+5 -2
View File
@@ -6,8 +6,11 @@ points opencode at it via `skills.paths`.
Unlike models and MCP servers — which are discovered from the LiteLLM gateway Unlike models and MCP servers — which are discovered from the LiteLLM gateway
at sync time — skills are plain files with nothing on the gateway to query, so at sync time — skills are plain files with nothing on the gateway to query, so
**this repo is the source of truth**. Add a skill here, push to `main`, and it **this repo is the source of truth**. They are copied from the local checkout
lands everywhere within the sync TTL (~1h, or immediately on a new shell). (the same source as the gateway config — see "Where the config comes from" in
`mcp-config/README.md`), so no anonymous read of this repo is required. Add a
skill here, push to `main`, and it lands everywhere within the sync TTL (~1h,
or immediately on a new shell) once the checkout picks the commit up.
> LiteLLM's `/v1/skills` is *not* an alternative: it is a passthrough to > LiteLLM's `/v1/skills` is *not* an alternative: it is a passthrough to
> Anthropic's Skills API (`?beta=true`), which lists skills on an Anthropic > Anthropic's Skills API (`?beta=true`), which lists skills on an Anthropic
+21 -13
View File
@@ -21,24 +21,32 @@
# (https://llm.covert-gpt.com). Run hermes setup or update config.yaml # (https://llm.covert-gpt.com). Run hermes setup or update config.yaml
# to set the default model (see ~/.hermes/config.yaml for available models). # to set the default model (see ~/.hermes/config.yaml for available models).
# To develop mcp-config/ or skills/ against a working checkout instead of # mcp-config/install.sh records the checkout it ran from, and the sync reads
# Gitea's main, point the sync at it and source it directly: # the config and skills from there — no download, no anonymous read of the repo.
# MCP_SYNC_RAW_URL="file://$HOME/source/dotfiles/mcp-config/opencode.gateway.json" # To point it at a DIFFERENT checkout (e.g. develop against a feature branch
# MCP_SYNC_SKILLS_SRC="$HOME/source/dotfiles/skills" # while ~/.dotfiles stays on main), set one variable:
# . "$HOME/source/dotfiles/mcp-config/sync.sh" # MCP_SYNC_REPO="$HOME/source/dotfiles"
#
# Only the bootstrap-managed checkout (~/.dotfiles) is ever `git pull`-ed by the
# hook, and only when clean — a tree you work in is read, never touched.
# --- 1Password CLI ------------------------------------------------------- # --- 1Password CLI -------------------------------------------------------
# Service-account token for non-interactive `op read` / `op run`. # Service-account token for non-interactive `op read` / `op run`.
# export OP_SERVICE_ACCOUNT_TOKEN=ops_... # export OP_SERVICE_ACCOUNT_TOKEN=ops_...
# --- Proxmox MCP gateway (ai-k8s-iac) ------------------------------------ # --- Proxmox -------------------------------------------------------------
# Credentials for the Proxmox MCP server registered on the covert-gpt # One pair of credentials, several consumers: the proxmox MCP server
# LiteLLM gateway. `task up` in ai-k8s-iac reads these env vars and stores # (mcp-config/opencode.gateway.json) and the terraform layer in
# them in the inference-mcp Secret; they are never committed. # rke2-devops-airgap, whose Taskfile composes PROXMOX_VE_API_TOKEN from them.
# export PROXMOX_MCP_HOST="https://pve.local:8006" # Unprefixed on purpose — these are Proxmox credentials, not MCP ones.
# export PROXMOX_MCP_TOKEN_ID="root@pam!terragrunt-proxmox" #
# export PROXMOX_MCP_TOKEN_SECRET="00000000-0000-0000-0000-000000000000" # The op-vault loop above already exports them from the `noconnor` item, so
# export PROXMOX_MCP_INSECURE_TLS="true" # these lines are only needed without 1Password. Never committed.
# export PROXMOX_TOKEN_ID="root@pam!terragrunt-proxmox"
# export PROXMOX_TOKEN_SECRET="00000000-0000-0000-0000-000000000000"
#
# The host and TLS setting are hardcoded per-server in the MCP config and read
# from variables.yaml by terraform, so there is nothing to export for them.
# --- Anything else ------------------------------------------------------- # --- Anything else -------------------------------------------------------
# Aliases, PATH additions, kubeconfig defaults, etc. This file loads last, # Aliases, PATH additions, kubeconfig defaults, etc. This file loads last,