use rmcp::handler::server::tool::ToolCallContext;
use rmcp::model::{
CallToolRequestParams, CallToolResult, ErrorData as McpError, ListToolsResult,
PaginatedRequestParams, ServerCapabilities, ServerInfo,
};
use rmcp::service::{RequestContext, RoleServer};
use rmcp::{ServerHandler, ServiceExt};
use crate::cli::global::GlobalFlags;
use super::PatchloomService;
impl ServerHandler for PatchloomService {
fn get_info(&self) -> ServerInfo {
let mut info = ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_instructions(
"Use these tools for ALL file operations. Prefer 'execute_plan' (or tx plans) for any multi-op or multi-file work to ensure atomicity and avoid races from parallel calls on the same paths. Use batch_replace/batch_tidy only for uniform ops across files. Per-call success does not guarantee combined success if you issue conflicting parallel writes.",
);
info.server_info.name = "patchloom".into();
info.server_info.version = env!("CARGO_PKG_VERSION").into();
info
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, McpError> {
Ok(ListToolsResult {
tools: self.tool_router.list_all(),
next_cursor: None,
meta: None,
})
}
async fn call_tool(
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
let tool_name = request.name.clone();
crate::verbose!("mcp: tool call -> {tool_name}");
let start = std::time::Instant::now();
let tc = ToolCallContext::new(self, request, context);
let result = self.tool_router.call(tc).await;
let duration_ms = start.elapsed().as_millis() as u64;
crate::verbose!(
"mcp: {tool_name} completed in {duration_ms}ms (ok={})",
result.is_ok()
);
self.log_tool_call(&tool_name, duration_ms, &result);
result
}
}
#[cfg(feature = "mcp-http")]
pub(crate) fn run_mcp_http_server(
global: &GlobalFlags,
log: Option<String>,
host: &str,
port: u16,
tls_cert: Option<&std::path::Path>,
tls_key: Option<&std::path::Path>,
) -> anyhow::Result<u8> {
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use rmcp::transport::{StreamableHttpServerConfig, StreamableHttpService};
use tokio_util::sync::CancellationToken;
let cwd = global.resolve_cwd()?;
let ct = CancellationToken::new();
let mut config =
StreamableHttpServerConfig::default().with_cancellation_token(ct.child_token());
if host != "127.0.0.1" && host != "::1" && host != "localhost" {
config = config.disable_allowed_hosts();
}
let log_clone = log;
let service = StreamableHttpService::new(
move || {
PatchloomService::new(cwd.clone(), log_clone.clone())
.map_err(|e| std::io::Error::other(e.to_string()))
},
std::sync::Arc::new(LocalSessionManager::default()),
config,
);
let app = axum::Router::new().nest_service("/mcp", service);
let addr: std::net::SocketAddr = format!("{host}:{port}")
.parse()
.map_err(|e| anyhow::anyhow!("invalid bind address: {e}"))?;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
if let (Some(cert), Some(key)) = (tls_cert, tls_key) {
let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert, key)
.await
.map_err(|e| anyhow::anyhow!("TLS config error: {e}"))?;
let handle = axum_server::Handle::new();
let h = handle.clone();
let ct2 = ct.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
ct2.cancel();
h.graceful_shutdown(Some(std::time::Duration::from_secs(5)));
});
let h_addr = handle.clone();
tokio::spawn(async move {
if let Some(real_addr) = h_addr.listening().await {
eprintln!("MCP HTTPS server listening on https://{real_addr}/mcp");
}
});
axum_server::bind_rustls(addr, tls_config)
.handle(handle)
.serve(app.into_make_service())
.await
.map_err(|e| anyhow::anyhow!("HTTPS server error: {e}"))?;
} else {
let ct2 = ct.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
ct2.cancel();
});
let listener = tokio::net::TcpListener::bind(addr)
.await
.map_err(|e| anyhow::anyhow!("failed to bind {addr}: {e}"))?;
eprintln!(
"MCP HTTP server listening on http://{}/mcp",
listener.local_addr()?
);
axum::serve(listener, app)
.with_graceful_shutdown(ct.cancelled_owned())
.await
.map_err(|e| anyhow::anyhow!("HTTP server error: {e}"))?;
}
Ok::<(), anyhow::Error>(())
})?;
Ok(crate::exit::SUCCESS)
}
pub(crate) fn run_mcp_server(global: &GlobalFlags, log: Option<String>) -> anyhow::Result<u8> {
let cwd = global.resolve_cwd()?;
let service = PatchloomService::new(cwd, log)?;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let server = service
.serve(rmcp::transport::stdio())
.await
.map_err(|e| anyhow::anyhow!("MCP server error: {e}"))?;
server
.waiting()
.await
.map_err(|e| anyhow::anyhow!("MCP server error: {e}"))?;
Ok::<(), anyhow::Error>(())
})?;
Ok(crate::exit::SUCCESS)
}