mcp-config: sync shared opencode skills from the repo
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) <noreply@anthropic.com>
This commit is contained in:
+19
-6
@@ -25,12 +25,20 @@ sync — the laptops pull from Gitea's `main`.)
|
|||||||
`/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
|
||||||
over), deploys the result to `~/.config/opencode/opencode.gateway.json`,
|
over), deploys the repo's **skills** (see below), deploys the config to
|
||||||
and exports `LITELLM_KEY` (from `~/.config/mcp-sync/key`) plus
|
`~/.config/opencode/opencode.gateway.json`, and exports `LITELLM_KEY`
|
||||||
`OPENCODE_CONFIG` (pointing at the deployed file — opencode does NOT
|
(from `~/.config/mcp-sync/key`) plus `OPENCODE_CONFIG` (pointing at the
|
||||||
auto-load extra files in its config dir, so without this export the file
|
deployed file — opencode does NOT auto-load extra files in its config dir,
|
||||||
is ignored). Silent + non-fatal: a Gitea hiccup never breaks the shell,
|
so without this export the file is ignored). Silent + non-fatal: a Gitea
|
||||||
last-good config stays in place.
|
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
|
- `install.sh` — one-time bootstrap (curl-pipe). Fetches `sync.sh`, stores the
|
||||||
user's key (0600), and adds the rc hook.
|
user's key (0600), and adds the rc hook.
|
||||||
|
|
||||||
@@ -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
|
new shell). `opencode.gateway.json` here only needs editing for structural
|
||||||
changes (a new provider, endpoint URLs).
|
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
|
For **per-user / per-target** tools (cluster access, etc.) do NOT add them
|
||||||
here — those run client-side with the user's own credentials. See
|
here — those run client-side with the user's own credentials. See
|
||||||
`charts/inference/MCP-GATEWAY.md`.
|
`charts/inference/MCP-GATEWAY.md`.
|
||||||
|
|||||||
@@ -20,5 +20,8 @@
|
|||||||
"Authorization": "Bearer {env:LITELLM_KEY}"
|
"Authorization": "Bearer {env:LITELLM_KEY}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"skills": {
|
||||||
|
"paths": ["{env:HOME}/.config/mcp-sync/skills"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-4
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# mcp-sync — refresh the shared LiteLLM-gateway opencode config on the laptop,
|
# 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
|
# Sourced from the user's shell rc (installed by install.sh) so it runs at the
|
||||||
# start of every interactive shell ("session start"). It is intentionally:
|
# start of every interactive shell ("session start"). It is intentionally:
|
||||||
@@ -19,12 +20,18 @@
|
|||||||
# (opencode cannot list models from a custom provider itself; the map
|
# (opencode cannot list models from a custom provider itself; the map
|
||||||
# must be materialized client-side.) MCP servers need no discovery step:
|
# must be materialized client-side.) MCP servers need no discovery step:
|
||||||
# the single /mcp/ endpoint aggregates every server the key can access.
|
# 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
|
# OPENCODE_CONFIG pointing at it. opencode does NOT auto-load extra files
|
||||||
# in its config dir — it only merges known locations, and OPENCODE_CONFIG
|
# in its config dir — it only merges known locations, and OPENCODE_CONFIG
|
||||||
# is merged between the global opencode.json and any project config. The
|
# 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.
|
# 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.
|
# resolves.
|
||||||
|
|
||||||
# --- config (overridable via env before sourcing) -------------------------
|
# --- 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.
|
# 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
|
||||||
|
# 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
|
# 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.
|
# (default 1h). Keeps every new shell from hammering Gitea/the gateway.
|
||||||
MCP_SYNC_TTL="${MCP_SYNC_TTL:-3600}"
|
MCP_SYNC_TTL="${MCP_SYNC_TTL:-3600}"
|
||||||
@@ -130,11 +144,63 @@ PYEOF
|
|||||||
[ -n "$_base" ] && rm -f "$_base"
|
[ -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_export_key
|
||||||
_mcp_sync_refresh
|
_mcp_sync_refresh
|
||||||
|
_mcp_sync_skills
|
||||||
|
|
||||||
# opencode only merges config from known locations; point OPENCODE_CONFIG at
|
# 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.
|
# 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"
|
[ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -15,11 +15,26 @@
|
|||||||
# ~/.config/mcp-sync/key — run mcp-config/install.sh once instead of
|
# ~/.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:
|
# setting it here. Only set it here on a box without mcp-sync:
|
||||||
# export LITELLM_KEY=sk-...
|
# 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 -------------------------------------------------------
|
# --- 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) ------------------------------------
|
||||||
|
# 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 -------------------------------------------------------
|
# --- Anything else -------------------------------------------------------
|
||||||
# Aliases, PATH additions, kubeconfig defaults, etc. This file loads last,
|
# Aliases, PATH additions, kubeconfig defaults, etc. This file loads last,
|
||||||
# so it can override anything the managed zshrc sets.
|
# so it can override anything the managed zshrc sets.
|
||||||
|
|||||||
Reference in New Issue
Block a user