pub mod control;
pub mod docs;
#[cfg(feature = "mcp-stdio")]
pub mod drain;
pub mod http_guard;
pub mod tools;
use anyhow::Result;
use tracing::info;
use crate::cli::Args;
use crate::settings::Settings;
#[cfg(feature = "mcp-stdio")]
pub async fn run_mcp_stdio(args: &Args, settings: Settings) -> Result<()> {
use rmcp::ServiceExt;
info!("Starting NetGet MCP STDIO server");
let service = tools::NetGetMcpService::new(args, settings).await?;
let (stdin, stdout) = rmcp::transport::stdio();
let server = service
.serve(drain::drain_on_eof_transport(stdin, stdout))
.await?;
info!("MCP STDIO server initialized, waiting for requests...");
server.waiting().await?;
info!("MCP STDIO server shut down");
Ok(())
}
#[cfg(feature = "mcp-http")]
pub async fn run_mcp_http(args: &Args, settings: Settings, port: u16) -> Result<()> {
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager, StreamableHttpServerConfig, StreamableHttpService,
};
use std::sync::Arc;
let listen_addr = args.listen_addr.as_deref().unwrap_or("127.0.0.1");
let bind_ip: std::net::IpAddr = listen_addr
.parse()
.map_err(|e| anyhow::anyhow!("--listen-addr {listen_addr:?} is not an IP address: {e}"))?;
let guard = Arc::new(http_guard::HttpGuard::new(
bind_ip,
args.resolve_mcp_token(),
)?);
let shared_state = tools::NetGetMcpService::create_shared_state(args, settings).await?;
let service = StreamableHttpService::new(
move || {
Ok(tools::NetGetMcpService::new_with_shared_state(
shared_state.clone(),
))
},
Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default(),
);
let app = axum::Router::new()
.nest_service("/mcp", service)
.layer(axum::middleware::from_fn_with_state(guard.clone(), admit));
let listener = tokio::net::TcpListener::bind((listen_addr, port)).await?;
let bind = listener.local_addr()?;
info!(
"NetGet MCP HTTP server listening on http://{}/mcp ({})",
bind,
if guard.requires_token() {
"bearer token required"
} else {
"loopback only: Host and Origin must be local"
}
);
axum::serve(listener, app).await?;
Ok(())
}
#[cfg(feature = "mcp-http")]
async fn admit(
axum::extract::State(guard): axum::extract::State<std::sync::Arc<http_guard::HttpGuard>>,
request: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
use axum::response::IntoResponse;
if let Err(refusal) = guard.check(request.headers(), request.uri()) {
tracing::warn!(
status = %refusal.status,
host = ?request.headers().get(http::header::HOST),
origin = ?request.headers().get(http::header::ORIGIN),
"refused MCP HTTP request: {}",
refusal.reason
);
return (refusal.status, refusal.reason).into_response();
}
let request = request.map(|body| {
axum::body::Body::new(http_body_util::Limited::new(
body,
http_guard::MAX_REQUEST_BODY_BYTES,
))
});
next.run(request).await
}