pub mod preview;
mod proxy;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use aither_core::llm::tool::Tools;
use aither_mcp::McpServer;
use aither_mcp::transport::StdioTransport;
use eyre::{Context as _, Result};
use serde::Serialize;
use tracing::info;
use waterui_mcp_protocol::{INSTRUCTIONS, register_session_tools};
use waterui_preview_protocol::hydrolysis::McpRunConfig;
use crate::hydrolysis::backend::HydrolysisBackend;
use crate::platform::TargetPlatform;
use crate::project::Project;
pub use proxy::ChildProxy;
#[derive(Debug)]
pub struct McpSessionRequest {
pub project_path: PathBuf,
pub width: u32,
pub height: u32,
pub scale_factor: f64,
pub sccache_path: Option<PathBuf>,
pub server_name: String,
}
pub async fn serve_mcp(request: McpSessionRequest) -> Result<()> {
let McpSessionRequest {
project_path,
width,
height,
scale_factor,
sccache_path,
server_name,
} = request;
let proxy = Arc::new(ChildProxy::new(
project_path.clone(),
width,
height,
scale_factor,
sccache_path.clone(),
));
let mut tools = Tools::new();
register_session_tools(&mut tools, Arc::clone(&proxy));
tools
.register(preview::PreviewTool::new(project_path, sccache_path))
.expect("static tool registration cannot fail");
proxy.rebuild().await;
let result = McpServer::new(
StdioTransport::new(),
tools,
server_name,
env!("CARGO_PKG_VERSION"),
)
.with_instructions(INSTRUCTIONS)
.run()
.await;
proxy.shutdown().await;
info!("water mcp: session ended");
result.map_err(|error| eyre::eyre!("MCP stdio server failed: {error}"))
}
const fn host_platform() -> TargetPlatform {
#[cfg(target_os = "macos")]
{
TargetPlatform::MacOS
}
#[cfg(target_os = "linux")]
{
TargetPlatform::Linux
}
#[cfg(target_os = "windows")]
{
TargetPlatform::Windows
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
panic!("`water mcp` requires macOS, Linux, or Windows");
}
}
#[derive(Serialize)]
struct McpJson {
#[serde(rename = "mcpServers")]
mcp_servers: McpJsonServers,
}
#[derive(Serialize)]
struct McpJsonServers {
app: McpJsonServer,
}
#[derive(Serialize)]
struct McpJsonServer {
command: &'static str,
args: [&'static str; 1],
}
const MCP_JSON: McpJson = McpJson {
mcp_servers: McpJsonServers {
app: McpJsonServer {
command: "water",
args: ["mcp"],
},
},
};
pub(crate) async fn ensure_mcp_json(project_root: &Path) -> io::Result<bool> {
let path = project_root.join(".mcp.json");
match smol::fs::metadata(&path).await {
Ok(_) => return Ok(false),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
let json = serde_json::to_vec_pretty(&MCP_JSON).map_err(io::Error::other)?;
smol::fs::write(&path, json).await?;
Ok(true)
}
pub(crate) async fn write_run_config(project: &Project, config: &McpRunConfig) -> Result<PathBuf> {
let path = project
.backend_path::<HydrolysisBackend>()
.join("mcp-run.json");
let json = serde_json::to_vec_pretty(config)
.wrap_err("failed to serialize the hydrolysis MCP run config")?;
smol::fs::write(&path, json)
.await
.wrap_err_with(|| format!("failed to write {}", path.display()))?;
Ok(path)
}