Skip to content

Commit 774fcca

Browse files
committed
feat(serve): spawn HTTP server alongside MCP with graceful shutdown
Add HttpServeOpts struct and update run_serve to optionally spawn an axum HTTP server as a tokio task before the blocking MCP stdio loop. Uses CancellationToken for coordinated shutdown when MCP exits. Validates --no-auth is only allowed on 127.0.0.1.
1 parent aee4858 commit 774fcca

2 files changed

Lines changed: 63 additions & 2 deletions

File tree

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1056,7 +1056,7 @@ async fn main() -> Result<()> {
10561056
eprintln!("No index found. Run 'engraph index <path>' first.");
10571057
std::process::exit(1);
10581058
}
1059-
engraph::serve::run_serve(&data_dir).await?;
1059+
engraph::serve::run_serve(&data_dir, None).await?;
10601060
}
10611061

10621062
Command::Write { action } => {

src/serve.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,11 +730,30 @@ impl rmcp::handler::server::ServerHandler for EngraphServer {
730730
}
731731
}
732732

733+
// ---------------------------------------------------------------------------
734+
// HTTP server options (populated by CLI flags in Task 7)
735+
// ---------------------------------------------------------------------------
736+
737+
pub struct HttpServeOpts {
738+
pub port: u16,
739+
pub host: String,
740+
pub no_auth: bool,
741+
}
742+
733743
// ---------------------------------------------------------------------------
734744
// Entry point
735745
// ---------------------------------------------------------------------------
736746

737-
pub async fn run_serve(data_dir: &Path) -> Result<()> {
747+
pub async fn run_serve(data_dir: &Path, http_opts: Option<HttpServeOpts>) -> Result<()> {
748+
if let Some(ref opts) = http_opts {
749+
if opts.no_auth && opts.host != "127.0.0.1" {
750+
anyhow::bail!(
751+
"--no-auth cannot be used with --host {} (only 127.0.0.1 is allowed)",
752+
opts.host
753+
);
754+
}
755+
}
756+
738757
let db_path = data_dir.join("engraph.db");
739758
let models_dir = data_dir.join("models");
740759

@@ -800,6 +819,15 @@ pub async fn run_serve(data_dir: &Path) -> Result<()> {
800819
let profile_arc = Arc::new(profile);
801820
let recent_writes: RecentWrites = Arc::new(Mutex::new(HashMap::new()));
802821

822+
// Clone Arcs for HTTP server before MCP consumes them
823+
let http_store = store_arc.clone();
824+
let http_embedder = embedder_arc.clone();
825+
let http_vault_path = vault_path_arc.clone();
826+
let http_profile = profile_arc.clone();
827+
let http_orchestrator = orchestrator.as_ref().map(Arc::clone);
828+
let http_reranker = reranker.as_ref().map(Arc::clone);
829+
let http_recent_writes = recent_writes.clone();
830+
803831
// Start file watcher for real-time index updates
804832
let mut exclude = config.exclude.clone();
805833
if let Some(ref prof) = *profile_arc
@@ -831,12 +859,45 @@ pub async fn run_serve(data_dir: &Path) -> Result<()> {
831859
recent_writes,
832860
};
833861

862+
// Cancellation token for coordinated shutdown of HTTP + MCP
863+
let cancel_token = tokio_util::sync::CancellationToken::new();
864+
865+
// Spawn HTTP server as a background task (before MCP blocks on stdio)
866+
if let Some(ref opts) = http_opts {
867+
let config = Config::load()?;
868+
let api_state = crate::http::ApiState {
869+
store: http_store,
870+
embedder: http_embedder,
871+
vault_path: http_vault_path,
872+
profile: http_profile,
873+
orchestrator: http_orchestrator,
874+
reranker: http_reranker,
875+
http_config: Arc::new(config.http.clone()),
876+
no_auth: opts.no_auth,
877+
recent_writes: http_recent_writes,
878+
rate_limiter: Arc::new(crate::http::RateLimiter::new(config.http.rate_limit)),
879+
};
880+
let router = crate::http::build_router(api_state);
881+
let addr = format!("{}:{}", opts.host, opts.port);
882+
let listener = tokio::net::TcpListener::bind(&addr).await?;
883+
let cancel = cancel_token.clone();
884+
eprintln!("HTTP server listening on http://{}", addr);
885+
tokio::spawn(async move {
886+
axum::serve(listener, router)
887+
.with_graceful_shutdown(cancel.cancelled_owned())
888+
.await
889+
.ok();
890+
});
891+
}
892+
834893
eprintln!("engraph MCP server starting...");
835894

836895
let transport = rmcp::transport::io::stdio();
837896
let server_handle = server.serve(transport).await?;
838897
server_handle.waiting().await?;
839898

899+
cancel_token.cancel(); // triggers HTTP graceful shutdown
900+
840901
// Shut down watcher cleanly after MCP transport exits
841902
let _ = watcher_shutdown.send(());
842903
if let Err(e) = watcher_handle.join() {

0 commit comments

Comments
 (0)