e4e2e063f13301a0d7db31575ae3f20a20cd8442
92 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b2547429ca |
fix(health): a passing tools/list is live, not healthy
Some checks failed
CI/CD / lint (pull_request) Successful in 1m16s
CI/CD / test (pull_request) Successful in 1m29s
CI/CD / typecheck (pull_request) Successful in 3m15s
CI/CD / smoke (pull_request) Failing after 2m0s
CI/CD / build (pull_request) Successful in 5m5s
CI/CD / publish (pull_request) Has been skipped
`mcpctl get instances` showed all eight servers healthy while the UniFi one
had never once reached its controller. The default probe is `tools/list`,
which MCP servers answer from a static in-process table — no credentials, no
upstream, ~3ms. It cannot fail for any reason the user cares about, so it was
reporting `healthy` for every process that managed to start.
Split the two passes:
healthy — readiness: `tools/call` on `healthCheck.tool`. The upstream
answered, so the server can actually do its job.
live — liveness: `tools/list` only. Process up, upstream unverified.
`live` is now the default for any server without a `healthCheck.tool`. It is
not a warning; it is an admission that nothing is watching that server. Probe
events name which probe ran and which tool ("Readiness check (list_sites)
passed"), so the events log distinguishes the two after the fact.
Also:
- `healthCheck.tool` is optional now, so the timings can be tuned without
inventing a readiness probe.
- `create server --health-check-tool/-args/-interval/-timeout/
-failure-threshold`, per the rule that everything applyable is a create
flag. Merges over a `--from-template` healthCheck rather than replacing it.
- `describe instance` explains a `live` verdict instead of leaving it cryptic.
- create.ts held a raw NUL byte in a string literal, which made grep treat the
whole file as binary and silently skip it. Escaped as `\0`.
Verified against the live fleet: with readiness probes configured, my-grafana
went unhealthy (Grafana API 403) and my-node-red degraded (connect timeout to
a Tailscale address) — both had read healthy for months.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114dg56YmVacyqhp5fitcTb
|
||
|
|
cbd3b95d97 |
fix(opencode): guard state parsing, lint the .tsx, correct an overstated doc claim
Three findings from a cross-branch review of the competing opencode
implementations, all of which are fair.
1. `readState` type-guards the parsed JSON now. A bare try/catch does not
cover it: `JSON.parse('null')` succeeds and returns null, so the catch never
fires and the next `state.project` throws a TypeError that takes the plugin
down. Verified the crash before fixing; a test pins the guard in the embedded
copies. Credit to the competing 'opencode-mine' branch, which had this right.
2. eslint now covers `src/opencode-ext/*.tsx`. The glob was `*.ts` only, so the
300-line TUI plugin — the largest file in the addon — was linted by nothing.
It was typechecked, which is why this went unnoticed. Confirmed the rules
actually fire on it rather than the file being silently skipped. The
'abhishek' branch was the only entry that got this right.
3. docs/opencode-extension.md overstated the security argument. "The token would
sit in a 0644 opencode.json" is not a point against a `type: local` stdio
bridge, which needs no token at all because it reads your own credentials.
That reason is a consequence of having picked the HTTP gateway, not a
justification for it. The docs now lead with the real reason — live
re-pointing without a restart — and state the trade honestly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNP
|
||
|
|
b7c0de2bf0 |
feat(claude): register the MCP server in user scope by default
`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
|
||
|
|
a9fcd83ed8 |
refactor(prime-agent): extract the /mcpctl switcher to typechecked source; pi --dry-run; docs
Some checks failed
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m24s
CI/CD / typecheck (pull_request) Successful in 3m20s
CI/CD / smoke (pull_request) Failing after 2m1s
CI/CD / build (pull_request) Successful in 2m37s
CI/CD / publish (pull_request) Has been skipped
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
|
||
|
|
b3a062ce28 |
feat(claude): active-project status line + /mcpctl switcher, and stop tests writing to ~/.claude
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
|
||
|
|
d7055a0953 |
fix(claude): one constant mcpctl MCP entry instead of one per project
`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 |
||
|
|
99f881dd67 |
feat(opencode): leader keybind, explicit unmount on switch, non-wrapping indicator
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 |
||
|
|
be2a5cb189 |
feat(opencode): native opencode integration — /mcpctl switcher, live project switching, footer indicator
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
|
||
|
|
3fa41e4d46 |
Merge main into feat/pi-extension
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m11s
CI/CD / lint (pull_request) Successful in 2m28s
CI/CD / test (pull_request) Successful in 1m24s
CI/CD / smoke (pull_request) Failing after 1m55s
CI/CD / build (pull_request) Successful in 4m45s
CI/CD / publish (pull_request) Has been skipped
|
||
|
|
90c49bcb22 |
refactor(prime-agent): drop the widget fallback, keep the tray status
With prime-agent-extension-status.patch in place the tray renders ctx.ui.setStatus() next to the model name, which is what a status line should be. The widget was a workaround for its absence and was never a substitute: widgetContainerBelow sits in the fullscreen *scroll* list, not the dock, so it scrolled away with the transcript, and with both set the project name appeared twice. The startup retries stay: resetExtensionUI() clears extension statuses just as it cleared widgets, so the value set during session_start is still wiped before it can be seen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
e638f81456 |
Merge main into feat/pi-extension
Some checks failed
CI/CD / lint (pull_request) Successful in 1m8s
CI/CD / typecheck (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / build (pull_request) Successful in 2m27s
CI/CD / smoke (pull_request) Failing after 3m30s
CI/CD / publish (pull_request) Has been skipped
|
||
|
|
a1893a7578 |
fix(prime-agent): publish the project indicator on turn_start, not only session_start
prime-agent emits session_start from exactly one place — reload() — and never at startup. A session_start-only handler therefore never ran on a fresh session, and the '/mcpctl -> already on X' path returns without reloading, so the indicator stayed blank exactly when it was most wanted. Also guard against the no-op UI context: the runtime hands extensions noOpUIContext until the TUI binds the real one, and every setter on it silently discards. Publishing under it would cache a label that never rendered. Now published on session_start (reload), turn_start (earliest reliable point on a fresh session), and on entering /mcpctl. Repeat calls are a no-op unless the label changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
e6db9cac83 | Merge main into feat/pi-extension (prime-agent switcher UX) | ||
|
|
ba05b2865c |
feat(prime-agent): filterable project picker + a visible active-project indicator
Two gaps the pi switcher had already closed. **The indicator was invisible.** `ctx.ui.setStatus` was the wrong channel: prime-agent stores extension statuses (`FooterDataProvider.setExtensionStatus`) but nothing ever reads them back — there is no `getExtensionStatuses()` call site anywhere in the app, so the value was recorded and never rendered. pi's `footer.js` does render them, which is why the same code showed a status line there and nothing here. The active project is now published as a *widget* (`extensionWidgetsBelow` → `renderWidgets()`), which prime-agent does render. `setStatus` is still called so pi keeps its footer entry. **No search.** With 356 projects the picker was a wall of `smoke-mcptoken-*`. Above 20 projects it now asks for a filter first; space-separated terms must all match, case-insensitively, against name or description, so `home auto` finds `homeautomation`. Active project sorts first, then alphabetical. Deliberately no client-side truncation here, unlike the pi picker: pi's `ExtensionSelectorComponent.updateList()` renders every option, so a long list fills the screen and has to be capped. prime-agent's selector windows the list itself and shows a true `(20/356)` counter — capping would replace an accurate total with a misleading one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
8b006263de |
fix(skills): write the frontmatter every Agent Skills host requires
Some checks failed
CI/CD / lint (pull_request) Successful in 1m18s
CI/CD / typecheck (pull_request) Successful in 1m15s
CI/CD / test (pull_request) Successful in 1m27s
CI/CD / build (pull_request) Successful in 2m17s
CI/CD / smoke (pull_request) Failing after 3m8s
CI/CD / publish (pull_request) Has been skipped
mcpd keeps `name` and `description` as columns, and a skill's `content` is
often just the body — `propose-learnings` starts straight at its `#` heading.
The sync wrote `full.content` verbatim, so the SKILL.md landed with no YAML
frontmatter and the host rejected it. Observed live:
[Skill warning]
~/.prime/agent/skills/propose-learnings/SKILL.md
description is required
Claude Code and pi require the same keys, so this affected every target; it
only surfaced now because prime-agent prints the warning at startup.
`ensureSkillFrontmatter` synthesises the block from the columns when it is
absent, and fills in only a missing `name`/`description` when the author
already supplied a header — an existing complete header is never rewritten.
Values are emitted as JSON strings (valid YAML double-quoted scalars) because
descriptions routinely contain `:` and `#`, which break a bare scalar.
Also: `--force` now re-fetches skills whose server content is unchanged.
Without it a skill already on disk could never be repaired by a client-side
fix — the content hash still matched, so it was skipped forever, which is
exactly what happened on the first attempt to repair the file above.
Three existing assertions compared SKILL.md to the raw server content; they
now assert the body survives rather than exact bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
|
||
|
|
47809a8942 |
fix(pi): correct the model's tool context after a project switch
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m13s
CI/CD / lint (pull_request) Successful in 2m30s
CI/CD / test (pull_request) Successful in 1m25s
CI/CD / build (pull_request) Successful in 2m28s
CI/CD / smoke (pull_request) Failing after 2m59s
CI/CD / publish (pull_request) Has been skipped
Switching projects twice left the agent believing it had lost MCP access entirely: it kept calling the previous project's tool names, got "Tool mc_<old>_begin_session not found", and concluded no MCP tools existed. Root cause is a pi constraint, not a bug in the switch. pi has no way to unregister a tool — `registerTool` only ever does `extension.tools.set(name)` — so the previous project's `mc_*` tools stay registered and merely go inactive. `setActiveTools` correctly drops them from the live set, but the conversation still contains the old project's tool listing, so the model keeps calling names that now answer "not found". Nothing was telling the model the tool set had changed. Now the switch injects a custom message naming the active project, stating that previously listed mcpctl tool names are dead, and listing what is actually callable. Custom messages are converted to user-role messages by `convertToLlm`, so they do reach the model (unlike `appendEntry`, which is explicitly excluded from context). `display: false` keeps it out of the transcript — the notification is what the human reads. The gate is also called out explicitly, in both the notification and the injected message. A freshly switched project gets a new mcp-session-id and is therefore gated again, so "1 tool(s) ready" is correct but reads like a failure; it now says which begin_session call unlocks the rest. `toolChangeAnnouncement` is exported and unit-tested rather than left as wording only reachable through a TUI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
24a3b8cc0a |
feat(pi): filter the project picker instead of scrolling hundreds of rows
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m10s
CI/CD / test (pull_request) Successful in 1m22s
CI/CD / lint (pull_request) Successful in 2m45s
CI/CD / smoke (pull_request) Failing after 3m35s
CI/CD / build (pull_request) Successful in 2m7s
CI/CD / publish (pull_request) Has been skipped
pi's selector is a plain arrow-key list: `ExtensionUIDialogOptions` has no search field and `ExtensionSelectorComponent` ignores typed characters, so filtering has to happen before the list is handed over. With 356 projects — most of them `smoke-proj-none-*` leftovers — arrowing to the one you want is hopeless. Above 20 projects the picker now asks for a filter first. Terms are space-separated and all must match as case-insensitive substrings, so `home auto` finds `homeautomation`. Blank shows everything, Esc cancels. The active project sorts first (most likely pick), then alphabetical. A result set over 50 is capped, and the title says what was dropped — a silently truncated list reads as "that's all of them". The ordering and matching are extracted into an exported `filterProjects` so they are unit-tested rather than eyeballed through a TUI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
739e679d1a |
fix(pi): repair the /mcpctl menu, skills target, and typecheck the extension
Some checks failed
CI/CD / lint (pull_request) Successful in 1m7s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / typecheck (pull_request) Successful in 2m49s
CI/CD / smoke (pull_request) Failing after 1m54s
CI/CD / build (pull_request) Successful in 4m21s
CI/CD / publish (pull_request) Has been skipped
The pi extension shipped in `src/pi-ext/` was covered by no tsconfig and no
eslint config, so nothing ever checked it against pi's API. Pointing tsc at
the published @earendil-works/pi-coding-agent types found the command surface
to be inert.
Fixes:
- `/mcpctl` did nothing. `ctx.ui.select` takes `string[]` and returns the
chosen string; it was called with `{value,label}` objects, so the menu
rendered five `[object Object]` rows and `choice === "status"` never
matched any branch. Labels are now plain strings mapped back to actions.
- The headless branch returned a status string from a handler typed
`Promise<void>`; pi drops it. Reports via notify instead.
- "Sync skills" omitted `--agent pi`, writing into ~/.claude/skills — in an
integration whose stated purpose is to not depend on ~/.claude — and said
so in its own success message. It also ran execSync with `stdio: "inherit"`,
painting raw output over pi's TUI, and interpolated the project name into a
shell string. Now execFile with `--agent pi` and captured output.
- Tool results typed `content[].type` as `string`; pi's AgentToolResult wants
the `"text"` literal.
- `callTool` asserted `Promise<unknown>` to `ToolCallResult`.
- Sanitising MCP tool names to `[a-z0-9_]` can collide (`docs.search` vs
`docs-search`). The colliding tool was silently never registered but still
reported active, so its calls were forwarded to the first tool. Names are
now disambiguated and tracked with the MCP tool they forward to.
- `registerWithPi` rewrote settings.json even when nothing changed. Since
parsing strips `//` comments, a no-op run destroyed them.
Guards, so this class of bug can't return:
- `src/pi-ext/tsconfig.json` checks the extension against the real published
pi types (dev dependency, not a shim — a shim drifting from the published
API is the exact failure being guarded). Wired into `pnpm typecheck`.
- eslint now covers `src/pi-ext/*.ts` like every other source file.
- A test fails if the embedded copy in `config/pi-extension.ts` is stale;
editing the sources without regenerating silently shipped old code.
Also: the branch added `config pi` without regenerating shell completions
(the committed-completions test was failing), and the doc advertised
`mcpctl pi sync-skills`, which does not exist. Both corrected, plus a note
on the session-token vs `mcpctl_pat_` bearer difference that would bite
against an authenticated `mcplocal serve`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
|
||
|
|
f1d84b0952 |
fix(config pi): embed extension sources so installed binary is self-contained
- add scripts/generate-pi-extension.ts to embed src/pi-ext sources as string constants (mirrors prime-agent's embedded switcher pattern). - config pi now writes the embedded sources by default, so it works from an /usr/bin install with no source tree (--extension-dir overrides for dev). Verified installed binary writes files byte-identical to source. - add installEmbeddedExtension test. |
||
|
|
f90e23d000 |
Merge remote-tracking branch 'origin/main' into feat/pi-extension
# Conflicts: # README.md # src/cli/src/commands/config.ts # src/cli/src/commands/skills.ts |
||
|
|
28f1a411fd |
feat(pi): add native pi integration — extension, config pi, skills --agent pi
- src/pi-ext/: self-contained pi extension (mcpctl-pi.ts + vendored mcp-http client) that talks JSON-RPC directly to mcplocal and registers project MCP tools as native pi tools. No MCP client, no ~/.claude. - Persistent per-project session so gated projects ungate on begin_session. - /mcpctl command: status, switch project (GUI), refresh tools, sync skills. - mcpctl config pi: installs extension, wires pi settings, persists active project, syncs skills into ~/.pi/agent/skills. - skills sync: add --agent pi (target install root). - docs + tests. |
||
|
|
ce7df10e06 |
feat(cli): show the active mcpctl project in prime-agent's footer
`/mcpctl` could switch projects but there was no way to see which one was active without running a command. prime-agent exposes the same status-bar channel the model name uses (`ctx.ui.setStatus`), so the switcher now publishes `mcpctl:<project>` there. Wired to `session_start`, which fires on startup *and* on every reload — including the reload the switch itself triggers — so the footer tracks settings.json without extra bookkeeping. Cleared when no project is mounted. Also fixes `notify(..., 'success')`: the API only accepts info|warning|error. Not a live bug (prime-agent falls through to the same showStatus path as 'info') but it fails a typecheck of the extension against the real ExtensionAPI, which is how it was found. The extension ships as a JSON-escaped string and is never compiled by our build, so it was typechecked out-of-tree against @earendil-works/pi-coding-agent's published types. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
170dc06496 |
fix(cli): close third review — token collision, migration, ownership
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m5s
CI/CD / lint (pull_request) Successful in 2m13s
CI/CD / test (pull_request) Successful in 1m20s
CI/CD / build (pull_request) Successful in 2m9s
CI/CD / smoke (pull_request) Failing after 2m44s
CI/CD / publish (pull_request) Has been skipped
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 |
||
|
|
fa7055ac5e |
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / typecheck (pull_request) Successful in 1m5s
CI/CD / test (pull_request) Successful in 3m7s
CI/CD / smoke (pull_request) Failing after 3m13s
CI/CD / build (pull_request) Successful in 2m12s
CI/CD / publish (pull_request) Has been skipped
Addresses the second round of `config prime-agent` review (10 findings). auth.json (config/prime-agent.ts) — the settings.json data-loss fix had a twin: - loadPrimeAgentAuth now fails loudly on corrupt JSON instead of swallow-and- rewrite, so one syntax error can no longer destroy the provider API key and every other project's credential. hasPrimeAgentAuth shares that guarantee. - writePrimeAgentAuth writes 0600 (preserving an existing file's mode) instead of the default umask — bearer tokens are no longer world-readable on first creation. state ownership (commands/skills.ts) — the ownership model edge cases: - orphan-removal guard now normalises a canonical scope (project name, or null for globals; legacy undefined adopted to current scope) instead of comparing null against undefined, so global-only syncs and pre-PR state can no longer leave stale skills on disk forever. - a same-named skill *tracked* to a different project is preserved (with a warning) rather than silently overwritten in the shared flat tree. - mcpServers auto-attach is gated behind !isPrimeAgent with an explicit warning (prime-agent's HTTP gateway must not mutate shared mcpd project attachments); this also makes the earlier dropped-attach concern explicit rather than silent. single-active project + switcher (config/prime-agent.ts, prime-agent-extension.ts): - registerPrimeAgentMcp tags the project's entry mcpctlManaged:true and removes other mcpctl-managed entries, so prime-agent has one *active* mcpctl project while preserving untagged servers (hand-configured sre, websearch, etc). - the /mcpctl extension now reads that tag as the single source of truth for the active project, fixing the false short-circuit / no-op switch. config.ts command: - an explicit -p now updates a differing up-tree .mcpctl-project marker (scope no longer silently reverts on the next sync), no-ops when it matches, and still never scopes $HOME. - a project left with no usable credential now exits non-zero (the /mcpctl extension checks child exit status, so it no longer reports a successful switch after provisioning failed). - skills sync is treated as best-effort: settings+auth determine switch success, so a skills error no longer falsely fails the switch. - token minting now revokes prior active `prime-agent` tokens before creating a fresh one (no more never-expiring token litter / lost-credential duplication). Tests (544 green): corrupt auth.json refusal, 0600 mode, mint-failure exit code, single-active dedup preserving untagged sre, cross-project overwrite preservation, and global-orphan removal on global-only sync. |
||
|
|
eb1642ab1a |
fix(cli): harden config prime-agent sync + install /mcpctl switcher extension
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / typecheck (pull_request) Successful in 1m6s
CI/CD / test (pull_request) Successful in 3m9s
CI/CD / build (pull_request) Successful in 2m16s
CI/CD / smoke (pull_request) Failing after 3m27s
CI/CD / publish (pull_request) Has been skipped
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). |
||
|
|
582f6f185b |
feat(cli): add mcpctl config prime-agent — proxy MCP + skills sync for prime-agent
Some checks failed
CI/CD / lint (pull_request) Successful in 1m6s
CI/CD / typecheck (pull_request) Successful in 2m10s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / smoke (pull_request) Failing after 1m53s
CI/CD / build (pull_request) Successful in 4m14s
CI/CD / publish (pull_request) Has been skipped
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
|
||
|
|
e85250fedf |
fix(cli): stop the MCP stdio bridge serialising requests
Some checks failed
CI/CD / lint (pull_request) Successful in 1m7s
CI/CD / typecheck (pull_request) Successful in 2m11s
CI/CD / test (pull_request) Successful in 1m20s
CI/CD / build (pull_request) Successful in 2m24s
CI/CD / smoke (pull_request) Failing after 3m23s
CI/CD / publish (pull_request) Has been skipped
The bridge's stdin loop awaited every request before reading the next line, so it handled exactly one at a time. A single slow call therefore stalled every later request — and because those requests were never even sent, nothing could time them out. The client saw silence, not an error. Observed 2026-08-05: two gitea calls through this bridge sat completely mute until Claude Code aborted them at its own 1800s idle limit, reporting "sent no response or progress". The upstream was healthy the whole time — a fresh session answered the same tools in 0.3s, and the gitea MCP server's own log showed the calls never reached it. They died queued in the bridge. JSON-RPC ids exist precisely so responses may return out of order, so nothing here needed a queue. Requests now dispatch concurrently and are tracked in a set; stdin close awaits them before the session DELETE, otherwise a concurrent call races the teardown and dies with a 404. We still serialise until the session id exists: it arrives on the first response, and firing later requests without it would open a second upstream session. A client sends `initialize` first and waits for the reply anyway, so this costs one round trip rather than throughput. Also makes the per-request timeout configurable via MCPCTL_MCP_TIMEOUT_MS (default unchanged at 30s) and names it in the timeout error, so a project with genuinely long tool calls can raise it instead of hitting a hardcoded wall. Tests pin both halves: a fast request must overtake a slow one (this fails on the old serial code — verified by reverting), and a failed request must still produce a JSON-RPC error carrying the original id rather than nothing. |
||
|
|
f614e9bb98 |
feat(proxy): favourite-index tool presentation (favourite/ + all/ + prefer instruction)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / typecheck (pull_request) Successful in 2m36s
CI/CD / smoke (pull_request) Failing after 1m53s
CI/CD / build (pull_request) Successful in 4m16s
CI/CD / publish (pull_request) Has been skipped
Measured winner from the DGX-Spark bake-off (toolsim.py, 145-tool catalog): a
curated favourite/<tool> shortlist + the full all/<server>/<tool> catalog + a
load-bearing "prefer favourite/ first" instruction nearly halved wander (37→20)
and 2.5x'd first-pick (2→5/8) vs a flat catalog. The instruction is load-bearing;
enriching descriptions did not help.
- New mcplocal plugin `favourite-index.ts`: composes AFTER gate (no-ops while
gated), reshapes the ungated upstream catalog into favourite/ + all/, injects
the instruction (onInitialize), and rewrites presented names back to canonical
server/tool in onToolCallBefore so normal routing + content-pipeline still run.
Gate/agent virtual tools pass through untouched; favourites are upstream-only.
- compose.ts: onInitialize now concatenates plugin instructions (was first-non-null)
so favindex can contribute its banner alongside the gate's.
- Per-project config `Project.favouriteIndex` {enabled, tools[], maxFavourites};
surfaced to the proxy via discovery; wired at project-mcp-endpoint when enabled.
- Usage derivation: mcpd tool-usage ranking over tool_call_trace events
(normalizing presented names → canonical), GET /api/v1/audit/tool-usage, and
`mcpctl favourites suggest|list`.
- CLI: `create project` gains --favourite/--favourite-index/--max-favourites;
favouriteIndex round-trips through get -o yaml | apply -f. Completions regenerated.
- Tests: plugin unit (presentation, rewrite routing, gated no-op, collisions),
compose merge, canonicalizeToolName, buildFavouriteIndex, + a live smoke test.
- Docs: docs/tool-presentation.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
6ef16f12b6 |
fix(cli): login over HTTPS + suggest last-used email
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m19s
CI/CD / lint (pull_request) Successful in 2m13s
CI/CD / test (pull_request) Successful in 1m18s
CI/CD / smoke (pull_request) Failing after 2m35s
CI/CD / build (pull_request) Successful in 2m25s
CI/CD / publish (pull_request) Has been skipped
auth.ts talked to mcpd over node:http unconditionally with port:url.port,
so an https:// mcpdUrl connected to the ingress on :80, got a 301 redirect
with an empty body (status < 400), and JSON.parse('') crashed login with
"Unexpected end of JSON input". api-client.ts already handled this; the auth
commands did not.
- All 4 auth request fns now pick https/443 and the TLS driver from the URL
protocol, mirroring api-client.ts.
- parseJsonResponse() guard turns any non-JSON <400 body into a clear
"Invalid <ctx> response from mcpd" error instead of a raw SyntaxError.
- login now suggests the previously-used email (when a session for the same
mcpd exists) so pressing Enter reuses it after a session expires.
- Regression tests drive the real default* request fns against a local
HTTP server (valid JSON, empty body, HTML redirect body, https driver
selection) and cover the email-suggestion behaviour.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
467757b966 |
feat(status): show SecretBackend health in mcpctl status
Some checks failed
CI/CD / lint (pull_request) Successful in 1m3s
CI/CD / test (pull_request) Successful in 1m17s
CI/CD / typecheck (pull_request) Successful in 2m33s
CI/CD / smoke (pull_request) Failing after 1m52s
CI/CD / build (pull_request) Successful in 4m55s
CI/CD / publish (pull_request) Has been skipped
Adds a 'Secrets:' line to mcpctl status (and a secretBackends array to JSON output) showing each backend healthy (✓) or, when tokenMeta.lastRotationError is set (e.g. a dead OpenBao token), red ✗ with the error inline. Makes a recurrence of BACKEND_TOKEN_DEAD visible at a glance instead of only in mcpd logs. Verified live: 'Secrets: bao* ✓, default ✓'. +3 tests (28 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
180e50a978 |
feat(cli): metadata.mcpServers auto-attach in mcpctl skills sync
Closes the third deferred item from PR-5: skills can declare upstream
MCP server dependencies via `metadata.mcpServers` and `mcpctl skills
sync` now attaches them to the active project. Same trust model as
postInstall/hooks: the publisher is responsible, the client just
asks mcpd to attach.
## Behaviour
- For each `{ name, fromTemplate?, project? }` entry:
- If the project already has the server attached → record as
`alreadyAttached`, no-op.
- If the server doesn't exist on mcpd → warn + skip (we don't
auto-create from template; that's a separate explicit decision
for the operator). The warning surfaces the suggested template
so the operator can decide.
- Otherwise → POST `/api/v1/projects/:id/servers { server: <name> }`.
- 409 from POST → treat as alreadyAttached (server-side idempotency).
- 404 from POST → treat as missing (race: server vanished mid-sync).
- Other errors collected per-server; sync continues.
- A dep with `project:` set to a non-active project is skipped during
this sync — keeps the active sync from making collateral changes
to other projects.
- Global skill syncs (no project context) skip mcpServers entirely
with a warning — there's no project to attach to.
## Files
### Added
- src/cli/src/utils/mcpservers-materialiser.ts (~140 LOC)
- src/cli/tests/utils/mcpservers-materialiser.test.ts (~190 LOC,
10 tests covering: parse-tolerance, fresh attach, alreadyAttached
short-circuit, missing-server warn+skip, missing-project errors-
out, 409→alreadyAttached, 404→missing, cross-project skip,
per-server error collection, empty-deps no-op)
### Edited
- src/cli/src/commands/skills.ts: applyOne calls
attachSkillMcpServers after hooks. Tracks per-skill attachments in
result.mcpServersAttached. Summary line surfaces "N mcpServers
attached".
## Verification
165 test files / 2193 tests green (up from 2182).
Real-world flow:
```yaml
# skill metadata.yaml
mcpServers:
- name: my-grafana
fromTemplate: grafana
project: monitoring
- name: my-loki
fromTemplate: loki
```
```bash
# As operator: ensure the named servers exist on mcpd first
mcpctl create server my-grafana --from-template grafana --env-from-secret grafana-creds
mcpctl create server my-loki --from-template loki
# Now publish the skill that declares them as deps. Sync will attach:
mcpctl skills sync
# → mcpctl: 1 installed, 2 mcpServers attached
mcpctl describe project monitoring # both servers now attached
```
## What's intentionally NOT in this PR
- Auto-creating servers from `fromTemplate` when they don't exist.
Provisioning infra from a skill push is a separate decision needing
explicit operator policy. v1 warns + skips; the warning includes
the suggested template name so the operator can act manually.
- Detaching a server when a skill drops it from mcpServers. Detach is
destructive (project loses access) and we can't tell whether the
detach is intentional vs. accidental drop. PR-7 can revisit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7ebc8b22d1 |
feat(cli): metadata.hooks materialisation in mcpctl skills sync
Closes the second-biggest deferred item from PR-5: skills can declare
PreToolUse / PostToolUse / SessionStart / Stop / SubagentStop /
Notification hooks in metadata.hooks, and `mcpctl skills sync` now
registers them in ~/.claude/settings.json with proper source-tagging.
## Why this needs source-tagging
mcpctl already managed ONE entry in settings.json (the SessionStart
hook for `mcpctl skills sync --quiet`, set up by PR-5 via
`installManagedSessionHook`). That entry uses `_mcpctl_managed: true`
to identify itself for idempotent updates.
Skill-declared hooks need finer scoping: skill A and skill B could
both register PreToolUse hooks, and removing skill A must not touch
skill B's. So entries written by `applyManagedHooks` carry both:
_mcpctl_managed: true ← same flag the SessionStart hook uses
_mcpctl_source: "<skill-name>" ← differentiates per-skill
Removing a skill (orphan cleanup) drops every entry tagged with
that skill's name. User-added entries (no marker) are preserved
verbatim. The earlier session-hook installer keeps working: its
`_mcpctl_managed: true` lacks the source tag, so the hooks-materialiser
ignores it on per-skill operations.
## Behaviour
- Skill installs/updates: applyManagedHooks(skillName, metadata.hooks)
is called after files are atomically materialised. The function
reads settings.json, drops this skill's previous entries from every
declared event (and from any event it previously had entries in but
no longer declares — so a skill can shrink scope), then re-inserts
the new tagged set.
- Skill orphan removal: removeManagedHooks(skillName) drops every
entry owned by the skill. Other skills + user hooks unaffected.
- Failure handling: hooks errors are logged via the warn() callback
but do not fail the sync. Same shape as postInstall — scoped, not
fatal.
## Files
### Added
- src/cli/src/utils/hooks-materialiser.ts (~140 LOC)
- src/cli/tests/utils/hooks-materialiser.test.ts (~165 LOC, 11 tests
covering: write-from-scratch, multi-source coexistence, user-hook
preservation, update replaces (not duplicates), shrink drops events,
remove targets only the named source, multiple events independent,
idempotent, empty/JSONC settings.json tolerance)
### Edited
- src/cli/src/commands/skills.ts: applyOne calls applyManagedHooks
when metadata.hooks is set; calls removeManagedHooks(skillName) when
a previously-installed skill no longer declares hooks; orphan
removal also drops the skill's hooks. New SyncResult field
`hooksApplied`. Earlier `meta` declaration deduped (it was redeclared
in the postInstall block).
## Verification
164 test files / 2182 tests green (up from 2171).
End-to-end on a real machine after this PR ships:
```yaml
# skill metadata.yaml
hooks:
PreToolUse:
- type: command
command: "audit-pretool.sh"
SessionStart:
- type: command
command: "claude-greeting.sh"
```
```
mcpctl skills sync
# → mcpctl: 1 installed, 1 hooks applied
jq '.hooks' ~/.claude/settings.json
# → entries tagged with _mcpctl_managed + _mcpctl_source
```
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d60ad52018 |
feat(cli): postInstall executor for mcpctl skills sync
Closes the biggest deferred item from PR-5. metadata.postInstall scripts now actually run when their hash changes, with audit emission back to mcpd. Trust model unchanged from the corporate-appliance design: mcpd is the source of truth, content is reviewed at publish time, the client just runs. No sandbox, no signing, no consent prompts. The controls that matter are already on the publishing side (RBAC, audit, reviewer queue). What we DO provide is ops hygiene: - Hard timeout (default 60 s; per-skill override via metadata.postInstallTimeoutSec). SIGTERM at the deadline, SIGKILL after a 2 s grace. - Hash pinning. The script's sha256 is recorded in ~/.mcpctl/skills-state.json. Re-syncs of an unchanged script are a cheap no-op. A re-published "same version, fixed script" still triggers re-execution because its hash changed. - Curated env. PATH/HOME/USER/SHELL inherited; everything else dropped. MCPCTL_SKILL_NAME / _VERSION / _DIR / _PROJECT injected so scripts have stable context. - Per-skill install log under ~/.mcpctl/skills/<name>/install.log. Bounded at 5 × 256 KB; old entries truncated from the front. - Audit event back to mcpd (POST /api/v1/audit-events, eventKind=skill_postinstall) on every run, including hostname so admins can see fleet rollout state. Best-effort — failures are warned but never block the sync. - Path-escape rejection. metadata.postInstall must resolve inside the skill bundle; relative paths that try to climb out are refused. - Auto-chmod 0755 on the script before spawn. Some upstreams ship 0644 + a shebang and expect a shell to handle it; we always spawn the path directly so we need +x. Failure semantics: on timeout or non-zero exit, the recorded postInstallHash is NOT updated. Next sync retries. Other skills in the same sync run continue regardless — postInstall errors are scoped, not fatal. ## Files ### Added - src/cli/src/utils/postinstall.ts (~200 LOC) - src/cli/tests/utils/postinstall.test.ts (~190 LOC, 10 tests covering success, env vars, chmod, non-zero exit, timeout via exec sleep, path-escape, missing script, log file shape + append-across-runs) ### Edited - src/cli/src/commands/skills.ts: applyOne now invokes runPostInstall + emitPostInstallAudit when metadata.postInstall is set and the script hash differs from prior state. New SyncResult fields: postInstallsRan, postInstallsSkipped. Summary line surfaces "N postInstall ran". --skip-postinstall flag now actually does what it says. ## Verification 163 test files / 2171 tests green (up from 2161). End-to-end on a real machine (after this PR ships and a skill with metadata.postInstall is published): ``` mcpctl skills sync # → mcpctl: 1 installed, 1 postInstall ran cat ~/.mcpctl/skills/<name>/install.log # see stdout/stderr mcpctl skills sync # idempotent — skipped ``` If the same skill is republished with a fixed script: ``` mcpctl skills sync # → mcpctl: 1 updated, 1 postInstall ran (hash changed → rerun) ``` If the script fails (exit 7): ``` mcpctl skills sync # → mcpctl: 1 updated, 1 errors mcpctl skills sync # state.postInstallHash NOT updated → retries ``` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
58e8e956ce |
feat(cli+mcpd): mcpctl skills sync + config claude extension
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>
|
||
|
|
356cbe87b5 |
feat(cli+mcplocal): mcpctl provider <name> {up,down,status} for managed LLMs
Some checks failed
CI/CD / typecheck (pull_request) Successful in 57s
CI/CD / test (pull_request) Successful in 1m23s
CI/CD / lint (pull_request) Successful in 3m1s
CI/CD / smoke (pull_request) Failing after 1m47s
CI/CD / build (pull_request) Successful in 5m58s
CI/CD / publish (pull_request) Has been skipped
Adds lifecycle control for managed local LLM providers (vllm-managed)
without the nuclear option of restarting mcplocal. Practical use:
mcpctl provider vllm-local down # release GPU memory now
mcpctl provider vllm-local up # warm up before the next chat
mcpctl provider vllm-local status # see state, pid, uptime
mcplocal exposes three new endpoints:
GET /llm/providers/:name/status → returns lifecycle state for
managed providers, { managed: false }
for unmanaged (anthropic, openai, …)
POST /llm/providers/:name/start → calls warmup() (202 + initial state)
POST /llm/providers/:name/stop → calls dispose() (200 + post-stop state)
Stop and start return 400 for non-managed providers — stopping an API-key
provider is meaningless. The CLI surfaces the error verbatim.
Restarting mcplocal would also free the GPU but drops the SSE connection
to mcpd and forces every virtual Llm to re-publish; this is the targeted,
non-disruptive escape hatch.
The completions test gained a `topLevelMarkers` filter so a sub-command
named `status` (under `provider`) doesn't trip the existing "non-project
commands must guard with __mcpctl_has_project" rule.
Tests: cli 437/437, mcplocal 731/731.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e4af16477c |
feat(cli): live "say hi" probe for server LLMs in mcpctl status
Some checks failed
CI/CD / lint (pull_request) Successful in 55s
CI/CD / test (pull_request) Successful in 1m13s
CI/CD / typecheck (pull_request) Successful in 3m10s
CI/CD / smoke (pull_request) Failing after 1m46s
CI/CD / build (pull_request) Successful in 3m24s
CI/CD / publish (pull_request) Has been skipped
Status was showing the server-side LLM list but not whether each one
actually serves inference. This adds a per-LLM probe that POSTs a
tiny prompt to /api/v1/llms/<name>/infer:
messages: [{ role: 'user', content: "Say exactly the word 'hi' and nothing else." }]
max_tokens: 8, temperature: 0
Each registered LLM gets a one-line health line:
Server LLMs: 2 registered (probing live "say hi"...)
fast qwen3-thinking ✓ "hi" 312ms
openai → qwen3-thinking http://litellm.../v1 key:litellm/API_KEY
heavy sonnet ✗ upstream auth failed: 401
anthropic → claude-sonnet-4-5 provider default no key
Probes run in parallel so a single slow LLM doesn't gate the others;
each has its own 15-second timeout. JSON/YAML output gains a
\`health: { ok, ms, say?, error? }\` field per server LLM so dashboards
get the same liveness signal.
Tests: 25/25 (was 24, +1 new for the failure-path render). Workspace
suite: 2006/2006 across 149 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0db37e92a4 |
feat(cli)+fix(mcpd): server-side LLM status + SPA fallback 500
Some checks failed
CI/CD / typecheck (pull_request) Successful in 58s
CI/CD / test (pull_request) Successful in 1m9s
CI/CD / lint (pull_request) Successful in 2m14s
CI/CD / smoke (pull_request) Failing after 1m39s
CI/CD / build (pull_request) Successful in 2m14s
CI/CD / publish (pull_request) Has been skipped
Two related fixes: 1. \`mcpctl status\` now lists mcpd-managed Llm rows (the ones created via \`mcpctl create llm\`) under a new "Server LLMs:" section, grouped by tier with type, model, upstream URL, and key reference. JSON/YAML output gains a \`serverLlms\` array. Bearer token (from \`mcpctl auth login\` / saved credentials) is passed through; if mcpd is unreachable or returns non-200 the section is silently omitted (the existing mcpd connectivity line already conveys that). 6 new tests cover happy path, empty list, token plumbing, and JSON shape. 2. SPA fallback at \`/ui/<deeplink>\` was returning 500 because we registered \`@fastify/static\` with \`decorateReply: false\` and then called \`reply.sendFile\`. Read index.html once at startup and serve it with \`reply.send(html)\` instead — also dodges a per-request stat call. Drop \`decorateReply: false\` so future code can use reply.sendFile if it ever needs to. Full suite: 2005/2005 across 149 files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
727e7d628c |
feat(agents): mcpctl chat REPL + agent CRUD + completions (Stage 5)
This is the moment the user can actually talk to an agent end-to-end:
mcpctl create llm qwen3-thinking --type openai --model qwen3-thinking \
--url http://litellm.nvidia-nim.svc.cluster.local:4000/v1 \
--api-key-ref litellm-key/API_KEY
mcpctl create agent reviewer --llm qwen3-thinking --project mcpctl-dev \
--description "I review security design — ask me after each major change."
mcpctl chat reviewer
Pieces:
* src/cli/src/commands/chat.ts (new) — REPL + one-shot. Streams the SSE
endpoint and prints text deltas to stdout as they arrive; tool_call /
tool_result events go to stderr in dim-style brackets so the chat
output stays clean. LiteLLM-style flags (--temperature / --top-p /
--top-k / --max-tokens / --seed / --stop / --allow-tool / --extra)
layer over agent.defaultParams. In-REPL slash-commands: /set KEY VAL,
/system <text>, /tools (list project's MCP servers), /clear (new
thread), /save (PATCH agent.defaultParams = current overrides),
/quit.
* src/cli/src/commands/create.ts — `create agent` mirroring the llm
pattern. Every yaml-applyable field has a corresponding flag (memory
rule); --default-temperature / --default-top-p / --default-top-k /
--default-max-tokens / --default-seed / --default-stop /
--default-extra / --default-params-file all populate agent.defaultParams.
* src/cli/src/commands/apply.ts — AgentSpecSchema accepts both `llm:
qwen3-thinking` shorthand and `llm: { name: ... }` long form; runs
after llms in the apply order so apiKey/llm references resolve. Round-
trips with `get agent foo -o yaml | apply -f -` (memory rule).
* src/cli/src/commands/get.ts — agentColumns (NAME, LLM, PROJECT,
DESCRIPTION, ID); RESOURCE_KIND mapping for yaml export.
* src/cli/src/commands/shared.ts — `agent`/`agents`/`thread`/`threads`
added to RESOURCE_ALIASES.
* src/cli/src/index.ts — wires createChatCommand into the program; passes
the resolved baseUrl + token so chat can stream SSE without going
through ApiClient (which only does buffered request/response).
* completions/mcpctl.{fish,bash} regenerated. scripts/generate-completions.ts
knows about agents (canonical + aliases) and emits a special-case
`chat)` block that completes the first arg with `mcpctl get agents`
names. tests/completions.test.ts: +9 new assertions covering agents in
the resource list, chat in the commands list, --llm flag for create
agent, agent-name completion for chat, etc.
CLI suite: 430/430 (was 421). Completions --check is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b1bccee50d |
test(describe): mock the ?reveal=true path on --show-values
Some checks failed
Follow-up to
|
||
|
|
1c5301289c |
refactor(wizard): rename --admin-token → --setup-token
Some checks failed
Any token with policy-write + auth/token admin works; root is a convenient default but a scoped service account is fine too. The previous naming misrepresented the permission floor as root-only. - flag: --admin-token → --setup-token - wizard field: adminToken → setupToken - prompt label: "OpenBao admin / root token" → "OpenBao setup token (needs policy write + auth/token admin perms; root is fine)" - file doc + one comment reworded - tests updated for the new label - regression test (token-absent-from-stdout) kept unchanged Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dd4246878d |
feat(openbao): wizard-provisioning + daily token rotation
Some checks failed
CI/CD / typecheck (pull_request) Successful in 55s
CI/CD / test (pull_request) Successful in 1m4s
CI/CD / lint (pull_request) Successful in 2m2s
CI/CD / smoke (pull_request) Failing after 1m36s
CI/CD / build (pull_request) Successful in 4m13s
CI/CD / publish (pull_request) Has been skipped
One-command setup replaces the 6-step manual flow — `mcpctl create
secretbackend bao --type openbao --wizard` takes the OpenBao admin token
once, provisions a narrow policy + token role, mints the first periodic
token, stores it on mcpd, verifies end-to-end, and prints the migration
command. The admin token is NEVER persisted.
The stored credential auto-rotates daily: mcpd mints a successor via the
token role (self-rotation capability is part of the policy it was issued
with), verifies the successor, writes it over the backing Secret, then
revokes the predecessor by accessor. TTL 720h means a week of rotation
failures still leaves 20+ days of runway.
Shared:
- New `@mcpctl/shared/vault` — pure HTTP wrappers (verifyHealth,
ensureKvV2, writePolicy, ensureTokenRole, mintRoleToken, revokeAccessor,
lookupSelf, testWriteReadDelete) and policy HCL builder.
mcpd:
- `tokenMeta Json @default("{}")` on SecretBackend. Self-healing schema
migration — empty default lets `prisma db push` add the column cleanly.
- SecretBackendRotator.rotateOne: mint → verify → persist → revoke-old →
update tokenMeta. Failures surface via `lastRotationError` on the row;
the old token keeps working.
- SecretBackendRotatorLoop: on startup rotates overdue backends, schedules
per-backend timers with ±10min jitter. Stops cleanly on shutdown.
- New `POST /api/v1/secretbackends/:id/rotate` (operation
`rotate-secretbackend` — added to bootstrap-admin's auto-migrated ops
alongside migrate-secrets, which was previously missing too).
CLI:
- `--wizard` on `create secretbackend` delegates to the interactive flow.
All prompts can be pre-answered via flags (--url, --admin-token,
--mount, --path-prefix, --policy-name, --token-role,
--no-promote-default) for CI.
- `mcpctl rotate secretbackend <name>` — convenience verb; hits the new
rotate endpoint.
- `describe secretbackend` renders a Token health section (healthy /
STALE / WARNING / ERROR) with generated/renewal/expiry timestamps and
last rotation error. Only shown when tokenMeta.rotatable is true — the
existing k8s-auth + static-token backends don't surface it.
Tests: 15 vault-client unit tests (shared), 8 rotator unit tests (mcpd),
3 wizard flow tests (cli, including a regression test that the admin
token never appears in stdout). Full suite 1885/1885 (+32). Completions
regenerated for the new flags.
Out of scope (explicit): kubernetes-auth wizard, Vault Enterprise
namespaces in the wizard path, rotation for non-wizard static-token
backends. See plan file for details.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
de854b1944 |
feat(project): Project.llmProvider semantically names an Llm resource
Why: Phases 0-3 built the server-managed Llm registry; this phase pivots the existing Project.llmProvider column from "local provider hint" to "named Llm reference" so operators can pick a centralised Llm per project. No schema change — the column stays a free-form string for backward compat. - `mcpctl create project --llm <name>` (+ `--llm-model <override>`) sets llmProvider/llmModel to a centralised Llm reference, or 'none' to disable. - `mcpctl describe project` fetches the Llm catalogue alongside prompts and flags values that don't resolve with a visible warning. 'none' is treated as an explicit disable, not an orphan. - `apply -f` doc comments updated; --llm-provider still accepted but now documented as naming an Llm resource. - New `resolveProjectLlmReference(mcpdClient, name)` helper in mcplocal's discovery: returns `registered`/`disabled`/`unregistered`/`unreachable`. The HTTP-mode proxy-model pipeline will consume this when it pivots to mcpd's /api/v1/llms/:name/infer proxy. - project-mcp-endpoint.ts cache-namespace path gets a comment explaining the new resolution order — behavior unchanged, just clarified. Tests: 6 resolver unit tests + 3 new describe-warning cases. Full suite 1853/1853 (+9 from Phase 3's 1844). TypeScript clean; completions regenerated for the new create-project flags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2127b41d9f |
feat: HTTP-mode mcplocal container + mcpctl test mcp + token-auth preHandler
Delivers the final piece of the mcptoken stack: a containerized,
network-accessible mcplocal that serves Streamable-HTTP MCP to off-host
clients (the vLLM use case), authenticated by project-scoped McpTokens.
New binary (same package, new entry):
- src/mcplocal/src/serve.ts — HTTP-only entry. Reads MCPLOCAL_MCPD_URL,
MCPLOCAL_MCPD_TOKEN, MCPLOCAL_HTTP_HOST/PORT, MCPLOCAL_CACHE_DIR from
env. No StdioProxyServer, no --upstream.
- src/mcplocal/src/http/token-auth.ts — Fastify preHandler that
validates mcpctl_pat_ bearers via mcpd's /api/v1/mcptokens/introspect.
30s positive / 5s negative TTL. Rejects wrong-project with 403.
Shared HTTP MCP client:
- src/shared/src/mcp-http/ — reusable McpHttpSession with initialize,
listTools, callTool, close. Handles http+https, SSE, id correlation,
distinct McpProtocolError / McpTransportError. Plus mcpHealthCheck
and deriveBaseUrl helpers.
New CLI verb `mcpctl test mcp <url>`:
- Flags: --token (also $MCPCTL_TOKEN), --tool, --args (JSON),
--expect-tools, --timeout, -o text|json, --no-health.
- Exit codes: 0 PASS, 1 TRANSPORT/AUTH FAIL, 2 CONTRACT FAIL.
Container + deploy:
- deploy/Dockerfile.mcplocal (Node 20 alpine, multi-stage, pnpm
workspace, CMD node src/mcplocal/dist/serve.js, VOLUME
/var/lib/mcplocal/cache, HEALTHCHECK on :3200/healthz).
- scripts/build-mcplocal.sh mirrors build-mcpd.sh.
- fulldeploy.sh is now a 4-step pipeline that also builds + rolls out
mcplocal (gated on `kubectl get deployment/mcplocal` so the script
stays green before the Pulumi stack lands).
Audit + cache:
- project-mcp-endpoint.ts passes MCPLOCAL_CACHE_DIR into FileCache at
both construction sites and, when request.mcpToken is present, calls
collector.setSessionMcpToken(id, ...) so audit events carry the
tokenName/tokenSha.
Tests:
- 9 unit cases on `mcpctl test mcp` (happy path, health miss,
expect-tools hit/miss, transport throw, tool isError, json report,
$MCPCTL_TOKEN env fallback, invalid --args).
- Smoke test src/mcplocal/tests/smoke/mcptoken.smoke.test.ts —
gated on healthz($MCPGW_URL), skipped cleanly when unreachable.
Covers happy path, wrong-project 403, --expect-tools contract
failure, and revocation 401 within the negative-cache window.
1773/1773 workspace tests pass. Pulumi resources (Deployment, Service,
Ingress, PVC, Secret, NetworkPolicy) still need to land in
../kubernetes-deployment before the smoke gate flips on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
efcfeeab65 |
feat(cli)!: migrate create rbac bindings to --roleBindings kv syntax
BREAKING: `mcpctl create rbac` no longer accepts `--binding` or
`--operation`. Use `--roleBindings` instead with key:value pairs:
# resource binding
--roleBindings role:view,resource:servers
--roleBindings role:view,resource:servers,name:my-ha
# operation binding (role:run is implied by action:)
--roleBindings action:logs
The on-disk YAML shape (`roleBindings: [{role, resource, name?}]` or
`{role:'run', action}`) is unchanged, so Git backups and existing
`apply -f` files continue to work. Only the command-line input format
changes.
The parser is extracted to src/cli/src/commands/rbac-bindings.ts so the
upcoming `mcpctl create mcptoken --bind <kv>` verb can reuse it.
Completions, tests, and the new parser unit test all pass (406/406).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
857f8c72ae |
fix: MCP proxy resilience — timeouts, parallel discovery, error propagation
All checks were successful
CI/CD / typecheck (pull_request) Successful in 49s
CI/CD / lint (pull_request) Successful in 1m49s
CI/CD / test (pull_request) Successful in 1m4s
CI/CD / build (pull_request) Successful in 1m49s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
CI/CD / smoke (pull_request) Successful in 10m3s
- McpdClient: add 30s AbortSignal timeout to all fetch calls (was infinite) - CLI bridge: return JSON-RPC error on stdout when HTTP fails (was silent) - Router: parallel tool/resource discovery via Promise.allSettled (was sequential — one slow server blocked all) - vllm-managed: 60s error cooldown prevents retry-on-every-call when vLLM is broken - Tests: McpdClient timeout suite (9), parallel discovery, vllm cooldown, bridge error response Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
af4b3fb702 |
feat: store backup config in DB secret instead of env var
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> |
||
|
|
98f3a3eda0 |
refactor: consolidate restore under backup command
mcpctl backup restore list/diff/to instead of separate mcpctl restore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
7818cb2194 |
feat: Git-based backup system replacing JSON bundle backup/restore
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> |
||
|
|
d773419ccd |
feat: enhanced MCP inspector with proxymodel switching and provenance view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
0995851810 |
feat: remove proxyMode — all traffic goes through mcplocal proxy
proxyMode "direct" was a security hole (leaked secrets as plaintext env vars in .mcp.json) and bypassed all mcplocal features (gating, audit, RBAC, content pipeline, namespacing). Removed from schema, API, CLI, and all tests. Old configs with proxyMode are accepted but silently stripped via Zod .transform() for backward compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |