feat(#2112): make http-mcp the sole transport for the built-in surface
This commit is contained in:
parent
603c3b79dd
commit
1ba44b77ac
6 changed files with 110 additions and 103 deletions
|
|
@ -947,42 +947,41 @@ in
|
|||
};
|
||||
|
||||
options.hyperhive.mcp.httpPort = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.port;
|
||||
default = null;
|
||||
example = 8790;
|
||||
type = lib.types.port;
|
||||
default = 8790;
|
||||
example = 8791;
|
||||
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-agent-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-agent-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.
|
||||
Loopback port the built-in hyperhive MCP surface is served on. HTTP
|
||||
is the *sole* transport for the built-in surface: a
|
||||
long-lived `hive-mcp-http` systemd unit runs
|
||||
`hive-agent-mcp --http 127.0.0.1:<port>` and `render_claude_config`
|
||||
points claude at the stable `http://127.0.0.1:<port>/mcp` URL. That
|
||||
URL survives the per-turn claude re-spawn (and a host-side hive-c0re
|
||||
restart — each tool call dials the control socket fresh), so there
|
||||
is no per-turn MCP re-registration race (a resumed stdio child could
|
||||
emit its first tool call before that turn's async
|
||||
`initialize`/`tools-list` completed, stranding the agent with `No
|
||||
such tool` — the http endpoint eliminates that). 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.
|
||||
|
||||
Failure-mode note: enabling this flips the MCP surface from a
|
||||
transient self-healing race (stdio child re-registers each turn)
|
||||
to a hard dependency on the `hive-mcp-http` daemon's uptime — while
|
||||
it's up there is no race ever, but while it's down claude hits a
|
||||
dead URL with no stdio fallback until the unit restarts (guarded by
|
||||
`Restart=always`, `RestartSec=3`). Net-better when up; operators
|
||||
should know the signature flips from "flaps + recovers" to "dead
|
||||
until the unit restarts". Also pick a port unique per host: two
|
||||
agents sharing a host that both set the same `httpPort` collide on
|
||||
bind and the loser Restart-loops.
|
||||
Failure-mode note: with no stdio fallback, if `hive-mcp-http` is
|
||||
down claude hits a dead URL until the unit restarts (guarded by
|
||||
`Restart=always`, `RestartSec=3`). Intended shape: no per-turn race
|
||||
while up, a bounded self-healing gap while restarting.
|
||||
|
||||
Safe as a single fixed default across all agents: each container
|
||||
runs in its own private network namespace (isolation is always-on —
|
||||
see docs/network.md), so `127.0.0.1:<port>` is per-container-private
|
||||
and cannot collide across agents. Override only if a container-local
|
||||
service already occupies this port.
|
||||
|
||||
Must match `mcp_config::DEFAULT_MCP_HTTP_PORT` (the harness always
|
||||
exports `HYPERHIVE_MCP_HTTP_PORT`, so the const is only a fallback).
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -1940,18 +1939,17 @@ 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 harness binaries' 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) {
|
||||
# surface — the *sole* transport for that surface; always
|
||||
# wired. 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 harness binaries' 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 = {
|
||||
description = "persistent streamable-http MCP daemon for the hyperhive surface";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "hive-ag3nt.service" ];
|
||||
|
|
@ -1959,11 +1957,12 @@ in
|
|||
serviceConfig = {
|
||||
ExecStart = "${pkgs.hyperhive}/bin/hive-agent-mcp --http 127.0.0.1:${toString config.hyperhive.mcp.httpPort}";
|
||||
SyslogIdentifier = "hive-mcp-http";
|
||||
# `always` (not `on-failure`): this endpoint is load-bearing when
|
||||
# `httpPort` is set — a down window is total hyperhive-MCP loss with
|
||||
# no stdio fallback and no per-turn self-heal (the URL just stays
|
||||
# dead). `always` also covers any unforeseen clean-return path and
|
||||
# restarts after a stray SIGTERM stops it out from under the harness.
|
||||
# `always` (not `on-failure`): this endpoint is load-bearing — the
|
||||
# sole hyperhive-MCP transport, so a down window is total
|
||||
# hyperhive-MCP loss with no stdio fallback and no per-turn
|
||||
# self-heal (the URL just stays dead). `always` also covers any
|
||||
# unforeseen clean-return path and restarts after a stray SIGTERM
|
||||
# stops it out from under the harness.
|
||||
Restart = "always";
|
||||
RestartSec = 3;
|
||||
User = userName;
|
||||
|
|
@ -2063,12 +2062,12 @@ in
|
|||
# `hive_c0re::agent_sockets::socket_path_for(name)` so lifecycle
|
||||
# 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.
|
||||
# Loopback URL of the persistent `hive-mcp-http` daemon that
|
||||
# `render_claude_config` points claude at for the built-in
|
||||
# surface (HTTP is the sole transport — no per-turn stdio child).
|
||||
# Kept in sync with the `hive-mcp-http` unit's `--http` port
|
||||
# above via the same option. Always set — network isolation is
|
||||
# unconditional, so a fixed per-container port is collision-free.
|
||||
HYPERHIVE_MCP_HTTP_PORT = toString config.hyperhive.mcp.httpPort;
|
||||
}
|
||||
// lib.optionalAttrs config.hyperhive.gui.enable {
|
||||
|
|
|
|||
Loading…
Reference in a new issue