Files
mcpctl/README.md
Michal d9d0a7a374 docs: update README for plugin system, add proxyModel tests
- Rewrite README Content Pipeline section as Plugin System section
  documenting built-in plugins (default, gate, content-pipeline),
  plugin hooks, and the relationship between gating and proxyModel
- Update all README examples to use --proxy-model instead of --gated
- Add unit tests: proxyModel normalization in JSON/YAML output (4 tests),
  Plugin Config section in describe output (2 tests)
- Add smoke tests: yaml/json output shows resolved proxyModel without
  gated field, round-trip compatibility (4 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 01:24:47 +00:00

470 lines
16 KiB
Markdown

# mcpctl
**kubectl for MCP servers.** A management system for [Model Context Protocol](https://modelcontextprotocol.io) servers — define, deploy, and connect MCP servers to Claude using familiar kubectl-style commands.
```
mcpctl get servers
NAME TRANSPORT REPLICAS DOCKER IMAGE DESCRIPTION
grafana STDIO 1 grafana/mcp-grafana:latest Grafana MCP server
home-assistant SSE 1 ghcr.io/homeassistant-ai/ha-mcp:latest Home Assistant MCP
docmost SSE 1 10.0.0.194:3012/michal/docmost-mcp:latest Docmost wiki MCP
```
## What is this?
mcpctl manages MCP servers the same way kubectl manages Kubernetes pods. You define servers declaratively in YAML, group them into projects, and connect them to Claude Code or any MCP client through a local proxy.
**The architecture:**
```
Claude Code <--STDIO--> mcplocal (local proxy) <--HTTP--> mcpd (daemon) <--Docker--> MCP servers
```
- **mcpd** — the daemon. Runs on a server, manages MCP server containers (Docker/Podman), stores configuration in PostgreSQL.
- **mcplocal** — local proxy. Runs on your machine, presents a single MCP endpoint to Claude that merges tools from all your servers. Handles namespacing (`grafana/search_dashboards`), plugin execution (gating, content pipelines), and prompt delivery.
- **mcpctl** — the CLI. Talks to mcpd (via mcplocal or directly) to manage everything.
## Quick Start
### 1. Install
```bash
# From RPM repository
sudo dnf config-manager --add-repo https://your-registry/api/packages/mcpctl/rpm.repo
sudo dnf install mcpctl
# Or build from source
git clone https://github.com/your-org/mcpctl.git
cd mcpctl
pnpm install
pnpm build
pnpm rpm:build # requires bun and nfpm
```
### 2. Connect to a daemon
```bash
# Login to an mcpd instance
mcpctl login --mcpd-url http://your-server:3000
# Check connectivity
mcpctl status
```
### 3. Create your first secret
Secrets store credentials that servers need — API tokens, passwords, etc.
```bash
mcpctl create secret grafana-token \
--data TOKEN=glsa_xxxxxxxxxxxx
```
### 4. Create your first server
A server is an MCP server definition — what Docker image to run, what transport it speaks, what environment it needs.
```bash
mcpctl create server grafana \
--docker-image grafana/mcp-grafana:latest \
--transport STDIO \
--env GRAFANA_URL=http://grafana.local:3000 \
--env GRAFANA_AUTH_TOKEN=secretRef:grafana-token:TOKEN
```
mcpd pulls the image, starts a container, and keeps it running. Check on it:
```bash
mcpctl get instances # See running containers
mcpctl logs grafana # View server logs
mcpctl describe server grafana # Full details
```
### 5. Create a project
A project groups servers together and configures how Claude interacts with them.
```bash
mcpctl create project monitoring \
--description "Grafana dashboards and alerting" \
--server grafana \
--proxy-model content-pipeline
```
### 6. Connect Claude Code
Generate the `.mcp.json` config for Claude Code:
```bash
mcpctl config claude --project monitoring
```
This writes a `.mcp.json` that tells Claude Code to connect through mcplocal. Restart Claude Code and your Grafana tools appear:
```
mcpctl console monitoring # Preview what Claude sees
```
## Declarative Configuration
Everything can be defined in YAML and applied with `mcpctl apply`:
```yaml
# infrastructure.yaml
secrets:
- name: grafana-token
data:
TOKEN: "glsa_xxxxxxxxxxxx"
servers:
- name: grafana
description: "Grafana dashboards and alerting"
dockerImage: grafana/mcp-grafana:latest
transport: STDIO
env:
- name: GRAFANA_URL
value: "http://grafana.local:3000"
- name: GRAFANA_AUTH_TOKEN
valueFrom:
secretRef:
name: grafana-token
key: TOKEN
projects:
- name: monitoring
description: "Infrastructure monitoring"
proxyModel: content-pipeline
servers:
- grafana
```
```bash
mcpctl apply -f infrastructure.yaml
```
Round-trip works too — export, edit, re-apply:
```bash
mcpctl get all --project monitoring -o yaml > backup.yaml
# edit backup.yaml...
mcpctl apply -f backup.yaml
```
## Plugin System (ProxyModel)
ProxyModel is mcpctl's plugin system. Each project is assigned a **plugin** that controls how Claude interacts with its servers. Plugins are composed from two layers: **TypeScript plugins** (MCP middleware hooks) and **YAML pipelines** (content transformation stages).
### Built-in Plugins
| Plugin | Includes gating | Content pipeline | Description |
|--------|:-:|:-:|---|
| **default** | Yes | Yes | Gate + content pipeline. The default for all projects. |
| **gate** | Yes | No | Gating only — `begin_session` gate with prompt delivery. |
| **content-pipeline** | No | No | Content transformation only — no gating. |
**Gating** means Claude initially sees only a `begin_session` tool. After calling it with a task description, relevant prompts are delivered and the full tool list is revealed. This keeps Claude's context focused.
```bash
# Create a gated project (default behavior)
mcpctl create project home --server my-ha --proxy-model default
# Create an ungated project (direct tool access, no gate)
mcpctl create project tools --server grafana --proxy-model content-pipeline
```
### Plugin Hooks
TypeScript plugins intercept MCP requests/responses at specific lifecycle points:
| Hook | When it fires |
|------|--------------|
| `onSessionCreate` | New MCP session established |
| `onSessionDestroy` | Session ends |
| `onInitialize` | MCP `initialize` request — can inject instructions |
| `onToolsList` | `tools/list` — can filter/modify tool list |
| `onToolCallBefore` | Before forwarding a tool call — can intercept |
| `onToolCallAfter` | After receiving tool result — can transform |
| `onResourcesList` | `resources/list` — can filter resources |
| `onResourceRead` | `resources/read` — can intercept resource reads |
| `onPromptsList` | `prompts/list` — can filter prompts |
| `onPromptGet` | `prompts/get` — can intercept prompt reads |
Plugins compose via `extends` — the `default` plugin extends both `gate` and `content-pipeline`, inheriting all their hooks.
### Content Pipelines
Content pipelines transform tool results through ordered stages before delivering to Claude:
| Pipeline | Stages | Use case |
|----------|--------|----------|
| **default** | `passthrough``paginate` (8KB pages) | Safe pass-through with pagination for large responses |
| **subindex** | `section-split``summarize-tree` | Splits large content into sections, returns a summary index |
#### How `subindex` Works
1. Upstream returns a large tool result (e.g., 50KB of device states)
2. `section-split` divides content into logical sections (2KB-15KB each)
3. `summarize-tree` generates a compact index with section summaries (~200 tokens each)
4. Client receives the index and can request specific sections via `_section` parameter
### Configuration
Set per-project:
```yaml
kind: project
name: home-automation
proxyModel: default
servers:
- home-assistant
- node-red
```
Via CLI:
```bash
mcpctl create project monitoring --server grafana --proxy-model content-pipeline
```
### Custom ProxyModels
Place YAML files in `~/.mcpctl/proxymodels/` to define custom pipelines:
```yaml
kind: ProxyModel
metadata:
name: my-pipeline
spec:
stages:
- type: section-split
config:
minSectionSize: 1000
maxSectionSize: 10000
- type: summarize-tree
config:
maxTokens: 150
maxDepth: 2
appliesTo: [toolResult, prompt]
cacheable: true
```
Inspect available plugins and pipelines:
```bash
mcpctl get proxymodels # List all plugins and pipelines
mcpctl describe proxymodel default # Pipeline details (stages, controller)
mcpctl describe proxymodel gate # Plugin details (hooks, extends)
```
## Resources
| Resource | What it is | Example |
|----------|-----------|---------|
| **server** | MCP server definition | Docker image + transport + env vars |
| **instance** | Running container (immutable) | Auto-created from server replicas |
| **secret** | Key-value credentials | API tokens, passwords |
| **template** | Reusable server blueprint | Community server configs |
| **project** | Workspace grouping servers | "monitoring", "home-automation" |
| **prompt** | Curated content for Claude | Instructions, docs, guides |
| **promptrequest** | Pending prompt proposal | LLM-submitted, needs approval |
| **rbac** | Access control bindings | Who can do what |
| **serverattachment** | Server-to-project link | Virtual resource for `apply` |
## Commands
```bash
# List resources
mcpctl get servers
mcpctl get instances
mcpctl get projects
mcpctl get prompts --project myproject
# Detailed view
mcpctl describe server grafana
mcpctl describe project monitoring
# Create resources
mcpctl create server <name> [flags]
mcpctl create secret <name> --data KEY=value
mcpctl create project <name> --server <srv> [--proxy-model <plugin>]
mcpctl create prompt <name> --project <proj> --content "..."
# Modify resources
mcpctl edit server grafana # Opens in $EDITOR
mcpctl patch project myproj proxyModel=default
mcpctl apply -f config.yaml # Declarative create/update
# Delete resources
mcpctl delete server grafana
# Logs and debugging
mcpctl logs grafana # Container logs
mcpctl console monitoring # Interactive MCP console
mcpctl console --inspect # Traffic inspector
# Backup and restore
mcpctl backup -o backup.json
mcpctl restore -i backup.json
# Project management
mcpctl --project monitoring get servers # Project-scoped listing
mcpctl --project monitoring attach-server grafana
mcpctl --project monitoring detach-server grafana
```
## Templates
Templates are reusable server configurations. Create a server from a template without repeating all the config:
```bash
# Register a template
mcpctl create template home-assistant \
--docker-image "ghcr.io/homeassistant-ai/ha-mcp:latest" \
--transport SSE \
--container-port 8086
# Create a server from it
mcpctl create server my-ha \
--from-template home-assistant \
--env-from-secret ha-secrets
```
## Gated Sessions
Projects using the `default` or `gate` plugin are **gated**. When Claude connects to a gated project:
1. Claude sees only a `begin_session` tool initially
2. Claude calls `begin_session` with a description of its task
3. mcplocal matches relevant prompts and delivers them
4. The full tool list is revealed
This keeps Claude's context focused — instead of dumping 100+ tools and pages of docs upfront, only the relevant ones are delivered based on the task at hand.
```bash
# Gated (default)
mcpctl create project monitoring --server grafana --proxy-model default
# Ungated (direct tool access)
mcpctl create project tools --server grafana --proxy-model content-pipeline
```
## Prompts
Prompts are curated content delivered to Claude through the MCP protocol. They can be plain text or linked to external MCP resources (like wiki pages).
```bash
# Create a text prompt
mcpctl create prompt deployment-guide \
--project monitoring \
--content-file docs/deployment.md \
--priority 7
# Create a linked prompt (content fetched live from an MCP resource)
mcpctl create prompt wiki-page \
--project monitoring \
--link "monitoring/docmost:docmost://pages/abc123" \
--priority 5
```
Claude can also **propose** prompts during a session. These appear as prompt requests that you can review and approve:
```bash
mcpctl get promptrequests
mcpctl approve promptrequest proposed-guide
```
## Interactive Console
The console lets you see exactly what Claude sees — tools, resources, prompts — and call tools interactively:
```bash
mcpctl console monitoring
```
The traffic inspector watches MCP traffic from other clients in real-time:
```bash
mcpctl console --inspect
```
## Architecture
```
┌──────────────┐ ┌─────────────────────────────────────────┐
│ Claude Code │ STDIO │ mcplocal (proxy) │
│ │◄─────────►│ │
│ (or any MCP │ │ Namespace-merging MCP proxy │
│ client) │ │ Gated sessions + prompt delivery │
│ │ │ Per-project endpoints │
└──────────────┘ │ Traffic inspection │
└──────────────┬──────────────────────────┘
│ HTTP (REST + MCP proxy)
┌──────────────┴──────────────────────────┐
│ mcpd (daemon) │
│ │
│ REST API (/api/v1/*) │
│ MCP proxy (routes tool calls) │
│ PostgreSQL (Prisma ORM) │
│ Docker/Podman container management │
│ Health probes (STDIO, SSE, HTTP) │
│ RBAC enforcement │
│ │
│ ┌───────────────────────────────────┐ │
│ │ MCP Server Containers │ │
│ │ │ │
│ │ grafana/ home-assistant/ ... │ │
│ │ (managed + proxied by mcpd) │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
Clients never connect to MCP server containers directly — all tool calls go through mcplocal → mcpd, which proxies them to the right container via STDIO/SSE/HTTP. This keeps containers unexposed and lets mcpd enforce RBAC and health checks.
**Tool namespacing**: When Claude connects to a project with servers `grafana` and `slack`, it sees tools like `grafana/search_dashboards` and `slack/send_message`. mcplocal routes each call through mcpd to the correct upstream server.
## Project Structure
```
mcpctl/
├── src/
│ ├── cli/ # mcpctl command-line interface (Commander.js)
│ ├── mcpd/ # Daemon server (Fastify 5, REST API)
│ ├── mcplocal/ # Local MCP proxy (namespace merging, gating)
│ ├── db/ # Database schema (Prisma) and migrations
│ └── shared/ # Shared types and utilities
├── deploy/ # Docker Compose for local development
├── stack/ # Production deployment (Portainer)
├── scripts/ # Build, release, and deploy scripts
├── examples/ # Example YAML configurations
└── completions/ # Shell completions (fish, bash)
```
## Development
```bash
# Prerequisites: Node.js 20+, pnpm 9+, Docker/Podman
# Install dependencies
pnpm install
# Start local database
pnpm db:up
# Generate Prisma client
cd src/db && npx prisma generate && cd ../..
# Build all packages
pnpm build
# Run tests
pnpm test:run
# Development mode (mcpd with hot-reload)
cd src/mcpd && pnpm dev
```
## License
MIT