feat(#2112): make http-mcp the sole transport for the built-in surface

This commit is contained in:
damocles 2026-07-11 00:32:51 +02:00
commit 1ba44b77ac
6 changed files with 110 additions and 103 deletions

View file

@ -11,6 +11,14 @@
/// tools as `mcp__<this>__<tool>` (e.g. `mcp__hyperhive__send`).
pub const SERVER_NAME: &str = "hyperhive";
/// Default loopback port the built-in hyperhive MCP surface is served on
/// (streamable HTTP, via the persistent `hive-mcp-http` daemon). Overridable
/// via `hyperhive.mcp.httpPort`; **must match that option's default** in
/// `nix/templates/harness-base.nix`. Safe as a single fixed value across all
/// agents because each container runs in its own private network namespace,
/// so `127.0.0.1:<port>` is per-container-private (no cross-agent collision).
pub const DEFAULT_MCP_HTTP_PORT: u16 = 8790;
/// Built-in claude tools always present in every session. Anything not
/// in this list (or added by `extra_builtin_tools`) literally doesn't
/// exist in the session. Web egress (`WebFetch`/`WebSearch`) are
@ -330,34 +338,30 @@ fn load_extra_mcp() -> std::collections::BTreeMap<String, ExtraMcpServer> {
}
/// Render the MCP config blob claude reads from `--mcp-config <path>`.
/// `mcp_binary` is the path (or PATH-resolvable name) of the
/// `hive-agent-mcp` bridge executable; `socket` is the hyperhive per-agent
/// socket bind-mounted into the container (forwarded to the child as
/// `--socket <path>`). Merges in any extra MCP servers declared via
/// `hyperhive.extraMcpServers` in the agent's NixOS config.
/// The built-in `hyperhive` surface is an HTTP entry pointing at the
/// persistent `hive-mcp-http` daemon (see [`DEFAULT_MCP_HTTP_PORT`]); there
/// is no per-turn stdio child for it. Merges in any extra MCP servers
/// declared via `hyperhive.extraMcpServers` (those stay stdio bridges).
#[must_use]
pub fn render_claude_config(mcp_binary: &str, socket: &std::path::Path) -> String {
pub fn render_claude_config() -> String {
let mut servers = serde_json::Map::new();
// When the harness is configured to run the built-in server as a
// persistent streamable-http daemon (loopback port in
// `HYPERHIVE_MCP_HTTP_PORT`), point claude at the stable URL instead of
// respawning a fresh stdio child each turn. The URL survives the per-turn
// claude re-spawn, so there is no per-turn re-registration race for the
// hyperhive surface. Extra servers (matrix/bash) stay stdio bridges.
let hyperhive_entry = match std::env::var("HYPERHIVE_MCP_HTTP_PORT")
// The built-in hyperhive surface is served exclusively over streamable
// HTTP by the persistent `hive-mcp-http` daemon (loopback, inside the
// agent's private network namespace). Point claude at the stable URL
// rather than respawning a fresh stdio child each turn: the URL survives
// the per-turn claude re-spawn, so there is no per-turn re-registration
// race for the hyperhive surface. Extra servers (matrix/bash) stay stdio
// bridges. The port comes from `HYPERHIVE_MCP_HTTP_PORT` (always set by
// the harness); `DEFAULT_MCP_HTTP_PORT` is the fallback matching the nix
// default.
let port = std::env::var("HYPERHIVE_MCP_HTTP_PORT")
.ok()
.and_then(|p| p.trim().parse::<u16>().ok())
{
Some(port) => serde_json::json!({
"type": "http",
"url": format!("http://127.0.0.1:{port}/mcp"),
}),
None => serde_json::json!({
"command": mcp_binary,
"args": ["--socket", socket.display().to_string()],
"env": {}
}),
};
.unwrap_or(DEFAULT_MCP_HTTP_PORT);
let hyperhive_entry = serde_json::json!({
"type": "http",
"url": format!("http://127.0.0.1:{port}/mcp"),
});
servers.insert(SERVER_NAME.to_owned(), hyperhive_entry);
// Auto-inject HYPERHIVE_STATE_DIR so extra MCP servers can resolve the
// agent's durable state dir without the agent author hard-coding it.

View file

@ -113,30 +113,25 @@ impl TurnFiles {
/// Returns an error if any of the config files cannot be written to disk.
pub async fn prepare(socket: &Path, label: &str) -> Result<Self> {
Ok(Self {
mcp_config: write_mcp_config(socket).await?,
mcp_config: write_mcp_config().await?,
system_prompt: write_system_prompt(socket, label).await?,
})
}
}
/// Drop the MCP config blob claude reads from `--mcp-config <path>`.
/// `socket` is the hyperhive per-container socket (forwarded to the child
/// as `--socket <path>`). The MCP server is the `hive-agent-mcp` binary
/// installed next to the running `hive-agent` (resolved as a sibling of
/// `/proc/self/exe`; PATH-resolvable name as the fallback).
/// The built-in hyperhive surface is served over HTTP by the persistent
/// `hive-mcp-http` daemon, so no per-turn stdio child is spawned; extra
/// servers declared via `hyperhive.extraMcpServers` are still stdio bridges.
///
/// # Errors
///
/// Returns an error if the config file cannot be written.
pub async fn write_mcp_config(socket: &Path) -> Result<PathBuf> {
pub async fn write_mcp_config() -> Result<PathBuf> {
let parent = crate::paths::config_dir();
tokio::fs::create_dir_all(&parent).await.ok();
let path = parent.join("claude-mcp-config.json");
let exe = std::env::current_exe()
.ok()
.and_then(|p| Some(p.parent()?.join("hive-agent-mcp")))
.map_or_else(|| "hive-agent-mcp".into(), |p| p.display().to_string());
let body = mcp_config::render_claude_config(&exe, socket);
let body = mcp_config::render_claude_config();
tokio::fs::write(&path, body).await?;
tracing::info!(path = %path.display(), "wrote claude MCP config");
Ok(path)