mcp-config: run MCP servers client-side, not on the gateway
Every API worth exposing (Gitea, Proxmox) is user-scoped, so hosting MCP
centrally means one shared token per server: everyone acts as the same
identity and attribution is lost. LiteLLM's per-request BYOK cannot cover
all of them either -- it builds its tool->server map from a headerless
background listing, and gitea-mcp hard-exits without a token, so it never
registers. A split brain (some MCPs server-side, some client-side) is worse
than either, so all of them move client-side.
Servers are declared local, spawned via npx with pinned versions, and read
each user's OWN credentials via {env:VAR}. No token is ever written to the
config; the file stays identical for every user.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,14 @@ Variables modules look for:
|
|||||||
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**
|
||||||
|
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
|
||||||
|
spawns, so the tools act as *you*. Keep them in `~/.zshrc.local`; they are
|
||||||
|
never written into any config file.
|
||||||
|
|
||||||
|
These are NOT used by `ai-k8s-iac` / `task up`: the LiteLLM gateway hosts no
|
||||||
|
MCP servers and stores no MCP credentials. See `mcp-config/README.md`.
|
||||||
|
|
||||||
Secrets never go in this repo or in `zsh/zshrc` — that file is shared
|
Secrets never go in this repo or in `zsh/zshrc` — that file is shared
|
||||||
config applied to every box.
|
config applied to every box.
|
||||||
|
|||||||
+73
-30
@@ -1,8 +1,39 @@
|
|||||||
# mcp-config — shared MCP gateway config for laptops
|
# mcp-config — shared opencode config for laptops
|
||||||
|
|
||||||
Single source of truth for the **shared** MCP servers every covert-gpt user
|
Single source of truth for what opencode sees: the **covert-gpt model provider**
|
||||||
gets through the LiteLLM gateway (`llm.covert-gpt.com`). Users install once;
|
(LiteLLM at `llm.covert-gpt.com`) and the **MCP servers**, which run
|
||||||
their config then refreshes automatically on each new shell.
|
**client-side**. Users install once; their config then refreshes automatically
|
||||||
|
on each new shell.
|
||||||
|
|
||||||
|
## MCP runs on the CLIENT, never on the gateway
|
||||||
|
|
||||||
|
The LiteLLM gateway hosts **no MCP servers** — deliberately. Every API worth
|
||||||
|
exposing (Gitea, Proxmox, UniFi, NetBird, k8s) is **user-scoped**, so hosting it
|
||||||
|
centrally means one shared token per server: every user acts as the same
|
||||||
|
identity and attribution is gone.
|
||||||
|
|
||||||
|
LiteLLM *can* do per-request BYOK (env values written `${X-Header}` are filled
|
||||||
|
from the caller's headers — verified working for proxmox on v1.87.0), but only
|
||||||
|
for servers that tolerate a credential-less spawn. LiteLLM builds its
|
||||||
|
tool→server mapping from a **headerless** background listing, and `gitea-mcp`
|
||||||
|
hard-exits without a token, so it never registers and its tool calls 404. The
|
||||||
|
gateway could therefore host only *some* servers — and a split brain (some MCPs
|
||||||
|
server-side, some client-side) is worse than either. So **all** MCP servers are
|
||||||
|
defined here, each reading the user's **own** credentials from their own
|
||||||
|
environment. The config is identical for everyone; only the secrets differ.
|
||||||
|
|
||||||
|
Credentials are referenced as `{env:VAR}` and are **never** written into the
|
||||||
|
config. Each user exports them in `~/.zshrc.local`:
|
||||||
|
|
||||||
|
| MCP server | env vars |
|
||||||
|
|---|---|
|
||||||
|
| `gitea` | `GITEA_KEY` |
|
||||||
|
| `proxmox` | `PROXMOX_MCP_TOKEN_ID`, `PROXMOX_MCP_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/`).
|
||||||
|
Without it every server dies with "command not found", which opencode surfaces
|
||||||
|
only as a dead server — easy to misread as a broken config.
|
||||||
|
|
||||||
This repo lives in Gitea at `public/dotfiles`. (It is mirrored from the
|
This repo lives in Gitea at `public/dotfiles`. (It is mirrored from the
|
||||||
`ai-k8s-iac` repo under `mcp-config/`; edit it there or here and keep them in
|
`ai-k8s-iac` repo under `mcp-config/`; edit it there or here and keep them in
|
||||||
@@ -10,16 +41,17 @@ sync — the laptops pull from Gitea's `main`.)
|
|||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
- `opencode.gateway.json` — the shared base config: the `covert-gpt` model
|
- `opencode.gateway.json` — the shared base config:
|
||||||
provider (LiteLLM's OpenAI-compatible endpoint at
|
- the `covert-gpt` model provider (LiteLLM's OpenAI-compatible endpoint at
|
||||||
`https://llm.covert-gpt.com/v1`, models map left empty — it is filled by
|
`https://llm.covert-gpt.com/v1`, models map left empty — it is filled by
|
||||||
discovery at sync time) and a single MCP entry for the gateway's
|
discovery at sync time), and
|
||||||
**aggregate** endpoint `https://llm.covert-gpt.com/mcp/` (trailing slash
|
- the **MCP servers**, each `type: local`, spawned by opencode as
|
||||||
required; the bare path 307-redirects, which MCP clients don't follow).
|
`npx -y <pkg>@<pinned-version>` on the user's own machine.
|
||||||
That one endpoint exposes every MCP server the user's key can access, so
|
|
||||||
new gateway servers need no client config change at all. **Identical for
|
**Identical for every user**: every credential (the LiteLLM key, the Gitea
|
||||||
every user**: the per-user key is injected via `{env:LITELLM_KEY}`, never
|
PAT, the Proxmox token) is injected via `{env:VAR}` and never written into
|
||||||
written into the file.
|
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
|
- `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 pulls the base config from Gitea, 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
|
||||||
@@ -50,34 +82,45 @@ own kubeconfig). Sync never touches those.
|
|||||||
|
|
||||||
## User setup (one time)
|
## User setup (one time)
|
||||||
|
|
||||||
Each user needs a LiteLLM **virtual key** scoped to the gateway servers (see
|
Each user needs a LiteLLM **virtual key** (for models) plus their **own**
|
||||||
`charts/inference/MCP-GATEWAY.md` in ai-k8s-iac for how keys are minted). Then:
|
credentials for each MCP server. Run `bootstrap.sh` (it installs Node, which
|
||||||
|
provides the `npx` the MCP servers need), then:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
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)
|
||||||
```
|
```
|
||||||
|
|
||||||
Open a new terminal, run `opencode`, and the gateway tools are there.
|
Then export your own MCP credentials in `~/.zshrc.local`:
|
||||||
|
|
||||||
## Adding a gateway server or model (admin)
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
Register it on the gateway (`charts/inference/values.yaml` →
|
Open a new terminal, run `opencode`, and the tools are there — acting as **you**.
|
||||||
`proxy_config.mcp_servers` for MCP servers, `model_list` for models),
|
|
||||||
deploy, smoke-test. That's it — clients auto-discover both: MCP servers
|
## Adding an MCP server (admin)
|
||||||
through the aggregate `/mcp/` endpoint, models via the `/v1/models` query
|
|
||||||
in `sync.sh`. Users pick them up within the sync TTL (or immediately on a
|
Add it to `mcp.<name>` in `opencode.gateway.json` as `type: local`, with a
|
||||||
new shell). `opencode.gateway.json` here only needs editing for structural
|
**pinned** package version and `{env:VAR}` refs for every secret, then push to
|
||||||
changes (a new provider, endpoint URLs).
|
`main`. Users pick it up within the sync TTL (or immediately on a new shell).
|
||||||
|
|
||||||
|
Two hard requirements:
|
||||||
|
- It must be an **npm package** (opencode spawns it with `npx`).
|
||||||
|
- Its credentials must be **per-user**, via `{env:VAR}`. Never commit a token,
|
||||||
|
and never introduce a shared one — that is the whole reason MCP is not on the
|
||||||
|
gateway.
|
||||||
|
|
||||||
|
Models are different: they live on the gateway
|
||||||
|
(`charts/inference/values.yaml` → `model_list`) and are auto-discovered by the
|
||||||
|
`/v1/models` query in `sync.sh`. No client change needed.
|
||||||
|
|
||||||
## Adding a skill (admin)
|
## Adding a skill (admin)
|
||||||
|
|
||||||
Skills have no gateway side. Add a folder under `skills/` and push to `main` —
|
Add a folder under `skills/` and push to `main` — see `skills/README.md`.
|
||||||
see `skills/README.md`. Verify with `opencode debug skill`.
|
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`.
|
|
||||||
|
|
||||||
## Notes / caveats
|
## Notes / caveats
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"covert-gpt": {
|
"gitea": {
|
||||||
"type": "remote",
|
"type": "local",
|
||||||
"url": "https://llm.covert-gpt.com/mcp/",
|
"command": ["npx", "-y", "gitea-mcp@0.0.10"],
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"headers": {
|
"environment": {
|
||||||
"Authorization": "Bearer {env:LITELLM_KEY}"
|
"GITEA_HOST": "https://git.nic-oconnor.com",
|
||||||
|
"GITEA_ACCESS_TOKEN": "{env:GITEA_KEY}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"proxmox": {
|
||||||
|
"type": "local",
|
||||||
|
"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_INSECURE_TLS": "true",
|
||||||
|
"PVE_READONLY": "false"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-4
@@ -11,15 +11,17 @@
|
|||||||
#
|
#
|
||||||
# What it does:
|
# What it does:
|
||||||
# 1. Fetch the shared opencode.gateway.json from Gitea (the single source of
|
# 1. Fetch the shared opencode.gateway.json from Gitea (the single source of
|
||||||
# truth: the covert-gpt provider + the aggregate MCP endpoint). The file
|
# truth: the covert-gpt provider + the client-side MCP servers). The file
|
||||||
# is identical for all users — the per-user key is injected via
|
# is identical for all users — EVERY credential (LiteLLM key, Gitea PAT,
|
||||||
# {env:LITELLM_KEY}, never baked into the file.
|
# 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.
|
||||||
# (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.
|
# they are declared in the config and run locally under `npx`, each with
|
||||||
|
# the user's OWN credentials. The gateway hosts no MCP servers at all —
|
||||||
|
# see mcp-config/README.md for why (user-scoped APIs, no shared tokens).
|
||||||
# 3. Deploy the repo's skills/ tree to ~/.config/mcp-sync/skills. Skills are
|
# 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
|
# 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).
|
# IS the source of truth, and the tree is fetched whole (repo tarball).
|
||||||
|
|||||||
Reference in New Issue
Block a user