Two bugs fixed: 1. Backup completeness: JSON backup API now includes prompts and templates. Previously these were silently dropped during backup/restore, causing data loss on migration. 2. STDIO proxy for docker-image servers: servers with dockerImage but no packageName/command (like docmost) now use k8s Attach to connect to the container's PID 1 stdin/stdout instead of exec. This fixes "has no packageName or command" errors. Changes: - backup-service.ts: add BackupPrompt/BackupTemplate types, export them - restore-service.ts: restore prompts (with project FK) and templates - mcp-proxy-service.ts: sendViaPersistentAttach for docker-image STDIO - orchestrator.ts: add attachInteractive to McpOrchestrator interface - kubernetes-orchestrator.ts: implement attachInteractive via k8s Attach - k8s-client-official.ts: expose Attach client Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
41 KiB
mcpctl — Comprehensive Project Summary
kubectl for Model Context Protocol servers.
mcpctl is a production-grade management system for MCP servers, providing a Kubernetes-inspired declarative interface for deploying, orchestrating, and observing MCP servers that connect to Claude and other LLM clients.
Table of Contents
- System Architecture
- Component Overview
- Resource Model & Design Decisions
- CLI Reference
- API Surface (mcpd)
- Database Schema
- Local Proxy (mcplocal)
- ProxyModel Plugin System
- Gated Sessions
- Content Pipeline & Stages
- LLM Provider Integration
- Caching
- Authentication & RBAC
- Audit Infrastructure & Trust Model
- Container Orchestration
- Deployment & Distribution
- Testing Strategy
- Technology Stack
- Project Structure
- Deferred & Future Work
1. System Architecture
Three-Tier Design
Claude Code / LLM Client
| (STDIO — MCP JSON-RPC protocol)
v
mcplocal (Local Daemon — developer machine)
| (HTTP REST)
v
mcpd (Remote Daemon — server/NAS, e.g. 10.0.0.194)
| (Docker/Podman API)
v
MCP Server Containers (isolated network)
┌────────────┐
┌─────────────────┐ HTTP ┌──────────────┐ │ PostgreSQL │
│ mcpctl CLI │──────────────>│ mcpd │──>│ │
│ (Commander.js) │ │ (Fastify 5) │ └────────────┘
└─────────────────┘ └──────┬───────┘
│ Docker/Podman API
v
┌──────────────┐
│ Containers │
│ (MCP servers)│
└──────────────┘
┌─────────────────┐ STDIO ┌──────────────┐ STDIO/HTTP ┌────────────┐
│ Claude / LLM │────────────>│ mcplocal │───────────────>│ MCP Servers│
│ │ │ (McpRouter) │ │ │
└─────────────────┘ └──────────────┘ └────────────┘
Key Principle
- mcpd owns the database (PostgreSQL) — the only component that talks to the DB
- mcplocal is stateless — config-only, no database, acts as intelligent proxy
- mcpctl stores only credentials —
~/.mcpctl/config.jsonand~/.mcpctl/credentials.json - All MCP servers run inside the mcpd container on the NAS via podman (container-in-container)
2. Component Overview
mcpctl (CLI)
- kubectl-like interface for managing the entire system
- Talks to mcplocal (local daemon) via HTTP REST, or directly to mcpd with
--direct - Distributed as RPM/DEB package via Gitea registry
- Built with Commander.js, Ink/React for TUI, Inquirer for prompts
mcplocal (Local Daemon)
- Runs on developer machine as a systemd user service
- Exposes MCP protocol via STDIO to Claude
- Exposes HTTP REST API for mcpctl management commands
- Core responsibilities:
- Tool namespacing and routing (
server/toolformat) - Gated sessions and prompt delivery
- Content pipeline (transformation stages)
- LLM integration for intelligent prompt selection
- Pipeline result caching
- Audit event collection
- Tool namespacing and routing (
mcpd (Remote Daemon)
- Server-side daemon on NAS/cloud (Fastify 5)
- Manages MCP server containers (Docker/Podman via dockerode)
- PostgreSQL for state, audit logs, access control
- Owns credentials (never exposed to mcplocal)
- REST API for all management operations
- MCP proxy endpoint for direct tool invocation
- Health probe runner for container monitoring
- Git-based backup system
@mcpctl/db (Database Layer)
- Prisma ORM with PostgreSQL
- 22 models, 11 migrations
- Template seeding from YAML files at startup
@mcpctl/shared (Shared Utilities)
- Constants, types, validation schemas (Zod)
- Secret encryption/decryption utilities
- Zero external dependencies beyond Zod
3. Resource Model & Design Decisions
ADR-001: Kubernetes-Style Resource Model
| mcpctl Resource | K8s Analogy | Behavior |
|---|---|---|
| Server | Deployment | Self-contained, complete definition. Contains image, command, transport, env refs, replicas. No external template dependencies at runtime. |
| Instance | Pod | Immutable, ephemeral, auto-managed by reconciliation loop. No create instance or edit instance. Delete triggers re-creation. |
| Secret | Secret | Holds sensitive key-value pairs. Servers reference via env[].valueFrom.secretRef. |
| Project | Namespace | Groups servers, configures ProxyModel and LLM provider. Generates .mcp.json. |
| Prompt | ConfigMap (sort of) | Instruction text delivered to Claude. Global or project-scoped. Priority-ranked. |
| Template | — | Blueprints for server creation. Used at create-time only, not runtime. |
| RbacDefinition | ClusterRoleBinding | Named policies with subjects and roleBindings. |
ADR-002: Profiles Replaced with Secrets
The original McpProfile resource tried to be secrets, configmaps, and project-server links simultaneously. Environment variables declared in profiles were never actually passed to running containers.
Decision: Replace with dedicated Secret resource following Kubernetes conventions:
servers:
- name: ha-mcp
env:
- name: HOMEASSISTANT_TOKEN
valueFrom:
secretRef:
name: ha-credentials
key: HOMEASSISTANT_TOKEN
ADR-003: Self-Contained Servers with Source Tracking
Servers store complete definitions (no runtime template dependencies). Optional source metadata enables registry-based upgrades via 3-way diff (old snapshot vs current server vs new template).
Rationale: Matches kubectl mental model. get server X -o yaml > new.yaml && edit && apply works naturally. Duplication is minimal (~10 lines YAML).
ADR-004: ConfigMaps Deferred
Only Secrets implemented. ConfigMap separation can be added later if needed. Keeps the model simple.
ADR-005: Apply-Compatible YAML Round-Trip
mcpctl get server ha-mcp -o yaml > s.yaml && mcpctl apply -f s.yaml must work:
get -o yaml/jsonstrips internal fields (id, createdAt, updatedAt, version, ownerId)- Output wrapped in resource key:
{ servers: [...] } describe -o yaml/jsonkeeps full raw output (for debugging)
ADR-006: CLI Design Principles
- Everything possible via
apply -fMUST also be possible viacreateCLI flags - Support
-o yamland-o jsonlike kubectl describeshows visually clean sectioned output with tables- Name resolution works everywhere (not just IDs)
- Instances are immutable (like pods) — no create/edit
4. CLI Reference
Global Options
--daemon-url <url> mcplocal daemon URL
--direct bypass mcplocal, connect directly to mcpd
-p, --project <name> Target project
-o, --output <format> table | json | yaml
-v, --version Show version
Resource Operations
| Command | Description |
|---|---|
mcpctl get <resource> [name] |
List resources or fetch by name/ID. Supports glob patterns (graf*). |
mcpctl describe <resource> <name> |
Detailed view with sections and tables. |
mcpctl create <resource> <name> [opts] |
Create resource. Mirrors apply -f capabilities. |
mcpctl edit <resource> <name> |
Open in $EDITOR as YAML, apply on save. |
mcpctl patch <resource> <name> key=val... |
Patch individual fields without editor. |
mcpctl delete <resource> <name> |
Delete resource. |
mcpctl apply -f <file> |
Declarative YAML/JSON application (like kubectl apply). Supports --dry-run. |
Supported Resources
servers, projects, instances, secrets, templates, users, groups, rbac, prompts, promptrequests, serverattachments (virtual), proxymodels (virtual, from mcplocal), all (project export)
Resource Aliases
server/srv → servers project/proj → projects
instance/inst → instances secret/sec → secrets
template/tpl → templates prompt → prompts
user → users group → groups
rbac/rbac-definition → rbac promptrequest/pr → promptrequests
serverattachment/sa → serverattachments proxymodel/pm → proxymodels
Lifecycle & Diagnostics
| Command | Description |
|---|---|
mcpctl status |
Show connectivity, auth status, LLM provider health, available models. |
mcpctl login |
Authenticate with mcpd (first login bootstraps initial user). |
mcpctl logout |
Clear stored credentials. |
mcpctl logs <name> [-t N] [-i index] |
Stream container logs. Resolves server name → running instance. |
mcpctl cache stats |
Show pipeline cache statistics per namespace. |
mcpctl cache clear [ns] [--older-than N] |
Clear pipeline cache. |
mcpctl backup |
Show git backup status, public SSH key. |
mcpctl backup log [-n N] |
Show backup commit history. |
mcpctl backup restore list/diff/to |
Restore to specific backup commit. |
Console & Inspection
| Command | Description |
|---|---|
mcpctl console [project] |
Interactive TUI — request/response timeline, tool inspection. |
mcpctl console --stdin-mcp |
MCP server mode over stdin/stdout (for Claude integration). |
mcpctl console --audit |
Browse audit events from mcpd interactively. |
Configuration
| Command | Description |
|---|---|
mcpctl config view |
Show current configuration. |
mcpctl config set <key> <value> |
Set config value (mcplocalUrl, mcpdUrl, registries, outputFormat, etc.). |
mcpctl config path |
Show config file path. |
mcpctl config setup |
Interactive configuration wizard. |
mcpctl config claude -p <project> |
Generate .mcp.json for Claude Code. |
Create Subcommands
mcpctl create server <name> [--package-name X] [--docker-image X] [--transport STDIO|SSE|STREAMABLE_HTTP]
[--runtime node|python] [--replicas N] [--env KEY=val] [--from-template name:version]
mcpctl create secret <name> [--data key=val ...] [--data-file path.json]
mcpctl create project <name> [-d desc] [--proxy-model default|gate|content-pipeline] [--server name ...]
mcpctl create user <email> [--password pass] [--name name]
mcpctl create group <name> [-d desc] [--member email ...]
mcpctl create rbac <name> [--subject kind:name] [--role-binding role:resource[:name]]
mcpctl create prompt <name> [--content text] [--project name] [--priority 1-10] [--link url]
Apply File Format
secrets:
- name: my-secret
data:
KEY: value
servers:
- name: my-server
transport: STDIO
packageName: "@modelcontextprotocol/server-example"
env:
- name: API_KEY
valueFrom:
secretRef:
name: my-secret
key: KEY
projects:
- name: my-project
proxyModel: default
servers:
- my-server
serverattachments:
- server: my-server
project: my-project
prompts:
- name: my-prompt
project: my-project
content: "Instruction text..."
priority: 5
5. API Surface (mcpd)
All endpoints under /api/v1/ require Bearer token auth except /auth/* and /health*.
Authentication
| Endpoint | Method | Description |
|---|---|---|
/auth/bootstrap |
POST | First-user setup (creates admin + bootstrap RBAC) |
/auth/status |
GET | {hasUsers: boolean} (unauthenticated) |
/auth/login |
POST | Returns token + user info |
/auth/logout |
POST | Invalidate session |
/auth/me |
GET | Current user identity |
/auth/impersonate |
POST | Create session for another user (requires run:impersonate) |
Servers
| Endpoint | Method | Description |
|---|---|---|
/servers |
GET | List all servers |
/servers/:id |
GET | Get server by CUID |
/servers |
POST | Create server (validates name uniqueness, image/package) |
/servers/:id |
PUT | Update server, re-reconciles replicas |
/servers/:id |
DELETE | Delete server + cascade-delete all instances |
Instances
| Endpoint | Method | Description |
|---|---|---|
/instances |
GET | List (optional ?serverId= filter) |
/instances/:id |
GET | Get instance |
/instances/:id |
DELETE | Delete instance, triggers reconciliation |
/instances/:id/inspect |
GET | Docker inspect output (state, port, IP) |
/instances/:id/logs |
GET | Container logs (?tail=N) |
Projects
| Endpoint | Method | Description |
|---|---|---|
/projects |
GET | List (RBAC-filtered) |
/projects/:id |
GET/POST/PUT/DELETE | CRUD by CUID or name |
/projects/:id/mcp-config |
GET | Generate .mcp.json |
/projects/:id/instructions |
GET | Get prompt + attached servers for system message |
/projects/:id/servers |
GET/POST | List/attach servers |
/projects/:id/servers/:name |
DELETE | Detach server |
Prompts & Prompt Requests
| Endpoint | Method | Description |
|---|---|---|
/prompts |
GET/POST | List/create approved prompts |
/prompts/:id |
PUT/DELETE | Update/delete (system prompts reset to default) |
/prompts/:id/regenerate-summary |
POST | Force re-generate summary/chapters |
/promptrequests |
GET/POST | List/create pending requests |
/promptrequests/:id/approve |
POST | Atomic delete request → create prompt |
/projects/:name/prompts/visible |
GET | Approved + session's pending |
/projects/:name/prompt-index |
GET | Compact index for gating |
Secrets, Users, Groups, RBAC
Standard CRUD on /secrets, /users, /groups, /rbac-definitions.
Health & Monitoring
| Endpoint | Method | Description |
|---|---|---|
/health/overview |
GET | System health, instance counts, error rate |
/health/instances/:id |
GET | Instance-specific health, uptime, latency |
/metrics |
GET | Request counts, error counts, last request time |
/healthz |
GET | Liveness probe |
Backup, Restore, Audit
| Endpoint | Method | Description |
|---|---|---|
/backup |
POST | Create encrypted bundle (servers, secrets, projects, users, groups, rbac) |
/restore |
POST | Restore bundle (merge/skip/overwrite strategy) |
/audit/events |
POST/GET | Batch insert from mcplocal / query with filters |
/audit/sessions |
GET | Session aggregates (first/last seen, event counts) |
/git/backup/init |
POST | Initialize git backup with SSH credentials |
/git/backup/status |
GET | Backup sync status |
/git/backup/sync |
POST | Manual trigger sync |
MCP Proxy
| Endpoint | Method | Description |
|---|---|---|
/mcp/proxy |
POST | Forward JSON-RPC to running MCP server instance. Dispatches by transport (STDIO via docker exec, SSE/HTTP via direct HTTP). Maintains persistent STDIO connections. |
6. Database Schema
PostgreSQL via Prisma ORM. 22 models across 11 migrations.
Core Models
User & Auth:
User— email/password (bcrypt), role (USER/ADMIN), optional OAuthSession— Bearer token with 30-day TTLGroup/GroupMember— user groups for RBAC
MCP Infrastructure:
McpServer— transport (STDIO/SSE/STREAMABLE_HTTP), docker image, package name, runtime (node/python), env vars (JSON), health check config, replicas, external URLMcpTemplate— reusable blueprints for server creation (mirrors McpServer fields)McpInstance— running containers, status (STARTING/RUNNING/STOPPING/STOPPED/ERROR), container ID, port, health status, events
Organization:
Project— LLM config (provider, model), proxy model, gated flag, prompt instructions, server overridesProjectServer— junction table linking projects to serversSecret— named secret bundles (data as encrypted JSON), versioned
Content:
Prompt— approved system prompts (global or project-scoped), priority, summary/chapters, optional link targetPromptRequest— pending prompt proposals from LLM sessions
Audit & Backup:
AuditLog— user action trail (action, resource, resourceId, details)AuditEvent— pipeline/gate/tool trace events from mcplocal (sessionId, projectName, eventKind, correlationId, userName)BackupPending— queue for git-based backup syncRbacDefinition— named RBAC policies
7. Local Proxy (mcplocal)
Request Flow
Claude (STDIO JSON-RPC)
↓
StdioProxyServer (reads from stdin)
↓
McpRouter.route(request)
├→ PluginSessionContext (per-session state)
├→ ProxyModelPlugin hooks (intercept/transform)
├→ Upstream lookup (tool name prefix → server)
└→ Response (with optional drill-down sections)
Router Responsibilities
- Manages upstream connections (STDIO child processes, HTTP)
- Maps tools/resources/prompts to servers via name prefix (
servername/toolname) - Maintains prompt index + system prompt cache (TTL-based)
- Dispatches plugin hooks via
getOrCreatePluginContext - Section storage for drill-down navigation
- Audit event collection and batching
- Link resolution (relative → absolute URLs)
Upstream Transports
| Transport | Implementation |
|---|---|
| STDIO | Spawns child process, bidirectional pipe, JSON-RPC over newline-delimited JSON |
| SSE | HTTP GET for event stream, POST for messages |
| Streamable HTTP | HTTP POST with JSON-RPC payloads |
HTTP Endpoints (mcplocal)
| Endpoint | Description |
|---|---|
GET /proxymodels |
List all models (YAML pipelines + TS plugins) |
GET /proxymodels/:name |
Single model details |
GET /proxymodels/stages |
List available stages |
POST /proxymodels/reload |
Force reload stages from disk |
GET /cache/stats |
Per-namespace cache statistics |
DELETE /cache |
Clear all or by age |
DELETE /cache/:namespace |
Clear specific namespace |
POST /mcp |
JSON-RPC request forwarding |
8. ProxyModel Plugin System
A ProxyModel is either a Pipeline (YAML) or a Plugin (TypeScript).
Plugin Interface
| 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 reads |
onPromptsList |
prompts/list — can filter prompts |
onPromptGet |
prompts/get — can intercept reads |
Built-in Plugins
| Plugin | Extends | Gating | Content Pipeline | Use Case |
|---|---|---|---|---|
gate |
— | Yes | No | Gating + prompt delivery only |
content-pipeline |
— | No | Yes | Content transformation only |
default |
gate + content-pipeline | Yes | Yes | Full pipeline (most common) |
Inheritance: Plugins can extend parents. Conflicting hooks from multiple parents cause load-time errors (except chainable lifecycle hooks which run sequentially).
Pipeline Configuration (YAML)
name: default
spec:
controller: gate
controllerConfig: { byteBudget: 8192 }
stages:
- type: passthrough
- type: paginate
config: { pageSize: 8000 }
appliesTo: [prompt, toolResult]
cacheable: true
Per-Session Context
Each session gets a PluginSessionContext providing:
- Session state (
Map<string, unknown>) - LLM provider, cache provider, structured logger
- Virtual tool/server registration
- Upstream routing and tool discovery
- Content processing and notifications
- Audit event emission
9. Gated Sessions
Problem
When Claude connects to an MCP server, it sees all tools immediately and starts using them. In a managed environment, you want to deliver relevant context (prompts/instructions) before granting tool access.
Solution: Keyword-Driven Prompt Retrieval
- Initialize: Instructions include prompt index + "call
begin_sessionimmediately" - Gated
tools/list: Onlybegin_sessionvisible - Claude calls
begin_sessionwith keywords describing the task - Prompt matching: Keywords matched against prompt summaries/chapters
- Ungating: Matched prompts returned +
tools/list_changednotification sent - Full access: All upstream tools now visible
Prompt Scoring
Formula: priority + (matchCount * priority)
- Priority alone is baseline — ensures global prompts compete for inclusion
- Tag matches multiply priority — relevant prompts score higher
- Priority 10 = always included (bypasses budget)
- 8KB byte budget cap; overflow prompts listed as index-only
Critical Design Lessons
What works:
- One gate tool (
begin_session), zero ambiguity - Instructions say "check its input schema" (not naming specific parameters)
- "immediately" and "required" prevent Claude from exploring first
- Tool names listed as preview in instructions (helps keyword generation)
tools/list_changednotification mandatory after ungating- Auto-ungate fallback if Claude bypasses gate
What fails:
- Naming parameters that don't match the schema
- Complex conditional instructions (Claude prefers simple paths)
- Multiple tools in gated state (Claude skips the gate)
- Gate instructions only in tool description (must be in initialize response)
- Burying the call-to-action after 200 lines of context
Complete Flow
Client mcplocal upstream
│── initialize ────────>│
│<── instructions ──────│ (gate instructions + prompt index + tool preview)
│── tools/list ────────>│
│<── [begin_session] ───│ (ONLY begin_session visible)
│── tools/call ────────>│
│ begin_session │── match prompts ─────────>│
│ {tags:[...]} │<── prompt content ────────│
│<── matched prompts ───│ (full content + encouragement)
│<── notification ──────│ (tools/list_changed)
│── tools/list ────────>│
│<── [108+ tools] ──────│ (ALL tools now visible)
│ │
│ Claude proceeds with full tool access
10. Content Pipeline & Stages
How It Works
Tool results pass through an ordered sequence of stages before reaching Claude:
- Each stage receives previous stage's content
- Returns
{content, sections?, metadata?} - Sections enable drill-down navigation
- Stage errors are caught — pipeline continues with previous content
Built-in Stages
| Stage | Purpose |
|---|---|
passthrough |
Identity transform (testing/baseline) |
paginate |
Split large content into numbered pages (8KB default). LLM-generated page titles (cached). |
section-split |
Split by structure: JSON arrays/objects → elements/keys, YAML → keys, prose → ## headers, code → function/class boundaries. Merges tiny sections, re-splits oversized ones. |
summarize-tree |
Hierarchical LLM-generated section summaries. Groups sections into trees. Cached. |
Section Drill-Down
After pipeline produces sections:
- Full content replaced with compact table of contents +
_resultId - Sections stored in session-scoped store (5-minute TTL)
- Client calls same tool with
_resultId+_sectionto retrieve specific section - Supports hierarchical navigation (sections within sections)
Custom Stages
Drop .js files in ~/.mcpctl/stages/:
export default async function myStage(input, context) {
// context.llm, context.cache, context.log available
return { content: transformedContent, sections: [...] };
}
Hot-reload with 300ms file watch debounce. Built-in stages take precedence.
11. LLM Provider Integration
Supported Providers
| Provider | Type | Tier |
|---|---|---|
| Gemini CLI | Local | Fast |
| Ollama | Local | Fast |
| DeepSeek | API | Fast/Heavy |
| OpenAI | API | Heavy |
| Anthropic | API | Heavy |
| vLLM | Local | Configurable |
| vLLM Managed | Auto-managed local | Configurable |
Tier System
- Fast tier: Quick, cheap models for pipeline stages and keyword extraction
- Heavy tier: Full models for complex prompt selection and summarization
- Legacy active: Single default provider (fallback)
LLM Adapter
Stages use a simple interface:
interface LLMProvider {
complete(prompt: string, options?): Promise<string>;
available(): boolean;
}
Resolution order: named provider → fast tier → heavy tier → active provider.
Multi-Provider Configuration
{
"llm": {
"providers": [
{ "name": "fast-local", "type": "ollama", "model": "llama3", "tier": "fast" },
{ "name": "heavy-api", "type": "openai", "model": "gpt-4", "tier": "heavy" }
]
}
}
12. Caching
Architecture: L1 Memory + L2 Disk
- L1 in-memory: LRU map (default 500 entries) for fast lookups
- L2 disk:
~/.mcpctl/cache/<namespace>/<key>.dat- Namespace:
provider--model--proxymodel(e.g.,openai--gpt-4o--content-pipeline) - Key: 16-char hex SHA256 prefix of content
- Value: raw content (no JSON wrapper)
- Namespace:
Configuration
| Option | Default | Examples |
|---|---|---|
maxSize |
256MB | "1GB", "10%" (of partition), 536870912 (bytes) |
ttlMs |
30 days | Any millisecond value |
maxMemoryEntries |
500 | L1 LRU cap |
dir |
~/.mcpctl/cache |
Custom path |
Management
mcpctl cache stats # Per-namespace breakdown
mcpctl cache clear # Clear everything
mcpctl cache clear openai--gpt-4--default # Clear specific namespace
mcpctl cache clear --older-than 7 # Clear entries older than 7 days
HTTP API
GET /cache/stats # Per-namespace stats
DELETE /cache # Clear all (or ?olderThan=N)
DELETE /cache/:namespace # Clear specific namespace
13. Authentication & RBAC
Auth Flow
mcpctl login→ prompts for email/password- First login (no users in system) → bootstrap: creates admin user + admin group + bootstrap RBAC
- POST
/auth/login→ returns 30-day bearer token - Token stored in
~/.mcpctl/credentials.json - CLI passes token to mcplocal config
- mcplocal attaches
Authorization: Bearer <token>to all mcpd requests - mcpd validates token against Session table
RBAC Model
Subjects: User (by email), Group, ServiceAccount
Roles & Capabilities:
| Role | Grants |
|---|---|
edit |
view, create, delete, edit, expose |
view |
view |
create |
create |
delete |
delete |
run |
run (for operations) |
expose |
expose, view |
Resources: *, servers, instances, secrets, projects, templates, users, groups, rbac, prompts, promptrequests
Binding Types:
- Resource binding:
{role: 'edit', resource: 'servers', name?: 'my-server'}- With
name: user can only access that specific resource - Without
name: user can access all resources of that type
- With
- Operation binding:
{role: 'run', action: 'impersonate'}- Grants permission for named operations (backup, restore, audit-purge, logs, impersonate)
Resolution
- CLI resolves name → CUID client-side before API calls
- RBAC hook resolves CUID → name before checking bindings
- List filtering:
getAllowedScope()computes allowed names,preSerializationhook filters arrays - Wildcard scope: user has unscoped binding → sees all resources
- Named scope: user has only name-scoped bindings → filtered to allowed names
14. Audit Infrastructure & Trust Model
Event Kinds
| Event Kind | Description |
|---|---|
pipeline_execution |
Full pipeline run summary (duration, stage count, sizes) |
stage_execution |
Individual stage detail (duration, input/output size, error) |
gate_decision |
Gate open/close with client intent and matched prompts |
prompt_delivery |
Which prompts were sent, match scores |
tool_call_trace |
Tool call with server + timing + result size |
rbac_decision |
Access control decisions |
session_bind |
Session initialization |
Trust Model
| Source | Verified | Meaning |
|---|---|---|
client |
false | Client LLM claims (begin_session intent, tags) |
mcplocal |
true | Server-side data (prompt matches, pipeline transforms) |
mcpd |
true | mcpd-originated events |
AuditCollector
Fire-and-forget batching: 50 events max, 5-second flush interval. POSTs to mcpd. Non-blocking — audit failures don't affect tool calls.
Correlation & Causality
correlationIdlinks related events (all events from one tool call)parentEventIdenables causal chains (gate_decision → pipeline_execution)userNametracks which user triggered the event- Designed for future graphiti knowledge graph ingestion
Per-Server Targeting
Different servers in a project can have different proxymodel configs via serverOverrides on the project resource. Resolution: server override → project default → null.
Future (Designed, Not Implemented)
- Virtual MCP Audit Server: mcpd-hosted virtual server providing
query_audit_log,get_session_timelinetools. Claude can directly query audit data. - Graphiti Integration: Causal graph with entity types (Session, Tool, Server, ProxyModel, Prompt, Stage) and edges (
triggered_by,transformed_by,verified_by). - Lab Parameter Simulation: Select any pipeline event, retrieve original input, re-run with different proxyModel/LLM/stages, side-by-side diff.
- Audit Level Config: Per-server
auditLevel: 'full' | 'hash-only' | 'disabled'.
15. Container Orchestration
Orchestrator Interface
McpOrchestrator abstracts container management (Docker/Podman today, Kubernetes in the future):
pullImage,createContainer,stopContainer,removeContainerinspectContainer,getContainerLogs,execInContainer,ping
Container Management
- Labels:
mcpctl.managed=truefor filtering - Network:
mcp-servers(configurable viaMCPD_MCP_NETWORK) - Resource limits: 512MB RAM, 0.5 CPU (configurable)
- Internal container IP exposed via inspect
Runtime Spawn Commands
| Runtime | Command |
|---|---|
| Node | npx --prefer-offline -y <packageName> |
| Python | uvx <packageName> |
| Custom | Explicit command field |
Health Probes
Periodic MCP tool-call probes (like K8s livenessProbe):
- Default interval: 15 seconds
- Dispatch by transport: STDIO (docker exec), HTTP (JSON-RPC)
- Failure threshold: 3 consecutive failures → unhealthy
- Updates instance
healthStatusandlastHealthCheck
Reconciliation Loop
Maintains desired replica count:
- If running < desired → start new instances
- If running > desired → stop excess instances
- Detects crashed containers → marks ERROR → triggers re-creation
Persistent STDIO Connections
For STDIO transport, mcpd maintains long-lived exec sessions (PersistentStdioClient) to avoid repeated docker exec overhead. Bidirectional streaming for interactive sessions.
16. Deployment & Distribution
Production Deployment
mcpd runs on 10.0.0.194 (NAS, managed via Portainer), NOT on the dev machine.
# Full deploy (preferred after merging)
bash fulldeploy.sh
fulldeploy.sh runs three steps:
scripts/build-mcpd.sh— build + push Docker image tomysources.co.uk/michal/mcpctl-mcpddeploy.sh— deploy stack to production via Portainer API athttp://10.0.0.194:9000scripts/release.sh— build RPM + publish to Gitea + install locally + smoke tests
Docker Images
| Image | Purpose |
|---|---|
mcpctl-mcpd |
Multi-stage build: Node 20 Alpine, includes git/ssh, Prisma |
mcpctl-node-runner |
Node 20 slim, runs npx -y for npm packages |
mcpctl-python-runner |
Python 3.12 slim, uses uv for Python packages |
All pushed to mysources.co.uk/michal/ registry.
Stack Services (Production)
postgres— PostgreSQL 16 (port 5432)mcpd— Daemon (port 3100)node-runner,python-runner— Base images- Networks:
mcpctl(management),mcp-servers(container communication)
RPM/DEB Distribution
source .env && bash scripts/release.sh
Installs via nfpm:
/usr/bin/mcpctl— CLI binary (bun compiled)/usr/bin/mcpctl-local— Local proxy binary (bun compiled)/usr/share/fish/vendor_completions.d/mcpctl.fish— Fish completions/usr/share/bash-completion/completions/mcpctl— Bash completions/usr/lib/systemd/user/mcplocal.service— Systemd user service
User install:
dnf config-manager --add-repo https://mysources.co.uk/api/packages/michal/rpm.repo
dnf install mcpctl
Git & PR Workflow
- Gitea at
http://10.0.0.194:3012(internal) /https://mysources.co.uk/michal/mcpctl(public) pr.shin project root creates PRs via Gitea APIghCLI not installed — usepr.shor direct API calls
17. Testing Strategy
Test Tiers
| Tier | Tool | Scope | When |
|---|---|---|---|
| Unit tests | Vitest | Package-level, mocked dependencies | pnpm test:run |
| DB tests | Vitest | Full Prisma + test PostgreSQL | pnpm --filter db exec vitest run (separate) |
| Smoke tests | Vitest | Live mcplocal + mcpd (not mocked) | pnpm test:smoke (post-deploy) |
Convention
- Every new feature MUST include smoke tests
- Smoke tests live in
src/mcplocal/tests/smoke/ - Use
SmokeMcpSessionfromtests/smoke/mcp-client.tsfor MCP protocol interactions - Smoke tests run automatically in the build/deploy pipeline
Critical Rules
- NEVER pipe pnpm test output to
tail,grep,head— pnpm hangs when it detects non-TTY - Always capture full output with
2>&1and read directly - DB tests excluded from workspace-root vitest (need test database)
- Tests integrated into pipeline:
build-rpm.shruns unit tests;release.shruns smoke tests
18. Technology Stack
| Layer | Technology |
|---|---|
| CLI Framework | Commander.js, Ink/React (TUI), Inquirer |
| API Server | Fastify 5, TypeScript strict mode |
| Database | PostgreSQL 16, Prisma ORM v6 |
| Container Runtime | Docker/Podman via dockerode |
| MCP Protocol | @modelcontextprotocol/sdk |
| Validation | Zod schemas everywhere |
| LLM Providers | OpenAI, Anthropic, Google Gemini, Ollama, DeepSeek, Groq, Mistral, OpenRouter, Azure |
| Testing | Vitest, coverage via v8 |
| Build | TypeScript project references, pnpm workspaces |
| Compilation | Bun (binary compilation for RPM) |
| Packaging | nfpm (RPM/DEB), Docker multi-stage |
| CI/CD | fulldeploy.sh → Portainer API + Gitea packages |
| Shell Completions | Fish + Bash (auto-generated via scripts/generate-completions.ts) |
Design Patterns
- Monorepo — pnpm workspaces with shared base TypeScript config
- Layered architecture — Routes → Services → Repositories (Prisma)
- Interface-based repositories — all data access through interfaces for testability
- Dependency injection — services receive dependencies via constructor
- Zod validation — all input validated at API boundary
- Plugin inheritance — composable ProxyModel plugins with conflict detection
- Content-addressed caching — SHA256 hash keys for deduplication
- TTL-based stores — prompt index (60s), system prompts (5min), sections (5min)
- Fire-and-forget audit — non-blocking event collection
- Declarative config — kubectl-style YAML/JSON for all resource management
19. Project Structure
mcpctl/
├── src/
│ ├── cli/ @mcpctl/cli CLI (Commander.js)
│ │ ├── src/commands/ 22 command handlers
│ │ ├── src/registry/ MCP server registry client
│ │ ├── src/formatters/ Output formatting (table/json/yaml)
│ │ └── src/auth/ Credential storage
│ ├── mcpd/ @mcpctl/mcpd Daemon (Fastify 5)
│ │ ├── src/routes/ 18 route handlers
│ │ ├── src/services/ 13 services
│ │ ├── src/repositories/ Data access layer
│ │ ├── src/middleware/ Auth, logging, error handling
│ │ └── src/validation/ Zod schemas, RBAC rules
│ ├── mcplocal/ @mcpctl/mcplocal Local proxy
│ │ ├── src/gate/ Session gating + tag matching
│ │ ├── src/proxymodel/ Plugin system + stages + cache
│ │ ├── src/providers/ 6 LLM providers
│ │ ├── src/upstream/ STDIO + HTTP upstream connections
│ │ ├── src/audit/ Event collection + batching
│ │ └── src/health/ Health monitoring
│ ├── db/ @mcpctl/db Database (Prisma)
│ │ ├── prisma/schema.prisma 22 models
│ │ └── prisma/migrations/ 11 migrations
│ └── shared/ @mcpctl/shared Constants, types, validation
├── deploy/ Dockerfiles + entrypoint
├── stack/ Production docker-compose + env
├── scripts/ Build, release, deploy scripts
├── completions/ Fish + Bash completions
├── templates/ MCP server YAML templates
├── docs/ Architecture + design docs
├── fulldeploy.sh Full build → deploy → release
├── deploy.sh Portainer stack deploy
├── pr.sh Gitea PR creation
├── nfpm.yaml RPM/DEB package metadata
├── vitest.config.ts Root test config
├── vitest.workspace.ts Workspace test config
├── tsconfig.base.json Base TypeScript config (strict)
└── pnpm-workspace.yaml Monorepo workspace definition
20. Deferred & Future Work
Deferred Tasks
| ID | Description | Status |
|---|---|---|
| 88 | Rename proxyMode: filtered → proxy | Deferred |
| 105-109 | Model Studio TUI | Deferred |
| 110 | RBAC for ProxyModels | Deferred |
| 113 | Model Studio docs | Deferred |
Future Architecture
- Virtual MCP Audit Server — Claude-queryable audit tools
- Graphiti Knowledge Graph — causal graph from audit events
- Lab Parameter Simulation — re-run pipelines with different configs
- Kubernetes Orchestrator — beyond Docker/Podman
- ConfigMaps — non-sensitive config separate from Secrets
- Multi-provider failover — automatic LLM provider cascading
Completed Major Features
- Project structure + monorepo setup
- MCP Registry Client (official, glama, smithery — 53 tests)
- Health Probe Runner (STDIO, SSE, Streamable HTTP — 12 tests)
- Container orchestration with reconciliation
- Full RBAC with name-scoped bindings
- Gated sessions with prompt scoring
- ProxyModel plugin system with inheritance
- Content pipeline with 4 built-in stages
- Pipeline cache (L1 memory + L2 disk)
- Audit infrastructure with trust model
- Git-based backup and restore
- Shell completions (Fish + Bash)
- RPM/DEB packaging and distribution
- Smoke test framework
- Console inspector for debugging