logaritmiskandClaude Opus 5 516d7d0b86 fix(daemon): keep serving while a gated server waits
The autostart loop awaited each Start ack, and a gated start does not ack
until its wait-for condition resolves. Because the accept loop and the
signal handler are installed after that loop, a server gated on a socket
that is not there yet left the daemon deaf for the whole gate: `xy list`
hung in the backlog and SIGTERM went unhandled. Nothing consumed the ack,
so drop the await and let the gates run concurrently with the accept loop.

Also clear started_at before publishing Waiting. A crash-and-regate kept
the dead process's start instant, so `xy list` showed a growing uptime for
a server that had been down for minutes -- in the one state whose whole
purpose is to explain what is going on.

And log the spawn error when a wait-for command cannot be run at all, so a
misspelled binary is distinguishable from a condition that is merely not
met yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-07 16:17:20 +02:00

xy — HTTP MCP server supervisor

Daemon + CLI that launches and supervises HTTP-based MCP servers.

Build

cargo build --release

Run

target/release/xy daemon       # foreground

Drop a server definition into $XDG_CONFIG_HOME/xy/servers/<name>.kdl (see examples/insikt.kdl) and xy reload.

Commands:

xy list
xy status <name>
xy start  <name|--all>
xy stop   <name|--all>
xy restart <name|--all>
xy reload
xy logs <name> [--tail N] [--follow]

Exit codes: 0 success, 1 operational error, 2 daemon unreachable, 3 config invalid.

Waiting on a dependency

A server can declare a precondition that must hold before it is started:

wait-for {
    path "~/.orbstack/run/docker.sock"
    timeout "180s"
    interval "1s"
}

Exactly one condition per block — path "<p>" (the path exists), tcp "<host:port>" (a connection succeeds), or command "<c>" with optional args (the process exits 0). timeout defaults to 120s and interval to 1s.

While waiting the server reports state waiting, and xy status <name> shows which condition it is blocked on and for how long. If the timeout expires the server is marked failed and is never spawned.

Waiting does not consume the restart budget: a slow dependency costs patience, not retries. This is what stops a Docker-backed server from being marked failed at login while the Docker daemon is still starting.

Start on login (macOS)

xy service install     # write the LaunchAgent, load it, start the daemon
xy service uninstall   # unload and remove the agent
xy service start       # load an installed agent
xy service stop        # unload until the next login
xy service status      # show agent state

xy service install snapshots the current PATH into the agent, because a launchd agent otherwise inherits only /usr/bin:/bin:/usr/sbin:/sbin and supervised servers would not find their toolchains. Re-run with --force after installing a new toolchain to refresh the snapshot.

xy service stop lasts until the next login. To disable start-on-login permanently, use xy service uninstall.

The daemon writes to $XDG_STATE_HOME/xy/logs/daemon.log. Failures that happen before the daemon starts logging — a missing binary, a malformed plist — are visible only to launchd:

launchctl print gui/$UID/se.aceofba.xy
S
Description
No description provided
Readme
303 KiB
Languages
Rust 100%