`config claude` wrote a per-directory `.mcp.json`, so you had to re-run it in
every checkout you opened — and in a repo that commits `.mcp.json` (this one
does) it dirtied the working tree. Every other integration is already global:
pi, prime-agent and opencode each have one active project, wired once.
Claude Code's user scope is `mcpServers` in `.claude.json`, which applies in
every directory and window. That is now the default. `--scope project`, or an
explicit `-o/--output`, keeps the old per-directory file for a repo that wants
its own pinned project. `--inspect` stays project-scope — it is a debugging
server you turn on for one checkout.
Details worth knowing:
- The file path is asymmetric: `$CLAUDE_CONFIG_DIR/.claude.json` when that is
set, but `$HOME/.claude.json` by default — beside `~/.claude/`, not inside
it. Verified against a live Claude Code run with an isolated config dir.
- `.claude.json` also holds onboarding state, caches and a per-project map
that Claude Code rewrites while running, so this merges into the document
and writes through a temp file + rename.
- User scope writes no `.mcpctl-project` marker: it scopes nothing to a
directory, and a marker beside `.claude.json` would sit in $HOME and scope
every repo under it.
- `statusline` now resolves directory-scoped `.mcp.json` first (a repo that
pinned itself wins), then user scope, then the marker.
Scope selection reads Commander's option source rather than process.argv —
argv is the test runner's command line when the command is driven in-process,
which the suite caught immediately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
The prime-agent switcher existed only as a 275-line string literal inside
prime-agent-extension.ts, so nothing typechecked or linted it — the exact gap
that let a wrong ctx.ui.select() option shape ship in the pi extension. It now
lives at src/prime-agent-ext/mcpctl-switch.ts with a generator, a tsconfig
checking it against the real @earendil-works/pi-coding-agent types, eslint
coverage and an embed-freshness test, matching pi and opencode.
The extraction was verified byte-identical before any edit, so the behaviour
shipped today is exactly what was captured. Linting it then found six problems
in code nothing had ever checked: object-truthiness null guards, a nullable
string conditional and a missing return type. All behaviour-preserving to fix,
but exactly the class of thing that ships silently when nothing is looking.
Also:
- `config pi` gains --dry-run, the last agent without it.
- The SessionStart hook installer now drops untagged duplicates of its own
exact command — rows left behind before the marker existed, or by a suite
that used to write into a real ~/.claude. Invisible in the UI; they just run
the sync twice per session. A hook the user wrote is never touched, even one
calling `mcpctl skills sync` with different flags.
- docs/claude-integration.md and docs/prime-agent-extension.md, the two
integrations that had no page.
prime-agent deliberately keeps its per-project MCP entry name rather than the
constant `mcpctl` claude and opencode now use: its switcher already unmounts the
previous project, so it never accumulates entries, and re-keying auth.json from
mcp:<project> to mcp:mcpctl would give up per-project token caching and needs a
migration. Documented as its own change rather than folded in here.
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
Three improvements taken from reading the sibling opencode branches
(feat/opencode-extension-abhishek in particular):
- `<leader>m` opens the project picker. Switching is the repeated action and
typing `/mcpctl` every time is friction; the other two commands stay
palette-only.
- A switch disconnects before re-adding. `mcp.add` under the same name does
re-point the tools on its own, but leaves it to opencode whether the previous
client is closed — and an abandoned one keeps its `mcp-session-id` alive on
mcplocal, which is exactly what holds a gated project open. Best-effort, so a
first mount still works.
- The footer label renders `wrapMode="none" truncate`. The home prompt row is
narrow enough that the default wrap broke `mcpctl:homeautomation` across two
lines mid-word; clipping the tail of a long name reads far better.
Verified against opencode 1.18.15: ctrl-x m opens the picker, the home footer is
now one line, and a disconnect-then-add switch still lands — the model called
`mcpctl_begin_session` and listed the new project's tools.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
Adds `mcpctl config opencode`, two opencode plugins and an `opencode` skills
sync target, so an mcpctl project can be switched from inside opencode's TUI
and the active one is visible at a glance.
Unlike `config claude` / `config prime-agent`, this writes NO MCP entry into
the host's config. opencode exposes an HTTP API for its own MCP registry
(`POST /mcp`), so the project is mounted through the running app:
- the token stays in ~/.mcpctl/opencode-state.json (0600) instead of a
mode-0644 opencode.json users paste into bug reports;
- switching projects takes effect on the next turn, with no restart.
Inside opencode:
/mcpctl filterable project picker; switches live
/mcpctl-status active project, mount state, gateway URL
/mcpctl-skills re-sync this project's skills
plus a `mcpctl:<project>` indicator in the prompt footer, next to the model
name and one line above the token counter.
Design notes:
- the MCP server is registered under a constant name, so tools keep a stable
`mcpctl_*` prefix and opencode's per-request tool resolution shows the new
project's tools by itself — no "your old tool names are dead" message to
the model, unlike the pi extension;
- an unchanged mount is never re-registered: mcp.add rebuilds the connection
and mcplocal binds a gated project's unlocked state to that connection's
mcp-session-id, so re-adding would re-lock a project begin_session had just
opened;
- the server plugin does not mount during setup — setup runs before the
server accepts connections and mcp.add calls back into it, which hangs
opencode on a blank screen before the TUI draws;
- the switcher shells out to this CLI (--skip-plugin --skip-marker) so token
minting, state and skills stay in one place;
- no usable credential aborts non-zero with the state file untouched, so a
failed switch leaves the previous project working rather than swapping it
for a mount that 401s.
`skills sync --agent opencode` installs into ~/.config/opencode/skill (XDG
aware) with the same shared-tree semantics as pi and prime-agent. The
credential plumbing shared with `config prime-agent` is lifted to one place and
parameterised by agent rather than copied.
The plugin sources are embedded in the CLI (generated, freshness-tested) so an
installed binary with no source tree can provision them, and are typechecked
against the real @opencode-ai/plugin types.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
Round 2 fixed the first review but introduced regressions of its own, all
of which only bite against state written by the previously installed build.
`config prime-agent`:
- Mint each credential under a unique `prime-agent-<stamp>` name again.
`McpToken` is unique on (name, projectId) and revoke is a soft delete, so
round 2's fixed `prime-agent` name could only ever be minted once per
project — and the revoke-first ordering destroyed the working credential
before discovering the mint would fail.
- Provision the credential BEFORE touching settings.json. Registering the
new project unmounts the previously active one, so a failed mint must not
be able to leave prime-agent with no working project at all. The command
now aborts with settings.json untouched.
- Retire only the token this auth.json actually held, once its replacement
is stored. Sweeping every `prime-agent*` token for the project would
revoke the credential another install (or a custom --output run) is
using; anything else that looks orphaned is reported, not deleted.
- Validate a pre-existing credential instead of trusting its presence: a
revoked or expired token used to short-circuit provisioning and leave
prime-agent broken while the command reported success. Matched by
tokenPrefix against the project's active tokens, so the secret is never
sent. Fails open when the API can't be consulted.
- Actually write auth.json 0600. `writeFile`'s mode is ignored for an
existing file and prime-agent creates auth.json itself at 0644, so chmod
after writing.
- Recognise the untagged mcpServers entries older CLIs wrote (canonical
proxy URL + an `mcp:<name>` mcpctl PAT in auth.json) so a switch unmounts
them instead of leaving two gateways live. Hand-configured servers have
no such credential and are still preserved. Same rule in the `/mcpctl`
switcher's active-project lookup.
- Add `--skip-marker`, and pass it from the `/mcpctl` switcher: the
extension runs from whatever directory prime-agent was started in, and
was silently re-scoping that repo's `.mcpctl-project`.
`skills sync --agent prime-agent`:
- Record ownership from the skill's own scope, not the syncing project's.
Globals were being pinned to whichever project happened to sync them,
after which every other project refused to update them forever.
- Never adopt legacy, ownership-less state into the current scope. Round 2
did, which deleted the other project's skills on the first sync after
upgrading. Such entries are attributed to the project that last wrote the
state file, and left alone when that isn't the project syncing now.
- Close the overwrite-guard bypass: a sync with no project, or a global
landing on a project-owned name, could still clobber and re-own a
tracked skill.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
Mirror `mcpctl config claude` for prime-agent (which talks to the same mcpctl
proxy MCP gateway over HTTP instead of stdio):
`mcpctl config prime-agent --project X`:
- registers the proxy MCP gateway in ~/.prime/agent/settings.json as
mcpServers.X = { type: "http", url: <gateway>/projects/X/mcp }, merging with
any existing servers (e.g. the bundled `sre` project) and preserving all other
settings
- writes a .mcpctl-project marker so later syncs resolve the project
- syncs the project's skills into ~/.prime/agent/skills/<name>/ as markdown
skills (prime-agent auto-discovers them at session start)
New `mcpctl skills sync --agent prime-agent` target re-syncs the tree later.
- src/cli/src/config/prime-agent.ts: settings.json read/merge/write helpers
- src/cli/src/utils/prime-agent-skills.ts: prime-agent sync (reuses
installSkillAtomic + skills-state; skips Claude-only hooks/postInstall)
- completes config.ts/skills.ts wiring; regenerated shell completions
- tests: commands/prime-agent.test.ts + utils/prime-agent-skills.test.ts
Final stage of v1.
Smoke (mcplocal/tests/smoke/virtual-llm.smoke.test.ts):
- Spins an in-process LlmProvider that returns canned content.
- Runs the registrar against the live mcpd in fulldeploy.
- Asserts: row appears with kind=virtual / status=active, infer
through /api/v1/llms/<name>/infer comes back through the SSE
relay with the provider's content + finish_reason, and a 503
appears immediately after registrar.stop() (publisher offline).
- Times out / cleanup paths idempotent so re-runs against the same
cluster don't litter rows. The 90-s heartbeat-stale flip and 4-h
GC are unit-tested — too slow for smoke.
Docs:
- New docs/virtual-llms.md: when to use this vs creating a regular
Llm row, how to opt-in via publish: true, the lifecycle table,
the inference-relay sequence, the v1 streaming caveat, the v2-v5
roadmap, and the full /api/v1/llms/_provider-* surface.
- agents.md cross-links virtual-llms.md alongside personalities/chat.
- README's Agents section gains a "Virtual LLMs" subsection.
Workspace suite: 2043/2043 (smoke files run separately). v1 closes.
Stage roadmap (each its own future PR):
v2 wake-on-demand · v3 virtual agents · v4 LB pool · v5 task queue
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The closing stage. mcpd now hosts the Stage 5 SPA, the Docker image
bundles the build artifact, a smoke test exercises the personality
HTTP surface end-to-end, and the user-facing docs spell out the
mental model.
mcpd:
- Add @fastify/static dep.
- New routes/web-ui.ts: registers /ui/* against a static bundle. Looks
for the bundle at $MCPD_WEB_ROOT, then /usr/share/mcpd/web (the
Docker image path), then a dev-tree fallback. Logs and skips
cleanly if missing — API-only deploys keep working.
- SPA fallback: any /ui/<path> that doesn't match a file falls through
to index.html so direct hits to react-router URLs work.
- /ui/* falls through to `kind: skip` in mapUrlToPermission, so the
static assets are served unauthenticated. Each API call from the
SPA still carries the bearer token.
Deploy:
- Dockerfile.mcpd builds the @mcpctl/web bundle in the same builder
stage and copies dist/ to /usr/share/mcpd/web in the runtime image.
Smoke (personality.smoke.test.ts):
- Live mcpd flow: create secret/llm/agent/personality, attach an
agent-direct prompt, verify the binding listing, reject double-
attach (409) + foreign-agent prompt (400), set defaultPersonality
by name, detach + delete cleanup.
Docs:
- New docs/personalities.md: VLAN-on-ethernet model, system-block
ordering table, three prompt scopes, CLI walkthrough, web UI
walkthrough, full API surface, RBAC notes.
- agents.md and chat.md cross-link.
- README's Agents section gains a Personalities subsection.
Test count after Stage 6:
mcpd: 801/801 cli: 430/430
web: 7/7 db: 58/62 (4 pre-existing)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the agents feature.
Smoke tests (run via `pnpm test:smoke` against a live mcpd at
$MCPD_URL, default https://mcpctl.ad.itaz.eu):
* tests/smoke/agent.smoke.test.ts — full CRUD round-trip:
create secret + Llm + agent with sampling defaults; `get agents`
surfaces it; `get agent foo -o yaml | apply -f` round-trips
identically; create + list a thread via the HTTP API; agent delete
leaves Llm + secret intact (Restrict + SetNull as designed). Self-
skips with a warning when /healthz is unreachable.
* tests/smoke/agent-chat.smoke.test.ts — gated on
MCPCTL_SMOKE_LLM_URL + MCPCTL_SMOKE_LLM_KEY. Provisions secret +
Llm + agent against a real upstream, runs `mcpctl chat -m … --no-
stream` (asserts a reply lands), then runs the streaming default
(asserts text on stdout + `(thread: …)` on stderr). The fast path
for verifying the in-cluster qwen3-thinking deployment:
MCPCTL_SMOKE_LLM_URL=http://litellm.nvidia-nim.svc.cluster.local:4000/v1 \
MCPCTL_SMOKE_LLM_MODEL=qwen3-thinking \
MCPCTL_SMOKE_LLM_KEY=$(pulumi config get --stack homelab \
secrets:litellmMcpctlGatewayToken) \
pnpm test:smoke
Docs:
* README.md — new "Agents" section under Resources with the
qwen3-thinking quickstart and links to docs/agents.md and
docs/chat.md. Adds llm + agent rows to the resources table.
* docs/agents.md (new) — full reference: data model, chat-parameter
table, HTTP API, RBAC mapping, tool-use loop semantics, yaml
round-trip shorthand, the kubernetes-deployment wiring recipe,
and a troubleshooting section (namespace collision, llm-in-use,
pending-row recovery, Anthropic-tool limitation).
* docs/chat.md (new) — user-facing `mcpctl chat` walkthrough:
modes, per-call flags, slash-commands, threads, and a
troubleshooting section.
* CLAUDE.md — adds a "Resource types" cheatsheet with one-line
pointers to each, including the new `agent` row that links to
the docs.
All suites still green: mcpd 759/759, mcplocal 715/715, cli 430/430.
Smoke tests typecheck and self-skip when no live mcpd is reachable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gitea's auto-generated .repo file contains internal IPs. Use a manual
repo file with the public mysources.co.uk baseurl instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite the Plugin System section to make the extends/inheritance
mechanism clear — show that default extends gate + content-pipeline,
explain hook inheritance and conflict resolution rules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Section 4 now uses --from-template instead of manual --docker-image
- Declarative YAML example uses fromTemplate + envFrom secretRef
- Backup section updated to git-based commands (was old JSON bundle)
- Consistent server naming (my-grafana from template, not bare grafana)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite README Content Pipeline section as Plugin System section
documenting built-in plugins (default, gate, content-pipeline),
plugin hooks, and the relationship between gating and proxyModel
- Update all README examples to use --proxy-model instead of --gated
- Add unit tests: proxyModel normalization in JSON/YAML output (4 tests),
Plugin Config section in describe output (2 tests)
- Add smoke tests: yaml/json output shows resolved proxyModel without
gated field, round-trip compatibility (4 tests)
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>
MCP server containers are managed by and proxied through mcpd,
not directly accessible. Updated diagram to show containers
nested inside mcpd boundary with explanation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comprehensive MCP server management with kubectl-style CLI.
Key features in this release:
- Declarative YAML apply/get round-trip with project cloning support
- Gated sessions with prompt intelligence for Claude
- Interactive MCP console with traffic inspector
- Persistent STDIO connections for containerized servers
- RBAC with name-scoped bindings
- Shell completions (fish + bash) auto-generated
- Rate-limit retry with exponential backoff in apply
- Project-scoped prompt management
- Credential scrubbing from git history
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>