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
balena CLI on PATH
- 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 artifact
checksums.txt.sigstore.json — cosign signature over checksums.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 —
mcpServers block in ~/.continue/config.yaml
- Cline — VS Code settings under
cline.mcpServers
- Goose —
goose configure (interactive) or ~/.config/goose/config.yaml
- LibreChat —
mcpServers: in librechat.yaml
If your client supports MCP via stdio, the binary will work — there's
nothing balenamcp-specific about the wiring.
12 of the 29 tools change state on real devices or in balenaCloud. A
reboot or device-purge can't be undone from inside the model. Every
destructive tool is flagged with destructiveHint: true in its MCP
annotation, and Claude Desktop (and other compliant MCP clients) prompts
you for confirmation before running them.
| Tool |
Effect |
Reversible? |
device-reboot |
Remote reboot |
yes (device comes back up) |
device-restart |
Restart containers (no reboot) |
yes |
device-shutdown |
Remote shutdown — manual power cycle to recover |
requires physical access |
device-purge |
Wipe /data on the device |
no — data is gone |
device-pin |
Pin a device to a specific release |
yes (device-track-fleet or re-pin) |
device-track-fleet |
Drop a device's pin and resume tracking the fleet's release |
yes (device-pin again) |
fleet-pin |
Pin a fleet to a specific release |
yes |
release-finalize |
Promote a draft release to final |
no — finals can't be un-finalized |
tag-set |
Create or update a tag |
yes (tag-rm) |
tag-rm |
Remove a tag |
yes (tag-set) |
env-set |
Set/update an env or config variable |
yes (env-rm or env-set again) |
env-rm |
Delete an env or config variable (needs yes: true to bypass the CLI's confirm prompt) |
yes (env-set again) |
Belt-and-suspenders gate: set BALENAMCP_REQUIRE_CONFIRM=1 and every
destructive tool will refuse to run unless the call carries
confirm: true in its arguments. Useful with MCP clients that don't
honor destructiveHint.
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 transport
server/setup.go — all tool definitions
main_test.go — in-process MCP client driving every tool in dry-run mode
livetest_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
main use Conventional Commits
(feat:, fix:, perf:, chore:, docs:, etc.). The PR title gate
enforces this on every PR.
- release-please watches
main and opens a "chore(main): release vX.Y.Z"
PR that bumps .release-please-manifest.json and rewrites CHANGELOG.md
based on the conventional-commit history since the last tag.
- Merging that PR pushes a
vX.Y.Z tag.
- goreleaser (in
release.yml) fires on the tag push: cross-compiles
for 5 targets, generates CycloneDX SBOMs via syft, signs checksums.txt
and 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 — the balena binary isn't on
PATH for whatever user the MCP host runs the server as. For desktop
apps that's the GUI's PATH, not your shell's. Add a symlink to
/usr/local/bin/balena or set the absolute path in the client's MCP
config under env / command.
- Auth errors —
balena login again 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=1 in the server's env (via the client's MCP
env: block) so the server itself refuses destructive calls without
confirm: true in the arguments. Documented above.
License
MIT