feat(#2038): wire persistent hive-mcp-http daemon behind hyperhive.mcp.httpPort

This commit is contained in:
damocles 2026-07-01 18:04:14 +02:00
commit 3c10b00460

View file

@ -828,6 +828,34 @@ in
'';
};
options.hyperhive.mcp.httpPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
example = 8790;
description = ''
Serve the built-in hyperhive MCP surface as a persistent
streamable-http daemon on this loopback port instead of the
default per-turn stdio child.
When null (the default) claude spawns a fresh `hive mcp` stdio
subprocess every turn the shape that carries the per-turn MCP
re-registration race (a resumed turn can emit its first tool call
before that turn's async `initialize`/`tools-list` completes,
stranding the agent with `No such tool`). When set, a long-lived
`hive-mcp-http` systemd unit runs `hive mcp --http 127.0.0.1:<port>`
and `render_claude_config` points claude at the stable
`http://127.0.0.1:<port>/mcp` URL, which survives the per-turn
claude re-spawn (and a host-side hive-c0re restart each tool call
dials the control socket fresh). Extra MCP servers (matrix/bash)
stay stdio bridges regardless.
Bound loopback-only; the rmcp streamable-http transport's default
`allowed_hosts` (`localhost` / `127.0.0.1` / `::1`) rejects Host
headers from anywhere else, so no auth token is required for a
container-local endpoint.
'';
};
config = {
warnings = lib.optional (config.hyperhive.allowedBashPatterns != [ ]) ''
hyperhive.allowedBashPatterns is deprecated and has no effect.
@ -1709,6 +1737,33 @@ in
};
};
# Persistent streamable-http MCP daemon for the built-in hyperhive
# surface. Only wired when `hyperhive.mcp.httpPort` is set; otherwise
# the surface stays the default per-turn stdio child (rendered by
# `render_claude_config`). Long-lived so claude reconnects to the
# stable URL each turn instead of respawning + re-registering a stdio
# subprocess (the per-turn MCP registration race). It dials the
# control socket (`/run/hive/mcp.sock`, the `hive` binary default)
# fresh on every tool call, so a host-side hive-c0re restart is
# transparent. `before = hive-ag3nt` so the URL is already listening
# by the time the harness renders the first turn's config; the
# harness/claude also reconnect on their own, so ordering is a
# latency nicety not a hard correctness dep.
systemd.services.hive-mcp-http = lib.mkIf (config.hyperhive.mcp.httpPort != null) {
description = "persistent streamable-http MCP daemon for the hyperhive surface";
wantedBy = [ "multi-user.target" ];
before = [ "hive-ag3nt.service" ];
environment.RUST_LOG = "info";
serviceConfig = {
ExecStart = "${pkgs.hyperhive}/bin/hive mcp --http 127.0.0.1:${toString config.hyperhive.mcp.httpPort}";
SyslogIdentifier = "hive-mcp-http";
Restart = "on-failure";
RestartSec = 3;
User = userName;
Group = userName;
};
};
# Re-fire the daemon when the matrix token appears (hive-c0re
# provisions it after agent containers come up). Without this
# the daemon would exit 0 silently on first boot and the MCP
@ -1802,6 +1857,13 @@ in
# bind-mounts and gateway upstream config stay in sync.
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";
}
// lib.optionalAttrs (config.hyperhive.mcp.httpPort != null) {
# Presence tells `render_claude_config` to point claude at the
# persistent `hive-mcp-http` daemon's loopback URL instead of a
# per-turn stdio child. Kept in sync with the `hive-mcp-http`
# unit's `--http` port above via the same option.
HYPERHIVE_MCP_HTTP_PORT = toString config.hyperhive.mcp.httpPort;
}
// lib.optionalAttrs config.hyperhive.gui.enable {
# Tells the harness which fixed VNC port weston bound, and (by
# its presence) that gui is enabled — the harness `/screen/ws`