balenamcp

command module
v1.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 6 Imported by: 0

README

BalenaMCP

CI Security OpenSSF Scorecard codecov Go Report Card Go Reference Latest Release Renovate Conventional Commits

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.

It exposes all three MCP primitives: tools (direct balena actions), prompts (guided multi-step workflows like canary rollouts and device diagnosis), and resources (read-only fleet/device/release state you attach as context). See the Tools, Prompts, and Resources sections below.

This is a personal fork of the original klutchell/balenamcp (since transferred to balena-io-experimental/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.25+ (only if you're building from source — pre-built binaries don't require it; go.mod pins the exact toolchain)

Install

Three options, pick whichever fits.

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

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@v1.2.2 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_ASSET_DIR unset (filesystem tools disabled) The single directory balenamcp may read from or write to on the host. Unset — the default — makes release-asset-download, release-asset-upload and ssh-key-add refuse to run at all; every other tool is a pure cloud call and is unaffected. When set, those tools take paths relative to this directory, and absolute paths, .. traversal and symlinks pointing outside it are rejected. Set it to a dedicated directory, not $HOME.
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
  • ContinuemcpServers block in ~/.continue/config.yaml
  • Cline — VS Code settings under cline.mcpServers
  • Goosegoose configure (interactive) or ~/.config/goose/config.yaml
  • LibreChatmcpServers: in librechat.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

42 of the 64 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; one device per call) yes
device-stop-service Stop a service container and leave it stopped yes (device-start-service)
device-start-service Start a stopped service container yes (device-stop-service)
device-shutdown Remote shutdown — manual power cycle to recover requires physical access
device-purge Wipe /data on the device (one device per call) no — data is gone
device-rm Permanently remove a device from balenaCloud (passes --yes) no
device-deactivate Release a device from its fleet. On paid plans this charges a one-month fee; free-tier accounts are not charged (passes --yes) yes (the device re-registers when it comes online)
device-move Move a device to another fleet yes (move it back)
device-register Register a new device with a fleet yes (device-rm)
device-os-update Host OS update. A takeover target re-partitions the disk, erasing all data no — takeover updates cannot be rolled back
device-ssh Run an arbitrary command on the device (host OS or a service container) depends on the command run
device-local-mode-set Enable/disable local mode (LAN dev access; suspends cloud updates) yes (toggle back)
device-public-url-set Expose a device's service on a public, unauthenticated URL (or disable it) yes (disable again)
device-rename Rename a device yes (rename again)
device-note Set a device's note, replacing any existing note no — the previous note is not kept
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 (fleet-track-latest or re-pin)
fleet-track-latest Drop a fleet's pin and resume tracking the latest release yes (fleet-pin again)
fleet-restart Restart containers on every device in a fleet yes
fleet-purge Wipe /data on every device in a fleet no — data is gone
fleet-rm Delete a fleet (passes --yes) no
fleet-create Create a new fleet yes (fleet-rm)
fleet-rename Rename a fleet yes (rename again)
release-finalize Promote a draft release to final no — finals can't be un-finalized
release-invalidate Mark a release invalid so it won't auto-deploy yes (release-validate)
release-validate Re-validate a previously invalidated release yes (release-invalidate)
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 (passes --yes) yes (env-set again)
env-rename Change an existing variable's value by numeric ID (despite the name) yes (set it back)
organization-create Create a new organization yes (organization-rm)
organization-rename Rename an organization yes (rename again)
organization-rm Delete an organization (passes --yes) no
api-key-revoke Revoke API key(s) by ID no
ssh-key-add Register an SSH public key on the account yes (ssh-key-rm)
ssh-key-rm Remove an SSH key (passes --yes). Removing the wrong one can lock a user out of their devices no — re-add the key from a local copy
release-asset-upload Upload a local file as a release asset yes (release-asset-delete)
release-asset-download Writes a file to the server's host filesystem yes (delete the file)
release-asset-delete Delete a release asset (passes --yes) no — the CLI documents this as impossible to undo

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 22 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
ssh-key-info One SSH key by its numeric ID
api-key-list balenaCloud API keys
device-detect Scan the local network (LAN) for balenaOS devices
device-local-mode-get Report whether local mode is enabled on a device
device-identify Blink a device's ACT LED so you can find the board on a shelf. Contacts the device but changes nothing — the LED stops on its own — so it needs no confirmation.
device-public-url Print a device's public URL, or with status: true report whether it is enabled. Turn it on or off with device-public-url-set.
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.

One device per call. The balena CLI accepts comma-separated lists on several device commands and acts on every element, which would let a single call reach an unbounded number of devices behind one confirmation — most sharply device purge, which wipes /data irreversibly. balenamcp rejects any value containing a comma on device-purge, device-restart, device-rm, device-deactivate, device-move, device-start-service and device-stop-service (on both the device and the service argument where both exist), and asks the agent to loop instead.

Host filesystem access is opt-in. release-asset-download, release-asset-upload and ssh-key-add are the only tools that read or write local files, and they refuse to run unless BALENAMCP_ASSET_DIR names a directory. All their paths are relative to it. Argv-slice construction stops shell injection but says nothing about path traversal, so the boundary is enforced explicitly: absolute paths and Windows volume names are refused, the joined path must stay inside the root after cleaning (rejecting ..), and it must still be inside after symlink resolution (rejecting a symlink planted in the root that points out of it). release-asset-delete touches no local files and works either way.

release-asset-download also requires output. The CLI would otherwise write to the server process's working directory, outside the configured root. An existing destination is an error unless overwrite: true, which stands in for the CLI's interactive overwrite prompt, which cannot be answered over MCP.

device-move requires fleet and device-rm / device-deactivate always pass --yes, so the server-side confirm gate (BALENAMCP_REQUIRE_CONFIRM) is the only confirmation layer on device removal. device-register takes device_type, which reaches the CLI as its camelCase --deviceType flag.

device-os-update requires version. Omitting it makes the CLI render an interactive version picker, and --include-draft is not exposed at all — upstream marks it mutually exclusive with --version, and when a version is given the CLI already derives draft support from the version string. Candidate versions come from os-versions (keyed by device type) and the device's current version from device-info; the CLI has no non-interactive way to list a specific device's update targets.

device-note requires its note text: the balena CLI documents that an omitted note is read from stdin, but the v25.2.5 implementation instead writes an empty string, silently clearing the note. The text also cannot start with -, because it reaches the CLI as a positional argument and would be parsed as a flag. device-rename requires new_name for the usual prompt-avoidance reason.

api-key-revoke is the sole exception: its CLI command is inherently list-shaped (balena api-key revoke 123,456) and its targets are credentials, not devices, so constraining it to one ID per call would buy no safety.

fleet-create requires organization and type, even though the balena CLI treats both as optional: the CLI falls back to an interactive dropdown when either is missing and the account has more than one candidate, and an interactive prompt over MCP cannot be answered — the call fails. The same reasoning makes new_name mandatory on fleet-rename. Use organization-list and device-type-list to discover valid values.

What balenamcp deliberately does not wrap

balenamcp wraps a subset of the balena CLI's ~90 commands. The gap is not an oversight backlog — the commands below are out of scope by decision, for the reasons given. This section exists so the question is settled once instead of being re-litigated at every CLI bump; the balena-cli-parity review should treat a command listed here as accounted for.

Group Commands Why it stays out
Build / deploy pipeline build, deploy, push, preload Need a local Docker daemon and a source tree. Long-running, and their streaming build output would blow past BALENAMCP_EXEC_TIMEOUT and flood the agent's context.
OS image / provisioning os configure, os download, os initialize, device init, local configure, local flash, util available-drives Need physical media or a downloaded image on the server host; local flash writes raw bytes to a block device. The useful read-only half of this group, os versions, is wrapped.
Device config files config generate, config inject, config read, config reconfigure, config write Operate on a mounted device or an image file, not the cloud API. Same host-hardware dependency as the group above.
Local network / SSH context join, leave, device tunnel join/leave reconfigure the local machine's balena environment. device tunnel opens local TCP ports and runs in the foreground indefinitely — the same reason device logs --tail is not exposed.
Auth login, logout balenamcp inherits the CLI's existing auth state (~/.balena/token) on purpose. Letting an agent change who the server is is a privilege-boundary problem, not a feature.
Third-party access support Grants balena support agents access to your devices and fleets for a duration. Technically a plain cloud call, but delegating access to a third party is a human decision.
CLI host diagnostics settings Prints local CLI configuration rather than balenaCloud state. Harmless and read-only — this is the cheapest entry here to reverse if a concrete need appears.
Credential minting api-key generate Prints a live, long-lived balenaCloud API key to stdout, and balenamcp returns stdout straight to the MCP client, where it may be persisted, summarized, or forwarded. Redacting it would leave a tool whose entire output is withheld. Generate API keys in the balenaCloud dashboard instead. api-key-list and api-key-revoke are wrapped, so an agent can still audit and revoke.

Standing rule for fleet-class variants. app create and block create have the same shape as fleet create and produce the other two fleet classes. Only the fleet form is wrapped: it is the class an agent operating a deployed estate actually manages, and mirroring every variant triples the surface for no new capability. Apply the same test to future variants — wrap the canonical form, not its aliases.

Prompts

Beyond tools, the server exposes MCP prompts — guided, multi-step workflows you invoke from your client (in Claude Desktop they appear in the prompt/slash picker). A prompt runs no balena commands itself; it returns a runbook that tells the model which tools to call and in what order, encoding the sequencing and safety ordering an experienced operator would follow. Destructive steps still go through the same destructiveHint / BALENAMCP_REQUIRE_CONFIRM guards as any other tool call.

Prompt Arguments What it walks the model through
diagnose-device uuid Pull status, logs, env, tags, and pin state for one device, then summarize a health verdict and likely root cause. Read-only.
fleet-health-report fleet Tally device status across a fleet, compare each device against the fleet's target release, flag what needs attention. Read-only.
safe-release-rollout fleet, release Canary-first rollout: record the rollback target, pin one device, verify it, then roll out fleet-wide — pausing for approval before each state change. Recovers via re-pin, release-invalidate, or fleet-track-latest.
rollback-device uuid Identify a previously known-good release and roll a single device back to it, after confirming the target; can release-invalidate the bad release.
audit-config fleet Compare device-level env/config variables against fleet defaults; surface drift, secret-shaped values (never echoed), and orphaned overrides. Read-only.
compare-releases release_a, release_b Diff two releases: per-service image-size deltas (how much bigger/smaller), composition changes, and asset differences. Read-only.
replicate-config source, target Copy env/config variables from one fleet/device to another, with a masked plan and an approval step before any write.
bulk-tag fleet, key, value (optional) Apply a tag to many devices in a fleet at once, with an approval step before any write.
deep-diagnose-device uuid Host-level diagnosis over device-ssh: memory, disk, load, failed services, and container state, then a resource-risk summary. Inspection commands only.
prepare-local-dev device (optional) Enable local mode for LAN development — discovering the device with device-detect first if not given — with an approval step.
rotate-api-keys fleet (optional) Review API keys and revoke the stale ones via api-key-revoke, with explicit approval before anything is revoked.

Resources

The server also exposes read-only balena state as MCP resources under the balena:// URI scheme. Where a tool is a single CLI call the model invokes, a resource is context you attach to the conversation — and each one composes several CLI calls into one JSON document, so a single read gives the model a coherent picture instead of forcing several separate tool calls. Composition degrades gracefully: if a sub-call fails (e.g. logs for an offline device) the document still returns the sections that succeeded and records the rest under an "errors" object with "partial": true.

Resource URI Type Aggregates
balena://account static whoami + organizations
balena://account/keys static registered SSH public keys + API key names (no secrets)
balena://gotchas static known balena CLI foot-guns + correct invocations (SSH one-shot, log streaming limits)
balena://fleets static all accessible fleets
balena://device-types static supported device types
balena://device/{uuid} template device status + recent logs + env/config + tags
balena://fleet/{org}/{fleet} template fleet metadata + devices + env/config + releases
balena://fleet/{org}/{fleet}/releases template the fleet's release history
balena://release/{id} template release metadata + docker-compose composition + assets
balena://os-versions/{type} template available balenaOS versions (stable + ESR + draft) for a device type

Fleet slugs are org/fleet, so the fleet templates take the two parts as separate path segments (e.g. balena://fleet/myorg/myfleet).

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 .

The device-purge and device-shutdown sub-tests gate on additional BALENA_LIVE_ALLOW_PURGE / BALENA_LIVE_ALLOW_SHUTDOWN opt-in env vars and skip by default; the release-finalize sub-test always runs but exercises only its error branch.

Layout:

  • main.go — entry point, flag parsing, stdio transport
  • server/setup.go — all tool definitions
  • server/prompts.go — guided workflow prompts
  • server/resources.gobalena:// resource documents
  • 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
Keeping up with the balena CLI

balenamcp shells out to whatever balena is on PATH, so upstream CLI changes (a renamed flag, a moved subcommand) can drift away from the argv our tools construct. The dry-run tests assert what we send, not what the current CLI accepts, so drift is caught two ways:

  • .balena-cli-version records the CLI version the wrapped command surface has been verified against. A Renovate customManager (defined in the shared renovate-config preset that renovate.json extends) watches the balena-cli npm package and opens a chore(deps) PR — with the upstream changelog attached — whenever a newer version ships. Review that PR for parameter drift and merge it only once the new version is confirmed clean; the merge is the record that it was reviewed.
  • For the actual review, the balena-cli-parity check compares the flags and subcommands we pass against the current CLI surface.
Coverage and test reporting

The hard gate is the coverage (>= 80%) job in .github/workflows/ci.yml, which enforces COVERAGE_MIN on a single Ubuntu cell and is a required check. It runs with -coverpkg=./... deliberately: the tests live in package main while most of the code lives in package server, so a per-package profile reads ~46% against a real ~96%.

Codecov sits alongside it for visibility only — PR comments, trend graphs and a per-surface breakdown of tools / prompts / resources, configured in codecov.yml. It is never a required check, and its uploads are non-blocking.

Test results are uploaded separately. The test job runs through gotestsum to emit a JUnit junit.xml per OS leg, tagged with a Codecov flag so Test Analytics can break flaky and slow tests out per platform. That is the axis this repo varies on — the BALENAMCP_ASSET_DIR path confinement resolves differently on macOS (/var/private/var) and Windows (8.3 short-name expansion), and both were caught that way. Both uploads run under !cancelled(), so a failing run still reports: gotestsum writes junit.xml before exiting non-zero, and a red suite is exactly when the analytics matter.

Release flow

Releases are automated via release-please

  1. Commits to main use Conventional Commits (feat:, fix:, perf:, chore:, docs:, etc.). The PR title gate enforces this on every PR.
  2. 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.
  3. Merging that PR pushes a vX.Y.Z tag.
  4. 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 writes its startup line, config warnings, and (in -dry-run) one line per call 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 errorsbalena 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

Documentation

Overview

Command balenamcp is a Model Context Protocol (MCP) server that wraps the balena CLI, letting MCP-aware AI assistants (Claude Desktop, Claude Code, Cursor, Cline, Continue, and others) drive balenaCloud through a structured tool interface instead of free-form shell invocation.

Usage

balenamcp [-dry-run]

The server speaks MCP over stdio. It is normally launched by an MCP client via that client's server-configuration mechanism (for example, "claude mcp add balena /usr/local/bin/balenamcp"), not run directly.

The -dry-run flag swaps real command execution for stubbed responses that report the argv that would have been invoked. Useful for tool development and CI smoke tests without a live balena login.

Prerequisites

The balena CLI must be installed and on PATH, and the invoking user must be authenticated (run "balena login" once). balenamcp shells out to the CLI for every operation; it does not talk to the balenaCloud API directly.

Tool surface

The server exposes 64 tools covering fleets, devices, releases, tags, env vars, organizations, SSH keys, API keys and release assets, plus 11 guided workflow prompts and read-only balena:// resources. 22 tools carry the MCP readOnlyHint and are safe to invoke without confirmation; 42 carry the destructiveHint so compliant clients can prompt the user before running them. The per-tool inventory lives in the README and in package server's documentation — this file deliberately does not repeat the list, which goes stale with every addition.

Every tool's identifier arguments are flag-shape guarded: arguments beginning with "-" are rejected server-side to prevent argv injection where an agent passes "--help" or similar as a UUID. Free-form values (tag values, env values) are intentionally exempt. List-accepting device commands are constrained to one device per call, and the tools that touch the host filesystem are confined to the directory named by BALENAMCP_ASSET_DIR (disabled entirely when unset).

Configuration

The balena CLI's own authentication state (typically ~/.balena/token) is inherited from the launching shell. Three environment variables tune the server itself:

  • BALENAMCP_EXEC_TIMEOUT — wall-clock cap in seconds for any single balena CLI subprocess (default 60).
  • BALENAMCP_REQUIRE_CONFIRM — when truthy, every destructive tool refuses to run unless the call carries confirm:true in its arguments.
  • BALENAMCP_ASSET_DIR — the single directory the release-asset download/upload and ssh-key-add tools may read or write; unset (the default) disables those tools entirely.

See the README's environment-variable table for the full semantics.

Verifying a release

Release archives are signed via Sigstore cosign keyless signing and published with CycloneDX SBOMs. See the project README's "Install" section for the cosign verify-blob invocation.

Source

https://github.com/schubydoo/balenamcp

Directories

Path Synopsis
Package server implements the MCP server for balenamcp — its tools, prompts, and resources.
Package server implements the MCP server for balenamcp — its tools, prompts, and resources.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL