use rmcp::ServiceExt as _;
use super::shutdown::ShutdownSignal;
pub async fn run_stdio(
mcp_server: crate::mcp::McplsServer,
mut shutdown_signal: ShutdownSignal,
) -> Result<(), crate::Error> {
let service = tokio::select! {
result = mcp_server.serve(rmcp::transport::stdio()) => {
result.map_err(|e| crate::Error::McpServerStart(Box::new(e)))?
}
() = shutdown_signal.recv() => {
tracing::info!("shutdown signal received during handshake, stopping stdio transport");
return Ok(());
}
};
tokio::select! {
result = service.waiting() => result
.map(|_| ())
.map_err(|source| crate::Error::TaskFailed {
task: crate::error::BackgroundTask::McpService,
source,
}),
() = shutdown_signal.recv() => {
tracing::info!("shutdown signal received, stopping stdio transport");
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use crate::bridge::WorkspaceRoots;
#[tokio::test]
async fn test_run_stdio_returns_promptly_when_stdin_is_already_closed() {
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::bridge::{NotificationCache, Translator};
use crate::config::McpConfig;
use crate::mcp::{McplsServer, SubscriptionRegistry};
let translator = Arc::new(Translator::new());
let notification_cache = Arc::new(Mutex::new(NotificationCache::new()));
let workspace_roots = WorkspaceRoots::default();
let subs = SubscriptionRegistry::new();
let server = McplsServer::new(
translator,
notification_cache,
workspace_roots,
subs,
crate::ProjectConfigStatus::NotIgnored,
McpConfig::default(),
);
let outcome = tokio::time::timeout(
std::time::Duration::from_secs(2),
super::run_stdio(server, super::ShutdownSignal::new()),
)
.await;
assert!(
outcome.is_ok(),
"run_stdio must not hang when stdin is already closed"
);
let result = outcome.unwrap();
assert_matches!(
result,
Err(crate::Error::McpServerStart(_)),
"expected a McpServerStart error from the failed handshake, got: {result:?}"
);
}
}