An AI co-pilot for your Linux fleet, shipped as a strictly-confined snap that physically cannot break anything.
FleetMind is a Model Context Protocol (MCP) server, written in Go and
distributed as a snap, that turns any Linux host into a structured,
read-only observation surface an LLM agent can interrogate: hardware,
processes, mounts, network, sensors, kernel, systemd, boot timings, and the
journal. The snap attaches only *-observe interfaces — no *-control
plug, no shell, no writable host paths outside $SNAP_COMMON — so the
security boundary is the snap manifest itself, enforced by AppArmor and the
kernel, not by prompt engineering. Even a compromised or prompt-injected
agent has no path to mutate the host, and installing on a new machine is a
single snap install away.
Why it's interesting
- Snap-confined, kernel-enforced. Strict confinement with only
*-observeplugs; no*-controlplug, no shell, no writable host paths. The threat model lives insnap/snapcraft.yamland is enforced by AppArmor, not by your system prompt — an exploited agent simply cannot escalate. - Fleet-aware. Run FleetMind on every node and they form a full-mesh
cluster via explicit, kubeadm-style join. An agent connected to any single
node can fan out tool calls across the whole fleet with
fleet_query("which host has the failing NVMe?", "show me the slowest boot in the cluster") and watch membership change live over SSE. - Batteries-included UI. Open
http://127.0.0.1:8765/ui/and you get a zero-dependency operator console: a live fleet roster on one side, a chat panel on the other, wired straight to Anthropic, OpenAI, or any OpenAI-compatible endpoint. Your API key stays in the browser — the fleetmind daemon never sees it.
Under the hood, the daemon speaks the Streamable HTTP MCP transport
on 127.0.0.1:8765 (port configurable) behind bearer-token auth, and every
external binary it shells out to (lsblk, ss, journalctl, dmesg) runs
through a hardened wrapper with fixed argv, LC_ALL=C, a 10-second timeout
and a 4 MiB output cap.
| Tool | What it returns |
|---|---|
system_info |
Hostname, kernel, architecture, /etc/os-release, uptime |
cpu_info |
Model, vendor, logical cores, flags, frequency bounds |
memory_info |
RAM and swap usage, full /proc/meminfo |
load_info |
1/5/15-minute load averages, running/total tasks |
list_processes, get_process |
Process snapshots from /proc/[pid] |
list_block_devices |
lsblk -J -O output |
list_mounts |
Mount table + statfs sizes |
list_network_interfaces, list_sockets |
Interface table; ss-derived socket inventory |
list_pci_devices, list_usb_devices |
Device IDs and bound kernel drivers |
kernel_info, list_kernel_modules |
Kernel build, cmdline, loaded modules |
list_dmi, list_sensors |
SMBIOS/DMI strings; hwmon temperatures, voltages, fans |
read_journal, read_dmesg |
Recent journald and kernel ring-buffer entries. read_journal supports boot:-N for previous-boot reads and match for server-side regex grep. |
boot_time, boot_blame, boot_critical_chain |
Per-phase boot timings, per-unit init time, and the critical chain (slowest predecessor at each After= hop) — read directly from org.freedesktop.systemd1 over the system D-Bus |
list_systemd_units, unit_status, list_timers |
systemd unit inventory (with state/type filters), per-unit property bag + journal tail, and all timer units with their next/last elapse timestamps |
list_fleet |
Every MCP server the local node sees in its fleet (fleet mode) |
fleet_query |
Fan-out a tool call to every node in the fleet (including self) and return per-node results (fleet mode) |
The six systemd-aware tools talk to org.freedesktop.systemd1 via the
well-known socket at /run/dbus/system_bus_socket (permitted by the base
AppArmor abstraction). Shelling out to systemd-analyze / systemctl would
not work under strict confinement because they bind abstract sockets outside
the snap's namespace; the direct-D-Bus approach sidesteps that entirely.
Requires Go ≥ 1.25.
make tidy build test lint
./bin/fleetmind --helpRun the daemon directly (outside a snap) with an explicit token:
FLEETMIND_TOKEN=devtoken ./bin/fleetmind --port 8765Smoke-test the MCP endpoint:
curl -s -H 'Authorization: Bearer devtoken' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
http://127.0.0.1:8765/mcp | headThe fleetmind binary ships a small operator console at
http://127.0.0.1:8765/ui/. It is a zero-dependency static SPA embedded via
go:embed; opening it in a browser gives you:
- a live view of the fleet roster, driven by the
/fleet/eventsSSE stream; - a chat panel that connects to the LLM provider of your choice (Anthropic, OpenAI, or any OpenAI-compatible base URL) and uses the FleetMind MCP tools to investigate the host you ask about.
The bearer token and LLM API key are kept in browser localStorage only — the
fleetmind server never sees the LLM key, and the LLM provider is called
directly from the browser.
Unit tests cover the /proc and /sys parsers, auth middleware, and helper
functions. Integration tests live in e2e/ and exercise the full MCP stack
(auth, transport, tool registry, and real host observability) on a live Linux
system.
# Fast unit tests
go test -race -count=1 ./...
# Integration tests (Linux only; starts an ephemeral in-process server)
go test -v -timeout=120s ./e2e/...See e2e/README.md for architecture details and instructions
for running inside an LXD system container.
make snap
sudo snap install --dangerous ./fleetmind_*.snap
# Connect non-auto-connect interfaces for log access:
sudo snap connect fleetmind:log-observe
sudo snap connect fleetmind:kernel-module-observe
# Retrieve the auto-generated bearer token:
sudo snap get fleetmind token
# Adjust the listen port if needed:
sudo snap set fleetmind port=9000
sudo snap restart fleetmindGeneric config block for any Streamable-HTTP-aware MCP client
(~/.claude.json, .mcp.json, Cursor mcp.json, etc.):
{
"mcpServers": {
"fleetmind": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp",
"headers": {
"Authorization": "Bearer <token from `snap get fleetmind token`>"
}
}
}
}Add with the CLI in one shot (project-scoped, writes to .mcp.json):
claude mcp add --transport http --scope project fleetmind \
http://127.0.0.1:8765/mcp \
--header "Authorization: Bearer $(sudo snap get fleetmind token)"Swap --scope project for --scope user to register it globally.
Run /mcp inside Claude Code to verify the connection and inspect the
tool list.
Every FleetMind tool is read-only by snap confinement, so it is safe to
pre-approve the whole server and skip per-call permission prompts. Add
to .claude/settings.json (project) or ~/.claude/settings.json
(global):
{
"permissions": {
"allow": ["mcp__fleetmind"]
}
}mcp__fleetmind covers every current and future tool on this server.
For a tighter grant, list specific tools: mcp__fleetmind__read_journal.
Drop a paragraph into your project or user CLAUDE.md so Claude
prefers these tools over shelling out. Example:
## FleetMind MCP
When the user asks about the local Linux host (hardware, processes,
mounts, network, logs, sensors, kernel), prefer the fleetmind MCP
tools over running shell commands. They are strictly read-only.
- "what's running?" → list_processes
- "why is disk full?" → list_mounts, list_block_devices
- "any kernel errors lately?" → read_dmesg, read_journal {priority:"err"}
- "what hardware is this?" → list_dmi, list_pci_devices, cpu_infoFor recurring workflows, bundle them into a slash command — e.g.
.claude/commands/host-audit.md that asks the agent to run a fixed
sequence of FleetMind tools and format a report.
Multiple FleetMind instances can form a static, full-mesh fleet so an LLM agent connected to any one node can enumerate every other MCP server it should also talk to. Discovery is explicit (kubeadm-style): you tell a new node the URL of an existing one, the new node fetches the roster, then connects to every member.
CLI flags (all also readable from snap config — keys fleet, join-url,
advertise-url):
| Flag | Meaning |
|---|---|
--fleet |
Enable fleet mode (mounts /fleet/* and runs the peer manager) |
--join-url <url> |
Bootstrap: URL of any existing fleet node. Empty = solo fleet |
--advertise-url <url> |
URL other peers should dial to reach this node. Defaults to http://<bind>:<port> |
--bind <ip> |
Interface to bind on. 127.0.0.1 for same-host fleets; a routable IP for cross-host |
Wire endpoints (all under the shared bearer token):
POST /fleet/join— body{"peer": {...}}; response{"peers": [...]}GET /fleet/peers— current roster as JSONGET /fleet/events— Server-Sent Events stream:heartbeat,peer_added,peer_removed. The current roster is replayed once on subscribe.
Heartbeats fire every 10 s; peers with no heartbeat for 30 s are evicted and a
peer_removed event is broadcast.
Inspect the fleet from any node via the new MCP tool:
curl -s -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"list_fleet","arguments":{}}}' \
http://127.0.0.1:8765/mcp# All three nodes share the same bearer token.
export FLEETMIND_TOKEN=devtoken
# Seed node (no --join-url).
./bin/fleetmind --fleet --port 8765 --advertise-url http://127.0.0.1:8765 &
# Two more nodes bootstrap from the seed.
./bin/fleetmind --fleet --port 8766 \
--advertise-url http://127.0.0.1:8766 \
--join-url http://127.0.0.1:8765 &
./bin/fleetmind --fleet --port 8767 \
--advertise-url http://127.0.0.1:8767 \
--join-url http://127.0.0.1:8765 &list_fleet on any of the three will then return all three members.
For a realistic, snap-confined test across machines without leaving your laptop,
scripts/fleet-up-lxd.sh automates the whole flow: build the snap, launch two
Ubuntu LXD VMs, install the snap in each, configure one as the seed and the
other as a joiner with a shared bearer token, then print the
claude mcp add command pre-filled with the seed's bridge IP and token.
scripts/fleet-up-lxd.sh # bring it up
scripts/fleet-down-lxd.sh # tear it downRequires lxc, snapcraft, jq, curl and openssl on PATH, plus the
in-flight bind-via-snap-config support (see the script's header for the
issue link).
sudo snap set fleetmind fleet=true
sudo snap set fleetmind advertise-url=http://<this-host>:8765
sudo snap set fleetmind join-url=http://<seed-host>:8765 # omit on the seed
sudo snap restart fleetmind- Strict confinement plus a curated plug list (
hardware-observe,system-observe,mount-observe,network-observe,log-observe,kernel-module-observe,network-bind,network) is the safety boundary. The snap has no*-controlplug, no shell, no writeable paths outside$SNAP_COMMONand$SNAP_DATA. Thenetworkplug is required so the daemon can dial other fleet members; it does not widen what the read-only tools can do. - Bearer-token auth gates both the MCP and
/fleet/*endpoints. The same token doubles as the shared fleet secret. It is generated by the daemon on first start (32 bytes fromcrypto/rand), persisted viasnapctl set token=…, and mirrored to$SNAP_COMMON/token(0600). - The listener binds
127.0.0.1by default. In fleet mode you may bind to a routable IP via--bindand supply an explicit--advertise-url; every peer URL is dialled with the bearer token. The MCP SDK adds DNS-rebinding protection out of the box. - External binaries (
lsblk,ss,journalctl,dmesg) are invoked through a small wrapper that pinsargv[0], setsLC_ALL=C, applies a 10-second timeout and caps stdout at 4 MiB.
cmd/fleetmind program entrypoint
e2e/ integration tests (MCP client/server round-trip)
internal/mcpserver server wiring, bearer auth, token bootstrap
internal/fleet peer registry, /fleet/* HTTP handlers, SSE manager
internal/tools one file per MCP tool, plus the registry
internal/exectool safe wrapper around os/exec
internal/procfs /proc parsers
internal/sysfs /sys parsers
internal/snapconf snapctl get/set wrapper
snap/ snapcraft.yaml and hooks
