plugins/lisa-expo-copilot/skills/maestro-mcp-setup/SKILL.md
Enable the Maestro CLI's built-in MCP server (`maestro mcp`, STDIO) for this machine, robustly. Detects the Maestro CLI and a usable Java runtime, installs/guides what is missing, and registers the MCP server at LOCAL (per-machine, uncommitted) scope with an absolute command path and injected JAVA_HOME/PATH so the spawn never dies on a non-login PATH. Use when an Expo project needs Maestro device-automation tools (list_devices, inspect_screen, take_screenshot, run flows). Never registers at project/committed scope — that reintroduces the fleet-wide "-32000" failure.
npx skillsauth add codyswanngt/lisa maestro-mcp-setupInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Wire the Maestro CLI's built-in MCP server (maestro mcp, STDIO) into the
current coding agent for this machine only, in a way that actually works.
The Maestro MCP server is intentionally NOT in the distributed plugin
.mcp.json. A static, committed entry always attempts to spawn, and coding
agents spawn stdio servers with a non-login PATH. So an always-on entry
fails visibly — Claude Code shows Failed to reconnect … -32000 every
session — on any machine missing the Maestro CLI or a resolvable Java
runtime, which is most of a typical fleet (CI runners, unattended QA/intake
agent boxes). That exact regression shipped once (expo plugin commit b12d5d3)
and was reverted (b1f3efd).
This skill fixes both failure modes without poisoning the fleet:
command
path plus injected JAVA_HOME / PATH env, so the server finds Java
regardless of the spawn's inherited PATH.Registration is always --scope local (per-machine, uncommitted). Never use
--scope project / a committed .mcp.json entry — that is the always-on form
that reds out every other machine.
MAESTRO_BIN="$(command -v maestro || true)"
[ -z "$MAESTRO_BIN" ] && [ -x "$HOME/.maestro/bin/maestro" ] && MAESTRO_BIN="$HOME/.maestro/bin/maestro"
echo "maestro: ${MAESTRO_BIN:-NOT FOUND}"
If not found, install it (official installer — confirm with the user first,
since it writes to ~/.maestro):
curl -fsSL "https://get.maestro.mobile.dev" | bash
MAESTRO_BIN="$HOME/.maestro/bin/maestro"
Resolve to an absolute path — a bare maestro in the registration relies on
the very PATH that is missing at spawn time.
Maestro needs a JDK (8+). Resolve JAVA_HOME honoring the project's version
manager — do not install a parallel global JDK when one is already managed:
# Prefer an already-active/managed Java before installing anything. Covers the
# common version managers (mise, asdf, SDKMAN) plus an ambient install.
if command -v mise >/dev/null 2>&1 && mise which java >/dev/null 2>&1; then
JAVA_BIN="$(mise which java)"
elif command -v asdf >/dev/null 2>&1 && asdf which java >/dev/null 2>&1; then
JAVA_BIN="$(asdf which java)"
elif [ -x "${SDKMAN_DIR:-$HOME/.sdkman}/candidates/java/current/bin/java" ]; then
# SDKMAN's `sdk` is a shell function absent in non-login shells; use its stable
# `candidates/java/current` symlink directly instead.
JAVA_BIN="${SDKMAN_DIR:-$HOME/.sdkman}/candidates/java/current/bin/java"
elif command -v java >/dev/null 2>&1; then
JAVA_BIN="$(command -v java)"
fi
echo "java: ${JAVA_BIN:-NOT FOUND}"
# Derive JAVA_HOME by asking the JVM itself — NOT `dirname "$JAVA_BIN"/..`, which
# is wrong when `java` is a mise/asdf shim or a symlink whose parent is not a JDK
# root. Fall back to the bin/.. parent only if the JVM query yields nothing usable.
if [ -n "$JAVA_BIN" ]; then
JAVA_HOME="$("$JAVA_BIN" -XshowSettings:properties -version 2>&1 \
| sed -n 's/^[[:space:]]*java\.home = //p' | head -n 1)"
{ [ -z "$JAVA_HOME" ] || [ ! -x "$JAVA_HOME/bin/java" ]; } &&
JAVA_HOME="$(cd "$(dirname "$JAVA_BIN")/.." && pwd)"
fi
If no Java is found: install through the project's version manager when present
(mise use -g java@21, asdf install java …, or sdk install java), or guide
the user to install a JDK. Prefer the managed path; only fall back to a global
install with explicit consent. Re-resolve JAVA_HOME afterward.
Build a PATH that puts the Maestro and Java bin dirs first, then register for
the current coding agent. $JAVA_BIN/$MAESTRO_BIN are absolute.
Claude Code:
# Refuse to register a broken server — both toolchain paths must resolve and be
# executable, or `claude mcp add` would create the exact -32000 entry this skill
# exists to avoid.
[ -n "$MAESTRO_BIN" ] && [ -x "$MAESTRO_BIN" ] || { echo "Maestro CLI unresolved — abort"; exit 1; }
[ -n "$JAVA_HOME" ] && [ -x "$JAVA_HOME/bin/java" ] || { echo "Java runtime unresolved — abort"; exit 1; }
# Use the real JDK bin (from JAVA_HOME), not the shim dir, on PATH.
JBIN_DIR="$JAVA_HOME/bin"; MBIN_DIR="$(dirname "$MAESTRO_BIN")"
claude mcp add --scope local maestro \
--env "JAVA_HOME=$JAVA_HOME" \
--env "PATH=$JBIN_DIR:$MBIN_DIR:$PATH" \
-- "$MAESTRO_BIN" mcp
Parity — same intent on other harnesses (local/per-machine scope, absolute
command, injected JAVA_HOME/PATH); translate the registration verb only:
codex mcp add maestro --env JAVA_HOME=… --env PATH=… -- "$MAESTRO_BIN" mcp (writes ~/.codex/config.toml; keep it out of the committed project config).~/.cursor/mcp.json (not the project .cursor/mcp.json) with command, args: ["mcp"], and env: { JAVA_HOME, PATH }.~/.config/opencode/opencode.json) mcp block with the absolute command and environment.env; Copilot uses type: "local" for stdio servers.In every case: local/user scope, never the committed project file.
claude mcp list # or the harness equivalent
Confirm maestro shows connected. If it still fails, the usual cause is
JAVA_HOME pointing at a JRE without bin/java, or a Maestro path that moved —
re-resolve both and re-register. Do not "fix" it by dropping to a bare
maestro mcp command; that reintroduces the PATH-less failure.
--scope project or editing
a committed .mcp.json, stop — that is the reverted fleet-wide regression.development
Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything.
tools
Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv.
tools
Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use.
tools
Route one unit of work to a remote execution surface. Reads the executionEnv parameter (local by default, codex-cloud or claude-web today), verifies the environment is provisioned and bound to this repository, submits a thin skill invocation, records the task identifier to .lisa/remote-dispatch.json, and exits without polling. Routing only — the remote runs the identical skill from the identical repository. Composable and inline: other skills invoke it via the Skill tool rather than users calling it directly.