From c0d67f023a6ab89475b4feb14a6ffdf1cae63f89 Mon Sep 17 00:00:00 2001 From: Nic O'Connor Date: Sun, 12 Jul 2026 12:26:17 -0400 Subject: [PATCH] mcp-config: sync shared opencode skills from the repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Models and MCP servers are discovered from the LiteLLM gateway at sync time (/v1/models, and the aggregate /mcp/ endpoint). Skills have no gateway side to query: LiteLLM's /v1/skills is a passthrough to Anthropic's Skills API, which lists skills on an Anthropic account rather than serving skill files, and /public/skill_hub is a marketplace catalog. So the repo is the source of truth and the tree is fetched whole, from the repo tarball. Deploy to ~/.config/mcp-sync/skills rather than ~/.config/opencode/skills so the atomic replace can never delete a skill the user wrote by hand; opencode picks the tree up via skills.paths, which expands {env:HOME} — verified — so the shared config stays identical for every user. Same throttle and non-fatal discipline as the config sync: a fetch that yields no skills/ leaves the last-good tree in place instead of emptying it. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp-config/README.md | 25 ++++++++--- mcp-config/opencode.gateway.json | 3 ++ mcp-config/sync.sh | 74 ++++++++++++++++++++++++++++++-- skills/README.md | 57 ++++++++++++++++++++++++ zsh/zshrc.local.example | 15 +++++++ 5 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 skills/README.md diff --git a/mcp-config/README.md b/mcp-config/README.md index 7ec9c88..d55a603 100644 --- a/mcp-config/README.md +++ b/mcp-config/README.md @@ -25,12 +25,20 @@ sync — the laptops pull from Gitea's `main`.) `/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. + over), deploys the repo's **skills** (see below), deploys the config to + `~/.config/opencode/opencode.gateway.json`, and exports `LITELLM_KEY` + (from `~/.config/mcp-sync/key`) plus `OPENCODE_CONFIG` (pointing at the + deployed file — opencode does NOT auto-load extra files in its config dir, + so without this export the file is ignored). Silent + non-fatal: a Gitea + hiccup never breaks the shell, last-good config stays in place. +- `../skills/` — the shared skill folders, deployed to + `~/.config/mcp-sync/skills` and wired in through `skills.paths` in the + config above. Skills are plain files, so — unlike models and MCP servers — + there is nothing on the gateway to discover and **the repo is the source of + truth**; the tree is fetched whole from the repo tarball. They deploy to + mcp-sync's own directory rather than `~/.config/opencode/skills` so the + atomic replace can never delete a skill the user wrote by hand. See + `skills/README.md`. - `install.sh` — one-time bootstrap (curl-pipe). Fetches `sync.sh`, stores the user's key (0600), and adds the rc hook. @@ -62,6 +70,11 @@ 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). +## Adding a skill (admin) + +Skills have no gateway side. Add a folder under `skills/` and push to `main` — +see `skills/README.md`. Verify with `opencode debug skill`. + 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 `charts/inference/MCP-GATEWAY.md`. diff --git a/mcp-config/opencode.gateway.json b/mcp-config/opencode.gateway.json index 441ccea..d7f10ca 100644 --- a/mcp-config/opencode.gateway.json +++ b/mcp-config/opencode.gateway.json @@ -20,5 +20,8 @@ "Authorization": "Bearer {env:LITELLM_KEY}" } } + }, + "skills": { + "paths": ["{env:HOME}/.config/mcp-sync/skills"] } } diff --git a/mcp-config/sync.sh b/mcp-config/sync.sh index 9e60dee..91fef55 100644 --- a/mcp-config/sync.sh +++ b/mcp-config/sync.sh @@ -1,6 +1,7 @@ #!/usr/bin/env sh # mcp-sync — refresh the shared LiteLLM-gateway opencode config on the laptop, -# auto-discover the gateway's models, and export the user's LiteLLM key. +# auto-discover the gateway's models, deploy the shared skills, and export the +# user's LiteLLM key. # # 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: @@ -19,12 +20,18 @@ # (opencode cannot list models from a custom provider itself; the map # 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 +# 3. Deploy the repo's skills/ tree to ~/.config/mcp-sync/skills. Skills are +# plain files, so there is nothing on the gateway to discover — the repo +# IS the source of truth, and the tree is fetched whole (repo tarball). +# They land in mcp-sync's OWN directory, not ~/.config/opencode/skills, +# so the atomic replace below can never eat a skill the user wrote by +# hand; opencode picks them up via skills.paths in the shared config. +# 4. 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} +# 5. Export LITELLM_KEY from ~/.config/mcp-sync/key so {env:LITELLM_KEY} # resolves. # --- config (overridable via env before sourcing) ------------------------- @@ -35,6 +42,13 @@ 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}" +MCP_SYNC_SKILLS_SUBDIR="${MCP_SYNC_SKILLS_SUBDIR:-skills}" +MCP_SYNC_SKILLS_DEST="${MCP_SYNC_SKILLS_DEST:-$MCP_SYNC_HOME/skills}" # 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}" @@ -130,11 +144,63 @@ PYEOF [ -n "$_base" ] && rm -f "$_base" } +# --- deploy the shared skills tree (throttled, non-fatal) ------------------ +_mcp_sync_skills() { + # Same TTL as the config, tracked on the deployed directory's own mtime. + if [ -d "$MCP_SYNC_SKILLS_DEST" ] && command -v find >/dev/null 2>&1; then + if find "$MCP_SYNC_SKILLS_DEST" -maxdepth 0 -mmin -"$(( MCP_SYNC_TTL / 60 ))" 2>/dev/null | grep -q .; then + return 0 + fi + fi + + mkdir -p "$MCP_SYNC_HOME" 2>/dev/null || return 0 + # Stage inside MCP_SYNC_HOME (not /tmp) so the publish below is a same- + # filesystem rename rather than a copy that can be seen half-finished. + _work="$(mktemp -d "$MCP_SYNC_HOME/.skills.XXXXXX" 2>/dev/null)" || return 0 + + _tree="" + case "$MCP_SYNC_SKILLS_SRC" in + /*|file://*) + # Working checkout: the source is the skills/ dir itself. + _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 && + [ -s "$_work/repo.tgz" ] && + mkdir -p "$_work/x" && tar -xzf "$_work/repo.tgz" -C "$_work/x" 2>/dev/null + then + # Gitea wraps the archive in a single top-level dir whose name we don't + # want to hardcode; find the skills/ subtree instead of assuming depth. + _tree="$(find "$_work/x" -maxdepth 2 -type d -name "$MCP_SYNC_SKILLS_SUBDIR" 2>/dev/null | head -1)" + fi + ;; + esac + if [ -z "$_tree" ] || [ ! -d "$_tree" ]; then rm -rf "$_work"; return 0; fi + + mkdir -p "$_work/stage" 2>/dev/null || { rm -rf "$_work"; return 0; } + cp -R "$_tree/." "$_work/stage/" 2>/dev/null || { rm -rf "$_work"; return 0; } + # Never replace a good tree with an empty one (truncated fetch, empty subdir). + if [ -z "$(ls -A "$_work/stage" 2>/dev/null)" ]; then rm -rf "$_work"; return 0; fi + + # Publish: swap the new tree in, keeping the old one until the move lands so a + # failure leaves the previous skills intact rather than no skills at all. + rm -rf "$MCP_SYNC_SKILLS_DEST.old" 2>/dev/null + if [ -d "$MCP_SYNC_SKILLS_DEST" ]; then + mv "$MCP_SYNC_SKILLS_DEST" "$MCP_SYNC_SKILLS_DEST.old" 2>/dev/null || { rm -rf "$_work"; return 0; } + fi + if ! mv "$_work/stage" "$MCP_SYNC_SKILLS_DEST" 2>/dev/null; then + mv "$MCP_SYNC_SKILLS_DEST.old" "$MCP_SYNC_SKILLS_DEST" 2>/dev/null + fi + rm -rf "$MCP_SYNC_SKILLS_DEST.old" "$_work" 2>/dev/null +} + _mcp_sync_export_key _mcp_sync_refresh +_mcp_sync_skills # 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 +unset -f _mcp_sync_export_key _mcp_sync_refresh _mcp_sync_skills 2>/dev/null || true diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..a6aaab3 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,57 @@ +# skills — shared opencode skills + +Skills every covert-gpt user gets. `mcp-config/sync.sh` deploys this directory +to `~/.config/mcp-sync/skills` on each box; the shared `opencode.gateway.json` +points opencode at it via `skills.paths`. + +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 +**this repo is the source of truth**. Add a skill here, push to `main`, and it +lands everywhere within the sync TTL (~1h, or immediately on a new shell). + +> 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 +> account. It does not serve skill files to a client, and opencode cannot read +> it. `/public/skill_hub` is a marketplace catalog, also not a skill source. + +## Layout + +One folder per skill, each with a `SKILL.md`: + +``` +skills/ + my-skill/ + SKILL.md # required + reference.md # optional supporting files +``` + +`SKILL.md` needs YAML frontmatter with `name` and `description`. `name` must be +1–64 chars, lowercase alphanumeric with single-hyphen separators, and match the +folder name. `description` is 1–1024 chars and is what the model reads to decide +whether to pull the skill in — write it to say *when to use this*, not just what +it is. + +```markdown +--- +name: my-skill +description: Use when doing X. Covers Y and Z. +--- + +Body: the instructions the model follows once the skill is loaded. +``` + +## Don't put per-user things here + +Same rule as the MCP gateway: this tree is **identical for every user**. Skills +that depend on one person's credentials, kubeconfig, or local paths belong in +that user's own `~/.config/opencode/skills/`, which sync never touches. + +## Verifying + +```sh +opencode debug skill # lists every skill opencode can see, with its path +``` + +If a skill you added here is missing, check that the sync ran (`ls +~/.config/mcp-sync/skills`) and that the folder name matches the frontmatter +`name`. diff --git a/zsh/zshrc.local.example b/zsh/zshrc.local.example index c8cfbd8..552768c 100644 --- a/zsh/zshrc.local.example +++ b/zsh/zshrc.local.example @@ -15,11 +15,26 @@ # ~/.config/mcp-sync/key — run mcp-config/install.sh once instead of # setting it here. Only set it here on a box without mcp-sync: # export LITELLM_KEY=sk-... +# +# To develop mcp-config/ or skills/ against a working checkout instead of +# Gitea's main, point the sync at it and source it directly: +# MCP_SYNC_RAW_URL="file://$HOME/source/dotfiles/mcp-config/opencode.gateway.json" +# MCP_SYNC_SKILLS_SRC="$HOME/source/dotfiles/skills" +# . "$HOME/source/dotfiles/mcp-config/sync.sh" # --- 1Password CLI ------------------------------------------------------- # Service-account token for non-interactive `op read` / `op run`. # export OP_SERVICE_ACCOUNT_TOKEN=ops_... +# --- Proxmox MCP gateway (ai-k8s-iac) ------------------------------------ +# Credentials for the Proxmox MCP server registered on the covert-gpt +# LiteLLM gateway. `task up` in ai-k8s-iac reads these env vars and stores +# them in the inference-mcp Secret; they are never committed. +# export PROXMOX_MCP_HOST="https://pve.local:8006" +# export PROXMOX_MCP_TOKEN_ID="root@pam!terragrunt-proxmox" +# export PROXMOX_MCP_TOKEN_SECRET="00000000-0000-0000-0000-000000000000" +# export PROXMOX_MCP_INSECURE_TLS="true" + # --- Anything else ------------------------------------------------------- # Aliases, PATH additions, kubeconfig defaults, etc. This file loads last, # so it can override anything the managed zshrc sets.