`mcpctl config claude --project X` writes user scope and never rewrites a
checkout's `.mcp.json`. The status line, however, preferred that file
unconditionally — so a legacy project-named entry an older mcpctl left behind
(`homeautomation` -> `mcpctl mcp -p homeautomation`) kept naming the old project
for good, and every switch looked like it had done nothing.
Reproduced live: with user scope on `sre`, `mcpctl statusline --directory
~/developer/michalzxc/claude/debug` printed `mcpctl:homeautomation` — a project
Claude Code also had in `disabledMcpServers` for that directory, so the line
named a server that was not even mounted.
Rank the sources by how deliberate each one is instead: a canonical `mcpctl`
pin, then user scope, then legacy residue, then the marker. A pin is a decision
and still wins; residue is not and no longer does. At every step, skip a server
Claude Code has switched off for that directory.
`config claude` now also warns when the working directory's `.mcp.json`
contradicts the switch, naming the file — the two scopes are merged rather than
chosen between, so nothing else would tell you.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wUmrfkVQR6CKcYKxENq7k
`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
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
`config claude` named the `.mcp.json` entry after the project, and the file is
merged rather than rewritten — so configuring a second project left the first
one mounted alongside it. Every project you had ever configured stayed
connected, with duplicate tool names and nothing saying which was active.
The entry is now always `mcpctl`, and switching rewrites what sits behind that
name. Claude Code can reconnect an existing MCP server from inside a session, so
a switch lands without restarting the app, and the tool prefix stays stable
across switches. Entries an older CLI wrote are retired on the next run —
recognised by the pairing that makes retiring them safe: our command, named
after the very project it bridges to. A hand-configured server is never touched.
The shaping lives in config/claude-mcp.ts as pure functions so the merge,
migration and active-project detection are unit-tested rather than inferred from
a command's side effects.
Also brings two parity gaps in line with `config opencode` / `config prime-agent`:
--dry-run, and --skip-marker for when the caller must not re-scope the directory
it runs in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
Phase 5 of the Skills + Revisions + Proposals work. Skills are now
materialised onto disk under ~/.claude/skills/<name>/, with
hash-pinned diff against mcpd, atomic per-skill install, and
preservation of locally-modified files. `mcpctl config claude --project X`
now wires the full pickup chain: writes .mcpctl-project marker, runs
the initial sync, installs the SessionStart hook so subsequent Claude
invocations stay in sync transparently.
## Sync algorithm
1. Resolve project: `--project` flag overrides; else walk up from cwd
looking for `.mcpctl-project`; else fall back to globals-only.
2. GET /api/v1/projects/:name/skills/visible (or
/api/v1/skills?scope=global without a project). Server returns
id + name + semver + scope + contentHash + metadata — no body, no
files. The contentHash is sha256 of the canonicalised body, computed
server-side; any reordering of keys produces the same hash, so it's
a stable diff key.
3. Load ~/.mcpctl/skills-state.json (lives outside ~/.claude/skills/
on purpose — Claude Code reads that tree and we don't want to
pollute it with our bookkeeping).
4. Diff:
- server skill not in state → INSTALL
- server skill, state contentHash matches → SKIP (cheap path)
- server skill, state contentHash differs → UPDATE (fetch full body)
- state skill not in server → orphan, REMOVE (preserve if locally
modified, unless --force)
5. Atomic per-skill install: write to <targetDir>.mcpctl-staging-<pid>/,
rename existing tree to .mcpctl-trash-<pid>, swap staging in,
rmtree the trash. A concurrent reader (Claude Code starting up)
never sees a partial tree.
6. State file updated with new versions, per-file SHA-256, install
path. saveState is atomic (temp + rename).
## Failure semantics
- `--quiet` mode (used by SessionStart hook): exit 0 on network /
timeout / mcpd error. Fail-open is non-negotiable here — we never
want a hung mcpd to block Claude Code starting up.
- Auth failure: exit 1, clear "run mcpctl login" message.
- Disk error during state save: exit 2.
- Per-skill errors are collected in the result and reported as a
count; one bad skill doesn't stop the others.
Network fetches run with concurrency 5. The server-side
`/visible` endpoint is metadata-only so the cheap path (everything
unchanged) needs exactly one HTTP roundtrip total.
## Files added
### CLI utilities (src/cli/src/utils/)
- skills-state.ts — load/save state, per-file sha256, edit detection.
- project-marker.ts — walk-up to find `.mcpctl-project`, bounded by
user home so we never search above $HOME.
- sessionhook.ts — install/remove a SessionStart hook entry tagged
with `_mcpctl_managed: true`. Idempotent. Defensive against
missing/empty/JSONC settings.json.
- skills-disk.ts — atomic install via staging-dir rename swap,
symmetric atomic delete via trash-dir rename. Path-escape attempts
in files{} are rejected.
### CLI command (src/cli/src/commands/)
- skills.ts — `mcpctl skills sync` Commander wrapper + the
`runSkillsSync(opts, deps)` library function (also called from
`mcpctl config claude --project`). Supports `--dry-run`, `--force`,
`--quiet`, `--keep-orphans`. `--skip-postinstall` is reserved
(postInstall execution lands in a follow-up PR, not this one).
### Wiring
- index.ts: registers `mcpctl skills` after `mcpctl review`.
- config.ts: `mcpctl config claude --project X` now writes the
`.mcpctl-project` marker, runs `runSkillsSync` in-process, and calls
`installManagedSessionHook('mcpctl skills sync --quiet')`. New flag
`--skip-skills` opts out (used by tests; useful for CI).
## Server-side change
- src/mcpd/src/services/skill.service.ts: getVisibleSkills now
computes contentHash on the fly from the canonical body shape the
client will reconstruct. Cheap (sha256 of ~few KB per skill); no
schema migration needed since hash is derived not stored.
## Tests
Four new utility test files (31 tests) under src/cli/tests/utils/:
- sessionhook.test.ts — creation, idempotency, command updates,
preservation of user hooks, removal, empty/JSONC tolerance.
- skills-disk.test.ts — atomic write, replacement without leftovers,
path-escape rejection, atomic delete, listing ignores
staging/trash artifacts.
- skills-state.test.ts — sha256 determinism, state round-trip,
schema-version drift handling, edit detection.
- project-marker.test.ts — cwd hit, walk-up, $HOME boundary, empty
marker, write+read round-trip.
The existing `mcpctl config claude` test (claude.test.ts) was updated
to pass `--skip-skills` so it stays focused on .mcp.json generation;
the new sync flow is covered by the utility tests.
Full suite: 162 test files / 2157 tests green (up from 158 / 2127).
## Deferred to a follow-up
- `metadata.hooks` materialisation into `~/.claude/settings.json` —
the data path exists, sync receives it; PR-7 or a focused follow-up
will write the `_mcpctl_managed: true` entries for declarative
hooks.
- `metadata.mcpServers` auto-attach via mcpd API — likewise.
- `metadata.postInstall` script execution — the most substantive
deferred piece. Current sync logs a TODO and skips. The corporate
trust model (publisher-side rigor, not client-side defence) means
this is straightforward to add once we wire the curated env +
timeout + audit emission. Orthogonal to file sync, easier to ship
separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
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>
- Wait for stdout.write callback before process.exit in STDIO transport
to prevent truncation of large responses (e.g. grafana tools/list)
- Handle MCP notification methods (notifications/initialized, etc.) in
router instead of returning "Method not found" error
- Use -p shorthand in config claude output
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mcp subcommand now has its own -p/--project option with
passThroughOptions(), so `mcpctl mcp --project NAME` works when Claude
spawns the process. Updated config claude to generate
args: ['mcp', '--project', project] and added Commander-level tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New `mcpctl mcp -p PROJECT` command: STDIO-to-StreamableHTTP bridge
that reads JSON-RPC from stdin and forwards to mcplocal project endpoint
- Rework `config claude` to write mcpctl mcp entry instead of fetching
server configs from API (no secrets in .mcp.json)
- Keep `config claude-generate` as backward-compat alias
- Fix discovery.ts auth token not being forwarded to mcpd (RBAC bypass)
- Update fish/bash completions for new commands
- 10 new MCP bridge tests, updated claude tests, fixed project-discovery test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace admin role with granular roles: view, create, delete, edit, run
- Two binding types: resource bindings (role+resource+optional name) and
operation bindings (role:run + action like backup, logs, impersonate)
- Name-scoped resource bindings for per-instance access control
- Remove role from project members (all permissions via RBAC)
- Add users, groups, RBAC CRUD endpoints and CLI commands
- describe user/group shows all RBAC access (direct + inherited)
- create rbac supports --subject, --binding, --operation flags
- Backup/restore handles users, groups, RBAC definitions
- mcplocal project-based MCP endpoint discovery
- Full test coverage for all new functionality
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude command manages .mcp.json files (generate from project, show, add,
remove entries). Project command provides CRUD for projects with profile
assignment management.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>