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:
222
README.md
222
README.md
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user