Skip to main content
Glama
padlik
by padlik

nmcli-mcp-server

MCP server exposing NetworkManager VPN operations (nmcli) as semantic, allowlisted tools.

Instead of giving an LLM agent arbitrary shell access, this server exposes seven fixed tools — vpn_connect("somevpn") instead of raw nmcli. All subprocess execution uses fixed-argument create_subprocess_exec (never a shell), and the only LLM-controlled input anywhere is a VPN id from a config-defined set.

Requires Linux with NetworkManager (nmcli) and iproute2 (ip).

Installation

git clone <repo-url> nmcli-mcp-server
cd nmcli-mcp-server
uv sync

No daemon, port, or systemd unit exists: the MCP client owns the server process lifecycle and stdio is the only transport (ADR-0004).

Related MCP server: MCP VPS Manager

MCP client configuration

Production (Linux host):

{
  "mcpServers": {
    "nmcli": {
      "command": "uv",
      "args": ["--directory", "/opt/nmcli-mcp-server", "run", "nmcli-mcp"]
    }
  }
}

Lima VM (from a macOS host — limactl relays stdio over SSH; the server runs inside the VM, unchanged):

{
  "mcpServers": {
    "nmcli": {
      "command": "limactl",
      "args": [
        "shell",
        "nsjail-test",
        "--",
        "uv",
        "--directory",
        "/home/paulpronko.guest/nmcli-mcp-server",
        "run",
        "nmcli-mcp"
      ]
    }
  }
}

TOML config reference

Resolved at startup, in order (first match wins):

  1. $NMCLI_MCP_CONFIG

  2. $XDG_CONFIG_HOME/nmcli-mcp-server/config.toml

  3. ~/.config/nmcli-mcp-server/config.toml

[[vpn]]
id = "somevpn"                 # tool-facing id; must match ^[a-z][a-z0-9-]*$, unique
connection = "Somecomp VPN"     # exact NetworkManager connection name
expected_routes = [             # optional; strict CIDRs (host bits rejected)
  "10.13.0.0/16",
  "10.12.0.0/16",
  "fd00:abcd::/64",
]

The config is loaded and validated once at startup. A missing file (all candidate paths listed in the error), an unreadable/unparseable file, an invalid or duplicate id, an empty connection, or a non-strict CIDR (e.g. 10.8.0.5/24) fails startup immediately. The NMCLI_MCP_CONFIG override exists primarily for tests; production should use exactly one XDG path.

VPN profiles are expected to be NetworkManager system connections with stored secrets (non-tty nmcli fails fast rather than prompting for them).

Tools

Tool

Kind

Behavior

vpn_list()

read-only

Configured ids and connection names (no subprocess).

vpn_status(name)

read-only

Active state and bound device for the connection.

vpn_diagnose(name)

read-only

Local usability check chain (see below).

network_status()

read-only

One row per NetworkManager device.

vpn_connect(name)

mutating

Idempotent: no-op when already active, else nmcli connection up + re-verify.

vpn_disconnect(name)

mutating

nmcli connection down + verified post-state.

vpn_reconnect(name)

mutating

Health-aware: skips work when already usable, else disconnect → connect → re-diagnose.

name must be an id from the config; an unknown id returns a structured error listing the available ids. Every tool returns structured JSON: ok: true with observations, or ok: false with an error kind and details (subprocess non-zero exits and timeouts included — a timed-out call returns a structured timeout error, never a hang). All logging goes to stderr; stdout carries only MCP protocol messages.

Semantics of usable

vpn_diagnose verifies local usability only (ADR-0003): NetworkManager responsive, profile exists, connection active, a tunnel device is bound, and every expected_routes CIDR resolves through that device per ip route get on a probe derived from the CIDR (first host address; the network address itself for /31/32). usable: true requires all checks to pass.

Locally routed ≠ end-to-end reachable. usable: true means the kernel would send traffic for the expected routes into the tunnel. It does not prove the peer is alive or any endpoint answers: a dead peer or firewalled endpoint still reports usable. Agents must compose vpn_diagnose with their own reachability checks before concluding a VPN works.

Integration testing in the Lima VM

The test rig lives in scripts/ (re-runnable):

./scripts/lima_setup.sh     # NetworkManager + WireGuard; NM pinned unmanaged on eth0
./scripts/lima_fixture.sh   # fake WireGuard "Test VPN" + fixture config.toml

lima_setup.sh configures NetworkManager to not manage the VM's default interface (eth0 stays on systemd-networkd, so Lima connectivity survives) and installs a polkit rule allowing netdev members to control networking over SSH sessions. lima_fixture.sh recreates the "Test VPN" connection carrying the expected routes and writes the fixture config under the VM user's home.

Integration tests run inside the VM:

limactl shell nsjail-test -- bash -lc '
  cd ~/nmcli-mcp-server &&
  ~/.local/bin/uv run pytest -m integration'

The suite expects NMCLI_MCP_CONFIG to point at the fixture config (~/nmcli-mcp-fixture/config.toml is the default written by lima_fixture.sh).

Development

uv run pytest tests/unit -q   # unit tests (mocked exec) run anywhere
uv run ruff check .
uv run vulture . vulture_whitelist.py --min-confidence 80

Python >= 3.10 (stdlib tomllib on 3.11+, tomli below). Architecture: server.py (MCP layer) → vpn.py (orchestration) → nmcli.py (sole nmcli/ip adapter) + routing.py (route-probe math) + config.py (TOML profiles).

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    F
    maintenance
    Enables AI agents to manage OpenWRT routers remotely via SSH, supporting system monitoring, network management, OpenThread Border Router configuration, and package management through natural language commands.
    19
    16
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables LLMs to securely manage Virtual Private Servers via SSH, with features including command execution, file operations, system monitoring, and service management.
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to debug local networks in plain language by pinging, sweeping subnets, tracing routes, resolving DNS, scanning ports, inspecting ARP tables and active connections, and checking URL reachability.
    MIT