claude-vllm read `~/.config/opencode/opencode.jsonc` directly. Same shape is
useful; sharing the actual file is not — a credential rotation in opencode would
silently change what Claude Code authenticates with, and it couples two tools'
configs for no reason.
It now has its own `$XDG_CONFIG_HOME/mcpctl/claude-vllm.jsonc`, shaped like
opencode's (`provider.<name>.options.{baseURL,apiKey}` plus a `models` map), and
takes priority. The pi and prime-agent homes stay as a fallback so the command
works before any config exists; reading opencode's file is dropped.
No key is stored in the tool. `apiKey` may be a literal in the 0600 file,
`${ENV_VAR}`, or a bare env var NAME (the form pi's models.json already uses),
so the secret can live in the environment instead of on disk. `--init` reads it
from stdin when `--api-key` is omitted — keeping it out of shell history and out
of the process table, where an argument is visible to every user via `ps`.
`--list` prints at most a 10-character prefix.
`--init` records the context window for every model it can see, not just the
active one: recording only the default meant `--model something-else` silently
fell back to Claude Code's assumed 200k on a 393k model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
Claude Code had neither of the things opencode, pi and prime-agent all have: a
visible active project, and a way to change it from inside a session. It has no
plugin API that can draw a widget or open a picker, but it does run a command
for its status line and it does load slash commands — which is enough for both.
- `mcpctl statusline` prints the active project (from .mcp.json, falling back
to a .mcpctl-project marker) and is wired into settings.json. It reads the
directory out of the JSON Claude Code pipes in, so it follows /cwd rather
than reporting wherever the binary was launched. Prints nothing when no
project is active: an empty line beats "none" on every unrelated repo.
- `/mcpctl [project]` switches and reminds you to reconnect from /mcp.
allowed-tools is scoped to the four exact mcpctl invocations it needs.
Three things found by running it rather than reasoning about it:
- Claude Code REWRITES settings.json against its own schema and strips
unknown keys from `statusLine` — our `_mcpctl_managed` marker came back
gone, so ownership is now determined by the command string. (Hooks keep
their marker; statusLine does not.) A composed line like
`my-prompt && mcpctl statusline` is deliberately not claimed.
- Every `!`-prefixed block in a slash command is permission-checked against
allowed-tools. Omitting `statusline` failed the whole command before the
model saw anything. A test now asserts every pre-executed command is
covered.
- Setting ANTHROPIC_AUTH_TOKEN *and* ANTHROPIC_API_KEY makes Claude Code warn
that auth may not work; claude-vllm now sets only the former and clears an
inherited API key.
Also fixes a pre-existing test-isolation bug this work would have made worse:
`config claude` wrote into the developer's real ~/.claude when the suite ran,
which is how an untagged duplicate of the skills-sync SessionStart hook got
there. Both the hook installer and the new UI installers now honour
CLAUDE_CONFIG_DIR (Claude Code's own override — correct behaviour first,
isolation second), `config claude` gains --claude-dir for parity with --pi-dir
and --opencode-dir, and the suite is verified to leave ~/.claude byte-identical.
Verified live: status line renders `mcpctl:homeautomation`, `/mcpctl docmost`
switches and the line updates to `mcpctl:docmost` in the same session.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
The gateway at llm.ad.itaz.eu is LiteLLM in front of vLLM, and LiteLLM already
serves the Anthropic Messages API on /v1/messages — verified with a real
completion. So Claude Code needs no bridge: pointing ANTHROPIC_BASE_URL at it is
the whole integration.
`claude-vllm` exists only to stop you pasting four exports each time. It reuses
what another agent is already configured with — ~/.pi/agent, ~/.prime/agent, then
opencode's config, first hit wins — taking the base URL and the credential from
the same source so one gateway's URL is never paired with another's key.
Beyond the obvious ANTHROPIC_* vars it sets two that are easy to miss:
- ANTHROPIC_SMALL_FAST_MODEL / ANTHROPIC_DEFAULT_HAIKU_MODEL, or the
background and summarisation calls ask the gateway for a real Haiku it does
not serve and every one 404s;
- CLAUDE_CODE_MAX_CONTEXT_TOKENS from the provider's declared contextWindow,
because Claude Code assumes 200k for models it has no table for — and
deepseek-v4-* is 393k, so it would auto-compact at half capacity.
On `claude-mcpctl`: mcpctl's LLM layer is a client, not a server. mcpd serves
/api/v1/llms (management) and its adapters call out to providers for gating,
prompt selection and agent chat; nothing serves /v1/messages. Routing Claude
through mcpctl would mean adding an Anthropic-shaped passthrough that re-wraps
LiteLLM — worth doing only if mcpctl in the LLM path buys something of its own
(per-project gating of model calls, prompt audit, budgets), which is a mcpd
endpoint rather than a wrapper script.
Verified: `claude-vllm --model deepseek-v4-fast -- -p "..."` completes against
deepseek on the homelab, with the unknown-model context warning gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
Instances are immutable and get recreated on any server edit, so anything an
MCP server wrote to its container filesystem was lost at exactly that point.
That ruled out every stateful MCP server, docs-mcp among them: its index is a
SQLite file (better-sqlite3 + sqlite-vec) and it has no external-database mode,
so no amount of Postgres helps.
A server or template can now declare volumes. The backing store is keyed on the
server, not the instance — `mcpctl-<server>-<name>` — which is the whole point:
an instance-scoped claim would be destroyed precisely when the data needs to
survive. On Kubernetes that is a PVC ensured in the servers namespace before the
pod is created and never deleted with it; on Docker, a named volume (named, not
anonymous, so `removeContainer`'s `v: true` leaves it alone).
Claims are ReadWriteOnce, so volumes and replicas > 1 are mutually exclusive;
validation rejects that pair instead of leaving the extra replicas
unschedulable. storageClassName is omitted rather than sent empty when no class
is configured — to Kubernetes those mean different things.
Also fixes a pre-existing bug in the same path: seedTemplates dropped `runtime`,
so every PyPI-backed template seeded from YAML silently defaulted to node and
would run `npx` against a package that only exists on PyPI. `unifi-network`
declares `runtime: python` and had been seeding with runtime unset.
Templates added, all self-hosted and none needing an API key:
- duckduckgo — no backing service at all
- searxng — needs a SearXNG engine (compose profile in stack/)
- docs-mcp — open-source Context7/Ref alternative, uses the new volume
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
Move backup SSH keys and repo URL from MCPD_BACKUP_REPO env var to a
"backup-ssh" secret in the database. Keys are auto-generated on first
init and stored back into the secret. Also fix ERR_HTTP_HEADERS_SENT
crash caused by reply.send() without return in routes when onSend hook
is registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Exempt /healthz and /health from rate limiter
- Increase rate limit from 500 to 2000 req/min
- Register backup routes even when disabled (status shows disabled)
- Guard restore endpoints with 503 when backup not configured
- Add retry with backoff on 429 in audit smoke tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DB is source of truth with git as downstream replica. SSH key generated
on first start, all resource mutations committed as apply-compatible YAML.
Supports manual commit import, conflict resolution (DB wins), disaster
recovery (empty DB restores from git), and timeline branches on restore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add warmup() to LlmProvider interface for eager subprocess startup
- ManagedVllmProvider.warmup() starts vLLM in background on project load
- ProviderRegistry.warmupAll() triggers all managed providers
- NamedProvider proxies warmup() to inner provider
- paginate stage generates LLM-powered descriptive page titles when
available, cached by content hash, falls back to generic "Page N"
- project-mcp-endpoint calls warmupAll() on router creation so vLLM
is loading while the session initializes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce a Helm-chart-like template system for MCP servers. Templates are
YAML files in templates/ that get seeded into the DB on startup. Users can
browse them with `mcpctl get templates`, inspect with `mcpctl describe
template`, and instantiate with `mcpctl create server --from-template=`.
Also adds Portainer deployment scripts, mcplocal systemd service,
Streamable HTTP MCP endpoint, and RPM packaging for mcpctl-local.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>