mod gateway;
mod protocol;
mod session;
mod transport;
use std::net::SocketAddr;
use std::path::Path;
use std::process::ExitCode;
use serde_json::{Value, json};
use crate::error::CliError;
use crate::installation::generation::{GENERATION_FILE_ENV, GENERATION_TOKEN_ENV};
use crate::server::GatewayOverrides;
pub(crate) const SERVER_NAME: &str = "nemo-relay";
const LAUNCH_ARGS: &[&str] = &["mcp"];
pub(crate) async fn run(server_args: &GatewayOverrides) -> Result<ExitCode, CliError> {
log::info!(
target: "nemo_relay.mcp",
event = "mcp_session_started";
"MCP session started"
);
if transparent_run_active() {
let gateway_url = std::env::var(crate::configuration::GATEWAY_URL_ENV).map_err(|_| {
CliError::Launch(format!(
"{} is required when {}=1",
crate::configuration::GATEWAY_URL_ENV,
crate::configuration::TRANSPARENT_RUN_ENV
))
})?;
let bootstrap_fingerprint =
crate::configuration::transparent_gateway_fingerprint(&gateway_url);
let lease = gateway::GatewayLease::borrow(gateway_url, bootstrap_fingerprint).await?;
log::info!(
target: "nemo_relay.mcp",
event = "gateway_acquired",
mode = "borrowed";
"MCP gateway acquired"
);
let frames = transport::spawn_stdin_reader()?;
return match session::run(lease, frames, tokio::io::stdout()).await {
Ok(()) => {
log::info!(
target: "nemo_relay.mcp",
event = "mcp_session_closed";
"MCP session closed"
);
Ok(ExitCode::SUCCESS)
}
Err(error) => {
log::error!(
target: "nemo_relay.mcp",
event = "mcp_session_failed",
error_kind = error.log_kind();
"MCP session failed"
);
Err(error)
}
};
}
let lease = gateway::GatewayPlan::resolve(server_args)
.await?
.acquire()
.await?;
log::info!(
target: "nemo_relay.mcp",
event = "gateway_acquired",
mode = "managed";
"MCP gateway acquired"
);
let frames = transport::spawn_stdin_reader()?;
match session::run(lease, frames, tokio::io::stdout()).await {
Ok(()) => {
log::info!(
target: "nemo_relay.mcp",
event = "mcp_session_closed";
"MCP session closed"
);
Ok(ExitCode::SUCCESS)
}
Err(error) => {
log::error!(
target: "nemo_relay.mcp",
event = "mcp_session_failed",
error_kind = error.log_kind();
"MCP session failed"
);
Err(error)
}
}
}
pub(crate) fn persistent_server(
relay: &Path,
generation_file: &Path,
generation_token: &str,
) -> Value {
json!({
"command": relay,
"args": LAUNCH_ARGS,
"env": {
"NEMO_RELAY_GATEWAY_BIND": crate::bootstrap::DEFAULT_BIND,
(GENERATION_FILE_ENV): generation_file,
(GENERATION_TOKEN_ENV): generation_token
}
})
}
fn transparent_run_active() -> bool {
std::env::var(crate::configuration::TRANSPARENT_RUN_ENV)
.ok()
.as_deref()
== Some("1")
}
fn default_mcp_bind() -> SocketAddr {
crate::bootstrap::DEFAULT_BIND
.parse()
.expect("default MCP gateway bind is valid")
}
#[cfg(test)]
async fn run_session<R, W>(reader: R, writer: W) -> Result<(), CliError>
where
R: tokio::io::AsyncBufRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
let lease = gateway::GatewayLease::test_pending();
session::serve_with_lease(lease, reader, writer).await
}
#[cfg(test)]
use gateway::{maintain_gateway_with, maintain_gateway_with_generation};
#[cfg(test)]
use protocol::{MCP_PROTOCOL_VERSION, jsonrpc_error, response_for};
#[cfg(test)]
use session::serve_stdio;
#[cfg(test)]
use transport::{MAX_MCP_FRAME_BYTES, read_bounded_frame};
#[cfg(test)]
#[path = "../../tests/coverage/shared/mcp_tests.rs"]
mod tests;