feat(#2659): serve hive-matrix-mcp over persistent streamable-http, drop stdio bridge

This commit is contained in:
damocles 2026-07-23 20:20:51 +02:00 committed by mara
commit a66b7ab298
21 changed files with 411 additions and 856 deletions

View file

@ -1,6 +1,8 @@
//! `hive-matrix-daemon` binary — long-running matrix-sdk Client + sync
//! loop per matrix account. Bridges incoming room events to hyperhive
//! wake signals and serves the unix socket the stdio MCP bridge talks to.
//! wake signals and serves its MCP tools directly over streamable-http
//! on `--http <addr>` — no stdio bridge, no separate bin claude has to
//! respawn every turn.
//!
//! Lifecycle:
//! 1. Read the configured account list (`accounts::configured()` —
@ -8,9 +10,9 @@
//! 2. For each account: whoami probe → recover `user_id` + `device_id`
//! → restore matrix-sdk session (no login flow), install the
//! message-event handler, and spawn its own sync loop.
//! 3. Serve the unix socket against an account→Client registry; each
//! MCP request routes to the account named in its `account` field
//! (the primary account when omitted).
//! 3. Serve the MCP tools against an account→Client registry; each tool
//! call routes to the account named in its `account` arg (the
//! primary account when omitted).
//!
//! Standalone-degraded boot: the PRIMARY account having no token file →
//! exit 0 cleanly so systemd's path-watcher restarts us once hive-c0re
@ -27,19 +29,21 @@
use std::sync::Arc;
use anyhow::{Context, Result};
use clap::Parser;
use matrix_sdk::{Client, config::SyncSettings};
mod accounts;
mod client;
mod handlers;
mod paths;
mod protocol;
mod socket;
mod timeline;
mod wake;
use hive_matrix_mcp::accounts::{AccountCfg, Registry};
use hive_matrix_mcp::client::PermanentBringUpError;
use hive_matrix_mcp::{accounts, client, mcp, paths, timeline, wake};
use accounts::{AccountCfg, Registry};
use client::PermanentBringUpError;
#[derive(Parser)]
#[command(name = "hive-matrix-daemon", about = "matrix-sdk client + MCP daemon")]
struct Cli {
/// Serve the MCP tools over streamable-http on this address (e.g.
/// `127.0.0.1:8792`). Bind loopback only.
#[arg(long)]
http: std::net::SocketAddr,
}
/// A per-account sync loop, boxed so loops for N accounts can be driven
/// concurrently on the main task. Deliberately NOT `Send`: matrix-sdk's
@ -71,8 +75,8 @@ async fn main() -> Result<()> {
.with_writer(std::io::stderr)
.init();
let cli = Cli::parse();
let cfgs = accounts::configured().context("read matrix account config")?;
let mcp_socket = paths::daemon_socket();
let multi = cfgs.len() > 1;
let primary = cfgs[0].name.clone();
let mut registry = Registry::new(primary);
@ -131,9 +135,9 @@ async fn main() -> Result<()> {
tracing::warn!(error = %format!("{e:#}"), "failed to write matrix-accounts snapshot");
}
// Serve the socket against the registry. Spawned before driving the
// sync loops so the MCP bridge can connect as soon as the first
// claude turn fires.
// Serve the MCP tools against the registry. Spawned before driving
// the sync loops so claude can reach the stable http URL as soon as
// the first turn fires.
let registry = Arc::new(registry);
// Heartbeat: periodically rewrite the accounts snapshot so its mtime
@ -155,10 +159,11 @@ async fn main() -> Result<()> {
}
});
let socket_listener = mcp_socket.clone();
let http_addr = cli.http;
let mcp_registry = Arc::clone(&registry);
tokio::spawn(async move {
if let Err(e) = socket::serve(&socket_listener, registry).await {
tracing::error!(error = %e, "mcp socket server exited");
if let Err(e) = mcp::serve_http(http_addr, mcp_registry).await {
tracing::error!(error = %e, "mcp http server exited");
}
});
@ -298,7 +303,7 @@ async fn bring_up_account(
// matrix todos so stale ones (rooms read / invites resolved while the
// daemon was down) don't linger, then let the first sweep rebuild the
// set to match current reality. Best-effort; the sweep converges.
let _ = crate::wake::send_todo_clear(None, true).await;
let _ = wake::send_todo_clear(None, true).await;
let sync_loop: SyncLoop = Box::pin(async move {
sync_client
.sync_with_callback(SyncSettings::default(), move |_response| {