Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

incus-mcp

MCP server for Incus container and VM manager.

210 operations = 209 grouped + 1 ROOT: read (87), write (75), execute (9), delete (34), admin (4).

Install

OIDC (Authentik app_password)

  1. In Authentik, create an OAuth2/OpenID provider for Incus (public client)
  2. Create an app_password token: Admin → Directory → Tokens → Create (intent: app_password, user: your user, expiring: off)
  3. Copy the token key — this goes into INCUS_PASSWORD
{
  "mcpServers": {
    "incus": {
      "command": "uvx",
      "args": ["--refresh", "--extra-index-url", "https://nikitatsym.github.io/incus-mcp/simple", "incus-mcp"],
      "env": {
        "INCUS_URL": "https://incus.example.com:8443",
        "INCUS_OIDC_ISSUER": "https://auth.example.com/application/o/incus/",
        "INCUS_OIDC_CLIENT_ID": "your-client-id",
        "INCUS_USERNAME": "your-username",
        "INCUS_PASSWORD": "your-authentik-app-password-token"
      }
    }
  }
}

The server handles OIDC token exchange and refresh automatically (Resource Owner Password Grant).

TLS client certificate

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 \
  -sha384 -keyout incus-mcp.key -out incus-mcp.crt \
  -nodes -days 3650 -subj "/CN=incus-mcp"
incus config trust add-certificate incus-mcp.crt
{
  "mcpServers": {
    "incus": {
      "command": "uvx",
      "args": ["--refresh", "--extra-index-url", "https://nikitatsym.github.io/incus-mcp/simple", "incus-mcp"],
      "env": {
        "INCUS_URL": "https://incus.example.com:8443",
        "INCUS_CLIENT_CERT": "/path/to/incus-mcp.crt",
        "INCUS_CLIENT_KEY": "/path/to/incus-mcp.key"
      }
    }
  }
}

Where to paste

  • Claude Code: ~/.claude.jsonmcpServers
  • Claude Desktop: Settings → Developer → Edit Config
  • Cursor: Settings → MCP Servers

Or use the setup page to generate the config.

v2.5 features

  • Write verification. Every verifiable write is checked sent-vs-returned recursively: a silently dropped key (e.g. config.limits.cpu accepted then ignored by Incus) is reported as an error naming the full path instead of a phantom "201 Created".
  • operation='schema'. Any group returns a machine-readable JSON Schema for an operation: params={"op": "CreateInstance"}.
  • Richer help. operation='help' renders each parameter with its type, ? for optional, T | None for nullable, and a bullet with the parameter's description.
  • _UNSET semantics. Omitting a parameter differs from passing null: omitted params never reach the API; an explicit null clears a server-side value on PUT/PATCH.
  • Non-blocking waiters. operation_wait_start / operation_wait_poll / operation_wait_cancel (+ waits_list) poll a long-running Incus operation in the background instead of blocking the session; wait_operation stays for short one-shot waits.
  • Post-terminal verify on async writes. When an async write's operation finishes, the target resource is fetched and verified; a drop surfaces as verify_error on the wait handle (non-blocking) or as the wait_operation error result (blocking).

Groups

Group Operations Description
incus_read 87 Instances, images, networks, storage, profiles, projects, cluster, certificates, operations, warnings, resources, metrics, operation waiters
incus_write 75 Create/update instances, images, networks, storage, profiles, projects, cluster, certificates
incus_execute 9 Instance state changes (start/stop/restart/freeze), exec commands
incus_delete 34 Delete all resource types
incus_admin 4 Server config, warnings management

Each group is a single MCP tool. Call with operation="help" to list available operations, or pass operation="OperationName" with params={...}.

Development

uv run python dev.py check runs lint (ruff + mypy --strict) and the test suite - the same command the pre-commit hook and CI use. Install the hook once:

git config core.hooksPath .githooks

Integration smoke tests hit a real Incus server and are excluded from the default run (-m integration). Provision a throwaway Incus and run them:

  • Linux host or VM: sudo scripts/e2e-env.sh && (. .e2e/env && uv run python dev.py e2e). The script installs Incus, does a minimal incus admin init, pre-pulls the e2e-alpine image, mints a client cert, and writes .e2e/env (git-ignored) with the matching INCUS_* vars. Idempotent - re-runs are no-ops.
  • macOS: Incus has no macOS daemon, so run the same recipe inside a Linux VM: limactl start template://ubuntu-lts, then clone the repo and run the Linux recipe inside the VM. This is the environment CI uses.

CI runs the same smokes in a dedicated e2e job (.github/workflows/build.yml) that gates the release build.

Swagger conformance

tests/test_swagger_conformance.py reads every registered operation off its own AST and checks method, path, query-param names, body-field names and enum values against Incus's swagger spec - Incus ignores names it does not know, so a typo returns 200 and silently does nothing. It runs in the default gate against the copy of doc/rest-api.yaml vendored in tests/data/, since no running Incus serves that spec. Re-pin it to a newer Incus with:

uv run --with pyyaml python scripts/fetch-incus-spec.py v7.3.0

About

MCP server for Incus container and VM manager

Resources

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages