README
¶
BalenaMCP
A Model Context Protocol server that wraps the balena CLI so MCP-aware agents (Claude Code, Claude Desktop, Cursor, Continue, Cline, Goose, etc.) can list fleets, inspect devices, manage tags and env vars, pin releases, reboot devices, and more.
This is a personal fork of klutchell/balenamcp
brought up to date with the current balena CLI and the current mark3labs/mcp-go.
Prerequisites
balenaCLI onPATH- An MCP-aware agent or IDE (Claude Code, Claude Desktop, Cursor, …)
- Go 1.23+ (only if you're building from source — pre-built binaries don't require it)
Install
Three options, pick whichever fits.
1. Pre-built binary from GitHub Releases (recommended)
Grab the archive for your OS/arch from Releases:
| OS | Arch | Archive |
|---|---|---|
| Linux | x86_64 | balenamcp_<version>_Linux_x86_64.tar.gz |
| Linux | arm64 | balenamcp_<version>_Linux_arm64.tar.gz |
| macOS | x86_64 (Intel) | balenamcp_<version>_Darwin_x86_64.tar.gz |
| macOS | arm64 (Apple Silicon) | balenamcp_<version>_Darwin_arm64.tar.gz |
| Windows | x86_64 | balenamcp_<version>_Windows_x86_64.zip |
Each release also publishes:
checksums.txt— SHA-256 of every artifactchecksums.txt.sigstore.json— cosign signature overchecksums.txt<archive>.sbom.cdx.json— CycloneDX Software Bill of Materials per archive<archive>.sbom.cdx.json.sigstore.json— cosign signature over each SBOM
Verifying a download (recommended)
The release is signed with cosign using Sigstore keyless signing — no public-key juggling needed. Install cosign, then:
# 1. Verify the signature on checksums.txt
cosign verify-blob \
--bundle checksums.txt.sigstore.json \
--certificate-identity-regexp \
'https://github.com/schubydoo/balenamcp/.github/workflows/release.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
checksums.txt
# 2. Verify your archive against the (now-trusted) checksums file
sha256sum --check checksums.txt --ignore-missing
If both commands succeed, you've cryptographically verified that the archive was built by this repo's release workflow (not a typo-squatter, not tampered with in transit).
Extract and install
tar -xzf balenamcp_<version>_Linux_x86_64.tar.gz
sudo install balenamcp /usr/local/bin/
balenamcp --help # or just `balenamcp` to start serving over stdio
2. go install (Go developers)
go install github.com/schubydoo/balenamcp@latest
Resolves to the highest semver tag, builds locally, and drops the binary
in $GOBIN (default $GOPATH/bin or ~/go/bin). For docs and package
info: https://pkg.go.dev/github.com/schubydoo/balenamcp.
@latest follows the most recent release; pin a specific version with
go install github.com/schubydoo/balenamcp@v0.1.0 if you'd rather not
auto-update.
3. Build from source
git clone https://github.com/schubydoo/balenamcp.git
cd balenamcp
go mod download
go build -o bin/balenamcp
Cross-compile examples:
GOOS=linux GOARCH=amd64 go build -o bin/balenamcp-linux-amd64
GOOS=windows GOARCH=amd64 go build -o bin/balenamcp-windows-amd64.exe
GOOS=darwin GOARCH=arm64 go build -o bin/balenamcp-darwin-arm64
For a versioned source build (matches release builds):
go build -ldflags='-s -w -X github.com/schubydoo/balenamcp/server.Version=v0.1.0' \
-trimpath -o bin/balenamcp .
Without the -X ldflag, the server reports dev as its version.
A -dry-run flag is available — the server prints the balena command it
would run instead of executing it. Useful for testing/debugging.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
BALENAMCP_EXEC_TIMEOUT |
60 (seconds) |
Wall-clock cap on any single balena CLI subprocess. Prevents device-logs --tail and similar long-running commands from blocking the MCP transport indefinitely. Set to a higher integer for slow networks; the server logs a warning and falls back to default if the value is non-positive or non-numeric. |
BALENAMCP_REQUIRE_CONFIRM |
unset (off) | When set to 1/true, every destructive tool refuses to run unless the call carries confirm: true in its arguments. A belt-and-suspenders safety net for MCP clients that ignore the destructiveHint annotation. Off by default — Claude Desktop and other compliant clients already prompt before invoking destructive tools, so the gate is redundant there. |
Authenticate
Before using any tool that touches balenaCloud, log in once on the host:
balena login
The MCP server inherits that session by shelling out to the same balena
binary.
MCP client setup
The server speaks stdio JSON-RPC — any MCP-compliant client wires it up the same way: point at the binary, optionally pass args, restart the client. Specifics below.
Claude Code
CLI (recommended):
claude mcp add balena /absolute/path/to/balenamcp
That edits ~/.claude.json for you. Use claude mcp add balena --scope project ...
to scope it to the current repo (.mcp.json in repo root) instead.
Or edit ~/.claude.json by hand:
{
"mcpServers": {
"balena": {
"command": "/absolute/path/to/balenamcp",
"args": []
}
}
}
The tools appear in the next Claude Code session. Run /mcp to confirm.
Claude Desktop
Add to claude_desktop_config.json:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux* | ~/.config/Claude/claude_desktop_config.json |
* Claude Desktop on Linux is unofficial; the path mirrors the XDG location that community builds use.
{
"mcpServers": {
"balena": {
"command": "/absolute/path/to/balenamcp",
"args": []
}
}
}
Restart Claude Desktop. The tools appear under the balena server.
Other MCP clients
Setup is similar — point the client at the binary as a stdio MCP server. Consult each tool's docs for the exact config file / command:
- Cursor — Settings → MCP → Add server, or
~/.cursor/mcp.json - Continue —
mcpServersblock in~/.continue/config.yaml - Cline — VS Code settings under
cline.mcpServers - Goose —
goose configure(interactive) or~/.config/goose/config.yaml - LibreChat —
mcpServers:inlibrechat.yaml
If your client supports MCP via stdio, the binary will work — there's nothing balenamcp-specific about the wiring.
Tools
⚠️ Destructive tools — read this first
12 of the 29 tools change state on real devices or in balenaCloud. A reboot or
device-purgecan't be undone from inside the model. Every destructive tool is flagged withdestructiveHint: truein its MCP annotation, and Claude Desktop (and other compliant MCP clients) prompts you for confirmation before running them.
Tool Effect Reversible? device-rebootRemote reboot yes (device comes back up) device-restartRestart containers (no reboot) yes device-shutdownRemote shutdown — manual power cycle to recover requires physical access device-purgeWipe /dataon the deviceno — data is gone device-pinPin a device to a specific release yes ( device-track-fleetor re-pin)device-track-fleetDrop a device's pin and resume tracking the fleet's release yes ( device-pinagain)fleet-pinPin a fleet to a specific release yes release-finalizePromote a draft release to final no — finals can't be un-finalized tag-setCreate or update a tag yes ( tag-rm)tag-rmRemove a tag yes ( tag-set)env-setSet/update an env or config variable yes ( env-rmorenv-setagain)env-rmDelete an env or config variable (needs yes: trueto bypass the CLI's confirm prompt)yes ( env-setagain)Belt-and-suspenders gate: set
BALENAMCP_REQUIRE_CONFIRM=1and every destructive tool will refuse to run unless the call carriesconfirm: truein its arguments. Useful with MCP clients that don't honordestructiveHint.
Read-only
The remaining 17 tools are read-only — they shell out to balena with no state change. Safe to call without confirmation.
| Tool | Purpose |
|---|---|
version |
balena CLI version |
whoami |
Current user / org / device session info |
fleet-list |
List all accessible fleets |
fleet-info |
Detailed info for one fleet |
device-list |
List devices, optionally filtered by fleet |
device-info |
Detailed info for one device |
device-logs |
Recent historical logs from a device, optionally per-service. Streaming (--tail) is not supported over MCP — run the balena CLI directly for continuous monitoring. Note: this tool's identifier arg is named device (accepts UUID, IP, or .local address) while every other device-* tool uses uuid. Kept this way to match the broader argument shape balena's CLI accepts here. |
device-type-list |
Supported balena device types |
release-list |
Releases of a fleet |
release-info |
Metadata or composition of one release |
release-asset-list |
Binary assets attached to a release |
tag-list |
Tags on a fleet or device or release |
env-list |
Env/config variables on a fleet or device, optionally per-service |
os-versions |
Available balenaOS versions for a device type |
organization-list |
Organizations the user belongs to |
ssh-key-list |
SSH keys registered in balenaCloud |
api-key-list |
balenaCloud API keys |
Argument constraints
tag-list / tag-set / tag-rm require exactly one of
fleet / device / release. env-list / env-set require exactly one
of fleet / device.
Development
go build -o bin/balenamcp # build
go test ./... # unit tests (in-process MCP client, dry-run)
For a live end-to-end sweep against real balenaCloud (requires balena login
and a sacrificial device):
BALENA_LIVE_FLEET=myorg/myfleet \
BALENA_LIVE_DEVICE=<uuid> \
BALENA_LIVE_RELEASE=<commit> \
BALENA_LIVE_RELEASE_ALT=<other-commit> \
go test -tags=integration -v -count=1 -run TestLiveSweep .
Irreversible sub-tests (device-purge, device-shutdown, release-finalize)
gate on additional BALENA_LIVE_ALLOW_* opt-in env vars and skip by default.
Layout:
main.go— entry point, flag parsing, stdio transportserver/setup.go— all tool definitionsmain_test.go— in-process MCP client driving every tool in dry-run modelivetest_test.go— build-tagged (integration) end-to-end sweep against real balenaCloud, opt-in via env vars
Release flow
Releases are automated via release-please
- Commits to
mainuse Conventional Commits (feat:,fix:,perf:,chore:,docs:, etc.). The PR title gate enforces this on every PR. - release-please watches
mainand opens a "chore(main): release vX.Y.Z" PR that bumps.release-please-manifest.jsonand rewritesCHANGELOG.mdbased on the conventional-commit history since the last tag. - Merging that PR pushes a
vX.Y.Ztag. - goreleaser (in
release.yml) fires on the tag push: cross-compiles for 5 targets, generates CycloneDX SBOMs via syft, signschecksums.txtand SBOMs with cosign keyless, uploads everything to the GitHub Release release-please already created.
End-to-end, a release looks like:
$ git commit -m "feat: add foo tool" # conventional commit
$ git push # to a branch + PR + merge
# ... release-please opens release PR ...
$ # merge the release PR ...
$ # ... goreleaser publishes binaries + SBOMs + signatures to GH Releases
Troubleshooting
Where to find the server's logs
Logs land wherever the MCP host writes them:
| Client | Path |
|---|---|
| Claude Desktop, macOS | ~/Library/Logs/Claude/mcp-server-balena.log |
| Claude Desktop, Windows | %APPDATA%\Claude\logs\mcp-server-balena.log |
| Claude Desktop, Linux | ~/.config/Claude/logs/mcp-server-balena.log |
| Claude Code | claude --mcp-debug enables verbose MCP logging to the current session; persistent logs live in ~/.claude/projects/<encoded-cwd>/ |
| Cursor / Continue / others | Check each client's own log dir; the server itself writes only its startup line to stderr |
Common errors
balena CLI error: exec: not found— thebalenabinary isn't onPATHfor whatever user the MCP host runs the server as. For desktop apps that's the GUI'sPATH, not your shell's. Add a symlink to/usr/local/bin/balenaor set the absolute path in the client's MCP config underenv/command.- Auth errors —
balena loginagain on the host where the server runs. We need to slow down your requests temporarily.— balenaCloud rate-limits fast sequential calls. The server doesn't queue or back off; it surfaces the message as the tool result. Wait a few seconds and retry, or pace the agent's calls. Most noticeable when sweeping many tools in a row (e.g. validation runs).- Tool annotated as destructive but the client didn't prompt — set
BALENAMCP_REQUIRE_CONFIRM=1in the server's env (via the client's MCPenv:block) so the server itself refuses destructive calls withoutconfirm: truein the arguments. Documented above.
License
MIT
Documentation
¶
There is no documentation for this package.