Skip to content

Lifecycle

lifecycle

Launchd lifecycle management for the AXM MCP server.

find_binary()

Locate the axm-mcp binary, preferring ~/.local/bin.

Source code in packages/axm-mcp/src/axm_mcp/lifecycle.py
Python
def find_binary() -> Path:
    """Locate the ``axm-mcp`` binary, preferring ``~/.local/bin``."""
    if _GLOBAL_BIN.is_file():
        return _GLOBAL_BIN

    path = shutil.which("axm-mcp")
    if path is None:
        print("axm-mcp binary not found on PATH", file=sys.stderr)  # noqa: T201
        raise SystemExit(1)

    resolved = Path(path)
    if _is_under_protected_dir(resolved):
        print(  # noqa: T201
            f"Warning: binary is under a macOS-protected directory ({resolved}).\n"
            "launchd services may fail with PermissionError.\n"
            "Fix: run 'uv tool install axm-mcp' or grant Full Disk Access,\n"
            "or use 'axm-mcp install --binary <path>'.",
            file=sys.stderr,
        )
    return resolved

generate_plist(port=DEFAULT_PORT, *, binary=None)

Render the launchd plist with the current binary path.

Source code in packages/axm-mcp/src/axm_mcp/lifecycle.py
Python
def generate_plist(port: int = DEFAULT_PORT, *, binary: Path | None = None) -> str:
    """Render the launchd plist with the current binary path."""
    bin_path = binary or find_binary()
    return PLIST_TEMPLATE.format(
        bin_path=bin_path,
        port=port,
        log_dir=LOG_DIR,
    )

install(port=DEFAULT_PORT, *, binary=None)

Generate the plist, write it, and load it via launchctl.

Source code in packages/axm-mcp/src/axm_mcp/lifecycle.py
Python
def install(port: int = DEFAULT_PORT, *, binary: Path | None = None) -> None:
    """Generate the plist, write it, and load it via launchctl."""
    plist_content = generate_plist(port, binary=binary)

    LOG_DIR.mkdir(parents=True, exist_ok=True)
    PLIST_PATH.parent.mkdir(parents=True, exist_ok=True)
    PLIST_PATH.write_text(plist_content)

    uid = os.getuid()
    launchctl = shutil.which("launchctl") or "launchctl"
    try:
        subprocess.run(  # noqa: S603
            [launchctl, "bootstrap", f"gui/{uid}", str(PLIST_PATH)],
            check=True,
            capture_output=True,
            text=True,
        )
    except subprocess.CalledProcessError as exc:
        print(f"Failed to load service: {exc.stderr.strip()}", file=sys.stderr)  # noqa: T201
        raise SystemExit(1) from exc

    print(f"Service installed and loaded (port {port})")  # noqa: T201

uninstall()

Stop the service and remove the plist.

Source code in packages/axm-mcp/src/axm_mcp/lifecycle.py
Python
def uninstall() -> None:
    """Stop the service and remove the plist."""
    if not PLIST_PATH.exists():
        print("Service not installed", file=sys.stderr)  # noqa: T201
        raise SystemExit(1)

    uid = os.getuid()
    launchctl = shutil.which("launchctl") or "launchctl"
    try:
        subprocess.run(  # noqa: S603
            [launchctl, "bootout", f"gui/{uid}", str(PLIST_PATH)],
            check=True,
            capture_output=True,
            text=True,
        )
    except subprocess.CalledProcessError:
        pass  # Service may already be stopped

    PLIST_PATH.unlink(missing_ok=True)
    print("Service uninstalled")  # noqa: T201