Skip to content

Cli

cli

AXM MCP CLI — Lifecycle management for the MCP server.

Subcommands

serve Start the Streamable HTTP server. status Check whether the server is running. stop Send SIGTERM to the running server.

Running axm-mcp with no subcommand preserves backward-compatible stdio mode.

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

Install the MCP server as a launchd service.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
@app.command
def install(
    *,
    port: Annotated[int, cyclopts.Parameter(help="Server port.")] = DEFAULT_PORT,
    binary: Annotated[
        Path | None,
        cyclopts.Parameter(help="Explicit binary path for the plist."),
    ] = None,
) -> None:
    """Install the MCP server as a launchd service."""
    from axm_mcp import lifecycle

    lifecycle.install(port, binary=binary)

is_axm_mcp_process(pid)

Return True only if pid's command line identifies an axm-mcp server.

Guards against OS PID reuse: an existence probe (:func:is_process_alive) cannot tell our server apart from an unrelated process that inherited the same PID. We inspect the target's command line via ps (portable to macOS and Linux; no /proc dependency, no psutil) and require the axm-mcp marker. Any failure (process vanished, ps error, mismatch) yields False — we never send SIGTERM on an unconfirmed identity.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
def is_axm_mcp_process(pid: int) -> bool:
    """Return True only if *pid*'s command line identifies an axm-mcp server.

    Guards against OS PID reuse: an existence probe (:func:`is_process_alive`)
    cannot tell our server apart from an unrelated process that inherited the
    same PID. We inspect the target's command line via ``ps`` (portable to
    macOS and Linux; no ``/proc`` dependency, no ``psutil``) and require the
    ``axm-mcp`` marker. Any failure (process vanished, ``ps`` error, mismatch)
    yields False — we never send SIGTERM on an unconfirmed identity.
    """
    try:
        result = subprocess.run(  # noqa: S603
            ["ps", "-p", str(pid), "-o", "command="],  # noqa: S607
            capture_output=True,
            text=True,
            timeout=3,
            check=False,
        )
    except (OSError, subprocess.SubprocessError):
        return False
    if result.returncode != 0:
        return False
    return AXM_MCP_MARKER in result.stdout

is_process_alive(pid)

Check whether a process with pid is running.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
def is_process_alive(pid: int) -> bool:
    """Check whether a process with *pid* is running."""
    try:
        os.kill(pid, 0)
    except ProcessLookupError:
        return False
    except PermissionError:
        return True
    return True

main()

Entry point for axm-mcp command.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
def main() -> None:
    """Entry point for ``axm-mcp`` command."""
    app()

read_pid()

Read PID from file, returning None if absent or invalid.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
def read_pid() -> int | None:
    """Read PID from file, returning None if absent or invalid."""
    if not PID_FILE.exists():
        return None
    try:
        return int(PID_FILE.read_text().strip())
    except (ValueError, OSError):
        return None

remove_pid_file()

Remove PID file if it exists.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
def remove_pid_file() -> None:
    """Remove PID file if it exists."""
    PID_FILE.unlink(missing_ok=True)

serve(*, host='127.0.0.1', port=DEFAULT_PORT)

Start the MCP server with Streamable HTTP transport.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
@app.command
def serve(
    *,
    host: Annotated[str, cyclopts.Parameter(help="Bind address.")] = "127.0.0.1",
    port: Annotated[int, cyclopts.Parameter(help="Bind port.")] = DEFAULT_PORT,
) -> None:
    """Start the MCP server with Streamable HTTP transport."""
    from axm_mcp import server as _server

    write_pid(os.getpid())
    try:
        _server.serve(host=host, port=port)
    finally:
        remove_pid_file()

status(*, host='127.0.0.1', port=DEFAULT_PORT)

Check whether the MCP server is running.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
@app.command
def status(
    *,
    host: Annotated[str, cyclopts.Parameter(help="Server host.")] = "127.0.0.1",
    port: Annotated[int, cyclopts.Parameter(help="Server port.")] = DEFAULT_PORT,
) -> None:
    """Check whether the MCP server is running."""
    url = f"http://{host}:{port}/health"
    try:
        resp = httpx.get(url, timeout=3)
        if resp.status_code == httpx.codes.OK:
            data = resp.json()
            tools = data.get("tools_count", "?")
            print(f"Server running on {host}:{port} ({tools} tools)")  # noqa: T201
        else:
            print(f"Server responded with status {resp.status_code}", file=sys.stderr)  # noqa: T201
            raise SystemExit(1)
    except (httpx.ConnectError, httpx.ConnectTimeout) as err:
        print("Server not running", file=sys.stderr)  # noqa: T201
        raise SystemExit(1) from err

stop()

Stop the running MCP server.

Before sending SIGTERM, the target PID's identity is verified against the axm-mcp command-line marker. If the PID has been reused by an unrelated process (or vanished), the signal is NOT sent: the stale PID file is removed and the command exits non-zero.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
@app.command
def stop() -> None:
    """Stop the running MCP server.

    Before sending SIGTERM, the target PID's identity is verified against the
    ``axm-mcp`` command-line marker. If the PID has been reused by an unrelated
    process (or vanished), the signal is NOT sent: the stale PID file is
    removed and the command exits non-zero.
    """
    pid = read_pid()

    if pid is None:
        print("Server not running (no PID file)", file=sys.stderr)  # noqa: T201
        raise SystemExit(1)

    if not is_process_alive(pid):
        remove_pid_file()
        print("Server not running (stale PID file cleaned up)", file=sys.stderr)  # noqa: T201
        raise SystemExit(1)

    if not is_axm_mcp_process(pid):
        remove_pid_file()
        print(  # noqa: T201
            f"Refusing to stop: PID {pid} is not an axm-mcp process "
            "(reused or vanished); stale PID file cleaned up",
            file=sys.stderr,
        )
        raise SystemExit(1)

    os.kill(pid, signal.SIGTERM)
    remove_pid_file()
    print(f"Sent SIGTERM to server (PID {pid})")  # noqa: T201

uninstall()

Uninstall the launchd service.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
@app.command
def uninstall() -> None:
    """Uninstall the launchd service."""
    from axm_mcp import lifecycle

    lifecycle.uninstall()

write_pid(pid)

Write PID file, creating parent directory if needed.

Source code in packages/axm-mcp/src/axm_mcp/cli.py
Python
def write_pid(pid: int) -> None:
    """Write PID file, creating parent directory if needed."""
    PID_DIR.mkdir(parents=True, exist_ok=True)
    PID_FILE.write_text(str(pid))