1pub mod preview;
11mod proxy;
12
13use std::io;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16
17use aither_core::llm::tool::Tools;
18use aither_mcp::McpServer;
19use aither_mcp::transport::StdioTransport;
20use eyre::{Context as _, Result};
21use serde::Serialize;
22use tracing::info;
23use waterui_mcp_protocol::{INSTRUCTIONS, register_session_tools};
24use waterui_preview_protocol::hydrolysis::McpRunConfig;
25
26use crate::hydrolysis::backend::HydrolysisBackend;
27use crate::platform::TargetPlatform;
28use crate::project::Project;
29
30pub use proxy::ChildProxy;
31
32#[derive(Debug)]
34pub struct McpSessionRequest {
35 pub project_path: PathBuf,
37 pub width: u32,
39 pub height: u32,
41 pub scale_factor: f64,
43 pub sccache_path: Option<PathBuf>,
45 pub server_name: String,
47}
48
49pub async fn serve_mcp(request: McpSessionRequest) -> Result<()> {
66 let McpSessionRequest {
67 project_path,
68 width,
69 height,
70 scale_factor,
71 sccache_path,
72 server_name,
73 } = request;
74
75 let proxy = Arc::new(ChildProxy::new(
76 project_path.clone(),
77 width,
78 height,
79 scale_factor,
80 sccache_path.clone(),
81 ));
82 let mut tools = Tools::new();
83 register_session_tools(&mut tools, Arc::clone(&proxy));
84 tools
88 .register(preview::PreviewTool::new(project_path, sccache_path))
89 .expect("static tool registration cannot fail");
90
91 proxy.rebuild().await;
94
95 let result = McpServer::new(
96 StdioTransport::new(),
97 tools,
98 server_name,
99 env!("CARGO_PKG_VERSION"),
100 )
101 .with_instructions(INSTRUCTIONS)
102 .run()
103 .await;
104
105 proxy.shutdown().await;
108 info!("water mcp: session ended");
109
110 result.map_err(|error| eyre::eyre!("MCP stdio server failed: {error}"))
111}
112
113const fn host_platform() -> TargetPlatform {
116 #[cfg(target_os = "macos")]
117 {
118 TargetPlatform::MacOS
119 }
120 #[cfg(target_os = "linux")]
121 {
122 TargetPlatform::Linux
123 }
124 #[cfg(target_os = "windows")]
125 {
126 TargetPlatform::Windows
127 }
128 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
129 {
130 panic!("`water mcp` requires macOS, Linux, or Windows");
131 }
132}
133
134#[derive(Serialize)]
138struct McpJson {
139 #[serde(rename = "mcpServers")]
140 mcp_servers: McpJsonServers,
141}
142
143#[derive(Serialize)]
145struct McpJsonServers {
146 app: McpJsonServer,
147}
148
149#[derive(Serialize)]
151struct McpJsonServer {
152 command: &'static str,
153 args: [&'static str; 1],
154}
155
156const MCP_JSON: McpJson = McpJson {
157 mcp_servers: McpJsonServers {
158 app: McpJsonServer {
159 command: "water",
160 args: ["mcp"],
161 },
162 },
163};
164
165pub(crate) async fn ensure_mcp_json(project_root: &Path) -> io::Result<bool> {
168 let path = project_root.join(".mcp.json");
169 match smol::fs::metadata(&path).await {
170 Ok(_) => return Ok(false),
171 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
172 Err(error) => return Err(error),
173 }
174 let json = serde_json::to_vec_pretty(&MCP_JSON).map_err(io::Error::other)?;
175 smol::fs::write(&path, json).await?;
176 Ok(true)
177}
178
179pub(crate) async fn write_run_config(project: &Project, config: &McpRunConfig) -> Result<PathBuf> {
182 let path = project
183 .backend_path::<HydrolysisBackend>()
184 .join("mcp-run.json");
185 let json = serde_json::to_vec_pretty(config)
186 .wrap_err("failed to serialize the hydrolysis MCP run config")?;
187 smol::fs::write(&path, json)
188 .await
189 .wrap_err_with(|| format!("failed to write {}", path.display()))?;
190 Ok(path)
191}