Merge origin/main into feat/agentic-teams

124 commits of main against 33 of agentic-teams. 22 files overlapped, 7
conflicted. Three needed real reconciliation rather than a side-pick:

**The `volumes` collision.** Both branches independently added a `volumes`
field to ContainerSpec meaning different things: main's is persistent storage
keyed by a server-derived `claimName` that deliberately outlives an instance;
mine is emptyDir scratch shared between an init container and the main
container. Worse, each emitted the pod-level `volumes` list from its own
helper, so spreading both would have silently kept only the last — a server
with a PVC would have lost it the moment a session scratch volume appeared.

Resolved by renaming the unreleased side: mine is now `scratchVolumes`, main's
keeps `volumes` since it is already shipped and in use. `buildPodVolumes` now
owns the whole pod-level list and emits both kinds; `buildPodExtras` is init
containers only. fsGroup/securityContext stays tied to the PVC case, because
an emptyDir is created writable by the container's own user and chowning it
is work with no purpose. Five tests pin the coexistence so a later change
cannot drop one kind again.

**The extracted permission mapper.** I moved `mapUrlToPermission` out of
main.ts into middleware/url-permission.ts so it could be tested; main kept it
inline and kept editing it. Kept the extraction and ported main's one change
across — the `/api/v1/audit/events/purge` mapping onto the existing
`audit-purge` operation. Verified rule-by-rule that the extracted version is a
superset: all 15 of main's rules plus the 5 agentic-teams ones.

**The NUL byte in create.ts.** Both branches fixed the same defect
independently, mine as `''` and main's as `'\0'`. Took main's.

The rest were one-liners: .gitignore comment wording, and an add/add on
proxy-long-running.test.ts where main's copy carries a progressive-delivery
test mine lacked — took main's in full.

Not changed, but worth flagging: main's generateDeploymentSpec hardcodes
`automountServiceAccountToken: false` while generatePodSpec honours
`spec.automountServiceAccountToken`. That asymmetry is main's own and predates
this merge, so it is left exactly as-is rather than altered inside a merge
commit.

Verified: 3138 tests pass (237 files), build clean, prisma schema valid.
Lint 809/80 vs 794/77 pre-merge — the delta is main's own debt; every file
touched during conflict resolution was checked and introduces no new errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
This commit is contained in:
Michal
2026-09-06 23:40:04 +01:00
223 changed files with 20890 additions and 640 deletions

222
README.md
View File

@@ -113,6 +113,166 @@ This writes a `.mcp.json` that tells Claude Code to connect through mcplocal. Re
mcpctl console monitoring # Preview what Claude sees
```
### Connect prime-agent
Prime-agent (Claude's open-source counterpart) talks to the same proxy MCP
gateway over HTTP rather than stdio. Register a project and sync its skills
into `~/.prime/agent/`:
```bash
mcpctl config prime-agent --project monitoring
```
This:
1. Provisions the project's bearer credential in `~/.prime/agent/auth.json`
(`mcp:monitoring`, written 0600) — either from `--token <pat>`, an existing
entry that is still active server-side, or a freshly minted project token.
This happens first: if no credential can be provisioned the command stops
here with a non-zero exit and leaves `settings.json` alone, so the project
you are currently on keeps working.
2. Registers the proxy MCP gateway in `~/.prime/agent/settings.json` as
`mcpServers.monitoring = { "type": "http", "url": "https://mcp.ad.itaz.eu/projects/monitoring/mcp" }`
(merging with any existing servers and preserving all other settings), and
unmounts the previously active mcpctl project so exactly one is live.
Servers you configured by hand are never touched.
3. Writes a `.mcpctl-project` marker (only if none exists higher up, and never
from `$HOME`) so later syncs resolve the project. Skip with `--skip-marker`.
4. Syncs the project's skills into `~/.prime/agent/skills/<name>/` as markdown
skills. The shared tree is ownership-tracked per project: it never deletes
another project's skills or an untracked hand-authored skill.
5. Installs a `/mcpctl` project-switcher extension into
`~/.prime/agent/extensions/` so you can switch mcpctl projects from inside
the prime-agent UI (skip with `--skip-extension`).
Re-sync later with:
```bash
mcpctl skills sync --agent prime-agent --project monitoring
```
Skip individual steps as needed:
```bash
mcpctl config prime-agent --project monitoring --token mcpctl_pat_xxx # provide token, don't mint
mcpctl config prime-agent --project monitoring --skip-skills # don't sync skills
mcpctl config prime-agent --project monitoring --skip-extension # don't install /mcpctl switcher
mcpctl config prime-agent --project monitoring --skip-marker # don't touch .mcpctl-project here
```
The `/mcpctl` switcher runs with `--skip-extension --skip-marker`, so switching
projects from inside prime-agent never re-scopes whichever repository
prime-agent happened to be started in.
See [docs/prime-agent-extension.md](docs/prime-agent-extension.md) for full details.
Preview the change without writing anything:
```bash
mcpctl config prime-agent --project monitoring --dry-run
```
`config claude` registers in **user scope** by default — one `mcpctl` MCP server
in `~/.claude.json` that applies in every directory and window, so you wire it
once rather than per checkout. Use `--scope project` (or `-o`) for a repo that
wants its own pinned project.
Inside Claude Code:
- `/mcpctl [project]` — switch projects, then reconnect the `mcpctl` server from
`/mcp` (no restart)
- the active project shows in the status line at the bottom of the screen
See [docs/claude-integration.md](docs/claude-integration.md) for full details.
### Connect pi
[pi](https://github.com/earendil-works/pi) does not support MCP, but it supports
**extensions** and **skills**. mcpctl ships a native pi integration that talks
JSON-RPC directly to mcplocal over HTTP — no MCP library, no `~/.claude`
dependency, so it keeps working even if you drop Claude entirely.
```bash
mcpctl config pi --project monitoring
```
This installs the `mcpctl` pi extension into `~/.pi/agent/extensions/mcpctl/`,
registers it (plus a pi skills dir) in `~/.pi/agent/settings.json`, persists the
active project in `~/.mcpctl/pi-state.json`, and runs an initial skills sync into
`~/.pi/agent/skills/`. Then start pi and the project's MCP tools are registered as
native pi tools (`mc_<project>_<tool>`).
Inside pi:
- `/mcpctl` — status, **switch project** (from the GUI), refresh tools, sync skills
- gated projects auto-ungate: call `begin_session` once and the full tool set opens
- re-sync with `mcpctl skills sync --agent pi --project monitoring`
See [docs/pi-extension.md](docs/pi-extension.md) for full details.
### Connect opencode
[opencode](https://opencode.ai) supports MCP natively *and* exposes an API for
its own MCP registry, so mcpctl mounts the project through the running app —
which means you can **switch projects from the TUI without restarting**.
```bash
mcpctl config opencode --project monitoring
```
This mints (or reuses) the project's gateway token into
`~/.mcpctl/opencode-state.json` (0600 — the token never lands in
`opencode.json`), installs a server plugin into
`~/.config/opencode/plugin/mcpctl.ts` and a TUI plugin into
`~/.config/opencode/mcpctl/`, registers the latter in
`~/.config/opencode/tui.json`, and syncs skills into
`~/.config/opencode/skill/`.
Inside opencode:
- `/mcpctl` (or `<leader>m`) — **switch project** from a filterable picker;
takes effect on the next turn, no restart
- `/mcpctl-status` — active project, mount state, gateway URL
- `/mcpctl-skills` — re-sync this project's skills
- the active project shows as `mcpctl:<project>` in the prompt footer, next to
the model name
Skip individual steps as needed:
```bash
mcpctl config opencode --project monitoring --token mcpctl_pat_xxx # provide token, don't mint
mcpctl config opencode --project monitoring --skip-skills # don't sync skills
mcpctl config opencode --project monitoring --skip-plugin # state only, leave plugins alone
mcpctl config opencode --project monitoring --skip-marker # don't touch .mcpctl-project here
mcpctl config opencode --project monitoring --dry-run # print the plan, write nothing
```
Re-sync skills on their own with
`mcpctl skills sync --agent opencode --project monitoring`.
See [docs/opencode-extension.md](docs/opencode-extension.md) for full details.
### Run Claude Code on the homelab LLM
The homelab gateway (LiteLLM in front of vLLM) already serves the Anthropic
Messages API, so Claude Code can talk to it directly — no bridge:
```bash
claude-vllm --init # write ~/.config/mcpctl/claude-vllm.jsonc (0600)
claude-vllm # use it
claude-vllm --model deepseek-v4-max
claude-vllm --list # show what it would use
claude-vllm -- -p "summarise this repo" # args after -- go to claude
```
It reads its own config (shaped like `opencode.jsonc`, but a separate file),
falling back to `~/.pi/agent` or `~/.prime/agent` so it works before you have
written one. The key may be a literal in the 0600 file, `${ENV_VAR}`, or an env
var name — it is never stored in the tool.
See [docs/claude-vllm.md](docs/claude-vllm.md), including why routing this
through mcpctl would add surface without adding capability.
## Declarative Configuration
Everything can be defined in YAML and applied with `mcpctl apply`:
@@ -237,7 +397,7 @@ name: home-automation
proxyModel: default
servers:
- home-assistant
- node-red
- unifi-network
```
Via CLI:
@@ -672,6 +832,66 @@ mcpctl create server my-ha \
--env-from-secret ha-secrets
```
### Persistent volumes
Instances are immutable — any server edit recreates them — so by default
anything an MCP server writes to its container filesystem is lost at that point.
A server (or template) can declare volumes that outlive its instances:
```bash
mcpctl create server docs \
--from-template docs-mcp \
--volume data:/data:20:longhorn # NAME:/mount/path[:SIZE_GB[:STORAGE_CLASS]]
```
```yaml
# ...or declaratively
volumes:
- name: data
mountPath: /data
sizeGb: 20
storageClass: longhorn
```
The backing store is named after the **server**, not the instance —
`mcpctl-<server>-<name>` — which is what lets the data survive. On Kubernetes
that is a PVC created in the servers namespace on first start and left in place
afterwards; on Docker it is a named volume. Neither is deleted when an instance
or the server goes away, so reclaiming the space is a deliberate
`kubectl delete pvc` / `docker volume rm`.
Notes:
- Claims are `ReadWriteOnce`, so a server with volumes is limited to one
replica. Asking for more is rejected at validation rather than leaving the
extra replicas unschedulable.
- A fresh claim mounts `root:root`, so mcpctl sets the pod's `fsGroup` to
**1000** — the conventional non-root uid in node/python images. Override with
`fsGroup` on the volume if an image uses a different one; containers running
as root are unaffected either way.
- `storageClass` defaults to `MCPD_VOLUME_STORAGE_CLASS`, and is omitted
entirely when neither is set. **Set it explicitly on any cluster with more
than one default StorageClass**, where an omitted class binds
nondeterministically.
- Growing a volume is an explicit `kubectl edit pvc` (the class must allow
expansion). mcpctl never resizes an existing claim, because most PVC fields
are immutable after binding.
- Backups capture the volume *declaration*, not its contents.
### Web search and docs lookup
The `duckduckgo`, `searxng` and `docs-mcp` templates give an agent web search and
version-pinned library documentation. All are self-hosted and none needs an API key.
`duckduckgo` needs no backing service at all:
```bash
mcpctl create server websearch --from-template duckduckgo
```
See [docs/web-search.md](docs/web-search.md) for the SearXNG engine setup (including
the `json` format setting, without which every search silently returns nothing) and
the `docs-mcp` index-persistence caveat.
## Gated Sessions
Projects using the `default` or `gate` plugin are **gated**. When Claude connects to a gated project: