Arcane MCP
by jmagar
README.md
# Arcane MCP
[](https://www.npmjs.com/package/arcane-mcp) [](https://github.com/jmagar/arcane-mcp/pkgs/container/arcane-mcp)
TypeScript MCP server for the Arcane Docker management API. Exposes a single `arcane` tool that routes all Docker environment, project, container, image, network, volume, system, registry, image-update, vulnerability, and GitOps operations through the Arcane service.
## Overview
Arcane MCP wraps the Arcane REST API behind a Model Context Protocol interface. An AI agent calls the `arcane` tool with an `action` and `subaction`. The server authenticates the request, enforces a confirmation gate on destructive operations, routes to the appropriate backend service, and returns JSON.
The server ships two MCP tools:
| Tool | Purpose |
| --- | --- |
| `arcane` | Unified action/subaction router for all Arcane API operations |
| `arcane_help` | Returns the full action/subaction reference as formatted text |
---
## Tools
### `arcane`
Call this tool with `action`, `subaction`, and optional `envId`, `id`, and `params`.
```json
{
"action": "container",
"subaction": "list",
"envId": "env-abc123"
}
```
#### Parameter rules
| Parameter | Type | Description |
| --- | --- | --- |
| `action` | string (enum) | Resource family: `environment`, `project`, `container`, `image`, `network`, `volume`, `system`, `image-update`, `vulnerability`, `registry`, `gitops` |
| `subaction` | string (enum) | Operation to perform (see per-family tables below) |
| `envId` | string (optional) | Target environment ID. Required by all families except `registry`. For `environment` subactions, `envId` and `id` are interchangeable for single-resource ops. |
| `id` | string (optional) | Resource ID for single-resource operations (get, delete, etc.) |
| `params` | object (optional) | Operation-specific payload. Pass `{ "confirm": true }` to authorize a destructive operation without elicitation. |
#### Destructive operation gate
Any operation in the DESTRUCTIVE set (listed per family below) is blocked unless one of three conditions is met:
1. `ARCANE_MCP_ALLOW_DESTRUCTIVE=true` — all destructive ops auto-confirm, no re-call needed.
2. `ARCANE_MCP_ALLOW_YOLO=true` — skips the interactive elicitation prompt; the tool must still be re-called with `params: { "confirm": true }` to proceed.
3. Default — if the MCP client supports elicitation forms, the server prompts the user interactively. If the client does not support elicitation, the server returns a prompt asking for a re-call with `params: { "confirm": true }`.
---
## Action families
### `environment`
Top-level Docker host connections. `envId` and `id` are interchangeable for single-resource operations.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `list` | List all environments | |
| `get` | Get an environment by ID | |
| `create` | Create a new environment; pass `params` with `apiUrl`, `apiKey`, and optional `name`, `enabled`, `isEdge` | |
| `update` | Update environment settings; pass `params` with fields to change | |
| `delete` | Permanently delete an environment | **yes** |
| `test` | Test the environment's API connection | |
Example:
```json
{"action": "environment", "subaction": "list"}
{"action": "environment", "subaction": "get", "id": "env-abc123"}
{"action": "environment", "subaction": "create", "params": {"apiUrl": "https://host.example.com", "apiKey": "..."}}
{"action": "environment", "subaction": "delete", "id": "env-abc123", "params": {"confirm": true}}
```
---
### `project`
Docker Compose stacks running inside an environment. Requires `envId`.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `list` | List all projects in the environment | |
| `get` | Get a project by ID | |
| `create` | Create a project; pass `params` with `name`, `composeContent`, and optional `envContent` | |
| `update` | Update a project; pass `params` with `composeContent`, `envContent`, or `name` | |
| `up` | Start a project (docker compose up) | |
| `down` | Stop a project (docker compose down) | **yes** |
| `restart` | Restart all containers in the project | **yes** |
| `pull` | Pull latest images without restarting | |
| `destroy` | Stop and remove containers, networks, volumes created by the compose file | **yes** |
| `redeploy` | Pull images and recreate the project | **yes** |
| `build` | Build images for the project; pass optional `params` with `services`, `provider`, `push`, `load` | |
Example:
```json
{"action": "project", "subaction": "list", "envId": "env-abc123"}
{"action": "project", "subaction": "up", "envId": "env-abc123", "id": "my-stack"}
{"action": "project", "subaction": "down", "envId": "env-abc123", "id": "my-stack", "params": {"confirm": true}}
{"action": "project", "subaction": "build", "envId": "env-abc123", "id": "my-stack", "params": {"services": ["web"], "push": true}}
```
---
### `container`
Individual Docker containers inside an environment. Requires `envId`.
Note: container logs are not available via the Arcane REST API. Use the Arcane web UI or `docker logs` directly.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `list` | List all containers | |
| `get` | Get a container by ID | |
| `create` | Create a container; pass `params` with `name`, `image`, and optional `cmd`, `env`, `ports`, `volumes`, `restartPolicy`, `labels`, `memory`, `cpus`, `privileged` | |
| `start` | Start a stopped container | |
| `stop` | Stop a running container | **yes** |
| `restart` | Restart a container | **yes** |
| `update` | Re-pull the container's image and recreate it using its existing config. No params accepted. | |
| `delete` | Remove a container | **yes** |
| `stats` | Get CPU, memory, and network stats for all containers in the environment | |
Example:
```json
{"action": "container", "subaction": "list", "envId": "env-abc123"}
{"action": "container", "subaction": "stats", "envId": "env-abc123"}
{"action": "container", "subaction": "stop", "envId": "env-abc123", "id": "container-id", "params": {"confirm": true}}
{"action": "container", "subaction": "create", "envId": "env-abc123", "params": {"name": "nginx", "image": "nginx:latest", "ports": {"80/tcp": [{"HostPort": "8080"}]}}}
```
---
### `image`
Docker images available in an environment. Requires `envId`.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `list` | List all images | |
| `get` | Get an image by ID | |
| `pull` | Pull an image; pass `params` with `imageName` and optional `tag` | |
| `delete` | Delete an image by ID | **yes** |
| `prune` | Remove all unused images | **yes** |
| `scan` | Scan an image for vulnerabilities (triggers Trivy scan) | |
Example:
```json
{"action": "image", "subaction": "list", "envId": "env-abc123"}
{"action": "image", "subaction": "pull", "envId": "env-abc123", "params": {"imageName": "nginx", "tag": "latest"}}
{"action": "image", "subaction": "scan", "envId": "env-abc123", "id": "sha256:abc..."}
{"action": "image", "subaction": "prune", "envId": "env-abc123", "params": {"confirm": true}}
```
---
### `network`
Docker networks in an environment. Requires `envId`.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `list` | List all networks | |
| `get` | Get a network by ID | |
| `create` | Create a network; pass `params` with `name` and `options` (`driver`, `internal`, `enableIPv6`, `labels`) | |
| `delete` | Delete a network by ID | **yes** |
| `prune` | Remove all unused networks | **yes** |
Example:
```json
{"action": "network", "subaction": "list", "envId": "env-abc123"}
{"action": "network", "subaction": "create", "envId": "env-abc123", "params": {"name": "my-net", "options": {"driver": "bridge"}}}
{"action": "network", "subaction": "prune", "envId": "env-abc123", "params": {"confirm": true}}
```
---
### `volume`
Docker volumes in an environment, with full backup and restore support. Requires `envId`.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `list` | List all volumes | |
| `get` | Get a volume by name | |
| `create` | Create a volume; pass `params` with `name` and optional `driver`, `driverOpts`, `labels` | |
| `delete` | Delete a volume by name | **yes** |
| `prune` | Remove all unused volumes | **yes** |
| `browse` | Browse a volume's directory tree; pass optional `params.path` (relative, no `..` allowed) | |
| `list-backups` | List all backups for a volume; requires `id` (volume name) | |
| `create-backup` | Create a new backup snapshot; requires `id` (volume name) | |
| `delete-backup` | Delete a backup by ID; pass `params.backupId` | **yes** |
| `restore` | Restore a volume to a backup state; requires `id` (volume name) and `params.backupId` | **yes** |
| `restore-files` | Restore specific files from a backup; requires `id`, `params.backupId`, and `params.paths` (string array) | **yes** |
#### Volume backup/restore workflow
1. List available backups: `subaction=list-backups`, `id=<volumeName>`
2. Create a new snapshot: `subaction=create-backup`, `id=<volumeName>`
3. Inspect a backup's contents: use `list-backups` to get the backup ID, then `restore-files` with `params.paths`
4. Full restore: `subaction=restore`, `id=<volumeName>`, `params: { backupId: "...", confirm: true }`
5. Partial restore: `subaction=restore-files`, `id=<volumeName>`, `params: { backupId: "...", paths: ["data/config.json"], confirm: true }`
6. Remove old snapshot: `subaction=delete-backup`, `params: { backupId: "...", confirm: true }`
Example:
```json
{"action": "volume", "subaction": "list-backups", "envId": "env-abc123", "id": "my-data-vol"}
{"action": "volume", "subaction": "create-backup", "envId": "env-abc123", "id": "my-data-vol"}
{"action": "volume", "subaction": "restore", "envId": "env-abc123", "id": "my-data-vol", "params": {"backupId": "bkp-xyz", "confirm": true}}
{"action": "volume", "subaction": "restore-files", "envId": "env-abc123", "id": "my-data-vol", "params": {"backupId": "bkp-xyz", "paths": ["etc/app.conf"], "confirm": true}}
{"action": "volume", "subaction": "browse", "envId": "env-abc123", "id": "my-data-vol", "params": {"path": "etc"}}
```
---
### `system`
Docker daemon-level operations for an environment. Requires `envId`.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `docker-info` | Get Docker daemon info (version, runtime, resources) | |
| `start-all` | Start all projects in the environment | |
| `stop-all` | Stop all projects in the environment | **yes** |
| `prune` | Prune unused Docker resources; pass `params` with boolean flags: `containers`, `images`, `networks`, `volumes`, `buildCache`, `dangling` | **yes** |
| `convert` | Convert a `docker run` command to Docker Compose YAML; pass `params.dockerRunCommand` | |
Example:
```json
{"action": "system", "subaction": "docker-info", "envId": "env-abc123"}
{"action": "system", "subaction": "prune", "envId": "env-abc123", "params": {"containers": true, "images": true, "networks": true, "volumes": false, "buildCache": true, "dangling": true, "confirm": true}}
{"action": "system", "subaction": "convert", "envId": "env-abc123", "params": {"dockerRunCommand": "docker run -d -p 80:80 nginx"}}
```
---
### `image-update`
Check whether images have newer versions available. Requires `envId`. Long-running checks use a 120-second timeout.
| Subaction | Description |
| --- | --- |
| `check-all` | Check all images in the environment for updates; pass optional `params.credentials` for private registries |
| `check` | Check a single image; pass either `id` (Arcane image ID) or `params.imageRef` (e.g. `nginx:latest`) |
| `check-batch` | Check multiple images; pass `params.imageRefs` (string array) and optional `params.credentials` |
| `summary` | Get a summary count: total images, images with updates, digest-only updates, errors |
The `check` subaction resolves the target in this order: `id` (Arcane internal imageId) → `params.imageRef` (image reference string). Passing both is undefined behavior; use one or the other.
Example:
```json
{"action": "image-update", "subaction": "summary", "envId": "env-abc123"}
{"action": "image-update", "subaction": "check", "envId": "env-abc123", "params": {"imageRef": "nginx:latest"}}
{"action": "image-update", "subaction": "check", "envId": "env-abc123", "id": "img-abc123"}
{"action": "image-update", "subaction": "check-batch", "envId": "env-abc123", "params": {"imageRefs": ["nginx:latest", "redis:7"]}}
{"action": "image-update", "subaction": "check-all", "envId": "env-abc123"}
```
---
### `vulnerability`
Image vulnerability scanning powered by Trivy. Requires `envId`.
| Subaction | Description |
| --- | --- |
| `summary` | Get counts by severity (critical, high, medium, low, unknown) across all scanned images |
| `list` | List all vulnerability entries with full CVE detail |
| `scanner-status` | Get the Trivy scanner's current operational status |
| `ignore` | Add a vulnerability to the ignore list; pass `params` with `imageId`, `vulnerabilityId`, `pkgName`, and optional `installedVersion`, `reason` |
| `unignore` | Remove an ignore entry; pass `id` (ignoreId) |
| `list-ignored` | List all active ignore entries |
Example:
```json
{"action": "vulnerability", "subaction": "summary", "envId": "env-abc123"}
{"action": "vulnerability", "subaction": "list", "envId": "env-abc123"}
{"action": "vulnerability", "subaction": "ignore", "envId": "env-abc123", "params": {"imageId": "img-abc", "vulnerabilityId": "CVE-2024-1234", "pkgName": "openssl", "reason": "mitigated"}}
{"action": "vulnerability", "subaction": "unignore", "envId": "env-abc123", "id": "ignore-entry-id"}
```
---
### `registry`
Container registry credentials. Global resource — `envId` is not required and is ignored.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `list` | List all configured registries | |
| `get` | Get a registry by ID | |
| `create` | Add a registry; pass `params` with `url`, `username`, `token`, and optional `description`, `enabled`, `insecure` | |
| `update` | Update a registry; pass `id` and `params` with fields to change (`url`, `username`, `token`, `description`, `enabled`, `insecure`) | |
| `delete` | Remove a registry | **yes** |
| `test` | Test connectivity to the registry | |
Example:
```json
{"action": "registry", "subaction": "list"}
{"action": "registry", "subaction": "create", "params": {"url": "registry.example.com", "username": "myuser", "token": "...", "enabled": true}}
{"action": "registry", "subaction": "test", "id": "reg-abc123"}
{"action": "registry", "subaction": "delete", "id": "reg-abc123", "params": {"confirm": true}}
```
---
### `gitops`
Git-backed Compose deployment syncs. Requires `envId`.
Warning: `gitops:sync` pulls from a remote repository and applies changes. This is a supply chain risk if the repository is compromised. Always confirm the sync target before proceeding.
| Subaction | Description | Destructive |
| --- | --- | --- |
| `list` | List all GitOps sync configurations | |
| `get` | Get a sync configuration by ID | |
| `create` | Create a sync config; pass `params` with `name`, `repositoryId`, `branch`, `composePath`, and optional `autoSync`, `syncInterval`, `projectName` | |
| `update` | Update a sync config; pass `id` and `params` with fields to change | |
| `delete` | Delete a sync configuration | **yes** |
| `sync` | Trigger an immediate sync from the remote repository | **yes** |
| `status` | Get the sync's last run status, commit, and next scheduled run | |
| `browse` | Browse the repository's file tree; pass optional `params.path` | |
#### GitOps workflow
1. List existing syncs: `subaction=list`
2. Check a sync's state: `subaction=status`, `id=<syncId>`
3. Inspect repository contents: `subaction=browse`, `id=<syncId>`
4. Trigger a sync: `subaction=sync`, `id=<syncId>`, `params: { confirm: true }`
Example:
```json
{"action": "gitops", "subaction": "list", "envId": "env-abc123"}
{"action": "gitops", "subaction": "status", "envId": "env-abc123", "id": "sync-abc123"}
{"action": "gitops", "subaction": "browse", "envId": "env-abc123", "id": "sync-abc123"}
{"action": "gitops", "subaction": "sync", "envId": "env-abc123", "id": "sync-abc123", "params": {"confirm": true}}
{"action": "gitops", "subaction": "create", "envId": "env-abc123", "params": {"name": "prod-stack", "repositoryId": "repo-abc", "branch": "main", "composePath": "stacks/web/docker-compose.yml", "autoSync": true, "syncInterval": 300}}
```
---
## Destructive operations reference
The following 22 operations require confirmation before execution:
| Action | Subaction |
| --- | --- |
| `environment` | `delete` |
| `project` | `down`, `restart`, `destroy`, `redeploy` |
| `container` | `stop`, `restart`, `delete` |
| `image` | `delete`, `prune` |
| `network` | `delete`, `prune` |
| `volume` | `delete`, `prune`, `delete-backup`, `restore`, `restore-files` |
| `system` | `prune`, `stop-all` |
| `registry` | `delete` |
| `gitops` | `delete`, `sync` |
---
## Installation
### Plugin marketplace
```bash
/plugin marketplace add jmagar/claude-homelab
/plugin install arcane-mcp @jmagar-claude-homelab
```
### Local development
```bash
npm install
npm run build
npm start
```
For watch mode:
```bash
just dev
```
### Docker
```bash
just up
just logs
```
---
## Configuration
Copy `.env.example` to `.env` and fill in the required values:
```bash
cp .env.example .env
chmod 600 .env
```
### Environment variables
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `ARCANE_API_URL` | yes | — | Base URL of your Arcane instance, e.g. `https://arcane.example.com` |
| `ARCANE_API_KEY` | yes | — | API key from Arcane Settings > API |
| `ARCANE_MCP_TOKEN` | yes | — | Bearer token for MCP server auth. Generate with: `openssl rand -hex 32` |
| `ARCANE_MCP_BIND_PORT` | no | `3000` | Internal container port the server binds to |
| `ARCANE_MCP_PORT` | no | `44332` | Host-side Docker port mapping |
| `ARCANE_MCP_TRANSPORT` | no | `http` | Transport mode: `http` or `stdio` |
| `ARCANE_MCP_AUTH_ENABLED` | no | `true` | Set to `false` to disable Bearer auth (use only behind a trusted proxy) |
| `ARCANE_MCP_ALLOW_YOLO` | no | `false` | `true` skips elicitation prompts; re-call with `params: { confirm: true }` is still required |
| `ARCANE_MCP_ALLOW_DESTRUCTIVE` | no | `false` | `true` auto-confirms all destructive operations. Use only in fully trusted automated environments. |
| `LOG_LEVEL` | no | `info` | Pino log level: `trace`, `debug`, `info`, `warn`, `error` |
#### Safety flag behavior
`ARCANE_MCP_ALLOW_YOLO` and `ARCANE_MCP_ALLOW_DESTRUCTIVE` control the confirmation gate independently:
- Both `false` (default): destructive ops show an elicitation dialog (if the MCP client supports it) or return a prompt to re-call with `params: { confirm: true }`.
- `ALLOW_YOLO=true`: skips the elicitation dialog. The agent must still re-call with `params: { confirm: true }`.
- `ALLOW_DESTRUCTIVE=true`: bypasses the gate entirely. No re-call needed. Use in CI or fully automated pipelines only.
---
## Authentication
All MCP endpoints require a Bearer token unless `ARCANE_MCP_AUTH_ENABLED=false`.
The following paths bypass authentication:
- `GET /health` — health check, always unauthenticated
- `/.well-known/*` — RFC 9728 OAuth discovery endpoint; reserved for future OAuth resource metadata
All other paths, including `/mcp`, require `Authorization: Bearer <ARCANE_MCP_TOKEN>`.
Token comparison uses `timingSafeEqual` to prevent timing attacks.
---
## Session management
The HTTP transport maintains per-session MCP server instances (up to 200 concurrent sessions). Sessions idle for more than 30 minutes are evicted. When the session cap is reached, the least-recently-used idle session is evicted first.
Clients that do not send an `initialize` request are bootstrapped into a pre-initialized session. These clients cannot use elicitation (the confirmation dialog) — they will always receive the "re-call with `params: { confirm: true }`" response for destructive operations.
---
## Development commands
```bash
just dev # Start with watch-mode TypeScript compilation
just build # Compile TypeScript to dist/
just typecheck # Type-check without emitting
just lint # Run Biome linter
just fmt # Run Biome formatter
just test # Run unit tests with Vitest
just up # Start Docker Compose service
just down # Stop Docker Compose service
just logs # Tail container logs
just health # Check /health endpoint
just setup # Create .env from .env.example
just gen-token # Generate a random Bearer token
just clean # Remove dist/, .cache/, coverage/
```
---
## Verification
After starting the server:
```bash
just typecheck
just lint
just test
just health
```
The health endpoint returns:
```json
{"status": "ok", "service": "arcane-mcp"}
```
---
## Related plugins
| Plugin | Category | Description |
|--------|----------|-------------|
| [homelab-core](https://github.com/jmagar/claude-homelab) | core | Core agents, commands, skills, and setup/health workflows for homelab management. |
| [overseerr-mcp](https://github.com/jmagar/overseerr-mcp) | media | Search movies and TV shows, submit requests, and monitor failed requests via Overseerr. |
| [unraid-mcp](https://github.com/jmagar/unraid-mcp) | infrastructure | Query, monitor, and manage Unraid servers: Docker, VMs, array, parity, and live telemetry. |
| [unifi-mcp](https://github.com/jmagar/unifi-mcp) | infrastructure | Monitor and manage UniFi devices, clients, firewall rules, and network health. |
| [gotify-mcp](https://github.com/jmagar/gotify-mcp) | utilities | Send and manage push notifications via a self-hosted Gotify server. |
| [swag-mcp](https://github.com/jmagar/swag-mcp) | infrastructure | Create, edit, and manage SWAG nginx reverse proxy configurations. |
| [synapse-mcp](https://github.com/jmagar/synapse-mcp) | infrastructure | Docker management (Flux) and SSH remote operations (Scout) across homelab hosts. |
| [syslog-mcp](https://github.com/jmagar/syslog-mcp) | infrastructure | Receive, index, and search syslog streams from all homelab hosts via SQLite FTS5. |
| [plugin-lab](https://github.com/jmagar/plugin-lab) | dev-tools | Scaffold, review, align, and deploy homelab MCP plugins with agents and canonical templates. |
## License
MIT
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessSyncing